Add IAM Profile FastAPI verifier

This commit is contained in:
2026-06-28 00:19:18 +02:00
parent d11912d460
commit ba325976da
4 changed files with 626 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ dependencies = [
"asyncpg>=0.29",
"httpx>=0.27",
"uvicorn[standard]>=0.30",
"cryptography>=42.0",
]
[project.optional-dependencies]

247
src/core_hub/iam_profile.py Normal file
View File

@@ -0,0 +1,247 @@
from __future__ import annotations
import base64
import json
import time
import urllib.parse
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
PRINCIPAL_TYPES = {"human", "service", "agent"}
ASSURANCE_LEVELS = {"aal0", "aal1", "aal2", "aal3", "break_glass"}
DEFAULT_SKEW_SECONDS = 60
class IamProfileError(ValueError):
"""Raised when a bearer token is not a valid NetKingdom IAM Profile token."""
@dataclass(frozen=True, slots=True)
class IamProfileClaims:
issuer: str
subject: str
audience: tuple[str, ...]
tenant: str
principal_type: str
groups: tuple[str, ...]
roles: tuple[str, ...]
scopes: tuple[str, ...]
assurance: dict[str, Any]
raw: dict[str, Any]
@dataclass(frozen=True, slots=True)
class IamProfileVerifier:
issuer: str
audience: str
jwks: dict[str, Any]
environment: str = "production"
skew_seconds: int = DEFAULT_SKEW_SECONDS
def verify_token(self, token: str) -> IamProfileClaims:
header, payload = _decode_jwt(token)
_verify_signature(header, token, self.jwks)
self._verify_issuer(payload)
self._verify_audience(payload)
self._verify_lifetime(payload)
return _claims_from_payload(payload)
def _verify_issuer(self, payload: dict[str, Any]) -> None:
issuer = payload.get("iss")
if (
not isinstance(issuer, str)
or _normalize_issuer(issuer) != _normalize_issuer(self.issuer)
):
raise IamProfileError("token issuer does not match configured issuer")
if self.environment == "production" and _is_local_issuer(issuer):
raise IamProfileError("production mode must reject local-development issuers")
def _verify_audience(self, payload: dict[str, Any]) -> None:
if self.audience not in _audiences(payload.get("aud")):
raise IamProfileError("token audience does not include this service")
def _verify_lifetime(self, payload: dict[str, Any]) -> None:
now = int(time.time())
skew = self.skew_seconds
exp = payload.get("exp")
iat = payload.get("iat")
nbf = payload.get("nbf")
if not isinstance(exp, int) or exp <= now - skew:
raise IamProfileError("token is expired or missing exp")
if not isinstance(iat, int) or iat > now + skew:
raise IamProfileError("token iat is missing or in the future")
if nbf is not None and (not isinstance(nbf, int) or nbf > now + skew):
raise IamProfileError("token nbf is invalid")
_bearer = HTTPBearer(auto_error=False)
_bearer_dependency = Depends(_bearer)
def iam_profile_dependency(verifier: IamProfileVerifier):
async def dependency(
credentials: HTTPAuthorizationCredentials | None = _bearer_dependency,
) -> IamProfileClaims:
if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="missing bearer token",
headers={"WWW-Authenticate": "Bearer"},
)
try:
return verifier.verify_token(credentials.credentials)
except IamProfileError as exc:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=str(exc),
) from exc
return dependency
def _b64url_decode(value: str) -> bytes:
padding_len = (4 - len(value) % 4) % 4
return base64.urlsafe_b64decode(value + ("=" * padding_len))
def _decode_jwt(token: str) -> tuple[dict[str, Any], dict[str, Any]]:
parts = token.split(".")
if len(parts) != 3:
raise IamProfileError("JWT must have three compact-serialization parts")
try:
header = json.loads(_b64url_decode(parts[0]))
payload = json.loads(_b64url_decode(parts[1]))
except (ValueError, json.JSONDecodeError) as exc:
raise IamProfileError(f"JWT could not be decoded: {exc}") from exc
if not isinstance(header, dict) or not isinstance(payload, dict):
raise IamProfileError("JWT header and payload must be JSON objects")
return header, payload
def _jwk_to_rsa_public_key(jwk: dict[str, Any]):
n = int.from_bytes(_b64url_decode(str(jwk["n"])), "big")
e = int.from_bytes(_b64url_decode(str(jwk["e"])), "big")
return rsa.RSAPublicNumbers(e, n).public_key()
def _verify_signature(header: dict[str, Any], token: str, jwks: dict[str, Any]) -> None:
if header.get("alg") != "RS256":
raise IamProfileError("JWT alg must be RS256")
kid = header.get("kid")
keys = jwks.get("keys") if isinstance(jwks.get("keys"), list) else []
matching = [key for key in keys if isinstance(key, dict) and key.get("kid") == kid]
if not matching:
raise IamProfileError("JWT kid was not found in JWKS")
parts = token.split(".")
signing_input = f"{parts[0]}.{parts[1]}".encode("ascii")
signature = _b64url_decode(parts[2])
try:
public_key = _jwk_to_rsa_public_key(matching[0])
public_key.verify(signature, signing_input, padding.PKCS1v15(), hashes.SHA256())
except (InvalidSignature, ValueError, KeyError) as exc:
raise IamProfileError("JWT signature verification failed") from exc
def _normalize_issuer(value: str) -> str:
return value.rstrip("/")
def _is_local_issuer(issuer: str) -> bool:
if issuer == "local-identity":
return True
parsed = urllib.parse.urlparse(issuer)
host = (parsed.hostname or "").lower()
if parsed.scheme == "http":
return True
return host in {"localhost", "127.0.0.1", "::1"} or host.endswith(".local")
def _audiences(value: Any) -> tuple[str, ...]:
if isinstance(value, str):
return (value,)
if isinstance(value, Sequence) and not isinstance(value, bytes):
return tuple(str(item) for item in value)
return ()
def _scopes(payload: dict[str, Any]) -> tuple[str, ...]:
values: list[str] = []
for name in ("scope", "scp"):
raw = payload.get(name)
if isinstance(raw, str):
values.extend(part for part in raw.split() if part)
elif isinstance(raw, list):
values.extend(str(part) for part in raw)
return tuple(sorted(set(values)))
def _string_list(payload: dict[str, Any], name: str) -> tuple[str, ...]:
raw = payload.get(name)
if not isinstance(raw, list):
raise IamProfileError(f"{name} must be a list")
return tuple(str(item) for item in raw)
def _claims_from_payload(payload: dict[str, Any]) -> IamProfileClaims:
required = {"iss", "sub", "aud", "exp", "iat", "tenant", "principal_type", "assurance"}
missing = sorted(required - set(payload))
if missing:
raise IamProfileError(f"token is missing required IAM Profile claims: {', '.join(missing)}")
subject = payload.get("sub")
issuer = payload.get("iss")
tenant = payload.get("tenant")
principal_type = payload.get("principal_type")
assurance = payload.get("assurance")
if not isinstance(subject, str) or not subject:
raise IamProfileError("sub must be a non-empty string")
if not isinstance(issuer, str):
raise IamProfileError("iss must be a string")
if not isinstance(tenant, str) or not tenant.startswith("tenant:"):
raise IamProfileError("tenant must be a string like tenant:platform")
if principal_type not in PRINCIPAL_TYPES:
raise IamProfileError("principal_type must be human, service, or agent")
if not isinstance(assurance, dict):
raise IamProfileError("assurance must be an object")
roles = _string_list(payload, "roles")
groups = _string_list(payload, "groups")
scopes = _scopes(payload)
if not scopes:
raise IamProfileError("scope or scp must be present")
_validate_assurance(assurance)
return IamProfileClaims(
issuer=issuer,
subject=subject,
audience=_audiences(payload.get("aud")),
tenant=tenant,
principal_type=str(principal_type),
groups=groups,
roles=roles,
scopes=scopes,
assurance=assurance,
raw=payload,
)
def _validate_assurance(assurance: dict[str, Any]) -> None:
level = assurance.get("level")
methods = assurance.get("methods")
mfa = assurance.get("mfa")
source = assurance.get("source")
if level not in ASSURANCE_LEVELS:
raise IamProfileError("assurance.level is unsupported")
if not isinstance(methods, list) or not all(isinstance(method, str) for method in methods):
raise IamProfileError("assurance.methods must be a list of strings")
if not isinstance(mfa, bool):
raise IamProfileError("assurance.mfa must be boolean")
if not isinstance(source, str) or not source:
raise IamProfileError("assurance.source must be a non-empty string")

