decentralized and customizable links page on top of atproto ligo.at
atproto link-in-bio python uv

wip oauth flow

+1514 -52
+1
.python-version
··· 1 + 3.13
+14
pyproject.toml
··· 1 + [project] 2 + name = "ligo-at" 3 + version = "0.0.1" 4 + description = "Decentralized links page on top of AT Protocol" 5 + readme = "readme.md" 6 + requires-python = ">=3.13" 7 + dependencies = [ 8 + "atproto>=0.0.62", 9 + "authlib>=1.3", 10 + "dnspython>=2.8.0", 11 + "flask[dotenv]>=3.1.2", 12 + "requests>=2.32", 13 + "requests-hardened>=1.0.0b3", 14 + ]
+163
src/atproto2/__init__.py
··· 1 + from dns.resolver import resolve as resolve_dns 2 + from typing import Any 3 + import requests 4 + 5 + from .atproto_oauth import is_valid_authserver_meta 6 + from .atproto_security import is_safe_url 7 + from .atproto_identity import is_valid_did, is_valid_handle 8 + 9 + PLC_DIRECTORY = "https://plc.directory" 10 + 11 + AuthserverUrl = str 12 + PdsUrl = str 13 + DID = str 14 + 15 + authservers: dict[PdsUrl, AuthserverUrl] = {} 16 + dids: dict[str, DID] = {} 17 + pdss: dict[DID, PdsUrl] = {} 18 + 19 + 20 + def resolve_identity(query: str) -> tuple[str, str, dict[str, Any]] | None: 21 + """Resolves an identity to a DID, handle and DID document, verifies handles bi directionally.""" 22 + 23 + if is_valid_handle(query): 24 + handle = query 25 + did = resolve_did_from_handle(handle) 26 + if not did: 27 + return None 28 + doc = resolve_doc_from_did(did) 29 + if not doc: 30 + return None 31 + handles = handles_from_doc(doc) 32 + if not handles or handle not in handles: 33 + return None 34 + return (did, handle, doc) 35 + 36 + if is_valid_did(query): 37 + # TODO: resolve did identity 38 + return None 39 + 40 + return None 41 + 42 + 43 + def handles_from_doc(doc: dict[str, list[str]]) -> list[str]: 44 + """Return all possible handles inside the DID document.""" 45 + handles: list[str] = [] 46 + for aka in doc.get("alsoKnownAs", []): 47 + if aka.startswith("at://"): 48 + handle = aka[5:] 49 + if is_valid_handle(handle): 50 + handles.append(handle) 51 + return handles 52 + 53 + 54 + def handle_from_doc(doc: dict[str, list[str]]) -> str | None: 55 + """Return the first handle inside the DID document.""" 56 + handles = handles_from_doc(doc) 57 + try: 58 + return handles[0] 59 + except IndexError: 60 + return None 61 + 62 + 63 + def resolve_did_from_handle(handle: str, reload: bool = False) -> str | None: 64 + """Returns the DID for a given handle""" 65 + 66 + if handle in dids and not reload: 67 + print(f"returning cached did for {handle}") 68 + return dids[handle] 69 + 70 + answer = resolve_dns(f"_atproto.{handle}", "TXT") 71 + for record in answer: 72 + value = str(record).replace('"', "") 73 + if value.startswith("did="): 74 + did = value[4:] 75 + if is_valid_did(did): 76 + return did 77 + 78 + return None 79 + 80 + 81 + def pds_endpoint_from_doc(doc: dict[str, list[dict[str, str]]]) -> str | None: 82 + """Returns the PDS endpoint from the DID document.""" 83 + 84 + for service in doc.get("service", []): 85 + if service.get("id") == "#atproto_pds": 86 + return service.get("serviceEndpoint") 87 + return None 88 + 89 + 90 + def resolve_pds_from_did(did: DID, reload: bool = False) -> PdsUrl | None: 91 + if did in pdss and not reload: 92 + print(f"returning cached pds for {did}") 93 + return pdss[did] 94 + 95 + doc = resolve_doc_from_did(did) 96 + if doc is None: 97 + return None 98 + pds = doc["service"][0]["serviceEndpoint"] 99 + pdss[did] = pds 100 + print(f"caching pds {pds} for {did}") 101 + return pds 102 + 103 + 104 + def resolve_doc_from_did( 105 + did: DID, 106 + directory: str = PLC_DIRECTORY, 107 + ) -> dict[str, Any] | None: 108 + if did.startswith("did:plc:"): 109 + response = requests.get(f"{directory}/{did}") 110 + if response.ok: 111 + return response.json() 112 + return None 113 + 114 + if did.startswith("did:web:"): 115 + # TODO: resolve did:web 116 + return None 117 + 118 + return None 119 + 120 + 121 + def resolve_authserver_from_pds( 122 + pds_url: PdsUrl, 123 + reload: bool = False, 124 + ) -> AuthserverUrl | None: 125 + """Returns the authserver URL for the PDS.""" 126 + 127 + if pds_url in authservers and not reload: 128 + print(f"returning cached authserver for PDS {pds_url}") 129 + return authservers[pds_url] 130 + 131 + assert is_safe_url(pds_url) 132 + endpoint = f"{pds_url}/.well-known/oauth-protected-resource" 133 + response = requests.get(endpoint) 134 + if response.status_code != 200: 135 + return None 136 + parsed: dict[str, list[str]] = response.json() 137 + authserver_url = parsed["authorization_servers"][0] 138 + print(f"caching authserver {authserver_url} for PDS {pds_url}") 139 + authservers[pds_url] = authserver_url 140 + return authserver_url 141 + 142 + 143 + def resolve_authserver_meta(authserver_url: str) -> dict[str, str] | None: 144 + """Returns metadata from the authserver""" 145 + assert is_safe_url(authserver_url) 146 + endpoint = f"{authserver_url}/.well-known/oauth-authorization-server" 147 + meta = http_get_json(endpoint) 148 + assert is_valid_authserver_meta(meta, authserver_url) 149 + return meta 150 + 151 + 152 + def http_get_json(url: str) -> Any | None: 153 + response = requests.get(url) 154 + if response.ok: 155 + return response.json() 156 + return None 157 + 158 + 159 + def http_get(url: str) -> str | None: 160 + response = requests.get(url) 161 + if response.ok: 162 + return response.text 163 + return None
+134
src/atproto2/atproto_identity.py
··· 1 + import re 2 + import sys 3 + import requests 4 + import dns.resolver 5 + from typing import Optional, Tuple 6 + 7 + from .atproto_security import hardened_http 8 + 9 + HANDLE_REGEX = r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$" 10 + DID_REGEX = r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$" 11 + 12 + 13 + def is_valid_handle(handle: str) -> bool: 14 + return re.match(HANDLE_REGEX, handle) is not None 15 + 16 + 17 + def is_valid_did(did: str) -> bool: 18 + return re.match(DID_REGEX, did) is not None 19 + 20 + 21 + def handle_from_doc(doc: dict) -> Optional[str]: 22 + for aka in doc.get("alsoKnownAs", []): 23 + if aka.startswith("at://"): 24 + handle = aka[5:] 25 + if is_valid_handle(handle): 26 + return handle 27 + return None 28 + 29 + 30 + # resolves an identity (handle or DID) to a DID, handle, and DID document. verifies handle bi-directionally. 31 + def resolve_identity(atid: str) -> Tuple[str, str, dict]: 32 + if is_valid_handle(atid): 33 + handle = atid 34 + did = resolve_handle(handle) 35 + if not did: 36 + raise Exception("Failed to resolve handle: " + handle) 37 + doc = resolve_did(did) 38 + if not doc: 39 + raise Exception("Failed to resolve DID: " + did) 40 + doc_handle = handle_from_doc(doc) 41 + if not doc_handle or doc_handle != handle: 42 + raise Exception("Handle did not match DID: " + handle) 43 + return did, handle, doc 44 + if is_valid_did(atid): 45 + did = atid 46 + doc = resolve_did(did) 47 + if not doc: 48 + raise Exception("Failed to resolve DID: " + did) 49 + handle = handle_from_doc(doc) 50 + if not handle: 51 + raise Exception("Handle did not match DID: " + handle) 52 + if resolve_handle(handle) != did: 53 + raise Exception("Handle did not match DID: " + handle) 54 + return did, handle, doc 55 + 56 + raise Exception("identifier not a handle or DID: " + atid) 57 + 58 + 59 + def resolve_handle(handle: str) -> Optional[str]: 60 + # first try TXT record 61 + try: 62 + for record in dns.resolver.resolve(f"_atproto.{handle}", "TXT"): 63 + val = record.to_text().replace('"', "") 64 + if val.startswith("did="): 65 + val = val[4:] 66 + if is_valid_did(val): 67 + return val 68 + except Exception: 69 + pass 70 + 71 + # then try HTTP well-known 72 + # IMPORTANT: 'handle' domain is untrusted user input. SSRF mitigations are necessary 73 + try: 74 + with hardened_http.get_session() as sess: 75 + resp = sess.get(f"https://{handle}/.well-known/atproto-did") 76 + except Exception: 77 + return None 78 + 79 + if resp.status_code != 200: 80 + return None 81 + did = resp.text.split()[0] 82 + if is_valid_did(did): 83 + return did 84 + return None 85 + 86 + 87 + def resolve_did(did: str) -> Optional[dict]: 88 + if did.startswith("did:plc:"): 89 + # NOTE: 'did' is untrusted input, but has been validated by regex by this point 90 + resp = requests.get(f"https://plc.directory/{did}") 91 + if resp.status_code != 200: 92 + return None 93 + return resp.json() 94 + 95 + if did.startswith("did:web:"): 96 + domain = did[8:] 97 + # IMPORTANT: domain is untrusted input. SSRF mitigations are necessary 98 + # "handle" validation works to check that domain is a simple hostname 99 + assert is_valid_handle(domain) 100 + try: 101 + with hardened_http.get_session() as sess: 102 + resp = sess.get(f"https://{domain}/.well-known/did.json") 103 + except requests.exceptions.ConnectionError: 104 + return None 105 + if resp.status_code != 200: 106 + return None 107 + return resp.json() 108 + raise ValueError("unsupported DID type") 109 + 110 + 111 + def pds_endpoint(doc: dict) -> str: 112 + for svc in doc["service"]: 113 + if svc["id"] == "#atproto_pds": 114 + return svc["serviceEndpoint"] 115 + raise Exception("PDS endpoint not found in DID document") 116 + 117 + 118 + if __name__ == "__main__": 119 + assert is_valid_did("did:web:example.com") 120 + assert is_valid_did("did:plc:abc123") 121 + assert is_valid_did("") is False 122 + assert is_valid_did("did:asdfasdf") is False 123 + handle = sys.argv[1] 124 + if not is_valid_handle(handle): 125 + print("invalid handle!") 126 + sys.exit(-1) 127 + assert handle is not None 128 + did = resolve_handle(handle) 129 + print(f"DID: {did}") 130 + assert did is not None 131 + doc = resolve_did(did) 132 + print(doc) 133 + resolve_identity(handle) 134 + resolve_identity(did)
+394
src/atproto2/atproto_oauth.py
··· 1 + from urllib.parse import urlparse 2 + from typing import Any 3 + import time 4 + import json 5 + from authlib.jose import JsonWebKey, Key 6 + from authlib.common.security import generate_token 7 + from authlib.jose import jwt 8 + from authlib.oauth2.rfc7636 import create_s256_code_challenge 9 + from requests import Response 10 + 11 + from .atproto_security import is_safe_url, hardened_http 12 + 13 + 14 + # Checks an Authorization Server metadata response against atproto OAuth requirements 15 + def is_valid_authserver_meta(obj: dict[str, Any] | None, url: str) -> bool: 16 + if obj is None: 17 + return False 18 + fetch_url = urlparse(url) 19 + issuer_url = urlparse(obj["issuer"]) 20 + assert issuer_url.hostname == fetch_url.hostname 21 + assert issuer_url.scheme == "https" 22 + assert issuer_url.port is None 23 + assert issuer_url.path in ["", "/"] 24 + assert issuer_url.params == "" 25 + assert issuer_url.fragment == "" 26 + 27 + assert "code" in obj["response_types_supported"] 28 + assert "authorization_code" in obj["grant_types_supported"] 29 + assert "refresh_token" in obj["grant_types_supported"] 30 + assert "S256" in obj["code_challenge_methods_supported"] 31 + assert "none" in obj["token_endpoint_auth_methods_supported"] 32 + assert "private_key_jwt" in obj["token_endpoint_auth_methods_supported"] 33 + assert "ES256" in obj["token_endpoint_auth_signing_alg_values_supported"] 34 + assert "atproto" in obj["scopes_supported"] 35 + assert obj["authorization_response_iss_parameter_supported"] is True 36 + assert obj["pushed_authorization_request_endpoint"] is not None 37 + assert obj["require_pushed_authorization_requests"] is True 38 + assert "ES256" in obj["dpop_signing_alg_values_supported"] 39 + if "require_request_uri_registration" in obj: 40 + assert obj["require_request_uri_registration"] is True 41 + assert obj["client_id_metadata_document_supported"] is True 42 + 43 + return True 44 + 45 + 46 + # Takes a Resource Server (PDS) URL, and tries to resolve it to an Authorization Server host/origin 47 + def resolve_pds_authserver(url: str) -> str: 48 + # IMPORTANT: PDS endpoint URL is untrusted input, SSRF mitigations are needed 49 + assert is_safe_url(url) 50 + with hardened_http.get_session() as sess: 51 + resp = sess.get(f"{url}/.well-known/oauth-protected-resource") 52 + resp.raise_for_status() 53 + # Additionally check that status is exactly 200 (not just 2xx) 54 + assert resp.status_code == 200 55 + authserver_url = resp.json()["authorization_servers"][0] 56 + return authserver_url 57 + 58 + 59 + # Does an HTTP GET for Authorization Server (entryway) metadata, verify the contents, and return the metadata as a dict 60 + # DEPRECATED: use atproto2.resolve_authserver_meta 61 + def fetch_authserver_meta(url: str) -> dict[str, Any]: 62 + # IMPORTANT: Authorization Server URL is untrusted input, SSRF mitigations are needed 63 + assert is_safe_url(url) 64 + with hardened_http.get_session() as sess: 65 + resp = sess.get(f"{url}/.well-known/oauth-authorization-server") 66 + resp.raise_for_status() 67 + 68 + authserver_meta = resp.json() 69 + # print("Auth Server Metadata: " + json.dumps(authserver_meta, indent=2)) 70 + assert is_valid_authserver_meta(authserver_meta, url) 71 + return authserver_meta 72 + 73 + 74 + def client_assertion_jwt( 75 + client_id: str, 76 + authserver_url: str, 77 + client_secret_jwk: Key, 78 + ) -> str: 79 + client_assertion = jwt.encode( 80 + {"alg": "ES256", "kid": client_secret_jwk["kid"]}, 81 + { 82 + "iss": client_id, 83 + "sub": client_id, 84 + "aud": authserver_url, 85 + "jti": generate_token(), 86 + "iat": int(time.time()), 87 + }, 88 + client_secret_jwk, 89 + ).decode("utf-8") 90 + return client_assertion 91 + 92 + 93 + def authserver_dpop_jwt( 94 + method: str, 95 + url: str, 96 + nonce: str, 97 + dpop_private_jwk: Key, 98 + ) -> str: 99 + dpop_pub_jwk = json.loads(dpop_private_jwk.as_json(is_private=False)) 100 + body = { 101 + "jti": generate_token(), 102 + "htm": method, 103 + "htu": url, 104 + "iat": int(time.time()), 105 + "exp": int(time.time()) + 30, 106 + } 107 + if nonce: 108 + body["nonce"] = nonce 109 + dpop_proof = jwt.encode( 110 + {"typ": "dpop+jwt", "alg": "ES256", "jwk": dpop_pub_jwk}, 111 + body, 112 + dpop_private_jwk, 113 + ).decode("utf-8") 114 + return dpop_proof 115 + 116 + 117 + # Prepares and sends a pushed auth request (PAR) via HTTP POST to the Authorization Server. 118 + # Returns "state" id HTTP response on success, without checking HTTP response status 119 + def send_par_auth_request( 120 + authserver_url: str, 121 + authserver_meta: dict[str, str], 122 + login_hint: str, 123 + client_id: str, 124 + redirect_uri: str, 125 + scope: str, 126 + client_secret_jwk: Key, 127 + dpop_private_jwk: Key, 128 + ) -> tuple[str, str, str, Response]: 129 + par_url = authserver_meta["pushed_authorization_request_endpoint"] 130 + state = generate_token() 131 + pkce_verifier = generate_token(48) 132 + 133 + # Generate PKCE code_challenge, and use it for PAR request 134 + code_challenge: str = create_s256_code_challenge(pkce_verifier) 135 + code_challenge_method = "S256" 136 + 137 + # Self-signed JWT using the private key declared in client metadata JWKS (confidential client) 138 + client_assertion = client_assertion_jwt( 139 + client_id, authserver_url, client_secret_jwk 140 + ) 141 + 142 + # Create DPoP header JWT; we don't have a server Nonce yet 143 + dpop_authserver_nonce = "" 144 + dpop_proof = authserver_dpop_jwt( 145 + "POST", par_url, dpop_authserver_nonce, dpop_private_jwk 146 + ) 147 + 148 + par_body: dict[str, str] = { 149 + "response_type": "code", 150 + "code_challenge": code_challenge, 151 + "code_challenge_method": code_challenge_method, 152 + "client_id": client_id, 153 + "state": state, 154 + "redirect_uri": redirect_uri, 155 + "scope": scope, 156 + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 157 + "client_assertion": client_assertion, 158 + } 159 + if login_hint: 160 + par_body["login_hint"] = login_hint 161 + # print(par_body) 162 + 163 + # IMPORTANT: Pushed Authorization Request URL is untrusted input, SSRF mitigations are needed 164 + assert is_safe_url(par_url) 165 + with hardened_http.get_session() as sess: 166 + resp = sess.post( 167 + par_url, 168 + headers={ 169 + "Content-Type": "application/x-www-form-urlencoded", 170 + "DPoP": dpop_proof, 171 + }, 172 + data=par_body, 173 + ) 174 + 175 + # Handle DPoP missing/invalid nonce error by retrying with server-provided nonce 176 + if resp.status_code == 400 and resp.json()["error"] == "use_dpop_nonce": 177 + dpop_authserver_nonce = resp.headers["DPoP-Nonce"] 178 + print(f"retrying with new auth server DPoP nonce: {dpop_authserver_nonce}") 179 + dpop_proof = authserver_dpop_jwt( 180 + "POST", par_url, dpop_authserver_nonce, dpop_private_jwk 181 + ) 182 + with hardened_http.get_session() as sess: 183 + resp = sess.post( 184 + par_url, 185 + headers={ 186 + "Content-Type": "application/x-www-form-urlencoded", 187 + "DPoP": dpop_proof, 188 + }, 189 + data=par_body, 190 + ) 191 + 192 + return pkce_verifier, state, dpop_authserver_nonce, resp 193 + 194 + 195 + # Completes the auth flow by sending an initial auth token request. 196 + # Returns token response (dict) and DPoP nonce (str) 197 + def initial_token_request( 198 + auth_request: dict[str, str], 199 + code: str, 200 + app_url: str, 201 + client_secret_jwk: Key, 202 + ) -> tuple[dict[str, str], str]: 203 + authserver_url = auth_request["authserver_iss"] 204 + 205 + # Re-fetch server metadata 206 + authserver_meta = fetch_authserver_meta(authserver_url) 207 + 208 + # Construct auth token request fields 209 + client_id = f"{app_url}oauth/metadata" 210 + redirect_uri = f"{app_url}oauth/callback" 211 + 212 + # Self-signed JWT using the private key declared in client metadata JWKS (confidential client) 213 + client_assertion = client_assertion_jwt( 214 + client_id, authserver_url, client_secret_jwk 215 + ) 216 + 217 + params = { 218 + "client_id": client_id, 219 + "redirect_uri": redirect_uri, 220 + "grant_type": "authorization_code", 221 + "code": code, 222 + "code_verifier": auth_request["pkce_verifier"], 223 + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 224 + "client_assertion": client_assertion, 225 + } 226 + 227 + # Create DPoP header JWT, using the existing DPoP signing key for this account/session 228 + token_url = authserver_meta["token_endpoint"] 229 + dpop_private_jwk = JsonWebKey.import_key( 230 + json.loads(auth_request["dpop_private_jwk"]) 231 + ) 232 + dpop_authserver_nonce = auth_request["dpop_authserver_nonce"] 233 + dpop_proof = authserver_dpop_jwt( 234 + "POST", token_url, dpop_authserver_nonce, dpop_private_jwk 235 + ) 236 + 237 + # IMPORTANT: Token URL is untrusted input, SSRF mitigations are needed 238 + assert is_safe_url(token_url) 239 + with hardened_http.get_session() as sess: 240 + resp = sess.post(token_url, data=params, headers={"DPoP": dpop_proof}) 241 + 242 + # Handle DPoP missing/invalid nonce error by retrying with server-provided nonce 243 + if resp.status_code == 400 and resp.json()["error"] == "use_dpop_nonce": 244 + dpop_authserver_nonce = resp.headers["DPoP-Nonce"] 245 + print(f"retrying with new auth server DPoP nonce: {dpop_authserver_nonce}") 246 + # print(server_nonce) 247 + dpop_proof = authserver_dpop_jwt( 248 + "POST", token_url, dpop_authserver_nonce, dpop_private_jwk 249 + ) 250 + with hardened_http.get_session() as sess: 251 + resp = sess.post(token_url, data=params, headers={"DPoP": dpop_proof}) 252 + 253 + token_body = resp.json() 254 + print(token_body) 255 + 256 + resp.raise_for_status() 257 + 258 + # IMPORTANT: the 'sub' field must be verified against the original request by code calling this function. 259 + 260 + return token_body, dpop_authserver_nonce 261 + 262 + 263 + # Returns token response (dict) and DPoP nonce (str) 264 + def refresh_token_request( 265 + user: dict, 266 + app_url: str, 267 + client_secret_jwk: Key, 268 + ) -> tuple[dict[str, str], str]: 269 + authserver_url = user["authserver_iss"] 270 + 271 + # Re-fetch server metadata 272 + authserver_meta = fetch_authserver_meta(authserver_url) 273 + 274 + # Construct token request fields 275 + client_id = f"{app_url}oauth/metadata" 276 + 277 + # Self-signed JWT using the private key declared in client metadata JWKS (confidential client) 278 + client_assertion = client_assertion_jwt( 279 + client_id, authserver_url, client_secret_jwk 280 + ) 281 + 282 + params = { 283 + "client_id": client_id, 284 + "grant_type": "refresh_token", 285 + "refresh_token": user["refresh_token"], 286 + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 287 + "client_assertion": client_assertion, 288 + } 289 + 290 + # Create DPoP header JWT, using the existing DPoP signing key for this account/session 291 + token_url = authserver_meta["token_endpoint"] 292 + dpop_private_jwk = JsonWebKey.import_key(json.loads(user["dpop_private_jwk"])) 293 + dpop_authserver_nonce = user["dpop_authserver_nonce"] 294 + dpop_proof = authserver_dpop_jwt( 295 + "POST", token_url, dpop_authserver_nonce, dpop_private_jwk 296 + ) 297 + 298 + # IMPORTANT: Token URL is untrusted input, SSRF mitigations are needed 299 + assert is_safe_url(token_url) 300 + with hardened_http.get_session() as sess: 301 + resp = sess.post(token_url, data=params, headers={"DPoP": dpop_proof}) 302 + 303 + # Handle DPoP missing/invalid nonce error by retrying with server-provided nonce 304 + if resp.status_code == 400 and resp.json()["error"] == "use_dpop_nonce": 305 + dpop_authserver_nonce = resp.headers["DPoP-Nonce"] 306 + print(f"retrying with new auth server DPoP nonce: {dpop_authserver_nonce}") 307 + # print(server_nonce) 308 + dpop_proof = authserver_dpop_jwt( 309 + "POST", token_url, dpop_authserver_nonce, dpop_private_jwk 310 + ) 311 + with hardened_http.get_session() as sess: 312 + resp = sess.post(token_url, data=params, headers={"DPoP": dpop_proof}) 313 + 314 + if resp.status_code not in [200, 201]: 315 + print(f"Token Refresh Error: {resp.json()}") 316 + 317 + resp.raise_for_status() 318 + token_body = resp.json() 319 + 320 + return token_body, dpop_authserver_nonce 321 + 322 + 323 + def pds_dpop_jwt( 324 + method: str, 325 + url: str, 326 + access_token: str, 327 + nonce: str, 328 + dpop_private_jwk: Key, 329 + ) -> str: 330 + dpop_pub_jwk = json.loads(dpop_private_jwk.as_json(is_private=False)) 331 + body = { 332 + "iat": int(time.time()), 333 + "exp": int(time.time()) + 10, 334 + "jti": generate_token(), 335 + "htm": method, 336 + "htu": url, 337 + # PKCE S256 is same as DPoP ath hashing 338 + "ath": create_s256_code_challenge(access_token), 339 + } 340 + if nonce: 341 + body["nonce"] = nonce 342 + dpop_proof = jwt.encode( 343 + {"typ": "dpop+jwt", "alg": "ES256", "jwk": dpop_pub_jwk}, 344 + body, 345 + dpop_private_jwk, 346 + ).decode("utf-8") 347 + return dpop_proof 348 + 349 + 350 + # Helper to demonstrate making a request (HTTP GET or POST) to the user's PDS ("Resource Server" in OAuth terminology) using DPoP and access token. 351 + # This method returns a 'requests' reponse, without checking status code. 352 + def pds_authed_req(method: str, url: str, user: dict, db: Any, body=None) -> Any: 353 + dpop_private_jwk = JsonWebKey.import_key(json.loads(user["dpop_private_jwk"])) 354 + dpop_pds_nonce = user["dpop_pds_nonce"] 355 + access_token = user["access_token"] 356 + 357 + # Might need to retry request with a new nonce. 358 + for i in range(2): 359 + dpop_jwt = pds_dpop_jwt( 360 + "POST", 361 + url, 362 + access_token, 363 + dpop_pds_nonce, 364 + dpop_private_jwk, 365 + ) 366 + 367 + with hardened_http.get_session() as sess: 368 + resp = sess.post( 369 + url, 370 + headers={ 371 + "Authorization": f"DPoP {access_token}", 372 + "DPoP": dpop_jwt, 373 + }, 374 + json=body, 375 + ) 376 + 377 + # If we got a new server-provided DPoP nonce, store it in database and retry. 378 + # NOTE: the type of error might also be communicated in the `WWW-Authenticate` HTTP response header. 379 + if resp.status_code in [400, 401] and resp.json()["error"] == "use_dpop_nonce": 380 + # print(resp.headers) 381 + dpop_pds_nonce = resp.headers["DPoP-Nonce"] 382 + print(f"retrying with new PDS DPoP nonce: {dpop_pds_nonce}") 383 + # update session database with new nonce 384 + cur = db.cursor() 385 + cur.execute( 386 + "UPDATE oauth_session SET dpop_pds_nonce = ? WHERE did = ?;", 387 + [dpop_pds_nonce, user["did"]], 388 + ) 389 + db.commit() 390 + cur.close() 391 + continue 392 + break 393 + 394 + return resp
+41
src/atproto2/atproto_security.py
··· 1 + from urllib.parse import urlparse 2 + import requests_hardened 3 + 4 + 5 + # this is a crude/partial filter that looks at HTTPS URLs and checks if they seem "safe" for server-side requests (SSRF). This is only a partial mitigation, the actual HTTP client also needs to prevent other attacks and behaviors. 6 + # this isn't a fully complete or secure implementation 7 + def is_safe_url(url: str) -> bool: 8 + parts = urlparse(url) 9 + if not ( 10 + parts.scheme == "https" 11 + and parts.hostname is not None 12 + and parts.hostname == parts.netloc 13 + and parts.username is None 14 + and parts.password is None 15 + and parts.port is None 16 + ): 17 + return False 18 + 19 + segments = parts.hostname.split(".") 20 + if not ( 21 + len(segments) >= 2 22 + and segments[-1] not in ["local", "arpa", "internal", "localhost"] 23 + ): 24 + return False 25 + 26 + if segments[-1].isdigit(): 27 + return False 28 + 29 + return True 30 + 31 + 32 + # configures a "hardened" requests wrapper 33 + hardened_http = requests_hardened.Manager( 34 + requests_hardened.Config( 35 + default_timeout=(2, 10), 36 + never_redirect=True, 37 + ip_filter_enable=True, 38 + ip_filter_allow_loopback_ips=False, 39 + user_agent_override="AtprotoCookbookOAuthFlaskDemo", 40 + ) 41 + )
+6 -48
src/main.py
··· 6 6 from urllib import request as http_request 7 7 import json 8 8 9 + from .atproto2 import resolve_did_from_handle, resolve_pds_from_did 10 + from .oauth import oauth 11 + 9 12 app = Flask(__name__) 10 13 _ = app.config.from_prefixed_env() 14 + app.register_blueprint(oauth) 11 15 12 16 pdss: dict[str, str] = {} 13 17 dids: dict[str, str] = {} 14 18 links: dict[str, list[dict[str, str]]] = {} 15 19 profiles: dict[str, tuple[str, str]] = {} 16 20 17 - PLC_DIRECTORY = "https://plc.directory" 18 21 SCHEMA = "one.nauta" 19 22 20 23 ··· 187 190 return profile, from_bluesky 188 191 189 192 190 - def resolve_pds_from_did(did: str, reload: bool = False) -> str | None: 191 - if did in pdss and not reload: 192 - app.logger.debug(f"returning cached pds for {did}") 193 - return pdss[did] 194 - 195 - response = http_get(f"{PLC_DIRECTORY}/{did}") 196 - if response is None: 197 - return None 198 - parsed = json.loads(response) 199 - pds = parsed["service"][0]["serviceEndpoint"] 200 - pdss[did] = pds 201 - app.logger.debug(f"caching pds {pds} for {did}") 202 - return pds 203 - 204 - 205 - def resolve_did_from_handle(handle: str, reload: bool = False) -> str | None: 206 - if handle in dids and not reload: 207 - app.logger.debug(f"returning cached did for {handle}") 208 - return dids[handle] 209 - 210 - response = http_get(f"https://dns.google/resolve?name=_atproto.{handle}&type=TXT") 211 - if response is None: 212 - return None 213 - parsed = json.loads(response) 214 - answers = parsed["Answer"] 215 - if len(answers) < 1: 216 - return handle 217 - data: str = answers[0]["data"] 218 - if not data.startswith("did="): 219 - return handle 220 - did = data[4:] 221 - dids[handle] = did 222 - app.logger.debug(f"caching did {did} for {handle}") 223 - return did 224 - 225 - 226 193 def get_record(pds: str, repo: str, collection: str, record: str) -> str | None: 227 194 response = http_get( 228 195 f"{pds}/xrpc/com.atproto.repo.getRecord?repo={repo}&collection={collection}&rkey={record}" ··· 263 230 @app.post("/auth/login") 264 231 def auth_login(): 265 232 handle = request.form.get("handle") 266 - password = request.form.get("password") 267 - if not handle or not password: 233 + if not handle: 268 234 return redirect("/login", 303) 269 235 if handle.startswith("@"): 270 236 handle = handle[1:] 271 - session_string: str | None 272 - try: 273 - client = Client() 274 - _ = client.login(handle, password) 275 - session_string = client.export_session_string() 276 - except AtProtocolError: 277 - return redirect("/login", 303) 278 - session["session"] = session_string 279 - return redirect("/editor", code=303) 237 + return redirect(app.url_for("oauth.oauth_start", username=handle))
+185
src/oauth.py
··· 1 + from authlib.jose import JsonWebKey, Key 2 + from flask import Blueprint, current_app, jsonify, redirect, request, session, url_for 3 + from urllib.parse import urlencode 4 + 5 + import json 6 + 7 + from .atproto2.atproto_oauth import initial_token_request, send_par_auth_request 8 + 9 + from .atproto2.atproto_security import is_safe_url 10 + 11 + from .atproto2 import ( 12 + pds_endpoint_from_doc, 13 + resolve_authserver_from_pds, 14 + resolve_authserver_meta, 15 + resolve_identity, 16 + ) 17 + 18 + oauth = Blueprint("oauth", __name__, url_prefix="/oauth") 19 + 20 + 21 + oauth_auth_requests: dict[str, dict[str, str]] = {} 22 + oauth_session: dict[str, dict[str, str]] = {} 23 + 24 + 25 + @oauth.get("/home") 26 + def oauth_home(): 27 + user_did = session["user_did"] 28 + user_handle = session["user_handle"] 29 + return f"{user_did} {user_handle}" 30 + 31 + 32 + @oauth.get("/start") 33 + def oauth_start(): 34 + # Identity 35 + username = request.args.get("username") 36 + if not username: 37 + return "missing ?username", 400 38 + login_hint = username 39 + identity = resolve_identity(username) 40 + if identity is None: 41 + return "couldnt resolve identity", 500 42 + did, handle, doc = identity 43 + pds_url = pds_endpoint_from_doc(doc) 44 + if not pds_url: 45 + return "pds not found", 404 46 + current_app.logger.debug(f"account PDS: {pds_url}") 47 + authserver_url = resolve_authserver_from_pds(pds_url) 48 + if not authserver_url: 49 + return "authserver not found", 404 50 + current_app.logger.debug(f"Authserver: {authserver_url}") 51 + 52 + assert is_safe_url(authserver_url) 53 + authserver_meta = resolve_authserver_meta(authserver_url) 54 + if not authserver_meta: 55 + return "no authserver meta", 404 56 + 57 + # Auth 58 + dpop_private_jwk: Key = JsonWebKey.generate_key("EC", "P-256", is_private=True) 59 + scope = "atproto transition:generic" 60 + 61 + app_url = request.url_root.replace("http://", "https://") 62 + redirect_uri = f"{app_url}oauth/callback" 63 + client_id = f"{app_url}oauth/metadata" 64 + 65 + CLIENT_SECRET_JWK = JsonWebKey.import_key(current_app.config["CLIENT_SECRET_JWK"]) 66 + 67 + pkce_verifier, state, dpop_authserver_nonce, resp = send_par_auth_request( 68 + authserver_url, 69 + authserver_meta, 70 + login_hint, 71 + client_id, 72 + redirect_uri, 73 + scope, 74 + CLIENT_SECRET_JWK, 75 + dpop_private_jwk, 76 + ) 77 + if resp.status_code == 400: 78 + print(f"PAR HTTP 400: {resp.json()}") 79 + resp.raise_for_status() 80 + 81 + par_request_uri = resp.json()["request_uri"] 82 + current_app.logger.debug(f"saving oauth_auth_request to DB state={state}") 83 + oauth_auth_requests[state] = { 84 + "authserver_iss": authserver_meta["issuer"], 85 + "did": did, 86 + "handle": handle, 87 + "pds_url": pds_url, 88 + "pkce_verifier": pkce_verifier, 89 + "scope": scope, 90 + "dpop_authserver_nonce": dpop_authserver_nonce, 91 + "dpop_private_jwk": dpop_private_jwk.as_json(is_private=True), 92 + } 93 + 94 + auth_url = authserver_meta["authorization_endpoint"] 95 + assert is_safe_url(auth_url) 96 + qparam = urlencode({"client_id": client_id, "request_uri": par_request_uri}) 97 + return redirect(f"{auth_url}?{qparam}") 98 + 99 + 100 + @oauth.get("/callback") 101 + def oauth_callback(): 102 + state = request.args["state"] 103 + authserver_iss = request.args["iss"] 104 + authorization_code = request.args["code"] 105 + 106 + auth_request = oauth_auth_requests.get(state) 107 + if auth_request is None: 108 + return redirect(url_for("oauth.oauth_home"), 303) 109 + 110 + current_app.logger.debug(f"Deleting auth request for state={state}") 111 + _ = oauth_auth_requests.pop(state) 112 + 113 + assert auth_request["authserver_iss"] == authserver_iss 114 + # assert state ???? 115 + 116 + app_url = request.url_root.replace("http://", "https://") 117 + CLIENT_SECRET_JWK = JsonWebKey.import_key(current_app.config["CLIENT_SECRET_JWK"]) 118 + tokens, dpop_authserver_nonce = initial_token_request( 119 + auth_request, 120 + authorization_code, 121 + app_url, 122 + CLIENT_SECRET_JWK, 123 + ) 124 + 125 + row = auth_request 126 + 127 + did = auth_request["did"] 128 + if row["did"]: 129 + # If we started with an account identifier, this is simple 130 + did, handle, pds_url = row["did"], row["handle"], row["pds_url"] 131 + assert tokens["sub"] == did 132 + else: 133 + # we started with auth server URL 134 + raise Exception() 135 + 136 + assert row["scope"] == tokens["scope"] 137 + 138 + oauth_session[did] = { 139 + "did": did, 140 + "handle": handle, 141 + "pds_url": pds_url, 142 + "authserver_iss": authserver_iss, 143 + "access_token": tokens["access_token"], 144 + "refresh_token": tokens["refresh_token"], 145 + "dpop_authserver_nonce": dpop_authserver_nonce, 146 + "dpop_private_jwk": auth_request["dpop_private_jwk"], 147 + } 148 + 149 + current_app.logger.debug("storing user did and handle") 150 + 151 + session["user_did"] = did 152 + session["user_handle"] = auth_request["handle"] 153 + 154 + return redirect(url_for("oauth.oauth_home")) 155 + 156 + 157 + @oauth.get("/metadata") 158 + def oauth_metadata(): 159 + host = request.host 160 + callback_endpoint = url_for("oauth.oauth_callback") 161 + metadata_endpoint = url_for("oauth.oauth_metadata") 162 + jwks_endpoint = url_for("oauth.oauth_jwks") 163 + return jsonify( 164 + { 165 + "client_id": f"https://{host}{metadata_endpoint}", 166 + "application_type": "web", 167 + "grant_types": ["authorization_code", "refresh_token"], 168 + "scope": "atproto transition:generic", 169 + "response_types": ["code"], 170 + "redirect_uris": [ 171 + f"https://{host}{callback_endpoint}", 172 + ], 173 + "dpop_bound_access_tokens": True, 174 + "token_endpoint_auth_method": "private_key_jwt", 175 + "token_endpoint_auth_signing_alg": "ES256", 176 + "jwks_uri": f"https://{host}{jwks_endpoint}", 177 + } 178 + ) 179 + 180 + 181 + @oauth.get("/jwks") 182 + def oauth_jwks(): 183 + CLIENT_SECRET_JWK = JsonWebKey.import_key(current_app.config["CLIENT_SECRET_JWK"]) 184 + CLIENT_PUB_JWK = json.loads(CLIENT_SECRET_JWK.as_json(is_private=False)) 185 + return jsonify({"keys": [CLIENT_PUB_JWK]})
-4
src/templates/login.html
··· 18 18 <span>handle</span> 19 19 <input type="text" name="handle" required /> 20 20 </label> 21 - <label> 22 - <span>app password</span> 23 - <input type="password" name="password" required /> 24 - </label> 25 21 <input type="submit" value="log in" /> 26 22 </form> 27 23 <footer>
+576
uv.lock
··· 1 + version = 1 2 + revision = 3 3 + requires-python = ">=3.13" 4 + 5 + [[package]] 6 + name = "annotated-types" 7 + version = "0.7.0" 8 + source = { registry = "https://pypi.org/simple" } 9 + sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } 10 + wheels = [ 11 + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, 12 + ] 13 + 14 + [[package]] 15 + name = "anyio" 16 + version = "4.11.0" 17 + source = { registry = "https://pypi.org/simple" } 18 + dependencies = [ 19 + { name = "idna" }, 20 + { name = "sniffio" }, 21 + ] 22 + sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } 23 + wheels = [ 24 + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, 25 + ] 26 + 27 + [[package]] 28 + name = "atproto" 29 + version = "0.0.62" 30 + source = { registry = "https://pypi.org/simple" } 31 + dependencies = [ 32 + { name = "click" }, 33 + { name = "cryptography" }, 34 + { name = "dnspython" }, 35 + { name = "httpx" }, 36 + { name = "libipld" }, 37 + { name = "pydantic" }, 38 + { name = "typing-extensions" }, 39 + { name = "websockets" }, 40 + ] 41 + sdist = { url = "https://files.pythonhosted.org/packages/ca/5a/f930010d7fcc53eca2138337ac9937dbaddf8d7dd9dcd40025dce49a885a/atproto-0.0.62.tar.gz", hash = "sha256:1aaf4555ca1b318b003ad3e9e46d141e9deb5a954360a743a5aa873cd8f17b93", size = 205366, upload-time = "2025-08-17T13:53:08.631Z" } 42 + wheels = [ 43 + { url = "https://files.pythonhosted.org/packages/db/0a/10c572949b9351a8b8d1532abc5f47ff6d2a675d1604e45673447bf17edb/atproto-0.0.62-py3-none-any.whl", hash = "sha256:d655d37bbf00659bcb6f166e00806096e083047bc91a8b90a260c5f2e6cab250", size = 416198, upload-time = "2025-08-17T13:53:06.7Z" }, 44 + ] 45 + 46 + [[package]] 47 + name = "authlib" 48 + version = "1.6.5" 49 + source = { registry = "https://pypi.org/simple" } 50 + dependencies = [ 51 + { name = "cryptography" }, 52 + ] 53 + sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" } 54 + wheels = [ 55 + { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" }, 56 + ] 57 + 58 + [[package]] 59 + name = "blinker" 60 + version = "1.9.0" 61 + source = { registry = "https://pypi.org/simple" } 62 + sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } 63 + wheels = [ 64 + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, 65 + ] 66 + 67 + [[package]] 68 + name = "certifi" 69 + version = "2025.10.5" 70 + source = { registry = "https://pypi.org/simple" } 71 + sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } 72 + wheels = [ 73 + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, 74 + ] 75 + 76 + [[package]] 77 + name = "cffi" 78 + version = "2.0.0" 79 + source = { registry = "https://pypi.org/simple" } 80 + dependencies = [ 81 + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, 82 + ] 83 + sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } 84 + wheels = [ 85 + { 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, upload-time = "2025-09-08T23:23:00.879Z" }, 86 + { 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, upload-time = "2025-09-08T23:23:02.231Z" }, 87 + { 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, upload-time = "2025-09-08T23:23:03.472Z" }, 88 + { 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, upload-time = "2025-09-08T23:23:04.792Z" }, 89 + { 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, upload-time = "2025-09-08T23:23:06.127Z" }, 90 + { 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, upload-time = "2025-09-08T23:23:07.753Z" }, 91 + { 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, upload-time = "2025-09-08T23:23:09.648Z" }, 92 + { 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, upload-time = "2025-09-08T23:23:10.928Z" }, 93 + { 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, upload-time = "2025-09-08T23:23:12.42Z" }, 94 + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, 95 + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, 96 + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, 97 + { 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, upload-time = "2025-09-08T23:23:18.087Z" }, 98 + { 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, upload-time = "2025-09-08T23:23:19.622Z" }, 99 + { 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, upload-time = "2025-09-08T23:23:20.853Z" }, 100 + { 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, upload-time = "2025-09-08T23:23:22.08Z" }, 101 + { 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, upload-time = "2025-09-08T23:23:23.314Z" }, 102 + { 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, upload-time = "2025-09-08T23:23:24.541Z" }, 103 + { 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, upload-time = "2025-09-08T23:23:26.143Z" }, 104 + { 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, upload-time = "2025-09-08T23:23:27.873Z" }, 105 + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, 106 + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, 107 + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, 108 + { 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, upload-time = "2025-09-08T23:23:29.347Z" }, 109 + { 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, upload-time = "2025-09-08T23:23:30.63Z" }, 110 + { 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, upload-time = "2025-09-08T23:23:31.91Z" }, 111 + { 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, upload-time = "2025-09-08T23:23:33.214Z" }, 112 + { 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, upload-time = "2025-09-08T23:23:34.495Z" }, 113 + { 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, upload-time = "2025-09-08T23:23:36.096Z" }, 114 + { 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, upload-time = "2025-09-08T23:23:37.328Z" }, 115 + { 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, upload-time = "2025-09-08T23:23:38.945Z" }, 116 + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, 117 + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, 118 + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, 119 + ] 120 + 121 + [[package]] 122 + name = "charset-normalizer" 123 + version = "3.4.3" 124 + source = { registry = "https://pypi.org/simple" } 125 + sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } 126 + wheels = [ 127 + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, 128 + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, 129 + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, 130 + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, 131 + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, 132 + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, 133 + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, 134 + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, 135 + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, 136 + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, 137 + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, 138 + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, 139 + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, 140 + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, 141 + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, 142 + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, 143 + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, 144 + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, 145 + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, 146 + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, 147 + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, 148 + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, 149 + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, 150 + ] 151 + 152 + [[package]] 153 + name = "click" 154 + version = "8.3.0" 155 + source = { registry = "https://pypi.org/simple" } 156 + dependencies = [ 157 + { name = "colorama", marker = "sys_platform == 'win32'" }, 158 + ] 159 + sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } 160 + wheels = [ 161 + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, 162 + ] 163 + 164 + [[package]] 165 + name = "colorama" 166 + version = "0.4.6" 167 + source = { registry = "https://pypi.org/simple" } 168 + 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" } 169 + wheels = [ 170 + { 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" }, 171 + ] 172 + 173 + [[package]] 174 + name = "cryptography" 175 + version = "45.0.7" 176 + source = { registry = "https://pypi.org/simple" } 177 + dependencies = [ 178 + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, 179 + ] 180 + sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } 181 + wheels = [ 182 + { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" }, 183 + { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" }, 184 + { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" }, 185 + { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" }, 186 + { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" }, 187 + { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" }, 188 + { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" }, 189 + { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" }, 190 + { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" }, 191 + { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" }, 192 + { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" }, 193 + { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" }, 194 + { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" }, 195 + { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" }, 196 + { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" }, 197 + { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" }, 198 + { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" }, 199 + { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" }, 200 + { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" }, 201 + { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" }, 202 + { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" }, 203 + { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" }, 204 + { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" }, 205 + { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" }, 206 + ] 207 + 208 + [[package]] 209 + name = "dnspython" 210 + version = "2.8.0" 211 + source = { registry = "https://pypi.org/simple" } 212 + sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } 213 + wheels = [ 214 + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, 215 + ] 216 + 217 + [[package]] 218 + name = "flask" 219 + version = "3.1.2" 220 + source = { registry = "https://pypi.org/simple" } 221 + dependencies = [ 222 + { name = "blinker" }, 223 + { name = "click" }, 224 + { name = "itsdangerous" }, 225 + { name = "jinja2" }, 226 + { name = "markupsafe" }, 227 + { name = "werkzeug" }, 228 + ] 229 + sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } 230 + wheels = [ 231 + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, 232 + ] 233 + 234 + [package.optional-dependencies] 235 + dotenv = [ 236 + { name = "python-dotenv" }, 237 + ] 238 + 239 + [[package]] 240 + name = "h11" 241 + version = "0.16.0" 242 + source = { registry = "https://pypi.org/simple" } 243 + sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } 244 + wheels = [ 245 + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, 246 + ] 247 + 248 + [[package]] 249 + name = "httpcore" 250 + version = "1.0.9" 251 + source = { registry = "https://pypi.org/simple" } 252 + dependencies = [ 253 + { name = "certifi" }, 254 + { name = "h11" }, 255 + ] 256 + sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 257 + wheels = [ 258 + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 259 + ] 260 + 261 + [[package]] 262 + name = "httpx" 263 + version = "0.28.1" 264 + source = { registry = "https://pypi.org/simple" } 265 + dependencies = [ 266 + { name = "anyio" }, 267 + { name = "certifi" }, 268 + { name = "httpcore" }, 269 + { name = "idna" }, 270 + ] 271 + sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 272 + wheels = [ 273 + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 274 + ] 275 + 276 + [[package]] 277 + name = "idna" 278 + version = "3.10" 279 + source = { registry = "https://pypi.org/simple" } 280 + sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 281 + wheels = [ 282 + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, 283 + ] 284 + 285 + [[package]] 286 + name = "itsdangerous" 287 + version = "2.2.0" 288 + source = { registry = "https://pypi.org/simple" } 289 + sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } 290 + wheels = [ 291 + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, 292 + ] 293 + 294 + [[package]] 295 + name = "jinja2" 296 + version = "3.1.6" 297 + source = { registry = "https://pypi.org/simple" } 298 + dependencies = [ 299 + { name = "markupsafe" }, 300 + ] 301 + sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } 302 + wheels = [ 303 + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, 304 + ] 305 + 306 + [[package]] 307 + name = "libipld" 308 + version = "3.1.1" 309 + source = { registry = "https://pypi.org/simple" } 310 + sdist = { url = "https://files.pythonhosted.org/packages/84/ac/21f2b0f9848c9d99a87e3cc626e7af0fc24883911ec5d7578686cc2a09d1/libipld-3.1.1.tar.gz", hash = "sha256:4b9a9da0ea5d848e9fa12c700027619a1e37ecc1da39dbd1424c0e9062f29e44", size = 4380425, upload-time = "2025-06-24T23:12:51.395Z" } 311 + wheels = [ 312 + { url = "https://files.pythonhosted.org/packages/e5/25/52f27b9617efb0c2f60e71bbfd4f88167ca7acd3aed413999f16e22b3e54/libipld-3.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8cd7d7b8b2e0a6ab273b697259f291edbd7cb1b9200ed746a41dcd63fb52017a", size = 280260, upload-time = "2025-06-24T23:11:44.376Z" }, 313 + { url = "https://files.pythonhosted.org/packages/bb/14/123450261a35e869732ff610580df39a62164d9e0aab58334c182c9453f8/libipld-3.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0251c6daa8eceee2ce7dc4f03422f3f1acdd31b04ebda39cab5f8af3dae30943", size = 276684, upload-time = "2025-06-24T23:11:45.266Z" }, 314 + { url = "https://files.pythonhosted.org/packages/bd/3e/6dd2daf43ff735a3f53cbeaeac1edb3ba92fa2e48c64257800ede82442e6/libipld-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d4598b094286998f770f383eedbfc04c1018ec8ebe6746db0eff5b2059a484a", size = 297845, upload-time = "2025-06-24T23:11:46.143Z" }, 315 + { url = "https://files.pythonhosted.org/packages/83/23/e4f89d9bf854c58a5d6e2f2c667425669ed795956003b28de429b0740e0f/libipld-3.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7212411cbce495dfae24c2b6757a5c2f921797fe70ec0c026e1a2d19ae29e59a", size = 305200, upload-time = "2025-06-24T23:11:47.128Z" }, 316 + { url = "https://files.pythonhosted.org/packages/40/43/0b1e871275502e9799589d03a139730c0dfbb36d1922ab213b105ace59ee/libipld-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffc2f978adda8a8309b55510ceda9fe5dc2431d4ff202ff77d84eb57c77d072f", size = 332153, upload-time = "2025-06-24T23:11:48.437Z" }, 317 + { url = "https://files.pythonhosted.org/packages/94/18/5e9cff31d9450e98cc7b4025d1c90bde661ee099ea46cfcb1d8a893e6083/libipld-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99163cc7822abcb028c55860e5341c77200a3ae90f4c158c27e2118a07e8809d", size = 344391, upload-time = "2025-06-24T23:11:49.786Z" }, 318 + { url = "https://files.pythonhosted.org/packages/63/ca/4d938862912ab2f105710d1cc909ec65c71d0e63a90e3b494920c23a4383/libipld-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80f142cbd4fa89ef514a4dd43afbd4ed3c33ae7061f0e1e0763f7c1811dea389", size = 299448, upload-time = "2025-06-24T23:11:50.723Z" }, 319 + { url = "https://files.pythonhosted.org/packages/2a/08/f6020e53abe4c26d57fe29b001ba1a84b5b3ad2d618e135b82877e42b59a/libipld-3.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4596a6e2c5e81b14b1432f3a6120b1d785fc4f74402cef39accf0041999905e4", size = 324096, upload-time = "2025-06-24T23:11:51.646Z" }, 320 + { url = "https://files.pythonhosted.org/packages/df/0f/d3d9da8f1001e9856bc5cb171a838ca5102da7d959b870a0c5f5aa9ef82e/libipld-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0cd275603ab3cc2394d40455de6976f01b2d85b4095c074c0c1e2692013f5eaa", size = 477593, upload-time = "2025-06-24T23:11:52.565Z" }, 321 + { url = "https://files.pythonhosted.org/packages/59/df/57dcd84e55c02f74bb40a246dd849430994bbb476e91b05179d749993c9a/libipld-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:16c999b3af996865004ff2da8280d0c24b672d8a00f9e4cd3a468da8f5e63a5a", size = 470201, upload-time = "2025-06-24T23:11:53.544Z" }, 322 + { url = "https://files.pythonhosted.org/packages/80/af/aee0800b415b63dc5e259675c31a36d6c261afff8e288b56bc2867aa9310/libipld-3.1.1-cp313-cp313-win32.whl", hash = "sha256:5d34c40a27e8755f500277be5268a2f6b6f0d1e20599152d8a34cd34fb3f2700", size = 173730, upload-time = "2025-06-24T23:11:54.5Z" }, 323 + { url = "https://files.pythonhosted.org/packages/54/a3/7e447f27ee896f48332254bb38e1b6c1d3f24b13e5029977646de9408159/libipld-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:5edee5f2ea8183bb6a151f149c9798a4f1db69fe16307e860a84f8d41b53665a", size = 179409, upload-time = "2025-06-24T23:11:55.356Z" }, 324 + { url = "https://files.pythonhosted.org/packages/f2/0b/31d6097620c5cfaaaa0acb7760c29186029cd72c6ab81c537cc1ddfb34e5/libipld-3.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:7307876987d9e570dcaf17a15f0ba210f678b323860742d725cf6d8d8baeae1f", size = 169715, upload-time = "2025-06-24T23:11:56.41Z" }, 325 + ] 326 + 327 + [[package]] 328 + name = "ligo-at" 329 + version = "0.0.1" 330 + source = { virtual = "." } 331 + dependencies = [ 332 + { name = "atproto" }, 333 + { name = "authlib" }, 334 + { name = "dnspython" }, 335 + { name = "flask", extra = ["dotenv"] }, 336 + { name = "requests" }, 337 + { name = "requests-hardened" }, 338 + ] 339 + 340 + [package.metadata] 341 + requires-dist = [ 342 + { name = "atproto", specifier = ">=0.0.62" }, 343 + { name = "authlib", specifier = ">=1.3" }, 344 + { name = "dnspython", specifier = ">=2.8.0" }, 345 + { name = "flask", extras = ["dotenv"], specifier = ">=3.1.2" }, 346 + { name = "requests", specifier = ">=2.32" }, 347 + { name = "requests-hardened", specifier = ">=1.0.0b3" }, 348 + ] 349 + 350 + [[package]] 351 + name = "markupsafe" 352 + version = "3.0.3" 353 + source = { registry = "https://pypi.org/simple" } 354 + sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } 355 + wheels = [ 356 + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, 357 + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, 358 + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, 359 + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, 360 + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, 361 + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, 362 + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, 363 + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, 364 + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, 365 + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, 366 + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, 367 + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, 368 + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, 369 + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, 370 + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, 371 + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, 372 + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, 373 + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, 374 + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, 375 + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, 376 + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, 377 + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, 378 + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, 379 + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, 380 + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, 381 + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, 382 + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, 383 + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, 384 + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, 385 + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, 386 + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, 387 + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, 388 + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, 389 + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, 390 + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, 391 + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, 392 + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, 393 + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, 394 + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, 395 + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, 396 + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, 397 + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, 398 + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, 399 + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, 400 + ] 401 + 402 + [[package]] 403 + name = "pycparser" 404 + version = "2.23" 405 + source = { registry = "https://pypi.org/simple" } 406 + sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } 407 + wheels = [ 408 + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, 409 + ] 410 + 411 + [[package]] 412 + name = "pydantic" 413 + version = "2.12.0" 414 + source = { registry = "https://pypi.org/simple" } 415 + dependencies = [ 416 + { name = "annotated-types" }, 417 + { name = "pydantic-core" }, 418 + { name = "typing-extensions" }, 419 + { name = "typing-inspection" }, 420 + ] 421 + sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } 422 + wheels = [ 423 + { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, 424 + ] 425 + 426 + [[package]] 427 + name = "pydantic-core" 428 + version = "2.41.1" 429 + source = { registry = "https://pypi.org/simple" } 430 + dependencies = [ 431 + { name = "typing-extensions" }, 432 + ] 433 + sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } 434 + wheels = [ 435 + { url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf", size = 2105825, upload-time = "2025-10-06T21:10:51.719Z" }, 436 + { url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb", size = 1910126, upload-time = "2025-10-06T21:10:53.145Z" }, 437 + { url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669", size = 1961472, upload-time = "2025-10-06T21:10:55.754Z" }, 438 + { url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f", size = 2063230, upload-time = "2025-10-06T21:10:57.179Z" }, 439 + { url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4", size = 2229469, upload-time = "2025-10-06T21:10:59.409Z" }, 440 + { url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62", size = 2347986, upload-time = "2025-10-06T21:11:00.847Z" }, 441 + { url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014", size = 2072216, upload-time = "2025-10-06T21:11:02.43Z" }, 442 + { url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d", size = 2193047, upload-time = "2025-10-06T21:11:03.787Z" }, 443 + { url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f", size = 2140613, upload-time = "2025-10-06T21:11:05.607Z" }, 444 + { url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257", size = 2327641, upload-time = "2025-10-06T21:11:07.143Z" }, 445 + { url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32", size = 2318229, upload-time = "2025-10-06T21:11:08.73Z" }, 446 + { url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl", hash = "sha256:a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d", size = 1997911, upload-time = "2025-10-06T21:11:10.329Z" }, 447 + { url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b", size = 2034301, upload-time = "2025-10-06T21:11:12.113Z" }, 448 + { url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl", hash = "sha256:ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb", size = 1977238, upload-time = "2025-10-06T21:11:14.1Z" }, 449 + { url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc", size = 1875626, upload-time = "2025-10-06T21:11:15.69Z" }, 450 + { url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67", size = 2045708, upload-time = "2025-10-06T21:11:17.258Z" }, 451 + { url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl", hash = "sha256:555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795", size = 1997171, upload-time = "2025-10-06T21:11:18.822Z" }, 452 + { url = "https://files.pythonhosted.org/packages/41/12/cec246429ddfa2778d2d6301eca5362194dc8749ecb19e621f2f65b5090f/pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b", size = 2107836, upload-time = "2025-10-06T21:11:20.432Z" }, 453 + { url = "https://files.pythonhosted.org/packages/20/39/baba47f8d8b87081302498e610aefc37142ce6a1cc98b2ab6b931a162562/pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a", size = 1904449, upload-time = "2025-10-06T21:11:22.185Z" }, 454 + { url = "https://files.pythonhosted.org/packages/50/32/9a3d87cae2c75a5178334b10358d631bd094b916a00a5993382222dbfd92/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674", size = 1961750, upload-time = "2025-10-06T21:11:24.348Z" }, 455 + { url = "https://files.pythonhosted.org/packages/27/42/a96c9d793a04cf2a9773bff98003bb154087b94f5530a2ce6063ecfec583/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4", size = 2063305, upload-time = "2025-10-06T21:11:26.556Z" }, 456 + { url = "https://files.pythonhosted.org/packages/3e/8d/028c4b7d157a005b1f52c086e2d4b0067886b213c86220c1153398dbdf8f/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31", size = 2228959, upload-time = "2025-10-06T21:11:28.426Z" }, 457 + { url = "https://files.pythonhosted.org/packages/08/f7/ee64cda8fcc9ca3f4716e6357144f9ee71166775df582a1b6b738bf6da57/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706", size = 2345421, upload-time = "2025-10-06T21:11:30.226Z" }, 458 + { url = "https://files.pythonhosted.org/packages/13/c0/e8ec05f0f5ee7a3656973ad9cd3bc73204af99f6512c1a4562f6fb4b3f7d/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b", size = 2065288, upload-time = "2025-10-06T21:11:32.019Z" }, 459 + { url = "https://files.pythonhosted.org/packages/0a/25/d77a73ff24e2e4fcea64472f5e39b0402d836da9b08b5361a734d0153023/pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be", size = 2189759, upload-time = "2025-10-06T21:11:33.753Z" }, 460 + { url = "https://files.pythonhosted.org/packages/66/45/4a4ebaaae12a740552278d06fe71418c0f2869537a369a89c0e6723b341d/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04", size = 2140747, upload-time = "2025-10-06T21:11:35.781Z" }, 461 + { url = "https://files.pythonhosted.org/packages/da/6d/b727ce1022f143194a36593243ff244ed5a1eb3c9122296bf7e716aa37ba/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4", size = 2327416, upload-time = "2025-10-06T21:11:37.75Z" }, 462 + { url = "https://files.pythonhosted.org/packages/6f/8c/02df9d8506c427787059f87c6c7253435c6895e12472a652d9616ee0fc95/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8", size = 2318138, upload-time = "2025-10-06T21:11:39.463Z" }, 463 + { url = "https://files.pythonhosted.org/packages/98/67/0cf429a7d6802536941f430e6e3243f6d4b68f41eeea4b242372f1901794/pydantic_core-2.41.1-cp314-cp314-win32.whl", hash = "sha256:a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159", size = 1998429, upload-time = "2025-10-06T21:11:41.989Z" }, 464 + { url = "https://files.pythonhosted.org/packages/38/60/742fef93de5d085022d2302a6317a2b34dbfe15258e9396a535c8a100ae7/pydantic_core-2.41.1-cp314-cp314-win_amd64.whl", hash = "sha256:1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae", size = 2028870, upload-time = "2025-10-06T21:11:43.66Z" }, 465 + { url = "https://files.pythonhosted.org/packages/31/38/cdd8ccb8555ef7720bd7715899bd6cfbe3c29198332710e1b61b8f5dd8b8/pydantic_core-2.41.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9", size = 1974275, upload-time = "2025-10-06T21:11:45.476Z" }, 466 + { url = "https://files.pythonhosted.org/packages/e7/7e/8ac10ccb047dc0221aa2530ec3c7c05ab4656d4d4bd984ee85da7f3d5525/pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4", size = 1875124, upload-time = "2025-10-06T21:11:47.591Z" }, 467 + { url = "https://files.pythonhosted.org/packages/c3/e4/7d9791efeb9c7d97e7268f8d20e0da24d03438a7fa7163ab58f1073ba968/pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e", size = 2043075, upload-time = "2025-10-06T21:11:49.542Z" }, 468 + { url = "https://files.pythonhosted.org/packages/2d/c3/3f6e6b2342ac11ac8cd5cb56e24c7b14afa27c010e82a765ffa5f771884a/pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762", size = 1995341, upload-time = "2025-10-06T21:11:51.497Z" }, 469 + ] 470 + 471 + [[package]] 472 + name = "python-dotenv" 473 + version = "1.1.1" 474 + source = { registry = "https://pypi.org/simple" } 475 + sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } 476 + wheels = [ 477 + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, 478 + ] 479 + 480 + [[package]] 481 + name = "requests" 482 + version = "2.32.5" 483 + source = { registry = "https://pypi.org/simple" } 484 + dependencies = [ 485 + { name = "certifi" }, 486 + { name = "charset-normalizer" }, 487 + { name = "idna" }, 488 + { name = "urllib3" }, 489 + ] 490 + sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } 491 + wheels = [ 492 + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, 493 + ] 494 + 495 + [[package]] 496 + name = "requests-hardened" 497 + version = "1.2.0" 498 + source = { registry = "https://pypi.org/simple" } 499 + dependencies = [ 500 + { name = "requests" }, 501 + ] 502 + sdist = { url = "https://files.pythonhosted.org/packages/0d/ab/3206848b4657be7902bb10af5686f71da450d9135340ecd6ee80da718557/requests_hardened-1.2.0.tar.gz", hash = "sha256:24ff13c798a22afc3465c24ff955b003c81f605e2ec30cbdbd40f28389cfca72", size = 7254, upload-time = "2025-09-26T12:20:20.518Z" } 503 + wheels = [ 504 + { url = "https://files.pythonhosted.org/packages/8d/0e/b521e2034f0984b3a446009223e8ec67bfae5e3d4a11b0066951d2df6515/requests_hardened-1.2.0-py3-none-any.whl", hash = "sha256:7d70b38bbfdea3f1d27d9149a5967f8c350b3496d232b1d4b031b7d0f2590ba9", size = 9197, upload-time = "2025-09-26T12:20:19.126Z" }, 505 + ] 506 + 507 + [[package]] 508 + name = "sniffio" 509 + version = "1.3.1" 510 + source = { registry = "https://pypi.org/simple" } 511 + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } 512 + wheels = [ 513 + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, 514 + ] 515 + 516 + [[package]] 517 + name = "typing-extensions" 518 + version = "4.15.0" 519 + source = { registry = "https://pypi.org/simple" } 520 + sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 521 + wheels = [ 522 + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, 523 + ] 524 + 525 + [[package]] 526 + name = "typing-inspection" 527 + version = "0.4.2" 528 + source = { registry = "https://pypi.org/simple" } 529 + dependencies = [ 530 + { name = "typing-extensions" }, 531 + ] 532 + sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } 533 + wheels = [ 534 + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, 535 + ] 536 + 537 + [[package]] 538 + name = "urllib3" 539 + version = "2.5.0" 540 + source = { registry = "https://pypi.org/simple" } 541 + sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } 542 + wheels = [ 543 + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, 544 + ] 545 + 546 + [[package]] 547 + name = "websockets" 548 + version = "13.1" 549 + source = { registry = "https://pypi.org/simple" } 550 + sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549, upload-time = "2024-09-21T17:34:21.54Z" } 551 + wheels = [ 552 + { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828, upload-time = "2024-09-21T17:33:10.987Z" }, 553 + { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487, upload-time = "2024-09-21T17:33:12.153Z" }, 554 + { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721, upload-time = "2024-09-21T17:33:13.909Z" }, 555 + { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609, upload-time = "2024-09-21T17:33:14.967Z" }, 556 + { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556, upload-time = "2024-09-21T17:33:17.113Z" }, 557 + { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993, upload-time = "2024-09-21T17:33:18.168Z" }, 558 + { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360, upload-time = "2024-09-21T17:33:19.233Z" }, 559 + { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745, upload-time = "2024-09-21T17:33:20.361Z" }, 560 + { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732, upload-time = "2024-09-21T17:33:23.103Z" }, 561 + { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709, upload-time = "2024-09-21T17:33:24.196Z" }, 562 + { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144, upload-time = "2024-09-21T17:33:25.96Z" }, 563 + { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134, upload-time = "2024-09-21T17:34:19.904Z" }, 564 + ] 565 + 566 + [[package]] 567 + name = "werkzeug" 568 + version = "3.1.3" 569 + source = { registry = "https://pypi.org/simple" } 570 + dependencies = [ 571 + { name = "markupsafe" }, 572 + ] 573 + sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } 574 + wheels = [ 575 + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, 576 + ]