From 1c8233208e7e8527f78a880f64218dcf156cbe4a Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 09:35:37 -0700 Subject: [PATCH 1/8] feat(learning): invite-key classroom enrolment; deprecate enroll() (PBA-L6b-040 follow-up) Aligns ClassroomManager with ClassroomRegistry at citrate-chain d89200c2 (#222). The invite is a key pair: - create() and rotate_invite_code() generate or accept a 32-byte invite secret and register keccak256(abi.encodePacked(inviteKey)). - enroll_with_invite(secret) looks up the teacher, signs enrollmentDigest(teacher, student, commitment) locally (EIP-191; bound to the registry and the pinned chain id) and calls enrollWithInvite(inviteKey, signature). - enroll() is deprecated. It forwards an invite secret and refuses plain codes. The selector-parity test is re-pinned to d89200c2 and now also asserts that the removed selectors are not encoded. Existing tests that encoded the old text-code flow are updated to the key-pair flow. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- citrate_sdk/learning.py | 131 +++++++++++----- tests/test_classroom_invite_key.py | 166 +++++++++++++++++++++ tests/test_learning.py | 36 +++-- tests/test_pba_r2_l6b_028_040_classroom.py | 33 ++-- tests/test_pba_r2_mutation_hardening.py | 4 +- 5 files changed, 298 insertions(+), 72 deletions(-) create mode 100644 tests/test_classroom_invite_key.py diff --git a/citrate_sdk/learning.py b/citrate_sdk/learning.py index 3c13962..7fa4317 100644 --- a/citrate_sdk/learning.py +++ b/citrate_sdk/learning.py @@ -15,13 +15,20 @@ from __future__ import annotations +import re import secrets +import warnings from typing import Any, cast from eth_abi import decode as abi_decode +from eth_abi import encode as abi_encode +from eth_account import Account +from eth_account.messages import encode_defunct +from eth_account.signers.local import LocalAccount +from eth_utils import keccak from ._chain_guard import expected_chain_id, pinned_send -from .abi import AbiInterface, enum_index, from_wei, keccak256_text, to_wei +from .abi import AbiInterface, enum_index, from_wei, to_wei from .errors import ConfigurationError from .types import ( ClassroomInfo, @@ -95,13 +102,38 @@ "function nextWithdrawalId() view returns (uint256)", ] +#: Domain tag of the enrolment proof (ClassroomRegistry.ENROLL_TAG). +_ENROLL_TAG = keccak(b"CitrateClassroomRegistry.Enroll.v1") +_ZERO_ADDRESS = "0x" + "00" * 20 + + +def _invite_account(invite_secret: str) -> LocalAccount: + """Parse an invite secret (a 32-byte secp256k1 key as 0x-hex).""" + if not isinstance(invite_secret, str) or not re.fullmatch(r"0x[0-9a-fA-F]{64}", invite_secret): + raise ValueError( + "invite secret must be a 0x-prefixed 32-byte key (the classroom invite is a key pair; " + "use the value ClassroomManager.create / rotate_invite_code returned)" + ) + try: + return cast(LocalAccount, Account.from_key(invite_secret)) + except (ValueError, TypeError): + raise ValueError("invite secret is not a valid secp256k1 key") + + +def _invite_commitment(invite_key: str) -> bytes: + """keccak256(abi.encodePacked(inviteKey)): the on-chain invite commitment.""" + return keccak(bytes.fromhex(invite_key[2:])) + + _CLASSROOM_TUPLE = "(address,string,uint256,uint256,uint256,bool)" CLASSROOM_REGISTRY_ABI = [ "function createClassroom(string name, uint256 maxStudents, bytes32 inviteCodeHash)", - # PBA-L6b-040: CHAIN-B-C009 changed this to take the raw code and hash it - # on-chain; the old bytes32 selector no longer exists on the contract. - "function enrollWithCode(bytes inviteCode)", + # citrate-chain #222 (d89200c2): invites are key pairs. The student submits + # the invite key (an address) and the invite secret's signature over + # enrollmentDigest(teacher, student, inviteCodeHash); enrollWithCode is gone. + "function enrollWithInvite(address inviteKey, bytes signature)", + "function codeToTeacher(bytes32 inviteCodeHash) view returns (address)", "function unenroll()", "function removeStudent(address student)", "function whitelistModel(bytes32 modelHash)", @@ -668,9 +700,9 @@ def __init__( self._gas_price = gas_price self._classroom_address = classroom_address self._iface = AbiInterface(CLASSROOM_REGISTRY_ABI) - #: The invite code used by the most recent ``create`` call (generated - #: when none was passed). Only its hash goes on-chain; share the code - #: with students out of band. + #: The invite secret from the most recent ``create`` / + #: ``rotate_invite_code`` call. Only the invite key's commitment goes + #: on-chain; share the secret with students out of band. self.last_invite_code: str | None = None def _require_address(self) -> str: @@ -678,50 +710,80 @@ def _require_address(self) -> str: raise ConfigurationError("ClassroomRegistry contract address not configured.") return self._classroom_address + def _new_invite(self, invite_code: str | None) -> bytes: + secret = invite_code if invite_code is not None else "0x" + secrets.token_bytes(32).hex() + account = _invite_account(secret) + self.last_invite_code = secret + return _invite_commitment(account.address) + def create(self, name: str, max_students: int, invite_code: str | None = None) -> str: """Create a new classroom. Data source: ClassroomRegistry.createClassroom(string, uint256, bytes32) via eth_sendTransaction. + The invite is a key pair (citrate-chain #222): only the commitment + ``keccak256(abi.encodePacked(inviteKey))`` goes on-chain. The invite + secret is left in ``last_invite_code``; share it with students out of + band. + Args: name: Classroom display name. max_students: Maximum enrollment capacity. - invite_code: Plain-text invite code (only its keccak256 goes on-chain). - If omitted, a random 128-bit code is generated - (``secrets.token_urlsafe(16)``) and left in ``last_invite_code``. + invite_code: Optional invite secret (0x-prefixed 32-byte key). A + fresh random one is generated if omitted. Plain-text codes are + no longer accepted. Returns: Transaction hash. """ addr = self._require_address() - # PBA-L6b-028: the old default was ``classroom-``; its hash - # is public at createClassroom and the block timestamp bounds the - # window, so it fell to a sub-second brute force. - code = invite_code or secrets.token_urlsafe(16) - self.last_invite_code = code - code_hash = keccak256_text(code) - code_hash_bytes = bytes.fromhex(code_hash[2:]) + commitment = self._new_invite(invite_code) data = self._iface.encode_function_data("createClassroom", [ - name, max_students, code_hash_bytes, + name, max_students, commitment, ]) return self._send_transaction(addr, data) - def enroll(self, invite_code: str) -> str: - """Enroll as a student in a classroom using an invite code. + def enroll_with_invite(self, invite_secret: str) -> str: + """Enroll the configured account using the classroom's invite secret. - Data source: ClassroomRegistry.enrollWithCode(bytes) via eth_sendTransaction. - The contract hashes the raw code itself (CHAIN-B-C009, PBA-L6b-040). + Data source: ClassroomRegistry.enrollWithInvite(address, bytes) via eth_sendTransaction. - Args: - invite_code: Plain-text invite code provided by teacher. - - Returns: - Transaction hash. + Signs ``enrollmentDigest(teacher, student, inviteCodeHash)`` (EIP-191) + with the invite secret locally. Only the invite key (an address) and the + signature go on-chain, and the signature is bound to this student, + this registry and the pinned chain id. """ addr = self._require_address() - data = self._iface.encode_function_data("enrollWithCode", [invite_code.encode("utf-8")]) + if not self._default_account: + raise ConfigurationError("defaultAccount not configured; required for write operations.") + account = _invite_account(invite_secret) + commitment = _invite_commitment(account.address) + raw = self._eth_call(addr, self._iface.encode_function_data("codeToTeacher", [commitment])) + (teacher,) = self._iface.decode_function_result("codeToTeacher", raw) + if not teacher or str(teacher).lower() == _ZERO_ADDRESS: + raise ValueError("this invite secret is not an active invite on the registry (unknown or rotated)") + digest = keccak(abi_encode( + ["bytes32", "uint256", "address", "address", "address", "bytes32"], + [_ENROLL_TAG, self._expected_chain_id, addr, teacher, self._default_account, commitment], + )) + signature = account.sign_message(encode_defunct(primitive=digest)).signature + data = self._iface.encode_function_data("enrollWithInvite", [account.address, bytes(signature)]) return self._send_transaction(addr, data) + def enroll(self, invite_code: str) -> str: + """Deprecated: use :meth:`enroll_with_invite`. + + The registry no longer accepts a raw invite code (citrate-chain #222). + This forwards to ``enroll_with_invite`` when given an invite secret and + raises ``ValueError`` for anything else. + """ + warnings.warn( + "ClassroomManager.enroll is deprecated; use enroll_with_invite(invite_secret)", + DeprecationWarning, + stacklevel=2, + ) + return self.enroll_with_invite(invite_code) + def unenroll(self) -> str: """Unenroll from current classroom (student-initiated). @@ -768,21 +830,22 @@ def remove_model(self, model_hash: str) -> str: data = self._iface.encode_function_data("removeModel", [hash_bytes]) return self._send_transaction(addr, data) - def rotate_invite_code(self, new_invite_code: str) -> str: - """Rotate the classroom's invite code (teacher only). + def rotate_invite_code(self, new_invite_code: str | None = None) -> str: + """Rotate the classroom's invite (teacher only). Data source: ClassroomRegistry.rotateInviteCode(bytes32) via eth_sendTransaction. Args: - new_invite_code: New plain-text invite code. + new_invite_code: Optional new invite secret (0x-prefixed 32-byte + key); a fresh one is generated if omitted and left in + ``last_invite_code``. Returns: Transaction hash. """ addr = self._require_address() - code_hash = keccak256_text(new_invite_code) - code_hash_bytes = bytes.fromhex(code_hash[2:]) - data = self._iface.encode_function_data("rotateInviteCode", [code_hash_bytes]) + commitment = self._new_invite(new_invite_code) + data = self._iface.encode_function_data("rotateInviteCode", [commitment]) return self._send_transaction(addr, data) def get_classroom(self, teacher_address: str) -> ClassroomInfo: diff --git a/tests/test_classroom_invite_key.py b/tests/test_classroom_invite_key.py new file mode 100644 index 0000000..2231018 --- /dev/null +++ b/tests/test_classroom_invite_key.py @@ -0,0 +1,166 @@ +"""ClassroomRegistry invite-key enrolment (citrate-chain d89200c2, #222). + +The registry's invite is now a key pair. The teacher registers +``keccak256(abi.encodePacked(inviteKey))``, where ``inviteKey`` is the address of +the invite secret. A student enrols with +``enrollWithInvite(inviteKey, signature)``, where ``signature`` is the invite +secret's EIP-191 signature over +``enrollmentDigest(teacher, student, inviteCodeHash) = +keccak256(abi.encode(ENROLL_TAG, chainid, registry, teacher, student, inviteCodeHash))``. +The digest here is computed independently of the SDK. +""" +from __future__ import annotations + +import warnings +from typing import Any + +import pytest +from eth_abi import decode as abi_decode +from eth_abi import encode as abi_encode +from eth_account import Account +from eth_account.messages import encode_defunct +from eth_utils import keccak, to_checksum_address + +from citrate_sdk.abi import keccak256 +from citrate_sdk.learning import ClassroomManager + +TEACHER = to_checksum_address("0x" + "01" * 20) +STUDENT = to_checksum_address("0x" + "0a" * 20) +REGISTRY = to_checksum_address("0x" + "02" * 20) +ENROLL_TAG = keccak(b"CitrateClassroomRegistry.Enroll.v1") + + +def _commitment(invite_key: str) -> bytes: + return keccak(bytes.fromhex(invite_key[2:])) + + +def _mgr(account: str, code_to_teacher: dict[bytes, str] | None = None, chain: int = 40204) -> tuple[ClassroomManager, list[Any]]: + sent: list[Any] = [] + + def rpc(method: str, params: Any) -> Any: + if method == "eth_chainId": + return hex(chain) + if method == "eth_sendTransaction": + sent.append(params[0]) + return "0xhash" + if method == "eth_call": + data = bytes.fromhex(params[0]["data"][2:]) + assert data[:4] == keccak(b"codeToTeacher(bytes32)")[:4] + teacher = (code_to_teacher or {}).get(data[4:36], "0x" + "00" * 20) + return "0x" + abi_encode(["address"], [teacher]).hex() + raise AssertionError(method) + return ClassroomManager(rpc, default_account=account, classroom_address=REGISTRY), sent + + +class TestCreate: + def test_create_generates_an_invite_key_and_registers_its_commitment(self) -> None: + mgr, sent = _mgr(TEACHER) + mgr.create("Grade 5", 30) + secret = mgr.last_invite_code + assert secret is not None and secret.startswith("0x") and len(secret) == 66 + invite_key = Account.from_key(secret).address + data = bytes.fromhex(sent[-1]["data"][2:]) + assert data[:4] == keccak(b"createClassroom(string,uint256,bytes32)")[:4] + name, max_students, commit = abi_decode(["string", "uint256", "bytes32"], data[4:]) + assert (name, max_students, commit) == ("Grade 5", 30, _commitment(invite_key)) + mgr.create("Grade 6", 30) + assert mgr.last_invite_code != secret + + def test_create_accepts_a_caller_supplied_invite_secret(self) -> None: + secret = "0x" + "5a" * 32 + mgr, sent = _mgr(TEACHER) + mgr.create("c", 3, invite_code=secret) + assert mgr.last_invite_code == secret + commit = abi_decode(["string", "uint256", "bytes32"], bytes.fromhex(sent[-1]["data"][10:]))[2] + assert commit == _commitment(Account.from_key(secret).address) + + @pytest.mark.parametrize("bad", ["teacher-chosen", "0x1234", "0x" + "zz" * 32, "0x" + "00" * 32]) + def test_create_refuses_a_non_key_invite_code(self, bad: str) -> None: + mgr, sent = _mgr(TEACHER) + with pytest.raises(ValueError, match="invite secret"): + mgr.create("c", 3, invite_code=bad) + assert sent == [] + + def test_rotate_uses_the_same_key_pair_scheme(self) -> None: + mgr, sent = _mgr(TEACHER) + mgr.rotate_invite_code() + secret = mgr.last_invite_code + assert secret is not None + data = bytes.fromhex(sent[-1]["data"][2:]) + assert data[:4] == keccak(b"rotateInviteCode(bytes32)")[:4] + assert data[4:36] == _commitment(Account.from_key(secret).address) + + +class TestEnrollWithInvite: + secret = "0x" + "7b" * 32 + + def _setup(self, chain: int = 40204) -> tuple[ClassroomManager, list[Any], str]: + invite_key = Account.from_key(self.secret).address + mgr, sent = _mgr(STUDENT, {_commitment(invite_key): TEACHER}, chain=chain) + return mgr, sent, invite_key + + def test_sends_invite_key_and_a_signature_bound_to_the_student(self) -> None: + mgr, sent, invite_key = self._setup() + mgr.enroll_with_invite(self.secret) + data = bytes.fromhex(sent[-1]["data"][2:]) + assert data[:4] == keccak(b"enrollWithInvite(address,bytes)")[:4] + key, sig = abi_decode(["address", "bytes"], data[4:]) + assert to_checksum_address(key) == invite_key + digest = keccak(abi_encode( + ["bytes32", "uint256", "address", "address", "address", "bytes32"], + [ENROLL_TAG, 40204, REGISTRY, TEACHER, STUDENT, _commitment(invite_key)], + )) + assert Account.recover_message(encode_defunct(primitive=digest), signature=sig) == invite_key + # The secret itself never appears in calldata. + assert self.secret[2:] not in sent[-1]["data"] + + def test_signature_is_bound_to_the_chain_id(self) -> None: + mgr, sent, invite_key = self._setup(chain=31337) + mgr._expected_chain_id = 31337 + mgr.enroll_with_invite(self.secret) + _, sig = abi_decode(["address", "bytes"], bytes.fromhex(sent[-1]["data"][10:])) + d40204 = keccak(abi_encode(["bytes32", "uint256", "address", "address", "address", "bytes32"], + [ENROLL_TAG, 40204, REGISTRY, TEACHER, STUDENT, _commitment(invite_key)])) + assert Account.recover_message(encode_defunct(primitive=d40204), signature=sig) != invite_key + + def test_unknown_or_rotated_invite_is_refused_before_sending(self) -> None: + mgr, sent = _mgr(STUDENT, {}) + with pytest.raises(ValueError, match="not an active invite"): + mgr.enroll_with_invite(self.secret) + assert sent == [] + + def test_malformed_secret_is_refused(self) -> None: + mgr, sent, _ = self._setup() + with pytest.raises(ValueError, match="invite secret"): + mgr.enroll_with_invite("classroom-code") + assert sent == [] + + def test_needs_a_student_account(self) -> None: + invite_key = Account.from_key(self.secret).address + mgr, _ = _mgr(STUDENT, {_commitment(invite_key): TEACHER}) + mgr._default_account = None + with pytest.raises(Exception, match="defaultAccount"): + mgr.enroll_with_invite(self.secret) + + def test_old_enroll_is_deprecated_and_delegates(self) -> None: + mgr, sent, _ = self._setup() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + mgr.enroll(self.secret) + assert any(issubclass(x.category, DeprecationWarning) for x in w) + assert sent[-1]["data"][2:10] == keccak(b"enrollWithInvite(address,bytes)")[:4].hex() + + def test_old_enroll_refuses_a_plain_text_code(self) -> None: + mgr, sent, _ = self._setup() + with pytest.raises(ValueError, match="invite secret"), warnings.catch_warnings(): + warnings.simplefilter("ignore") + mgr.enroll("secret-code") + assert sent == [] + + +def test_removed_selector_is_not_encoded() -> None: + from citrate_sdk.abi import AbiInterface + from citrate_sdk.learning import CLASSROOM_REGISTRY_ABI + sels = {fn.selector for fn in AbiInterface(CLASSROOM_REGISTRY_ABI)._funcs.values()} + assert keccak256(b"enrollWithCode(bytes)")[:4] not in sels + assert keccak256(b"enrollWithCode(bytes32)")[:4] not in sels diff --git a/tests/test_learning.py b/tests/test_learning.py index 33b5f20..8216d68 100644 --- a/tests/test_learning.py +++ b/tests/test_learning.py @@ -12,7 +12,7 @@ import pytest -from citrate_sdk.abi import AbiInterface, from_wei, keccak256_text, to_wei +from citrate_sdk.abi import AbiInterface, from_wei, to_wei from citrate_sdk.errors import ConfigurationError from citrate_sdk.learning import ( CLASSROOM_REGISTRY_ABI, @@ -289,18 +289,14 @@ def test_missing_classroom_address(self): with pytest.raises(ConfigurationError, match="ClassroomRegistry"): mgr.unenroll() - def test_enroll_calldata(self): - """enroll sends the raw invite code; the contract hashes it (CHAIN-B-C009, - PBA-L6b-040 — this test used to pin the removed bytes32-hash ABI).""" + def test_enroll_is_deprecated_and_refuses_plain_codes(self): + """enroll() forwards to enroll_with_invite (citrate-chain #222 removed + enrollWithCode); a plain-text code is refused before any send.""" rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) - mgr.enroll("secret-code-123") - tx = rpc.call_args_list[-1][0][1][0] - expected = _classroom_iface.encode_function_data( - "enrollWithCode", [b"secret-code-123"] - ) - assert tx["data"] == expected - assert tx["to"] == FAKE_CLASSROOM_ADDR + with pytest.warns(DeprecationWarning), pytest.raises(ValueError, match="invite secret"): + mgr.enroll("secret-code-123") + assert all(c[0][0] != "eth_sendTransaction" for c in rpc.call_args_list) def test_unenroll_calldata(self): """unenroll sends correct calldata.""" @@ -336,14 +332,16 @@ def test_remove_model_calldata(self): assert tx["data"] == expected def test_rotate_invite_code_calldata(self): - """rotate_invite_code hashes new code and sends correct calldata.""" + """rotate_invite_code registers the new invite key's commitment.""" + from eth_account import Account + from eth_utils import keccak rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) - mgr.rotate_invite_code("new-secret") + secret = "0x" + "4d" * 32 + mgr.rotate_invite_code(secret) tx = rpc.call_args_list[-1][0][1][0] - expected_hash = keccak256_text("new-secret") expected = _classroom_iface.encode_function_data( - "rotateInviteCode", [bytes.fromhex(expected_hash[2:])] + "rotateInviteCode", [keccak(bytes.fromhex(Account.from_key(secret).address[2:]))] ) assert tx["data"] == expected @@ -409,10 +407,10 @@ def test_deposit_selector(self): expected_selector = keccak256(b"deposit()")[:4].hex() assert data[2:10] == expected_selector - def test_enroll_with_code_selector(self): - """enrollWithCode(bytes) selector matches the contract (PBA-L6b-040).""" + def test_enroll_with_invite_selector(self): + """enrollWithInvite(address,bytes) selector matches the contract (citrate-chain #222).""" iface = AbiInterface(CLASSROOM_REGISTRY_ABI) - data = iface.encode_function_data("enrollWithCode", [b"code"]) + data = iface.encode_function_data("enrollWithInvite", ["0x" + "11" * 20, b"sig"]) from citrate_sdk.abi import keccak256 - expected_selector = keccak256(b"enrollWithCode(bytes)")[:4].hex() + expected_selector = keccak256(b"enrollWithInvite(address,bytes)")[:4].hex() assert data[2:10] == expected_selector diff --git a/tests/test_pba_r2_l6b_028_040_classroom.py b/tests/test_pba_r2_l6b_028_040_classroom.py index 4c220c6..521b85d 100644 --- a/tests/test_pba_r2_l6b_028_040_classroom.py +++ b/tests/test_pba_r2_l6b_028_040_classroom.py @@ -10,7 +10,8 @@ ABI. ClassroomRegistry now takes ``enrollWithCode(bytes inviteCode)`` and hashes it on-chain, so every SDK enroll hit a selector the contract does not have. The parity test below pins every ClassroomRegistry selector the SDK encodes to the -contract source (citrate-chain contracts/src/ClassroomRegistry.sol @ 21726055). +contract source (citrate-chain contracts/src/ClassroomRegistry.sol @ d89200c2, +where #222 replaced enrollWithCode with enrollWithInvite). It also caught ``getClassroom``: it returns a ``Classroom`` struct with a dynamic member (ABI: one tuple, behind an offset), which the SDK decoded flat. """ @@ -20,15 +21,18 @@ from typing import Any from eth_abi import encode as abi_encode +from eth_account import Account +from eth_utils import keccak from citrate_sdk.abi import AbiInterface, keccak256, keccak256_text from citrate_sdk.learning import CLASSROOM_REGISTRY_ABI, ClassroomManager -# Signatures as declared in ClassroomRegistry.sol @ citrate-chain 21726055 +# Signatures as declared in ClassroomRegistry.sol @ citrate-chain d89200c2 (#222) # (external/public functions and public-mapping getters the SDK calls). CONTRACT_SIGNATURES = { "createClassroom": "createClassroom(string,uint256,bytes32)", - "enrollWithCode": "enrollWithCode(bytes)", + "enrollWithInvite": "enrollWithInvite(address,bytes)", + "codeToTeacher": "codeToTeacher(bytes32)", # public mapping getter "unenroll": "unenroll()", "removeStudent": "removeStudent(address)", "whitelistModel": "whitelistModel(bytes32)", @@ -69,23 +73,17 @@ def test_every_sdk_selector_matches_the_contract() -> None: assert fn.selector == keccak256(CONTRACT_SIGNATURES[name].encode())[:4], name -def test_enroll_sends_the_raw_code_as_bytes() -> None: - sent: dict[str, Any] = {} - _manager(sent).enroll("secret-code") - data = sent["tx"]["data"] - assert data[2:10] == keccak256(b"enrollWithCode(bytes)")[:4].hex() - assert data[10:] == abi_encode(["bytes"], [b"secret-code"]).hex() - - def test_default_invite_code_is_unguessable_and_returned() -> None: sent: dict[str, Any] = {} mgr = _manager(sent) t0 = int(time.time() * 1000) mgr.create("Grade 5", 30) code = mgr.last_invite_code - assert code is not None and len(code) >= 22 and not code.startswith("classroom-") + # citrate-chain #222: the invite is a 32-byte key; the commitment is + # keccak256(abi.encodePacked(inviteKey address)). + assert code is not None and len(code) == 66 and not code.startswith("classroom-") commit = bytes.fromhex(sent["tx"]["data"][2:])[4 + 64: 4 + 96] - assert commit == bytes.fromhex(keccak256_text(code)[2:]) + assert commit == keccak(bytes.fromhex(Account.from_key(code).address[2:])) # The audit's brute force (+/-2 s of millisecond timestamps) finds nothing. for ms in range(t0 - 2_000, t0 + 2_000): assert bytes.fromhex(keccak256_text(f"classroom-{ms}")[2:]) != commit @@ -93,13 +91,14 @@ def test_default_invite_code_is_unguessable_and_returned() -> None: assert mgr.last_invite_code != code -def test_explicit_invite_code_is_used_verbatim() -> None: +def test_explicit_invite_secret_is_used_verbatim() -> None: sent: dict[str, Any] = {} mgr = _manager(sent) - mgr.create("Grade 5", 30, invite_code="teacher-chosen") - assert mgr.last_invite_code == "teacher-chosen" + secret = "0x" + "3c" * 32 + mgr.create("Grade 5", 30, invite_code=secret) + assert mgr.last_invite_code == secret commit = bytes.fromhex(sent["tx"]["data"][2:])[4 + 64: 4 + 96] - assert commit == bytes.fromhex(keccak256_text("teacher-chosen")[2:]) + assert commit == keccak(bytes.fromhex(Account.from_key(secret).address[2:])) def test_get_classroom_decodes_the_struct_return() -> None: diff --git a/tests/test_pba_r2_mutation_hardening.py b/tests/test_pba_r2_mutation_hardening.py index 2e233b0..843e406 100644 --- a/tests/test_pba_r2_mutation_hardening.py +++ b/tests/test_pba_r2_mutation_hardening.py @@ -226,11 +226,11 @@ def rpc(method: str, params: Any) -> Any: class TestLearningManagers: - def test_create_targets_the_registry_with_a_22_char_code(self) -> None: + def test_create_targets_the_registry_with_a_32_byte_invite_secret(self) -> None: rpc, sent = _rpc_capture() mgr = ClassroomManager(rpc, default_account=ACCT, classroom_address=ADDR) mgr.create("c", 3) - assert sent[-1]["to"] == ADDR and len(mgr.last_invite_code or "") == 22 + assert sent[-1]["to"] == ADDR and len(mgr.last_invite_code or "") == 66 def test_create_pool_targets_the_pool(self) -> None: rpc, sent = _rpc_capture() From 05f43478342a5e64c5e13bdfb875b0bb21f82f4d Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 09:37:07 -0700 Subject: [PATCH 2/8] fix(identity,crypto): future-iat check, host-agreement regression test, tighter share-shape match - verify_id_token refuses an iat beyond the clock tolerance in the future (parity with the JS SDK). - Transport gate: the host-agreement check gets its own IPv6 zone-id regression tests, so dropping it now fails 4 tests. - The share guard's structural match needs x in 1..255 and a y of at least 16 bytes (even-length hex or bytes), so coordinate-like caller metadata such as {x: 1, y: "10"} is no longer refused. The earlier probes were updated to share-length y and are all still refused. - CHANGELOG notes round 3 under the unreleased 0.6.2. Hand mutants (all killed): host-agreement dropped, future-iat dropped, share min length 16->1, x range dropped, bytes min length dropped, x check dropped. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- CHANGELOG.md | 10 +++ citrate_sdk/crypto.py | 25 +++++-- citrate_sdk/identity/jwt.py | 2 + tests/test_hardening_round3.py | 102 +++++++++++++++++++++++++++ tests/test_pba_r2_followup_shares.py | 14 ++-- 5 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 tests/test_hardening_round3.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5596e36..d0130dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,16 @@ All notable changes to `citrate-labs-sdk` are documented here. This project adhe - The deploy guard also refuses values shaped like shares, not only the known field names. +- **Round 3 (still 0.6.2, unreleased):** + - `ClassroomManager` now follows the invite-key flow of ClassroomRegistry + (citrate-chain #222). `create()` and `rotate_invite_code()` return the + invite secret in `last_invite_code`. `enroll_with_invite(secret)` signs + the enrolment. `enroll()` is deprecated. + - `verify_id_token` refuses an `iat` beyond the clock tolerance in the + future (parity with the JS SDK). + - The share guard matches only share-shaped values (x in 1..255 and a y of + at least 16 bytes), so coordinate-like metadata is no longer refused. + ### Changed (breaking) - `IdentityClient.refresh(refresh_token, expected_sub)`: `expected_sub` is diff --git a/citrate_sdk/crypto.py b/citrate_sdk/crypto.py index 3c9e484..daa3549 100644 --- a/citrate_sdk/crypto.py +++ b/citrate_sdk/crypto.py @@ -28,15 +28,30 @@ _MAX_GUARD_DEPTH = 32 -_HEX_RE = re.compile(r"^(0x)?[0-9a-fA-F]+$") +#: A share's y is at least 16 bytes (the SDK shares 32-byte keys), as +#: even-length hex, optionally 0x-prefixed. +_SHARE_Y_HEX_RE = re.compile(r"^(0x)?(?:[0-9a-fA-F]{2}){16,}$") +_MIN_SHARE_BYTES = 16 + + +def _share_x(x: Any) -> bool: + if isinstance(x, bool): + return False + if isinstance(x, int): + return 1 <= x <= 255 + return isinstance(x, str) and x.isascii() and x.isdigit() and 1 <= int(x) <= 255 def _looks_like_share(d: dict[Any, Any]) -> bool: - """A raw Shamir share ({x, y} with y as hex/bytes) or a holder-wrapped - share record ({holder_public_key/holderPublicKey, envelope}).""" + """A raw Shamir share ({x in 1..255, y of share length as hex or bytes}) + or a holder-wrapped share record ({holder_public_key/holderPublicKey, + envelope}). Short or coordinate-like values are not treated as shares.""" y = d.get("y") - if "x" in d and (isinstance(y, (bytes, bytearray)) or (isinstance(y, str) and _HEX_RE.match(y))): - return True + if "x" in d and _share_x(d["x"]): + if isinstance(y, (bytes, bytearray)) and len(y) >= _MIN_SHARE_BYTES: + return True + if isinstance(y, str) and _SHARE_Y_HEX_RE.match(y): + return True return "envelope" in d and ("holder_public_key" in d or "holderPublicKey" in d) diff --git a/citrate_sdk/identity/jwt.py b/citrate_sdk/identity/jwt.py index a280212..448ac64 100644 --- a/citrate_sdk/identity/jwt.py +++ b/citrate_sdk/identity/jwt.py @@ -99,6 +99,8 @@ def verify_id_token( raise IdTokenError("token has no numeric exp claim") if not _is_number(payload.get("iat")): raise IdTokenError("token has no numeric iat claim") + if cast(float, payload["iat"]) > now + tol: + raise IdTokenError("token issued in the future (iat)") if now > cast(float, exp) + tol: raise IdTokenError("token expired") nbf = payload.get("nbf") diff --git a/tests/test_hardening_round3.py b/tests/test_hardening_round3.py new file mode 100644 index 0000000..f852555 --- /dev/null +++ b/tests/test_hardening_round3.py @@ -0,0 +1,102 @@ +"""Hardening round 3 (Python). + +1. Transport gate: the urllib3/urllib.parse host-agreement check has its own + regression tests (IPv6 zone-id forms), so dropping it cannot go unnoticed. +2. ID tokens: an ``iat`` beyond the clock tolerance in the future is refused + (parity with the JS SDK). +3. Share guard: the structural match needs an x in 1..255 and a share-length + y (>= 16 bytes), so ordinary coordinate-like metadata is not refused. +""" +from __future__ import annotations + +import base64 +import json +from typing import Any + +import pytest +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa + +from citrate_sdk._url_security import InsecureTransportError, enforce_transport_security +from citrate_sdk.crypto import assert_no_key_share_material +from citrate_sdk.errors import CitrateError +from citrate_sdk.identity.jwt import IdTokenError, verify_id_token + + +@pytest.mark.parametrize("url", [ + "http://[::1%25eth0]:8545", + "http://[::1%25evil.com]:8545", + "http://[::1%2540evil.com]:8545", + "http://[fe80::1%25en0]:8545", +]) +def test_ipv6_zone_id_forms_are_refused_by_host_agreement(url: str) -> None: + with pytest.raises(InsecureTransportError, match="urllib.parse sees host|unparseable|not allowed"): + enforce_transport_security(url) + + +def test_plain_ipv6_loopback_still_passes() -> None: + assert enforce_transport_security("http://[::1]:8545") == "http://[::1]:8545" + + +_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) +_n = _key.public_key().public_numbers() + + +def _b64(b: bytes) -> str: + return base64.urlsafe_b64encode(b).rstrip(b"=").decode() + + +JWKS = [{"kty": "RSA", "kid": "k", "n": _b64(_n.n.to_bytes(256, "big")), "e": _b64(_n.e.to_bytes(3, "big"))}] +NOW = 1_900_000_000 + + +def _tok(iat: int) -> str: + h = _b64(json.dumps({"alg": "RS256", "kid": "k"}).encode()) + p = _b64(json.dumps({"iss": "https://i", "aud": "a", "sub": "u", "iat": iat, "exp": NOW + 3600}).encode()) + return f"{h}.{p}.{_b64(_key.sign(f'{h}.{p}'.encode(), padding.PKCS1v15(), hashes.SHA256()))}" + + +def _verify(tok: str) -> dict[str, Any]: + return verify_id_token(tok, "https://i", "a", JWKS, now_ms=NOW * 1000) + + +def test_iat_within_tolerance_is_accepted() -> None: + assert _verify(_tok(NOW + 60))["sub"] == "u" + + +@pytest.mark.parametrize("iat", [NOW + 61, NOW + 10**6]) +def test_future_iat_is_refused(iat: int) -> None: + with pytest.raises(IdTokenError, match="issued in the future"): + _verify(_tok(iat)) + + +Y32 = "ab" * 32 + + +@pytest.mark.parametrize("meta", [ + {"x": 1, "y": "10"}, + {"point": {"x": 1, "y": "ff"}}, + {"theme": {"x": 0, "y": "abcdef"}}, + {"x": 0, "y": Y32}, + {"x": 256, "y": Y32}, + {"x": "one", "y": Y32}, + {"x": 1, "y": "ab" * 15}, + {"x": 1, "y": "abc" * 11}, + {"x": 1, "y": b"\x01\x02"}, + {"grid": json.dumps({"x": 3, "y": "1234"})}, +]) +def test_coordinate_like_metadata_is_not_refused(meta: dict[str, Any]) -> None: + assert_no_key_share_material(meta) + + +@pytest.mark.parametrize("meta", [ + {"a": {"x": 1, "y": Y32}}, + {"a": {"x": "255", "y": "0x" + Y32}}, + {"a": {"x": 7, "y": "ab" * 16}}, + {"a": {"x": 2, "y": bytes(32)}}, + {"a": {"x": 2, "y": "1" * 64}}, + {"blob": json.dumps([{"x": 2, "y": Y32}])}, +]) +def test_share_shaped_values_are_still_refused(meta: dict[str, Any]) -> None: + with pytest.raises(CitrateError, match="shaped like a key share"): + assert_no_key_share_material(meta) diff --git a/tests/test_pba_r2_followup_shares.py b/tests/test_pba_r2_followup_shares.py index feb7225..c7c87ee 100644 --- a/tests/test_pba_r2_followup_shares.py +++ b/tests/test_pba_r2_followup_shares.py @@ -67,15 +67,15 @@ class TestStructuralGuard: @pytest.mark.parametrize("meta", [ {"myShares": shares}, - {"a": [{"x": 1, "y": "ab12"}]}, - {"a": {"x": "1", "y": "0xab"}}, + {"a": [{"x": 1, "y": "ab12" * 16}]}, + {"a": {"x": "1", "y": "0x" + "ab" * 32}}, {"blob": json.dumps({"parts": shares})}, - {"blob": json.dumps([{"x": 2, "y": "cd"}])}, + {"blob": json.dumps([{"x": 2, "y": "cd" * 32}])}, {"w": {"holder_public_key": "02" + "11" * 32, "envelope": "{}"}}, {"w": [{"holderPublicKey": "02" + "11" * 32, "envelope": "{}"}]}, - {"y": {"x": 1, "y": b"\\x01"}}, - {"padded": ' {"x": 1, "y": "ab"} '}, - {"big": json.dumps({"pad": "a" * 1_100_000, "s": {"x": 1, "y": "ab"}})}, + {"y": {"x": 1, "y": b"\\x01" * 32}}, + {"padded": ' {"x": 1, "y": "abababababababababababababababababababababababababababababababab"} '}, + {"big": json.dumps({"pad": "a" * 1_100_000, "s": {"x": 1, "y": "ab" * 32}})}, ], ids=lambda m: str(list(m)[0])) def test_share_shaped_values_are_refused(self, meta: dict[str, Any]) -> None: with pytest.raises(CitrateError, match="key share|key-share"): @@ -104,7 +104,7 @@ def test_json_in_string_nesting_counts_toward_depth(self) -> None: def test_message(self) -> None: with pytest.raises(CitrateError, match=r"shaped like a key share \(\{x, y\} or a wrapped share record\) in public deploy calldata\. Deliver key shares"): - assert_no_key_share_material({"a": {"x": 1, "y": "ab"}}) + assert_no_key_share_material({"a": {"x": 1, "y": "ab" * 32}}) def test_deploy_refuses_renamed_share_field(self, tmp_path: Path) -> None: mp = tmp_path / "m.onnx" From cbfd680d5fa28fffe6ac1e596a5184d90b023710 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 09:47:26 -0700 Subject: [PATCH 3/8] test(crypto): share-shape boundary cases (parity with JS) Adds x/y boundary cases for the tightened share-shape match: string and non-int x, 15/16-byte y, and list-valued y. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- tests/test_hardening_round3.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_hardening_round3.py b/tests/test_hardening_round3.py index f852555..2671d16 100644 --- a/tests/test_hardening_round3.py +++ b/tests/test_hardening_round3.py @@ -84,6 +84,15 @@ def test_future_iat_is_refused(iat: int) -> None: {"x": 1, "y": "abc" * 11}, {"x": 1, "y": b"\x01\x02"}, {"grid": json.dumps({"x": 3, "y": "1234"})}, + {"x": "0", "y": Y32}, + {"x": "256", "y": Y32}, + {"x": "1a", "y": Y32}, + {"x": " 12", "y": Y32}, + {"x": "", "y": Y32}, + {"x": True, "y": Y32}, + {"x": [5], "y": Y32}, + {"x": 1, "y": bytes(15)}, + {"x": 1, "y": [Y32]}, ]) def test_coordinate_like_metadata_is_not_refused(meta: dict[str, Any]) -> None: assert_no_key_share_material(meta) @@ -96,6 +105,9 @@ def test_coordinate_like_metadata_is_not_refused(meta: dict[str, Any]) -> None: {"a": {"x": 2, "y": bytes(32)}}, {"a": {"x": 2, "y": "1" * 64}}, {"blob": json.dumps([{"x": 2, "y": Y32}])}, + {"a": {"x": 255, "y": Y32}}, + {"a": {"x": "1", "y": Y32}}, + {"a": {"x": 1, "y": bytes(16)}}, ]) def test_share_shaped_values_are_still_refused(meta: dict[str, Any]) -> None: with pytest.raises(CitrateError, match="shaped like a key share"): From 863ca40d1f238430a78443c3687e846457774add Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 13:41:17 -0700 Subject: [PATCH 4/8] fix(learning): validate invite secrets as secp256k1 keys independently of eth-keys _invite_account checks 1 <= secret < n itself and reports any key-library error as ValueError, so the result no longer depends on the installed eth-keys version (the locked 0.7.0 accepts a zero key; secrets >= n raised a bare Exception). Tests are in tests/test_hardening_round4.py. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- citrate_sdk/learning.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/citrate_sdk/learning.py b/citrate_sdk/learning.py index 7fa4317..4a92b89 100644 --- a/citrate_sdk/learning.py +++ b/citrate_sdk/learning.py @@ -107,17 +107,27 @@ _ZERO_ADDRESS = "0x" + "00" * 20 +#: secp256k1 group order; a private key must be in [1, n). +_SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + def _invite_account(invite_secret: str) -> LocalAccount: - """Parse an invite secret (a 32-byte secp256k1 key as 0x-hex).""" + """Parse an invite secret (a 32-byte secp256k1 key as 0x-hex). + + The range is checked here rather than left to the installed eth-keys + version, and any parse failure is reported as ValueError. + """ if not isinstance(invite_secret, str) or not re.fullmatch(r"0x[0-9a-fA-F]{64}", invite_secret): raise ValueError( "invite secret must be a 0x-prefixed 32-byte key (the classroom invite is a key pair; " "use the value ClassroomManager.create / rotate_invite_code returned)" ) + if not 1 <= int(invite_secret, 16) < _SECP256K1_N: + raise ValueError("invite secret is out of range for a secp256k1 key") try: return cast(LocalAccount, Account.from_key(invite_secret)) - except (ValueError, TypeError): - raise ValueError("invite secret is not a valid secp256k1 key") + except Exception as e: # eth-keys raises different types across versions + raise ValueError(f"invite secret is not a valid secp256k1 key: {e}") from None def _invite_commitment(invite_key: str) -> bytes: From 8b73e19a505b594bf13130ff95de6088893b9e8e Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 13:41:38 -0700 Subject: [PATCH 5/8] fix(crypto): one strict share parser and a guard that covers it; shared test vectors - parse_share_y is the single share-value parser (optional 0x/0X, even-length hex, nothing else). reconstruct_key_from_shares uses it, so whitespace and trailing characters are rejected. - The deploy guard's y match is a deliberate superset: after removing whitespace, any run of >= 16 bytes of hex digits. A property test asserts that the guard refuses every y the parser accepts. - An integral float x (1.0) counts as an integer x, matching JS number semantics. - tests/fixtures/share_guard_vectors.json is shared byte-for-byte with citrate-sdk-js (sha256 pinned in the test). Both guards run the same refuse and accept vectors. - An odd-length share-sized hex y moves from the accepted to the refused round-3 cases. Hand mutants (12) all killed; log kept in the lane record. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- citrate_sdk/crypto.py | 30 +- tests/fixtures/share_guard_vectors.json | 399 ++++++++++++++++++++++++ tests/test_hardening_round3.py | 3 +- tests/test_hardening_round4.py | 125 ++++++++ 4 files changed, 552 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/share_guard_vectors.json create mode 100644 tests/test_hardening_round4.py diff --git a/citrate_sdk/crypto.py b/citrate_sdk/crypto.py index daa3549..f59d747 100644 --- a/citrate_sdk/crypto.py +++ b/citrate_sdk/crypto.py @@ -30,8 +30,27 @@ #: A share's y is at least 16 bytes (the SDK shares 32-byte keys), as #: even-length hex, optionally 0x-prefixed. -_SHARE_Y_HEX_RE = re.compile(r"^(0x)?(?:[0-9a-fA-F]{2}){16,}$") _MIN_SHARE_BYTES = 16 +_STRICT_HEX_RE = re.compile(r"(?:0[xX])?((?:[0-9a-fA-F]{2})+)") +_HEX_RUN_RE = re.compile(r"[0-9a-fA-F]{%d,}" % (2 * _MIN_SHARE_BYTES)) +_WS_RE = re.compile(r"\s+") + + +def parse_share_y(y: str) -> bytes: + """The SDK's one share-value parser: optional 0x/0X, then an even number of + hex digits and nothing else (no whitespace, no trailing junk). Raises + ValueError otherwise. ``reconstruct_key_from_shares`` uses it.""" + m = _STRICT_HEX_RE.fullmatch(y) if isinstance(y, str) else None + if m is None: + raise ValueError("share y must be an even-length hex string") + return bytes.fromhex(m.group(1)) + + +def _share_y_like(y: str) -> bool: + """Deliberately LENIENT (a superset of ``parse_share_y`` and of the lenient + parsers other consumers may use): after removing whitespace, any run of + >= 16 bytes of hex digits counts (a 0x/0X prefix cannot join the run).""" + return _HEX_RUN_RE.search(_WS_RE.sub("", y)) is not None def _share_x(x: Any) -> bool: @@ -39,6 +58,9 @@ def _share_x(x: Any) -> bool: return False if isinstance(x, int): return 1 <= x <= 255 + if isinstance(x, float): + # JSON "1.0" is the Number 1 in JS: treat an integral float as an integer. + return x.is_integer() and 1 <= x <= 255 return isinstance(x, str) and x.isascii() and x.isdigit() and 1 <= int(x) <= 255 @@ -50,7 +72,7 @@ def _looks_like_share(d: dict[Any, Any]) -> bool: if "x" in d and _share_x(d["x"]): if isinstance(y, (bytes, bytearray)) and len(y) >= _MIN_SHARE_BYTES: return True - if isinstance(y, str) and _SHARE_Y_HEX_RE.match(y): + if isinstance(y, str) and _share_y_like(y): return True return "envelope" in d and ("holder_public_key" in d or "holderPublicKey" in d) @@ -757,9 +779,9 @@ def reconstruct_key_from_shares(self, shares: list[dict[str, str]], threshold: i if not (x_text.isascii() and x_text.isdigit() and 1 <= len(x_text) <= 3): raise CitrateError(f"Invalid share: x must be an integer in 1..255, got {x_text!r}") try: - y = bytes.fromhex(share["y"]) + y = parse_share_y(share["y"]) except ValueError: - raise CitrateError("Invalid share: y is not hex") + raise CitrateError("Invalid share: y is not hex (an even-length hex string is required)") shares_tuples.append((int(x_text), y)) try: diff --git a/tests/fixtures/share_guard_vectors.json b/tests/fixtures/share_guard_vectors.json new file mode 100644 index 0000000..cb52977 --- /dev/null +++ b/tests/fixtures/share_guard_vectors.json @@ -0,0 +1,399 @@ +{ + "_comment": "Shared share-guard test vectors. The SAME file is committed to citrate-sdk-js and citrate-sdk-python (tests/fixtures/share_guard_vectors.json); each SDK's guard must refuse every refuse:true entry and accept every refuse:false entry. Keep the two copies byte-identical.", + "version": 1, + "vectors": [ + { + "name": "int x, 64-hex y", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "abababababababababababababababababababababababababababababababab" + } + } + }, + { + "name": "x 255", + "refuse": true, + "meta": { + "a": { + "x": 255, + "y": "abababababababababababababababababababababababababababababababab" + } + } + }, + { + "name": "decimal-string x", + "refuse": true, + "meta": { + "a": { + "x": "7", + "y": "abababababababababababababababababababababababababababababababab" + } + } + }, + { + "name": "uppercase y", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "ABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB" + } + } + }, + { + "name": "0x-prefixed y", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "0xabababababababababababababababababababababababababababababababab" + } + } + }, + { + "name": "0X-prefixed y", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "0Xabababababababababababababababababababababababababababababababab" + } + } + }, + { + "name": "16-byte y", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "abababababababababababababababab" + } + } + }, + { + "name": "float x 1.0", + "refuse": true, + "meta": { + "a": { + "x": 1.0, + "y": "abababababababababababababababababababababababababababababababab" + } + } + }, + { + "name": "float x 255.0", + "refuse": true, + "meta": { + "a": { + "x": 255.0, + "y": "abababababababababababababababababababababababababababababababab" + } + } + }, + { + "name": "JSON string with float x", + "refuse": true, + "meta": { + "blob": "{\"x\": 1.0, \"y\": \"abababababababababababababababababababababababababababababababab\"}" + } + }, + { + "name": "y with spaces between bytes", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab" + } + } + }, + { + "name": "y with trailing space", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "abababababababababababababababababababababababababababababababab " + } + } + }, + { + "name": "y with leading space", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": " abababababababababababababababababababababababababababababababab" + } + } + }, + { + "name": "y with trailing newline", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "abababababababababababababababababababababababababababababababab\n" + } + } + }, + { + "name": "y with trailing junk char", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "ababababababababababababababababababababababababababababababababz" + } + } + }, + { + "name": "y odd length", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "ababababababababababababababababababababababababababababababababa" + } + } + }, + { + "name": "y with tabs", + "refuse": true, + "meta": { + "a": { + "x": 1, + "y": "abab\tabab\tabab\tabab\tabab\tabab\tabab\tabab\tabab\tabab\tabab\tabab\tabab\tabab\tabab\tabab" + } + } + }, + { + "name": "array of shares", + "refuse": true, + "meta": { + "s": [ + { + "x": 1, + "y": "abababababababababababababababababababababababababababababababab" + }, + { + "x": 2, + "y": "abababababababababababababababababababababababababababababababab" + } + ] + } + }, + { + "name": "nested depth 4", + "refuse": true, + "meta": { + "a": { + "b": { + "c": { + "d": { + "x": 3, + "y": "abababababababababababababababababababababababababababababababab" + } + } + } + } + } + }, + { + "name": "double-encoded JSON", + "refuse": true, + "meta": { + "blob": "\"{\\\"x\\\": 1, \\\"y\\\": \\\"abababababababababababababababababababababababababababababababab\\\"}\"" + } + }, + { + "name": "renamed field of shares", + "refuse": true, + "meta": { + "myShares": [ + { + "x": "1", + "y": "abababababababababababababababababababababababababababababababab" + } + ] + } + }, + { + "name": "wrapped share record", + "refuse": true, + "meta": { + "w": { + "holderPublicKey": "021111111111111111111111111111111111111111111111111111111111111111", + "envelope": "{}" + } + } + }, + { + "name": "wrapped share record snake", + "refuse": true, + "meta": { + "w": { + "holder_public_key": "021111111111111111111111111111111111111111111111111111111111111111", + "envelope": "{}" + } + } + }, + { + "name": "deny-listed name", + "refuse": true, + "meta": { + "keyShares": [] + } + }, + { + "name": "coordinate x 1 y '10'", + "refuse": false, + "meta": { + "x": 1, + "y": "10" + } + }, + { + "name": "point ff", + "refuse": false, + "meta": { + "point": { + "x": 1, + "y": "ff" + } + } + }, + { + "name": "theme x 0", + "refuse": false, + "meta": { + "theme": { + "x": 0, + "y": "abcdef" + } + } + }, + { + "name": "float coordinates", + "refuse": false, + "meta": { + "x": 1.5, + "y": 2.5 + } + }, + { + "name": "15-byte y", + "refuse": false, + "meta": { + "x": 1, + "y": "ababababababababababababababab" + } + }, + { + "name": "x 0 with share-length y", + "refuse": false, + "meta": { + "x": 0, + "y": "abababababababababababababababababababababababababababababababab" + } + }, + { + "name": "x 256 with share-length y", + "refuse": false, + "meta": { + "x": 256, + "y": "abababababababababababababababababababababababababababababababab" + } + }, + { + "name": "float x 1.5 with share-length y", + "refuse": false, + "meta": { + "x": 1.5, + "y": "abababababababababababababababababababababababababababababababab" + } + }, + { + "name": "x 'one'", + "refuse": false, + "meta": { + "x": "one", + "y": "abababababababababababababababababababababababababababababababab" + } + }, + { + "name": "x ' 12'", + "refuse": false, + "meta": { + "x": " 12", + "y": "abababababababababababababababababababababababababababababababab" + } + }, + { + "name": "x list", + "refuse": false, + "meta": { + "x": [ + 5 + ], + "y": "abababababababababababababababababababababababababababababababab" + } + }, + { + "name": "y list", + "refuse": false, + "meta": { + "x": 1, + "y": [ + "abababababababababababababababababababababababababababababababab" + ] + } + }, + { + "name": "plain text y", + "refuse": false, + "meta": { + "x": 1, + "y": "hello world, this is not hex at all!" + } + }, + { + "name": "short hex run with junk", + "refuse": false, + "meta": { + "x": 1, + "y": "abababababababababababababababzzababababababababababababababab" + } + }, + { + "name": "model hash field", + "refuse": false, + "meta": { + "model_hash": "abababababababababababababababababababababababababababababababab" + } + }, + { + "name": "envelope only", + "refuse": false, + "meta": { + "envelope": "e" + } + }, + { + "name": "json text array", + "refuse": false, + "meta": { + "text": "[1, 2, 3]" + } + }, + { + "name": "non-json string", + "refuse": false, + "meta": { + "blob": "{not json" + } + } + ] +} diff --git a/tests/test_hardening_round3.py b/tests/test_hardening_round3.py index 2671d16..eb1b94e 100644 --- a/tests/test_hardening_round3.py +++ b/tests/test_hardening_round3.py @@ -81,7 +81,6 @@ def test_future_iat_is_refused(iat: int) -> None: {"x": 256, "y": Y32}, {"x": "one", "y": Y32}, {"x": 1, "y": "ab" * 15}, - {"x": 1, "y": "abc" * 11}, {"x": 1, "y": b"\x01\x02"}, {"grid": json.dumps({"x": 3, "y": "1234"})}, {"x": "0", "y": Y32}, @@ -105,6 +104,8 @@ def test_coordinate_like_metadata_is_not_refused(meta: dict[str, Any]) -> None: {"a": {"x": 2, "y": bytes(32)}}, {"a": {"x": 2, "y": "1" * 64}}, {"blob": json.dumps([{"x": 2, "y": Y32}])}, + # Odd-length share-sized hex: lenient decoders elsewhere still read it. + {"a": {"x": 1, "y": "abc" * 11}}, {"a": {"x": 255, "y": Y32}}, {"a": {"x": "1", "y": Y32}}, {"a": {"x": 1, "y": bytes(16)}}, diff --git a/tests/test_hardening_round4.py b/tests/test_hardening_round4.py new file mode 100644 index 0000000..9b34237 --- /dev/null +++ b/tests/test_hardening_round4.py @@ -0,0 +1,125 @@ +"""Hardening round 4 (Python). + +1. Invite secrets are validated as secp256k1 private keys independently of the + installed eth-keys version: 1 <= secret < n, and any parse failure is a + ValueError. +2. The share guard and the SDK's share parser cannot drift: the guard refuses + every y the parser would accept, and the parser is strict (no whitespace, + no junk, even length). +3. Both SDKs run the same test vectors (tests/fixtures/share_guard_vectors.json, + byte-identical in citrate-sdk-js; the sha256 below pins it). +""" +from __future__ import annotations + +import hashlib +import json +import secrets as pysecrets +from pathlib import Path +from typing import Any + +import pytest + +from citrate_sdk import KeyManager, crypto +from citrate_sdk.crypto import assert_no_key_share_material +from citrate_sdk.errors import CitrateError +from citrate_sdk.learning import ClassroomManager + +SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +VECTORS = Path(__file__).parent / "fixtures" / "share_guard_vectors.json" +VECTORS_SHA256 = "78d317f898cea7c2a808fc34e6566c144cc88709057175e29ac75867b4a1f8d5" + + +def _mgr() -> tuple[ClassroomManager, list[Any]]: + sent: list[Any] = [] + + def rpc(method: str, params: Any) -> Any: + if method == "eth_chainId": + return hex(40204) + sent.append(params) + return "0xhash" + return ClassroomManager(rpc, default_account="0x" + "01" * 20, classroom_address="0x" + "02" * 20), sent + + +@pytest.mark.parametrize("secret", [ + "0x" + "00" * 32, + hex(SECP256K1_N), + "0x" + format(SECP256K1_N + 1, "064x"), + "0x" + "ff" * 32, +]) +def test_out_of_range_invite_secrets_are_value_errors(secret: str) -> None: + mgr, sent = _mgr() + with pytest.raises(ValueError, match="invite secret"): + mgr.create("c", 3, invite_code=secret) + assert sent == [] + + +@pytest.mark.parametrize("secret", ["0x" + "00" * 31 + "01", "0x" + format(SECP256K1_N - 1, "064x")]) +def test_range_edges_are_accepted(secret: str) -> None: + mgr, sent = _mgr() + mgr.create("c", 3, invite_code=secret) + assert mgr.last_invite_code == secret and len(sent) == 1 + + +def test_vector_file_is_the_shared_copy() -> None: + assert hashlib.sha256(VECTORS.read_bytes()).hexdigest() == VECTORS_SHA256 + + +_VECS = json.loads(VECTORS.read_text())["vectors"] + + +@pytest.mark.parametrize("vec", _VECS, ids=[v["name"] for v in _VECS]) +def test_shared_vectors(vec: dict[str, Any]) -> None: + if vec["refuse"]: + with pytest.raises(CitrateError): + assert_no_key_share_material(vec["meta"]) + else: + assert_no_key_share_material(vec["meta"]) + + +@pytest.mark.parametrize("y", ["ab cd" + "ab" * 30, "ab" * 32 + " ", " " + "ab" * 32, "ab" * 32 + "\n", + "ab" * 32 + "z", "ab" * 32 + "a", "0x", "", "xyz"]) +def test_share_parser_is_strict(y: str) -> None: + with pytest.raises(ValueError): + crypto.parse_share_y(y) + with pytest.raises(CitrateError): + KeyManager("0x" + "11" * 32).reconstruct_key_from_shares([{"x": "1", "y": y}], threshold=1) + + +@pytest.mark.parametrize("y", ["ab" * 32, "AB" * 32, "0x" + "ab" * 32, "0X" + "cd" * 16]) +def test_share_parser_accepts_canonical_hex(y: str) -> None: + assert len(crypto.parse_share_y(y)) >= 16 + + +def test_guard_refuses_everything_the_parser_accepts() -> None: + """The invariant that keeps the guard and the parser from drifting: any y the + SDK parser accepts (at share length) is refused by the guard.""" + for n in (16, 17, 32, 64): + for _ in range(25): + raw = pysecrets.token_bytes(n) + for y in (raw.hex(), raw.hex().upper(), "0x" + raw.hex(), "0X" + raw.hex()): + assert len(crypto.parse_share_y(y)) == n + with pytest.raises(CitrateError): + assert_no_key_share_material({"x": 1 + n % 200, "y": y}) + + +def test_range_check_holds_even_if_the_key_library_accepts(monkeypatch: pytest.MonkeyPatch) -> None: + """The range check must not depend on eth-keys rejecting n.""" + from citrate_sdk import learning + + class _Acct: + address = "0x" + "12" * 20 + + monkeypatch.setattr(learning.Account, "from_key", staticmethod(lambda k: _Acct())) + with pytest.raises(ValueError, match="out of range"): + learning._invite_account(hex(SECP256K1_N)) + + +def test_any_key_library_error_becomes_value_error(monkeypatch: pytest.MonkeyPatch) -> None: + from citrate_sdk import learning + + def boom(k: str) -> None: + raise Exception("Invalid privkey") + + monkeypatch.setattr(learning.Account, "from_key", staticmethod(boom)) + with pytest.raises(ValueError, match="not a valid secp256k1 key"): + learning._invite_account("0x" + "11" * 32) From d9afe929e97845b4a376325a80a41f04970963f3 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 13:41:46 -0700 Subject: [PATCH 6/8] ci: add a locked-env job that runs lint, types and tests from uv.lock The shared python job installs unpinned dependencies. The new job runs uv sync --frozen and then ruff, mypy and pytest inside it, so CI exercises the same dependency versions as a local frozen install. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 326c406..71bfd61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,27 @@ jobs: - name: uv.lock matches pyproject.toml run: uv lock --check + # The shared python job installs with an unpinned `pip install -e .[dev]`, + # which can resolve newer dependencies than uv.lock. This job runs the same + # lint, type and test steps inside the frozen lockfile environment, so CI + # and a local `uv sync --frozen` see the same dependency versions. + locked-env: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.11' + - run: python -m pip install "uv==0.12.19" + - name: Install from uv.lock (frozen) + run: uv sync --frozen --all-extras --python 3.11 + - name: Lint (ruff) + run: uv run --frozen ruff check . + - name: Type-check (mypy) + run: uv run --frozen mypy . + - name: Test (pytest, locked dependencies) + run: uv run --frozen pytest -q + # PBA-L6b-003 tripwire on the BUILT wheel: build it the way a release does, # import citrate_sdk from the unpacked wheel only, and assert that no subset # of the deploy_model calldata reconstructs the model key. The source-tree From 060e4fb2a31e9c8dcd3c7da544844786635e2f86 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 14:47:36 -0700 Subject: [PATCH 7/8] chore(deps): refresh locked dependencies flagged by pip-audit uv.lock pinned versions of aiohttp, click, idna and urllib3 that pip-audit flags, and eth-keys 0.7.0 / eth-account 0.13.7. Upgraded with uv lock --upgrade-package: aiohttp 3.14.3, click 8.5.0, idna 3.20, urllib3 2.8.0, eth-keys 0.8.0, eth-account 0.14.0, plus their transitive updates. A locked-environment pip-audit is now clean, and the full frozen suite passes. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- uv.lock | 290 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 159 insertions(+), 131 deletions(-) diff --git a/uv.lock b/uv.lock index 24cf0ad..3519900 100644 --- a/uv.lock +++ b/uv.lock @@ -18,7 +18,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -28,112 +28,129 @@ dependencies = [ { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, - { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, - { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, - { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, - { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, - { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, - { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, - { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, - { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, - { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, - { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, - { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, - { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, - { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, - { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, - { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, - { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, - { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, - { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, - { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, - { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, - { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, - { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/4a99fb425c5e0cad715eea7bd190aff46f38b959a0a2dadb993705d34b26/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b", size = 765848, upload-time = "2026-07-23T01:52:08.217Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/43b85dc55b8e950dc644babe762add781319ea881b57b33d2cce12017d12/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a", size = 517476, upload-time = "2026-07-23T01:52:10.846Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9e/73b582c4dbbc3c12ef4473822475effaabf1f934b56f14f5b03fe5d3a2af/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5", size = 515334, upload-time = "2026-07-23T01:52:12.636Z" }, + { url = "https://files.pythonhosted.org/packages/79/03/e98c3c9e05a5bdf97defe5ff9169baba4f0ec9a901f2d60e0f060c2f051e/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f", size = 1708830, upload-time = "2026-07-23T01:52:14.538Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2c/26e60b694844dfd2176c57f913a22d0cd6a16f9ff202cbda7580d0328b98/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43", size = 1674012, upload-time = "2026-07-23T01:52:16.486Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/672df92e3172cd876aacfa97a952ac560877eb169384b2991ac5b273de4c/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9", size = 1767015, upload-time = "2026-07-23T01:52:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c5/228dec7bfec1c373cc2217cdeb47d6456dcd7a13a4c55144930a75ae3851/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8", size = 1858700, upload-time = "2026-07-23T01:52:20.08Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ff/cb36724e8c8d17f90ada567a9ff3efe1d6e9b549fba697a242aece180f21/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479", size = 1714075, upload-time = "2026-07-23T01:52:22.071Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3a/296a4135c6366376263aeef54b15caca1f07676c2ae0c525d7832f2f808a/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b", size = 1588234, upload-time = "2026-07-23T01:52:23.757Z" }, + { url = "https://files.pythonhosted.org/packages/7d/81/9d5d853ef892dc066d1eb6db0e87a47348b920c1c879aa554612fdbd9d79/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d", size = 1677300, upload-time = "2026-07-23T01:52:25.861Z" }, + { url = "https://files.pythonhosted.org/packages/68/96/021d386ae32d9b26d4b88df2e794546232ff56bb6be952bf6be227c0bbc7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d", size = 1691501, upload-time = "2026-07-23T01:52:28Z" }, + { url = "https://files.pythonhosted.org/packages/29/9f/af66adce26a14af135c003cbd0f44ccaa68cebd30ff8ac99ca47fb4958f7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2", size = 1735113, upload-time = "2026-07-23T01:52:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/2f/90/28c390d4c9851effe52ac25b5a2e1d92246acd00728b4fc7975dafb67484/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48", size = 1577486, upload-time = "2026-07-23T01:52:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/db/c2/00e23a1bf2abb70dd353f6987db7e7f2491d0261f7363997738c71c98f95/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f", size = 1751353, upload-time = "2026-07-23T01:52:33.688Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7d/d51a706a8cbfa57f0611127daf61ab3ae02ab8420b0407412079227d1c65/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32", size = 1698681, upload-time = "2026-07-23T01:52:38.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b0/90bd5cd9fdd9787cb4211d284d1fb8401339a933cb0227a15b71e789232f/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e", size = 456733, upload-time = "2026-07-23T01:52:41.823Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/fe5b8f6a71ae112bc677163d0b0701bda5dc15005249582258ede0eb88c7/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c", size = 480460, upload-time = "2026-07-23T01:52:43.905Z" }, + { url = "https://files.pythonhosted.org/packages/54/00/45e98b6645cd7f00a4b78b749ebd309094b0eaeb2d2e96157eadbc0d0050/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb", size = 453479, upload-time = "2026-07-23T01:52:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, ] [[package]] @@ -655,14 +672,11 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] [[package]] @@ -1295,7 +1309,7 @@ wheels = [ [[package]] name = "eth-account" -version = "0.13.7" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bitarray" }, @@ -1309,9 +1323,9 @@ dependencies = [ { name = "pydantic" }, { name = "rlp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/cf/20f76a29be97339c969fd765f1237154286a565a1d61be98e76bb7af946a/eth_account-0.13.7.tar.gz", hash = "sha256:5853ecbcbb22e65411176f121f5f24b8afeeaf13492359d254b16d8b18c77a46", size = 935998, upload-time = "2025-04-21T21:11:21.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/f1/e1e35b67d0f36f0186cfc0502da7984560c03d1fe51e36e3bbe6d6167ba7/eth_account-0.14.0.tar.gz", hash = "sha256:2c8291b1a8fcbd29a55b07f75f0a0aaffd04704d3ec28d1396587f18a5541c6d", size = 8366399, upload-time = "2026-08-23T01:42:07.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/18/088fb250018cbe665bc2111974301b2d59f294a565aff7564c4df6878da2/eth_account-0.13.7-py3-none-any.whl", hash = "sha256:39727de8c94d004ff61d10da7587509c04d2dc7eac71e04830135300bdfc6d24", size = 587452, upload-time = "2025-04-21T21:11:18.346Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9d/1e28b566ec2da18fd3d8410731b551518333cdf66b7fbeb1ad87c64b8145/eth_account-0.14.0-py3-none-any.whl", hash = "sha256:efdcb57f32f133e9152510e44772a4bcfe519317dfd8f7e7c5ead8189f73a2b6", size = 586264, upload-time = "2026-08-23T01:42:05.106Z" }, ] [[package]] @@ -1330,34 +1344,35 @@ pycryptodome = [ [[package]] name = "eth-keyfile" -version = "0.8.1" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eth-keys" }, { name = "eth-utils" }, + { name = "py-ecc" }, { name = "pycryptodome" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/66/dd823b1537befefbbff602e2ada88f1477c5b40ec3731e3d9bc676c5f716/eth_keyfile-0.8.1.tar.gz", hash = "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1", size = 12267, upload-time = "2024-04-23T20:28:53.862Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/e1/eb8cc218abd7e7ee8eeafbb9c1deef17e9fdda9c3f39af23479899745136/eth_keyfile-0.10.0.tar.gz", hash = "sha256:3003b20000d68203e8fbf45456851a524f859a7432d2fae73be4a9aebeb4b8e1", size = 19864, upload-time = "2026-08-21T17:21:46.996Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/fc/48a586175f847dd9e05e5b8994d2fe8336098781ec2e9836a2ad94280281/eth_keyfile-0.8.1-py3-none-any.whl", hash = "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64", size = 7510, upload-time = "2024-04-23T20:28:51.063Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8d/3a4b86db08c99d0b43b2e4e37ddd658252fb8794e447eb64c9c84d2960dc/eth_keyfile-0.10.0-py3-none-any.whl", hash = "sha256:6b8b1e2528ecd3c53f9f733c061a8ddc7aa40b689f15c2f38b73ee6fc5d274fd", size = 9484, upload-time = "2026-08-21T17:21:45.855Z" }, ] [[package]] name = "eth-keys" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eth-typing" }, { name = "eth-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/11/1ed831c50bd74f57829aa06e58bd82a809c37e070ee501c953b9ac1f1552/eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814", size = 30166, upload-time = "2025-04-07T17:40:21.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/58/f54660cffe3f39aad2d80d13b072973ee9134b6cdfd8b4d086419eda997b/eth_keys-0.8.0.tar.gz", hash = "sha256:11549b251876fccd7caedd6905e494ea2309aec352ec2579b00ef9978017a964", size = 31311, upload-time = "2026-08-21T17:01:21.923Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/25/0ae00f2b0095e559d61ad3dc32171bd5a29dfd95ab04b4edd641f7c75f72/eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf", size = 20656, upload-time = "2025-04-07T17:40:20.441Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f5/24806598664a2fbc65d5ed4f0e9e986e7f9061e3b43738412c8df7ba1cbd/eth_keys-0.8.0-py3-none-any.whl", hash = "sha256:a7b94222638cccbdf2b5dae5c365d883a96826d82bb0faeb56baa65375f514ae", size = 20461, upload-time = "2026-08-21T17:01:20.767Z" }, ] [[package]] name = "eth-rlp" -version = "2.2.0" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eth-utils" }, @@ -1365,9 +1380,9 @@ dependencies = [ { name = "rlp" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/ea/ad39d001fa9fed07fad66edb00af701e29b48be0ed44a3bcf58cb3adf130/eth_rlp-2.2.0.tar.gz", hash = "sha256:5e4b2eb1b8213e303d6a232dfe35ab8c29e2d3051b86e8d359def80cd21db83d", size = 7720, upload-time = "2025-02-04T21:51:08.134Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/e1/9719acaa45e6f158ebfc260a97edc71591264c1d09701cf8a30a687a36b0/eth_rlp-3.0.0.tar.gz", hash = "sha256:9663e54a4a1c1c847d2d328c1d07e4174ec1c082953fbb42b60e61c501c4931c", size = 17981, upload-time = "2026-08-22T22:01:51.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/3b/57efe2bc2df0980680d57c01a36516cd3171d2319ceb30e675de19fc2cc5/eth_rlp-2.2.0-py3-none-any.whl", hash = "sha256:5692d595a741fbaef1203db6a2fedffbd2506d31455a6ad378c8449ee5985c47", size = 4446, upload-time = "2025-02-04T21:51:05.823Z" }, + { url = "https://files.pythonhosted.org/packages/ab/35/a3071f2a7ee701f99c8562865edbc426991d0964543d2947901b4135f259/eth_rlp-3.0.0-py3-none-any.whl", hash = "sha256:32f355261c36ad2c369db098170f873123795667e50d496b034f369e2c9d2346", size = 15221, upload-time = "2026-08-22T22:01:50.235Z" }, ] [[package]] @@ -1678,20 +1693,20 @@ wheels = [ [[package]] name = "hexbytes" -version = "1.3.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7f/87/adf4635b4b8c050283d74e6db9a81496063229c9263e6acc1903ab79fbec/hexbytes-1.3.1.tar.gz", hash = "sha256:a657eebebdfe27254336f98d8af6e2236f3f83aed164b87466b6cf6c5f5a4765", size = 8633, upload-time = "2025-05-14T16:45:17.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/4f/eabe45c58f2d27cd0b338ecc41b0b475a3751ed70eb1a21db08497e3ceec/hexbytes-2.0.0.tar.gz", hash = "sha256:01312fcd5c57e8a8d2d7dd3274dcf84ea50422aff2abcc2d9fd89ad6a32498e5", size = 21344, upload-time = "2026-08-21T20:22:09.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/e0/3b31492b1c89da3c5a846680517871455b30c54738486fc57ac79a5761bd/hexbytes-1.3.1-py3-none-any.whl", hash = "sha256:da01ff24a1a9a2b1881c4b85f0e9f9b0f51b526b379ffa23832ae7899d29c2c7", size = 5074, upload-time = "2025-05-14T16:45:16.179Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/c7049aabd9e05efebc29b10208b11e5ae5cf819f48685ffced6abf871d10/hexbytes-2.0.0-py3-none-any.whl", hash = "sha256:5425bd7ac83cdd9791c13a5bf97cfe9b9609a304b1ef3ab146adfd50de06cf0e", size = 18866, upload-time = "2026-08-21T20:22:08.654Z" }, ] [[package]] name = "idna" -version = "3.11" +version = "3.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] @@ -2983,6 +2998,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "py-ecc" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/96/e73075d5c885274efada2fbc5db6377022036c2f5b4b470dbcf4106e07d5/py_ecc-8.0.0.tar.gz", hash = "sha256:56aca19e5dc37294f60c1cc76666c03c2276e7666412b9a559fa0145d099933d", size = 51193, upload-time = "2025-04-14T16:14:03.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/58/383335eac96d2f1aba78741c6ce128c54e7eba2ea1dc47408257d751d35c/py_ecc-8.0.0-py3-none-any.whl", hash = "sha256:c0b2dfc4bde67a55122a392591a10e851a986d5128f680628c80b405f7663e13", size = 47814, upload-time = "2025-04-14T16:14:01.827Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -3886,11 +3914,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] From efa4fe9475d885a122eb0b1d5b8425402ea845f2 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 14:47:37 -0700 Subject: [PATCH 8/8] ci: audit the locked dependency set in the locked-env job Adds pip-audit (pinned) over the uv.lock environment, so a vulnerable pin in the lockfile fails CI, not only a vulnerable latest release. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012cD3fDq5vhh2YWZPU2SV6H --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71bfd61..6f99bbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,8 @@ jobs: run: uv run --frozen mypy . - name: Test (pytest, locked dependencies) run: uv run --frozen pytest -q + - name: Dependency audit of the locked set (pip-audit) + run: uv run --frozen --with "pip-audit==2.10.0" pip-audit --skip-editable # PBA-L6b-003 tripwire on the BUILT wheel: build it the way a release does, # import citrate_sdk from the unpacked wheel only, and assert that no subset