260
tests/test_iam_profile.py Normal file
View File

@@ -0,0 +1,260 @@
from __future__ import annotations
import base64
import hashlib
import json
import time
from typing import Any
from urllib.parse import parse_qs, urlencode, urlparse
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from fastapi import Depends, FastAPI, Request
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi.testclient import TestClient
from core_hub.iam_profile import IamProfileClaims, IamProfileVerifier, iam_profile_dependency
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def jwk_from_key(private_key: rsa.RSAPrivateKey, kid: str) -> dict[str, str]:
numbers = private_key.public_key().public_numbers()
n = b64url(numbers.n.to_bytes((numbers.n.bit_length() + 7) // 8, "big"))
e = b64url(numbers.e.to_bytes((numbers.e.bit_length() + 7) // 8, "big"))
return {"kty": "RSA", "use": "sig", "alg": "RS256", "kid": kid, "n": n, "e": e}
def sign_jwt(private_key: rsa.RSAPrivateKey, kid: str, payload: dict[str, Any]) -> str:
header = {"alg": "RS256", "typ": "JWT", "kid": kid}
header_b64 = b64url(json.dumps(header, separators=(",", ":")).encode())
payload_b64 = b64url(json.dumps(payload, separators=(",", ":")).encode())
signing_input = f"{header_b64}.{payload_b64}".encode("ascii")
signature = private_key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256())
return f"{header_b64}.{payload_b64}.{b64url(signature)}"
def pkce_challenge(verifier: str) -> str:
return b64url(hashlib.sha256(verifier.encode("ascii")).digest())
class FixtureIssuer:
def __init__(self, issuer: str, audience: str) -> None:
self.issuer = issuer.rstrip("/")
self.audience = audience
self.private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
self.kid = "iam-profile-fixture"
self.codes: dict[str, str] = {}
@property
def jwks(self) -> dict[str, list[dict[str, str]]]:
return {"keys": [jwk_from_key(self.private_key, self.kid)]}
def token(self, *, subject: str = "user:alice", audience: str | None = None) -> str:
now = int(time.time())
return sign_jwt(
self.private_key,
self.kid,
{
"iss": self.issuer,
"sub": subject,
"aud": [audience or self.audience, "profile-consumer"],
"exp": now + 600,
"iat": now,
"nbf": now - 5,
"jti": "fixture-token",
"tenant": "tenant:platform",
"principal_type": "human",
"preferred_username": "alice",
"email": "alice@example.test",
"groups": ["netkingdom-admins"],
"roles": ["admin"],
"scope": "openid profile email hub:read",
"assurance": {
"level": "aal2",
"methods": ["pwd", "otp"],
"mfa": True,
"source": "local-identity",
"at": now,
},
},
)
def make_issuer_app(fixture: FixtureIssuer) -> FastAPI:
app = FastAPI()
@app.get("/.well-known/openid-configuration")
def discovery() -> dict[str, Any]:
return {
"issuer": fixture.issuer,
"authorization_endpoint": f"{fixture.issuer}/authorize",
"token_endpoint": f"{fixture.issuer}/token",
"userinfo_endpoint": f"{fixture.issuer}/userinfo",
"jwks_uri": f"{fixture.issuer}/jwks",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "client_credentials"],
"id_token_signing_alg_values_supported": ["RS256"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["openid", "profile", "email", "hub:read"],
}
@app.get("/jwks")
def jwks() -> dict[str, list[dict[str, str]]]:
return fixture.jwks
@app.get("/authorize")
def authorize(request: Request) -> RedirectResponse:
query = request.query_params
redirect_uri = query["redirect_uri"]
if query.get("response_type") != "code" or query.get("code_challenge_method") != "S256":
return RedirectResponse(f"{redirect_uri}?error=invalid_request")
code_challenge = query.get("code_challenge")
if not code_challenge:
return RedirectResponse(f"{redirect_uri}?error=invalid_request&error_description=pkce")
code = "fixture-code"
fixture.codes[code] = code_challenge
params = urlencode({"code": code, "state": query.get("state", "")})
return RedirectResponse(f"{redirect_uri}?{params}")
@app.post("/token")
async def token(request: Request) -> JSONResponse:
body = (await request.body()).decode("utf-8")
form = {name: values[0] for name, values in parse_qs(body).items()}
code = form.get("code", "")
verifier = form.get("code_verifier", "")
if fixture.codes.get(code) != pkce_challenge(verifier):
return JSONResponse({"error": "invalid_grant"}, status_code=400)
return JSONResponse(
{
"access_token": fixture.token(),
"id_token": fixture.token(),
"token_type": "Bearer",
"expires_in": 600,
}
)
return app
def make_protected_app(verifier: IamProfileVerifier) -> FastAPI:
app = FastAPI()
require_claims = iam_profile_dependency(verifier)
claims_dependency = Depends(require_claims)
@app.get("/protected")
def protected(claims: IamProfileClaims = claims_dependency) -> dict[str, Any]:
return {
"sub": claims.subject,
"tenant": claims.tenant,
"roles": list(claims.roles),
"groups": list(claims.groups),
"scopes": list(claims.scopes),
"assuranceLevel": claims.assurance["level"],
}
return app
def obtain_access_token(client: TestClient) -> str:
verifier = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
auth = client.get(
"/authorize",
params={
"response_type": "code",
"client_id": "core-hub-test",
"redirect_uri": "http://127.0.0.1/callback",
"scope": "openid profile email hub:read",
"state": "state-1",
"nonce": "nonce-1",
"code_challenge": pkce_challenge(verifier),
"code_challenge_method": "S256",
},
follow_redirects=False,
)
assert auth.status_code in {302, 307}
code = parse_qs(urlparse(auth.headers["location"]).query)["code"][0]
token = client.post(
"/token",
data={
"grant_type": "authorization_code",
"client_id": "core-hub-test",
"code": code,
"redirect_uri": "http://127.0.0.1/callback",
"code_verifier": verifier,
},
)
assert token.status_code == 200
return token.json()["access_token"]
def test_fastapi_service_accepts_iam_profile_pkce_token() -> None:
fixture = FixtureIssuer("http://issuer.local", "core-hub")
issuer_client = TestClient(make_issuer_app(fixture), base_url=fixture.issuer)
token = obtain_access_token(issuer_client)
verifier = IamProfileVerifier(
issuer=fixture.issuer,
audience="core-hub",
jwks=fixture.jwks,
environment="local",
)
protected_client = TestClient(make_protected_app(verifier))
response = protected_client.get("/protected", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 200
assert response.json() == {
"sub": "user:alice",
"tenant": "tenant:platform",
"roles": ["admin"],
"groups": ["netkingdom-admins"],
"scopes": ["email", "hub:read", "openid", "profile"],
"assuranceLevel": "aal2",
}
def test_fastapi_service_rejects_missing_bearer_token() -> None:
fixture = FixtureIssuer("http://issuer.local", "core-hub")
verifier = IamProfileVerifier(
issuer=fixture.issuer,
audience="core-hub",
jwks=fixture.jwks,
environment="local",
)
protected_client = TestClient(make_protected_app(verifier))
response = protected_client.get("/protected")
assert response.status_code == 401
assert response.headers["www-authenticate"] == "Bearer"
def test_fastapi_service_rejects_wrong_audience() -> None:
fixture = FixtureIssuer("http://issuer.local", "core-hub")
token = fixture.token(audience="another-service")
verifier = IamProfileVerifier(
issuer=fixture.issuer,
audience="core-hub",
jwks=fixture.jwks,
environment="local",
)
protected_client = TestClient(make_protected_app(verifier))
response = protected_client.get("/protected", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 403
assert response.json()["detail"] == "token audience does not include this service"
def test_production_verifier_rejects_local_development_issuer() -> None:
fixture = FixtureIssuer("http://issuer.local", "core-hub")
token = fixture.token()
verifier = IamProfileVerifier(issuer=fixture.issuer, audience="core-hub", jwks=fixture.jwks)
protected_client = TestClient(make_protected_app(verifier))
response = protected_client.get("/protected", headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 403
assert response.json()["detail"] == "production mode must reject local-development issuers"

118
uv.lock generated
View File

@@ -104,6 +104,63 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289 },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 },
]
[[package]]
name = "click"
version = "8.4.2"
@@ -132,6 +189,7 @@ source = { virtual = "." }
dependencies = [
{ name = "alembic" },
{ name = "asyncpg" },
{ name = "cryptography" },
{ name = "fastapi" },
{ name = "httpx" },
{ name = "pydantic" },
@@ -153,6 +211,7 @@ requires-dist = [
{ name = "aiosqlite", marker = "extra == 'dev'", specifier = ">=0.20" },
{ name = "alembic", specifier = ">=1.13" },
{ name = "asyncpg", specifier = ">=0.29" },
{ name = "cryptography", specifier = ">=42.0" },
{ name = "fastapi", specifier = ">=0.115" },
{ name = "httpx", specifier = ">=0.27" },
{ name = "playwright", marker = "extra == 'dev'", specifier = ">=1.60.0" },
@@ -164,6 +223,56 @@ requires-dist = [
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30" },
]
[[package]]
name = "cryptography"
version = "49.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100 },
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978 },
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422 },
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503 },
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779 },
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683 },
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874 },
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283 },
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844 },
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290 },
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612 },
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804 },
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026 },
{ url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892 },
{ url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835 },
{ url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239 },
{ url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593 },
{ url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961 },
{ url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145 },
{ url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719 },
{ url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209 },
{ url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285 },
{ url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441 },
{ url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869 },
{ url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948 },
{ url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153 },
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947 },
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429 },
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968 },
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758 },
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863 },
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983 },
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173 },
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298 },
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338 },
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650 },
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820 },
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968 },
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547 },
]
[[package]]
name = "fastapi"
version = "0.138.1"
@@ -450,6 +559,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 },
]
[[package]]
name = "pydantic"
version = "2.13.4"