generated from coulomb/repo-seed
Add IAM Profile FastAPI verifier
This commit is contained in:
@@ -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
247
src/core_hub/iam_profile.py
Normal 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
260
tests/test_iam_profile.py
Normal 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
118
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user