From c61030d190b517c3ca37e9e2c475df9ed5cbbc0b Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:10:44 -0700 Subject: [PATCH 01/12] fix(crypto): never publish Shamir key shares in deploy calldata (PBA-L6b-003) deploy_model with threshold_shares > 0 put every share of the model AES key into tx_data["encryption_metadata"]["key_shares"], i.e. public calldata. Anyone reading the chain rebuilt the key and decrypted the uploaded model. - encrypt_model refuses threshold_shares > 0; the new encrypt_model_with_key_shares requires share_holder_public_keys (one distinct secp256k1 key per share), ECDH-wraps each share to its holder (existing V2 envelope) and returns the envelopes APART from the public metadata. deploy_model hands them back on ModelDeployment .key_share_envelopes for off-chain delivery; the metadata carries only {threshold, total_shares}. - KeyManager.unwrap_key_share (holder side, owner-pinned) and reconstruct_key_from_shares(shares, threshold) with the threshold taken from the caller, never the share. - deploy_model runs assert_no_key_share_material over the whole tx payload (caller metadata included) before signing. - Variant (PBA-L4-005 twin): finite_field validates every share (integer x in 1..255, distinct, equal non-empty y), verify_shares now checks extra shares lie on the polynomial, Lagrange is general in x. - examples/encrypted_inference.py uses holder keys and off-chain shares. - Tripwires: scripts/keyshare_leak_tripwire.py (independent GF(2^8)); tests decode the signed raw tx and assert no calldata subset rebuilds the key; scripts/check_keyshare_wheel.py runs the same against the built wheel in CI and before publish (fails on the published 0.6.0 wheel). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- .github/workflows/ci.yml | 15 ++ .github/workflows/publish.yml | 7 + citrate_sdk/client.py | 23 ++- citrate_sdk/crypto.py | 252 ++++++++++++++++++++++++----- citrate_sdk/finite_field.py | 117 ++++++++------ citrate_sdk/models.py | 3 + examples/encrypted_inference.py | 34 +++- scripts/check_keyshare_wheel.py | 124 ++++++++++++++ scripts/keyshare_leak_tripwire.py | 146 +++++++++++++++++ tests/test_crypto.py | 29 ++-- tests/test_pba_r2_l6b_keyshares.py | 246 ++++++++++++++++++++++++++++ 11 files changed, 881 insertions(+), 115 deletions(-) create mode 100644 scripts/check_keyshare_wheel.py create mode 100644 scripts/keyshare_leak_tripwire.py create mode 100644 tests/test_pba_r2_l6b_keyshares.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bdf9b0..85ad896 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,21 @@ jobs: python: uses: CitrateNetwork/.github/.github/workflows/reusable-python-ci.yml@db5f1a6ddaec28275708265b56762eb1dfc3571b # main + # 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 + # twin is tests/test_pba_r2_l6b_keyshares.py. + keyshare-wheel-tripwire: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install --upgrade pip && pip install -e . + - name: Key-share leak tripwire (built wheel) + run: python scripts/check_keyshare_wheel.py + # CL-C2 / SW-092: fail the build if the vendored federation_contract artifact # drifts from the canonical federation intermediate (which federation's # contract-artifact-drift gate keeps fresh vs citrate-chain). The reusable diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index facda9b..50d1f72 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -81,6 +81,13 @@ jobs: - name: Check package run: twine check dist/* + # PBA-L6b-003: refuse to publish a wheel whose deploy_model calldata can + # rebuild the model key. Runs against the built wheel, not the source tree. + - name: Key-share leak tripwire (built wheel) + run: | + pip install -e . + python scripts/check_keyshare_wheel.py dist/*.whl + - name: Upload artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: diff --git a/citrate_sdk/client.py b/citrate_sdk/client.py index 96368a8..4f2175a 100644 --- a/citrate_sdk/client.py +++ b/citrate_sdk/client.py @@ -13,7 +13,7 @@ from ._generated import contract as _contract from ._url_security import enforce_transport_security -from .crypto import EncryptionConfig, KeyManager +from .crypto import EncryptionConfig, KeyManager, assert_no_key_share_material from .errors import CitrateError, ModelNotFoundError from .ipfs import upload_to_ipfs from .models import InferenceRequest, InferenceResult, ModelConfig, ModelDeployment @@ -197,14 +197,22 @@ def deploy_model( # Encrypt model if requested encrypted_data = None encryption_metadata = None + key_share_envelopes: list[dict[str, Any]] | None = None if config.encrypted: if not config.encryption_config: config.encryption_config = EncryptionConfig() - encrypted_data, encryption_metadata = self.key_manager.encrypt_model( - model_data, config.encryption_config - ) + if config.encryption_config.threshold_shares: + # PBA-L6b-003: shares come back wrapped to their holders and + # separate from the public metadata; they never enter calldata. + encrypted_data, encryption_metadata, key_share_envelopes = ( + self.key_manager.encrypt_model_with_key_shares(model_data, config.encryption_config) + ) + else: + encrypted_data, encryption_metadata = self.key_manager.encrypt_model( + model_data, config.encryption_config + ) # Upload to IPFS ipfs_hash = self._upload_to_ipfs(encrypted_data or model_data) @@ -222,6 +230,10 @@ def deploy_model( if encryption_metadata: tx_data["encryption_metadata"] = encryption_metadata + # PBA-L6b-003: this calldata is public. Refuse to send if anything in it, + # including caller-supplied metadata, carries a key-share field. + assert_no_key_share_material(tx_data) + # Call model deployment precompile. SPY-B-007: the address is read from # the vendored canonical table (ModelDeploy = 0x..0100), NOT a hardcoded # `0x0100..0100` literal. The pre-fix literals were wrong in the high byte @@ -241,7 +253,8 @@ def deploy_model( ipfs_hash=ipfs_hash, encrypted=config.encrypted, access_price=config.access_price, - deployment_time=int(time.time()) + deployment_time=int(time.time()), + key_share_envelopes=key_share_envelopes, ) def inference( diff --git a/citrate_sdk/crypto.py b/citrate_sdk/crypto.py index 9b3ebce..c98d376 100644 --- a/citrate_sdk/crypto.py +++ b/citrate_sdk/crypto.py @@ -9,7 +9,8 @@ from typing import Any, cast from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.kdf.hkdf import HKDF from eth_account import Account @@ -19,6 +20,48 @@ from .errors import CitrateError from .finite_field import reconstruct_secret_bytes, split_secret_bytes +#: Field names that only ever hold Shamir share material (PBA-L6b-003). Deploy +#: calldata is public; ``CitrateClient.deploy_model`` refuses a payload that +#: carries any of them, at any depth. +SHARE_FIELD_DENYLIST = frozenset({"key_shares", "keyShares", "key_share_envelopes", "keyShareEnvelopes"}) +_MAX_GUARD_DEPTH = 32 + + +def assert_no_key_share_material(value: Any, _depth: int = 0) -> None: + """Raise CitrateError if ``value`` carries a key-share field (PBA-L6b-003).""" + if isinstance(value, dict): + items = list(value.items()) + elif isinstance(value, (list, tuple)): + items = [(None, v) for v in value] + else: + return + if _depth > _MAX_GUARD_DEPTH: + raise CitrateError( + f"deploy_model: metadata is nested more than {_MAX_GUARD_DEPTH} levels deep; refusing to " + "publish calldata that cannot be fully checked for key-share material (PBA-L6b-003)." + ) + for k, v in items: + if k in SHARE_FIELD_DENYLIST: + raise CitrateError( + f"deploy_model: refusing to publish key-share material ({k!r}) in public deploy " + "calldata. Deliver key shares to their holders off-chain (PBA-L6b-003)." + ) + assert_no_key_share_material(v, _depth + 1) + + +def canonical_public_key_hex(public_key: str) -> str: + """Normalise a secp256k1 public key (hex, 0x optional, SEC1 compressed or + uncompressed) to uncompressed lowercase hex. Raises CitrateError if it is + not a valid curve point.""" + try: + raw = bytes.fromhex(public_key[2:] if public_key.startswith("0x") else public_key) + point = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), raw) + except Exception: + raise CitrateError("invalid secp256k1 public key") + return point.public_bytes( + serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint + ).hex() + def _zeroize(buf: bytearray | None) -> None: """Best-effort in-place wipe of key material (SECREM-02 K3 / @@ -129,31 +172,113 @@ def encrypt_model( config: Encryption configuration Returns: - Tuple of (encrypted_data, encryption_metadata) + Tuple of (encrypted_data, encryption_metadata). The metadata is + PUBLIC (``deploy_model`` writes it to calldata) and never carries key + material. + + Raises: + CitrateError: if ``config.threshold_shares > 0``. Threshold sharing + goes through :meth:`encrypt_model_with_key_shares`, which returns + the holder-wrapped shares separately from the metadata + (PBA-L6b-003). """ - # Generate random key and nonce - key = secrets.token_bytes(32) # 256-bit key - nonce = secrets.token_bytes(12) # 96-bit nonce for GCM + if config.threshold_shares: + raise CitrateError( + "encrypt_model: threshold_shares > 0 needs encrypt_model_with_key_shares(), " + "which wraps each share to a named holder and returns the shares apart " + "from the public metadata. Shares are never placed in metadata (PBA-L6b-003)." + ) + ciphertext, metadata, _ = self._encrypt_model(model_data, config, None) + return ciphertext, metadata - # Encrypt data - aesgcm = AESGCM(key) - ciphertext = aesgcm.encrypt(nonce, model_data, None) + def encrypt_model_with_key_shares( + self, + model_data: bytes, + config: 'EncryptionConfig' + ) -> tuple[bytes, dict[str, Any], list[dict[str, Any]]]: + """ + Encrypt a model and split its key for threshold recovery (PBA-L6b-003). - # Create encryption metadata - metadata = { - "algorithm": config.algorithm, - "nonce": nonce.hex(), - "key_derivation": config.key_derivation, - "encrypted_key": self._encrypt_key_for_owner(key), - "access_control": config.access_control - } + Each Shamir share is ECDH-wrapped (V2 envelope) to one of + ``config.share_holder_public_keys`` and returned as the third element. + Deliver each envelope to its holder OFF-CHAIN. The returned metadata + carries only the share parameters, never share values. - # Add threshold sharing if enabled - if config.threshold_shares > 0: - key_shares = self._create_key_shares(key, config.threshold_shares, config.total_shares) - metadata["key_shares"] = key_shares + Returns: + (encrypted_data, public_metadata, key_share_envelopes) + """ + plan = self._plan_key_sharing(config) + if plan is None: + raise CitrateError( + "encrypt_model_with_key_shares: threshold_shares must be > 0; use encrypt_model()." + ) + return self._encrypt_model(model_data, config, plan) - return ciphertext, metadata + def _encrypt_model( + self, + model_data: bytes, + config: 'EncryptionConfig', + plan: tuple[int, int, list[str]] | None, + ) -> tuple[bytes, dict[str, Any], list[dict[str, Any]]]: + key = bytearray(secrets.token_bytes(32)) # 256-bit key + nonce = secrets.token_bytes(12) # 96-bit nonce for GCM + try: + ciphertext = AESGCM(bytes(key)).encrypt(nonce, model_data, None) + # Everything in here is PUBLIC: deploy_model writes it to calldata. + metadata: dict[str, Any] = { + "algorithm": config.algorithm, + "nonce": nonce.hex(), + "key_derivation": config.key_derivation, + "encrypted_key": self._encrypt_key_for_owner(bytes(key)), + "access_control": config.access_control + } + envelopes: list[dict[str, Any]] = [] + if plan is not None: + threshold, total, holders = plan + # PBA-L6b-003: the old code put every raw share into the metadata, + # i.e. into public calldata. Shares are now wrapped to holders and + # returned beside the metadata, never inside it. + envelopes = self._create_key_shares(bytes(key), threshold, total, holders) + metadata["key_sharing"] = {"threshold": threshold, "total_shares": total} + return ciphertext, metadata, envelopes + finally: + _zeroize(key) + + @staticmethod + def _plan_key_sharing(config: 'EncryptionConfig') -> tuple[int, int, list[str]] | None: + """Validate a threshold-sharing request (PBA-L6b-003). None when off.""" + threshold = config.threshold_shares + if not threshold: + return None + total = config.total_shares + if (isinstance(threshold, bool) or isinstance(total, bool) + or not isinstance(threshold, int) or not isinstance(total, int) + or threshold < 1 or threshold > total or total > 255): + raise CitrateError( + f"encrypt_model: invalid share parameters (threshold_shares={threshold!r}, " + f"total_shares={total!r}); need integers with 1 <= threshold_shares <= " + "total_shares <= 255." + ) + holders = config.share_holder_public_keys + if not holders: + raise CitrateError( + "encrypt_model: threshold_shares > 0 requires share_holder_public_keys. Key " + "shares are never written to deploy metadata (it is public calldata); each " + "share is wrapped to a named holder key and returned for off-chain delivery " + "(PBA-L6b-003). Pass one holder public key per share, or set threshold_shares to 0." + ) + if len(holders) != total: + raise CitrateError( + f"encrypt_model: need one holder public key per share (total_shares={total}, " + f"share_holder_public_keys has {len(holders)}) (PBA-L6b-003)." + ) + canonical = [canonical_public_key_hex(h) for h in holders] + if len(set(canonical)) != len(canonical): + raise CitrateError( + "encrypt_model: share_holder_public_keys must be distinct; one holder with " + "several shares defeats the threshold (PBA-L6b-003)." + ) + return threshold, total, canonical def decrypt_model( self, @@ -522,37 +647,70 @@ def _decrypt_key_from_owner(self, encrypted_key_package: str) -> bytes: aesgcm = AESGCM(owner_key) return aesgcm.decrypt(nonce, encrypted_key, None) - def _create_key_shares(self, key: bytes, threshold: int, total: int) -> list[dict[str, str]]: - """Create Shamir's secret shares for key using proper finite field arithmetic""" - shares_tuples = split_secret_bytes(key, threshold, total) + def _create_key_shares( + self, key: bytes, threshold: int, total: int, holder_public_keys: list[str] + ) -> list[dict[str, Any]]: + """Split ``key`` and ECDH-wrap share i to holder i (PBA-L6b-003). - shares = [] - for x, share_bytes in shares_tuples: - shares.append({ - "x": str(x), - "y": share_bytes.hex(), - "threshold": str(threshold) + Returns one record per share: ``x`` and ``threshold`` (not secret), the + holder's public key, and the V2 ``envelope`` holding the share value. + Raw share values never leave this function. + """ + if len(holder_public_keys) != total: + raise CitrateError("_create_key_shares: need exactly one holder public key per share") + records: list[dict[str, Any]] = [] + for (x, share_bytes), holder in zip(split_secret_bytes(key, threshold, total), holder_public_keys, strict=True): + envelope = self.encrypt_data(share_bytes.hex(), holder) + records.append({ + "x": x, + "threshold": threshold, + "holder_public_key": holder, + "envelope": envelope, }) + return records - return shares + def unwrap_key_share(self, share: dict[str, Any], owner_public_key: str) -> dict[str, str]: + """Holder side of PBA-L6b-003: open a key-share envelope addressed to this key. - def reconstruct_key_from_shares(self, shares: list[dict[str, str]]) -> bytes: - """Reconstruct key from Shamir's shares using proper Lagrange interpolation""" + ``owner_public_key`` pins the sender (the model owner); an envelope from + anyone else is refused. Returns the share in the form + :meth:`reconstruct_key_from_shares` takes. + """ + mine = self.ecdh_manager.get_public_key_uncompressed().hex() + if not hmac.compare_digest(canonical_public_key_hex(str(share["holder_public_key"])), mine): + raise CitrateError("unwrap_key_share: this share is addressed to a different holder key.") + y = self.decrypt_data(share["envelope"], canonical_public_key_hex(owner_public_key)) + return {"x": str(share["x"]), "y": y, "threshold": str(share["threshold"])} + + def reconstruct_key_from_shares(self, shares: list[dict[str, str]], threshold: int) -> bytes: + """Reconstruct a key from Shamir shares (Lagrange interpolation). + + PBA-L4-005 / PBA-L6b-003: ``threshold`` comes from the CALLER, never + from the shares (an attacker-supplied share could lower it), and share + x values are validated before interpolation. + """ + if isinstance(threshold, bool) or not isinstance(threshold, int) or not 1 <= threshold <= 255: + raise CitrateError(f"reconstruct_key_from_shares: threshold must be an integer in 1..255, got {threshold!r}") if not shares: raise CitrateError("No shares provided") - - threshold = int(shares[0]["threshold"]) if len(shares) < threshold: raise CitrateError("Insufficient shares for key reconstruction") - # Convert shares back to tuples format shares_tuples = [] for share in shares: - x = int(share["x"]) - y = bytes.fromhex(share["y"]) - shares_tuples.append((x, y)) + x_text = str(share["x"]) + 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"]) + except ValueError: + raise CitrateError("Invalid share: y is not hex") + shares_tuples.append((int(x_text), y)) - return reconstruct_secret_bytes(shares_tuples, threshold) + try: + return reconstruct_secret_bytes(shares_tuples, threshold) + except ValueError as e: + raise CitrateError(str(e)) class EncryptionConfig: @@ -564,13 +722,25 @@ def __init__( key_derivation: str = "HKDF-SHA256", access_control: bool = True, threshold_shares: int = 0, - total_shares: int = 0 + total_shares: int = 0, + share_holder_public_keys: list[str] | None = None, ): + """ + Args: + threshold_shares: Shamir threshold for splitting the model key; 0 + (default) disables key sharing. + total_shares: number of shares. + share_holder_public_keys: REQUIRED when ``threshold_shares > 0`` + (PBA-L6b-003): one distinct secp256k1 public key per share. Each + share is ECDH-wrapped to its holder and returned for off-chain + delivery; shares are never written to deploy calldata. + """ self.algorithm = algorithm self.key_derivation = key_derivation self.access_control = access_control self.threshold_shares = threshold_shares self.total_shares = total_shares + self.share_holder_public_keys = share_holder_public_keys def generate_model_key() -> str: diff --git a/citrate_sdk/finite_field.py b/citrate_sdk/finite_field.py index 95e5bae..f3c6ad4 100644 --- a/citrate_sdk/finite_field.py +++ b/citrate_sdk/finite_field.py @@ -121,6 +121,9 @@ def __init__(self, threshold: int, total_shares: int): threshold: Minimum number of shares needed to reconstruct total_shares: Total number of shares to create """ + if isinstance(threshold, bool) or isinstance(total_shares, bool) or \ + not isinstance(threshold, int) or not isinstance(total_shares, int): + raise ValueError("Threshold and total shares must be integers") if threshold <= 0: raise ValueError("Threshold must be positive") if threshold > total_shares: @@ -161,6 +164,37 @@ def split_secret(self, secret: bytes) -> list[tuple[int, bytes]]: return shares + @staticmethod + def validate_shares(shares: list[tuple[int, bytes]]) -> None: + """Validate a share set before any interpolation (PBA-L4-005 variant). + + Every share, not only the first ``threshold``, must have an integer x in + 1..255 (x = 0 is the secret itself: a share there dictates the output), + x values must be distinct, and every y must be non-empty bytes of one + common length. Raises ValueError on the first violation. + """ + if not shares: + raise ValueError("No shares provided") + seen: set[int] = set() + length: int | None = None + for share in shares: + if not isinstance(share, tuple) or len(share) != 2: + raise ValueError("Invalid share: expected an (x, y) tuple") + x, y = share + if isinstance(x, bool) or not isinstance(x, int) or x < 1 or x > 255: + raise ValueError(f"Invalid share: x must be an integer in 1..255, got {x!r}") + if x in seen: + raise ValueError(f"Invalid share set: duplicate share x = {x}") + seen.add(x) + if not isinstance(y, (bytes, bytearray)): + raise ValueError("Invalid share: y must be bytes") + if length is None: + length = len(y) + elif len(y) != length: + raise ValueError("Invalid share set: all shares must have the same length") + if not length: + raise ValueError("Invalid share set: share y is empty") + def reconstruct_secret(self, shares: list[tuple[int, bytes]]) -> bytes: """ Reconstruct secret from shares @@ -171,26 +205,19 @@ def reconstruct_secret(self, shares: list[tuple[int, bytes]]) -> bytes: Returns: Reconstructed secret bytes """ + self.validate_shares(shares) if len(shares) < self.threshold: raise ValueError(f"Need at least {self.threshold} shares, got {len(shares)}") # Use first threshold shares active_shares = shares[:self.threshold] - - # Ensure all shares have same length share_length = len(active_shares[0][1]) - if not all(len(share[1]) == share_length for share in active_shares): - raise ValueError("All shares must have the same length") - # Reconstruct each byte position secret_bytes = [] for byte_pos in range(share_length): - # Extract byte values for this position points = [(x, share_bytes[byte_pos]) for x, share_bytes in active_shares] - - # Use Lagrange interpolation to find f(0) - reconstructed_byte = self._lagrange_interpolation(points, 0) - secret_bytes.append(reconstructed_byte) + # Lagrange interpolation at x = 0 + secret_bytes.append(self._lagrange_interpolation(points, 0)) return bytes(secret_bytes) @@ -215,61 +242,49 @@ def _evaluate_polynomial_at_point(self, polynomials: list[list[int]], x: int) -> def _lagrange_interpolation(self, points: list[tuple[int, int]], x: int) -> int: """ - Lagrange interpolation to find f(x) given points - - Args: - points: List of (x_i, y_i) points - x: Point to evaluate at + Lagrange interpolation: f(x) from the given points. - Returns: - f(x) value + Callers validate first (``validate_shares``): x values are distinct + integers in 1..255, so every denominator is nonzero; ``GF256.divide`` + still raises on a zero divisor as a backstop. """ result = 0 - for i, (x_i, y_i) in enumerate(points): - # Calculate Lagrange basis polynomial L_i(x) numerator = 1 denominator = 1 - for j, (x_j, _) in enumerate(points): - if i != j: - # For x=0, numerator becomes (0 - x_j) = -x_j = x_j (in GF(2^8)) - numerator = GF256.multiply(numerator, x_j) - denominator = GF256.multiply(denominator, GF256.subtract(x_i, x_j)) - - # L_i(x) = numerator / denominator - if denominator == 0: - raise ValueError("Denominator is zero in Lagrange interpolation") - - lagrange_coeff = GF256.divide(numerator, denominator) - - # Add y_i * L_i(x) to result - result = GF256.add(result, GF256.multiply(y_i, lagrange_coeff)) - + if i == j: + continue + # (x - x_j) = x XOR x_j in GF(2^8); at x = 0 this is x_j. + numerator = GF256.multiply(numerator, GF256.subtract(x, x_j)) + denominator = GF256.multiply(denominator, GF256.subtract(x_i, x_j)) + result = GF256.add(result, GF256.multiply(y_i, GF256.divide(numerator, denominator))) return result def verify_shares(self, shares: list[tuple[int, bytes]]) -> bool: """ - Verify that shares are consistent (can be used to detect tampering) - - Args: - shares: List of shares to verify - - Returns: - True if shares are consistent + Check that a share set is structurally valid and CONSISTENT: every share + beyond the first ``threshold`` must lie on the polynomial those first + ``threshold`` shares define (PBA-L4-005 variant; the old version only + checked that interpolation did not raise). + + With exactly ``threshold`` shares there is no redundancy, so any + structurally valid set is consistent by definition and a forged share + cannot be detected. Use verifiable secret sharing when that matters. """ - if len(shares) < self.threshold: - return False - try: - # Try to reconstruct with different combinations - for i in range(len(shares) - self.threshold + 1): - test_shares = shares[i:i + self.threshold] - self.reconstruct_secret(test_shares) - return True - - except Exception: + self.validate_shares(shares) + except ValueError: + return False + if len(shares) < self.threshold: return False + base = shares[:self.threshold] + for x_extra, y_extra in shares[self.threshold:]: + for b, expected in enumerate(y_extra): + points = [(x, y[b]) for x, y in base] + if self._lagrange_interpolation(points, x_extra) != expected: + return False + return True def split_secret_bytes(secret: bytes, threshold: int, total_shares: int) -> list[tuple[int, bytes]]: diff --git a/citrate_sdk/models.py b/citrate_sdk/models.py index 49a9fef..2b16c27 100644 --- a/citrate_sdk/models.py +++ b/citrate_sdk/models.py @@ -73,6 +73,9 @@ class ModelDeployment: deployment_time: int gas_used: int | None = None deployment_cost: int | None = None + #: PBA-L6b-003: holder-wrapped key shares when threshold sharing was + #: requested. Deliver each to its holder off-chain; none of it is on-chain. + key_share_envelopes: list[dict[str, Any]] | None = None @dataclass diff --git a/examples/encrypted_inference.py b/examples/encrypted_inference.py index 700b005..b634b2e 100644 --- a/examples/encrypted_inference.py +++ b/examples/encrypted_inference.py @@ -63,13 +63,26 @@ def main() -> None: print(f"Created sensitive model: {model_path}") - # Configure encryption with threshold sharing + # Key-share holders (simulating three other parties). In production these + # are the holders' own public keys; their private keys never leave them. + alice, bob, carol = KeyManager(), KeyManager(), KeyManager() + + # Configure encryption with threshold sharing. PBA-L6b-003: every share is + # ECDH-wrapped to one named holder and returned on the deployment result for + # OFF-CHAIN delivery. Shares are never written to the deploy transaction + # (its calldata is public). threshold_shares > 0 without holder keys is + # refused. encryption_config = EncryptionConfig( algorithm="AES-256-GCM", key_derivation="HKDF-SHA256", access_control=True, - threshold_shares=2, # Need 2 shares to decrypt - total_shares=3 # Create 3 total shares + threshold_shares=2, # Need 2 shares to rebuild the model key + total_shares=3, # One share per holder + share_holder_public_keys=[ + alice.get_public_key(), + bob.get_public_key(), + carol.get_public_key(), + ], ) # Configure encrypted model deployment @@ -98,12 +111,17 @@ def main() -> None: print(f"Transaction: {deployment.tx_hash}") print(f"Encrypted: {deployment.encrypted}") - # Demonstrate key sharing scenario + # Demonstrate key sharing: deliver each wrapped share to its holder + # off-chain (e.g. over an authenticated channel); any 2 holders can + # then rebuild the model key. Nothing here touched the chain. print("\n🔑 Demonstrating key sharing...") - - # Create additional key managers (simulating other parties) - alice = KeyManager() - bob = KeyManager() + envelopes = deployment.key_share_envelopes or [] + print(f"Wrapped key shares to deliver off-chain: {len(envelopes)}") + owner_pubkey = client.key_manager.get_public_key() + share_a = alice.unwrap_key_share(envelopes[0], owner_pubkey) + share_b = bob.unwrap_key_share(envelopes[1], owner_pubkey) + rebuilt = alice.reconstruct_key_from_shares([share_a, share_b], threshold=2) + print(f"Alice + Bob rebuilt a {len(rebuilt)}-byte model key") print(f"Alice address: {alice.get_address()}") print(f"Bob address: {bob.get_address()}") diff --git a/scripts/check_keyshare_wheel.py b/scripts/check_keyshare_wheel.py new file mode 100644 index 0000000..4bafe0c --- /dev/null +++ b/scripts/check_keyshare_wheel.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""PBA-L6b-003 tripwire against the BUILT wheel, not the source tree. + +Builds the wheel exactly as a release would (``pip wheel --no-deps``), or takes +one as an argument, unpacks it, imports ``citrate_sdk`` from the unpacked wheel +only, and drives the public ``CitrateClient.deploy_model`` with a capturing RPC: + +1. ``threshold_shares > 0`` without holder keys must be refused before any send; +2. with holder keys, no subset of the RLP-decoded deploy calldata may rebuild + the model key (scripts/keyshare_leak_tripwire.py); +3. the holders must still rebuild the key from their envelopes. + +Usage: python scripts/check_keyshare_wheel.py [path/to/citrate_labs_sdk-*.whl] +""" +from __future__ import annotations + +import importlib.util +import json +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] + + +def fail(msg: str) -> None: + print(f"PBA-L6b-003 wheel tripwire FAILED: {msg}", file=sys.stderr) + sys.exit(1) + + +def main() -> None: + work = Path(tempfile.mkdtemp(prefix="keyshare-wheel-")) + if len(sys.argv) > 1: + wheel = Path(sys.argv[1]).resolve() + else: + subprocess.run([sys.executable, "-m", "pip", "wheel", "--no-deps", "-q", "-w", str(work), str(ROOT)], check=True) + wheels = sorted(work.glob("citrate_labs_sdk-*.whl")) + if not wheels: + fail("pip wheel produced no citrate_labs_sdk wheel") + wheel = wheels[-1] + unpacked = work / "unpacked" + with zipfile.ZipFile(wheel) as z: + z.extractall(unpacked) + sys.path.insert(0, str(unpacked)) + for mod in [m for m in sys.modules if m == "citrate_sdk" or m.startswith("citrate_sdk.")]: + del sys.modules[mod] + import citrate_sdk # noqa: E402 + if not Path(citrate_sdk.__file__).resolve().is_relative_to(unpacked.resolve()): + fail(f"imported citrate_sdk from {citrate_sdk.__file__}, not from the wheel") + import rlp # type: ignore[import-untyped] # noqa: E402 + + from citrate_sdk import CitrateClient, KeyManager # noqa: E402 + from citrate_sdk.crypto import EncryptionConfig # noqa: E402 + from citrate_sdk.errors import CitrateError # noqa: E402 + from citrate_sdk.models import ModelConfig # noqa: E402 + + spec = importlib.util.spec_from_file_location("tw", ROOT / "scripts" / "keyshare_leak_tripwire.py") + assert spec and spec.loader + tw = importlib.util.module_from_spec(spec) + spec.loader.exec_module(tw) + + owner = "0x" + "11" * 32 + holders = ["0x" + "22" * 32, "0x" + "33" * 32, "0x" + "44" * 32] + model = work / "m.onnx" + model.write_bytes(b"SECRET-MODEL-WEIGHTS" * 100) + box: dict[str, Any] = {} + + def rpc(method: str, params: Any = None) -> Any: + if method == "eth_getTransactionCount": + return "0x0" + if method == "eth_chainId": + return hex(40204) + if method == "eth_gasPrice": + return "0x1" + if method == "eth_sendRawTransaction": + box["raw"] = params[0] + return "0x" + "ab" * 32 + raise AssertionError(method) + + client = CitrateClient("http://localhost:8545", private_key=owner) + stubs: dict[str, Any] = { + "_rpc_call": rpc, + "_upload_to_ipfs": lambda data: "bafyFAKE", + "_wait_for_receipt": lambda h: {"logs": []}, + "_extract_model_id_from_receipt": lambda r: "0x01", + } + for name, fn in stubs.items(): # offline: RPC and IPFS are stubbed + setattr(client, name, fn) + + try: + client.deploy_model(model, ModelConfig(name="m", encrypted=True, encryption_config=EncryptionConfig( + threshold_shares=2, total_shares=3))) + fail("threshold_shares > 0 without share_holder_public_keys was not refused") + except CitrateError: + pass + if "raw" in box: + fail("a transaction was sent for the refused deploy") + + pubs = [KeyManager(k).get_public_key() for k in holders] + dep = client.deploy_model(model, ModelConfig(name="m", encrypted=True, encryption_config=EncryptionConfig( + threshold_shares=2, total_shares=3, share_holder_public_keys=pubs))) + calldata = bytes(rlp.decode(bytes.fromhex(box["raw"][2:]))[5]) + meta = json.loads(calldata.decode())["encryption_metadata"] + key = KeyManager(owner)._decrypt_key_from_owner(meta["encrypted_key"]) + reveals, how = tw.reveals_key(calldata, key) + if reveals: + fail(f"deploy calldata reveals the model key ({how})") + + envs = dep.key_share_envelopes or [] + if len(envs) != 3: + fail(f"expected 3 key-share envelopes, got {len(envs)}") + owner_pub = KeyManager(owner).get_public_key() + s0 = KeyManager(holders[0]).unwrap_key_share(envs[0], owner_pub) + s1 = KeyManager(holders[1]).unwrap_key_share(envs[1], owner_pub) + if KeyManager(holders[0]).reconstruct_key_from_shares([s0, s1], threshold=2) != key: + fail("holders could not rebuild the key") + print(f"PBA-L6b-003 wheel tripwire PASSED for {wheel.name} (citrate_sdk {citrate_sdk.__version__})") + + +if __name__ == "__main__": + main() diff --git a/scripts/keyshare_leak_tripwire.py b/scripts/keyshare_leak_tripwire.py new file mode 100644 index 0000000..c913966 --- /dev/null +++ b/scripts/keyshare_leak_tripwire.py @@ -0,0 +1,146 @@ +"""PBA-L6b-003 tripwire: does public deploy calldata reveal the model AES key? + +Deliberately independent of the SDK code under test: its own GF(2^8) +arithmetic (AES polynomial 0x11b, the one ``citrate_sdk.finite_field`` uses), +its own Lagrange interpolation, no share validation. A regression in the SDK's +Shamir code can neither hide a leak nor fake a pass here. + +``reveals_key(calldata, key)`` answers "can an observer who holds only the +calldata rebuild the key?" by trying: + +1. the key bytes verbatim (hex or base64) anywhere in the text; +2. explicit share objects (``{"x": .., "y": ..}``) anywhere in the decoded + JSON: every subset is interpolated at x = 0; +3. bare key-length hex strings anywhere (share values with x stripped): every + subset of up to ``MAX_BARE_SUBSET`` of them, under every assignment of x in + 1..``MAX_BARE_X``, is interpolated at x = 0. + +Nested JSON strings are parsed recursively. Used by +tests/test_pba_r2_l6b_keyshares.py (source tree) and +scripts/check_keyshare_wheel.py (built wheel). +""" +from __future__ import annotations + +import base64 +import itertools +import json +import re +from typing import Any + +MAX_EXPLICIT_SUBSET = 8 +MAX_BARE_SUBSET = 3 +MAX_BARE_X = 8 + + +def _mul(a: int, b: int) -> int: + p = 0 + for _ in range(8): + if b & 1: + p ^= a + hi = a & 0x80 + a = (a << 1) & 0xFF + if hi: + a ^= 0x1B + b >>= 1 + return p + + +# Test-only lookup tables (timing does not matter here; speed does). +_MUL = [[_mul(a, b) for b in range(256)] for a in range(256)] +_INV: list[int | None] = [None] + [next(b for b in range(1, 256) if _MUL[a][b] == 1) for a in range(1, 256)] + + +def _inv(a: int) -> int | None: + return _INV[a] + + +def interpolate_at_zero(points: list[tuple[int, bytes]]) -> bytes | None: + length = len(points[0][1]) + if any(len(y) != length for _, y in points): + return None + xs = [x for x, _ in points] + if len(set(xs)) != len(xs): + return None + out = bytearray(length) + for i, (xi, yi) in enumerate(points): + num, den = 1, 1 + for j, xj in enumerate(xs): + if i == j: + continue + num = _MUL[num][xj] + den = _MUL[den][xi ^ xj] + inv = _inv(den) + if inv is None: + return None + row = _MUL[_MUL[num][inv]] + for b in range(length): + out[b] ^= row[yi[b]] + return bytes(out) + + +def _walk(value: Any, explicit: list[tuple[int, bytes]], strings: list[str], depth: int) -> None: + if depth > 16 or value is None: + return + if isinstance(value, str): + strings.append(value) + t = value.strip() + if t[:1] in ("{", "[") and len(t) < 1_000_000: + try: + _walk(json.loads(t), explicit, strings, depth + 1) + except ValueError: + pass + return + if isinstance(value, list): + for v in value: + _walk(v, explicit, strings, depth + 1) + return + if isinstance(value, dict): + x, y = value.get("x"), value.get("y") + try: + xi = int(x) if x is not None else None + except (TypeError, ValueError): + xi = None + if xi is not None and isinstance(y, str): + try: + explicit.append((xi & 0xFF, bytes.fromhex(y[2:] if y.startswith("0x") else y))) + except ValueError: + pass + for v in value.values(): + _walk(v, explicit, strings, depth + 1) + + +def reveals_key(calldata: bytes | str, key: bytes) -> tuple[bool, str]: + """Return (True, how) if ``calldata`` lets an observer rebuild ``key``.""" + text = calldata.decode("utf-8", "replace") if isinstance(calldata, bytes) else calldata + if key.hex() in text.lower() or base64.b64encode(key).decode() in text: + return True, "key bytes appear verbatim" + + explicit: list[tuple[int, bytes]] = [] + strings: list[str] = [] + try: + root: Any = json.loads(text) + except ValueError: + root = text + _walk(root, explicit, strings, 0) + + for size in range(1, min(MAX_EXPLICIT_SUBSET, len(explicit)) + 1): + for subset in itertools.combinations(explicit, size): + if interpolate_at_zero(list(subset)) == key: + return True, "explicit shares x=" + ",".join(str(x) for x, _ in subset) + + want = len(key) * 2 + pattern = re.compile(r"(?:0x)?([0-9a-fA-F]{%d})(?![0-9a-fA-F])" % want) + seen: set[str] = set() + bare: list[bytes] = [] + for s in strings: + for m in pattern.finditer(s): + h = m.group(1).lower() + if h not in seen: + seen.add(h) + bare.append(bytes.fromhex(h)) + for size in range(1, min(MAX_BARE_SUBSET, len(bare)) + 1): + for ys in itertools.combinations(bare, size): + for xs in itertools.product(range(1, MAX_BARE_X + 1), repeat=size): + if interpolate_at_zero(list(zip(xs, ys, strict=True))) == key: + return True, "bare key-length values at x=" + ",".join(map(str, xs)) + return False, "" diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 51ecc95..4443af9 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -142,26 +142,35 @@ def test_derive_shared_key(self): assert alice_shared == bob_shared def test_key_shares_creation(self): - """Test Shamir's secret sharing key creation""" + """Shamir shares are wrapped to their holders (PBA-L6b-003): the + records carry x, threshold, the holder key and an ECDH envelope, and + never a raw share value.""" key_manager = KeyManager() + holders = [KeyManager() for _ in range(3)] key = b"test_key_32_bytes_long_for_sharing" - shares = key_manager._create_key_shares(key, threshold=2, total=3) + shares = key_manager._create_key_shares( + key, threshold=2, total=3, holder_public_keys=[h.get_public_key() for h in holders] + ) assert len(shares) == 3 for share in shares: - assert "x" in share - assert "y" in share - assert "threshold" in share - assert share["threshold"] == "2" + assert set(share) == {"x", "threshold", "holder_public_key", "envelope"} + assert share["threshold"] == 2 + assert "y" not in share def test_key_reconstruction(self): - """Test key reconstruction from shares""" + """Holders unwrap their shares and rebuild the key with the caller's threshold.""" key_manager = KeyManager() + holders = [KeyManager() for _ in range(3)] original_key = b"test_key_32_bytes_long_for_sharing" - shares = key_manager._create_key_shares(original_key, threshold=2, total=3) - reconstructed_key = key_manager.reconstruct_key_from_shares(shares[:2]) + shares = key_manager._create_key_shares( + original_key, threshold=2, total=3, holder_public_keys=[h.get_public_key() for h in holders] + ) + owner_pub = key_manager.get_public_key() + opened = [holders[i].unwrap_key_share(shares[i], owner_pub) for i in (0, 1)] + reconstructed_key = key_manager.reconstruct_key_from_shares(opened, threshold=2) # With real Shamir's Secret Sharing, original key should be perfectly reconstructed assert reconstructed_key == original_key @@ -174,7 +183,7 @@ def test_insufficient_shares_error(self): ] with pytest.raises(CitrateError, match="Insufficient shares"): - key_manager.reconstruct_key_from_shares(shares) + key_manager.reconstruct_key_from_shares(shares, threshold=3) def test_sign_transaction(self): """Test transaction signing""" diff --git a/tests/test_pba_r2_l6b_keyshares.py b/tests/test_pba_r2_l6b_keyshares.py new file mode 100644 index 0000000..6c98998 --- /dev/null +++ b/tests/test_pba_r2_l6b_keyshares.py @@ -0,0 +1,246 @@ +"""PBA-L6b-003 (CRITICAL) and the Python variant of PBA-L4-005. + +L6b-003: ``deploy_model`` with ``threshold_shares > 0`` put every Shamir share +of the model AES key into ``tx_data["encryption_metadata"]["key_shares"]``, +which ``_send_transaction`` hex-encodes into public calldata. Anyone reading the +chain rebuilt the key and decrypted the uploaded model. + +Fix under test: shares never enter metadata or calldata. Each share is wrapped +to a named holder key (ECDH V2 envelope) and returned on the deployment result +for off-chain delivery; ``threshold_shares > 0`` without holder keys is refused. + +These tests drive the real entry point (``CitrateClient.deploy_model``); only the +RPC and the IPFS upload are stubbed. The signed raw transaction is RLP-decoded +exactly as an observer would, and scripts/keyshare_leak_tripwire.py (own GF(2^8) +code) checks whether any subset of the calldata rebuilds the key. +""" +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +from typing import Any + +import pytest +import rlp # type: ignore[import-untyped] +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +from citrate_sdk import CitrateClient, KeyManager +from citrate_sdk.crypto import EncryptionConfig +from citrate_sdk.errors import CitrateError +from citrate_sdk.finite_field import ( + ShamirSecretSharing, + reconstruct_secret_bytes, + split_secret_bytes, +) +from citrate_sdk.models import ModelConfig + +_spec = importlib.util.spec_from_file_location( + "keyshare_leak_tripwire", Path(__file__).resolve().parents[1] / "scripts" / "keyshare_leak_tripwire.py" +) +assert _spec and _spec.loader +tripwire = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(tripwire) + +OWNER = "0x" + "11" * 32 +HOLDER_KEYS = ["0x" + "22" * 32, "0x" + "33" * 32, "0x" + "44" * 32] +MODEL = b"SECRET-MODEL-WEIGHTS" * 100 + + +def _client(tmp_path: Path) -> tuple[CitrateClient, dict[str, Any], Path]: + mp = tmp_path / "m.onnx" + mp.write_bytes(MODEL) + client = CitrateClient("http://localhost:8545", private_key=OWNER) + box: dict[str, Any] = {} + + def fake_rpc(method: str, params: Any = None) -> Any: + if method == "eth_getTransactionCount": + return "0x0" + if method == "eth_chainId": + return hex(40204) + if method == "eth_gasPrice": + return "0x1" + if method == "eth_sendRawTransaction": + box["raw"] = params[0] + return "0x" + "ab" * 32 + raise AssertionError(method) + + def fake_upload(data: bytes) -> str: + box["blob"] = data + return "bafyFAKE" + + stubs: dict[str, Any] = { + "_rpc_call": fake_rpc, + "_upload_to_ipfs": fake_upload, + "_wait_for_receipt": lambda h: {"logs": []}, + "_extract_model_id_from_receipt": lambda r: "0x01", + } + for name, fn in stubs.items(): # offline: RPC and IPFS are stubbed + setattr(client, name, fn) + return client, box, mp + + +def _calldata(raw_hex: str) -> bytes: + fields = rlp.decode(bytes.fromhex(raw_hex[2:])) + return bytes(fields[5]) + + +def _owner_key(calldata: bytes) -> bytes: + meta = json.loads(calldata.decode())["encryption_metadata"] + return KeyManager(OWNER)._decrypt_key_from_owner(meta["encrypted_key"]) + + +def _holder_pubs() -> list[str]: + return [KeyManager(k).get_public_key() for k in HOLDER_KEYS] + + +def _cfg(**enc: Any) -> ModelConfig: + base: dict[str, Any] = {"threshold_shares": 2, "total_shares": 3} + base.update(enc) + return ModelConfig(name="m", encrypted=True, encryption_config=EncryptionConfig(**base)) + + +class TestDeployCalldataCarriesNoShares: + def test_threshold_without_holders_is_refused_before_upload_or_send(self, tmp_path: Path) -> None: + client, box, mp = _client(tmp_path) + with pytest.raises(CitrateError, match="share_holder_public_keys"): + client.deploy_model(mp, _cfg()) + assert "raw" not in box and "blob" not in box + + def test_encrypt_model_refuses_threshold_sharing(self) -> None: + with pytest.raises(CitrateError, match="encrypt_model_with_key_shares"): + KeyManager(OWNER).encrypt_model(MODEL, EncryptionConfig(threshold_shares=2, total_shares=3)) + + def test_no_subset_of_calldata_reconstructs_the_key(self, tmp_path: Path) -> None: + client, box, mp = _client(tmp_path) + dep = client.deploy_model(mp, _cfg(share_holder_public_keys=_holder_pubs())) + calldata = _calldata(box["raw"]) + key = _owner_key(calldata) + # Sanity: that really is the model key. + meta = json.loads(calldata.decode())["encryption_metadata"] + assert AESGCM(key).decrypt(bytes.fromhex(meta["nonce"]), box["blob"], None) == MODEL + assert tripwire.reveals_key(calldata, key) == (False, "") + text = calldata.decode() + assert "key_shares" not in text and "envelope" not in text and "wrapped_key" not in text + assert meta["key_sharing"] == {"threshold": 2, "total_shares": 3} + assert dep.key_share_envelopes is not None and len(dep.key_share_envelopes) == 3 + + def test_holders_rebuild_the_key_offchain_and_outsiders_cannot(self, tmp_path: Path) -> None: + client, box, mp = _client(tmp_path) + dep = client.deploy_model(mp, _cfg(share_holder_public_keys=_holder_pubs())) + key = _owner_key(_calldata(box["raw"])) + owner_pub = KeyManager(OWNER).get_public_key() + envs = dep.key_share_envelopes or [] + s0 = KeyManager(HOLDER_KEYS[0]).unwrap_key_share(envs[0], owner_pub) + s2 = KeyManager(HOLDER_KEYS[2]).unwrap_key_share(envs[2], owner_pub) + assert KeyManager(HOLDER_KEYS[0]).reconstruct_key_from_shares([s0, s2], threshold=2) == key + with pytest.raises(CitrateError, match="different holder"): + KeyManager(HOLDER_KEYS[1]).unwrap_key_share(envs[0], owner_pub) + with pytest.raises(CitrateError): + KeyManager("0x" + "55" * 32).unwrap_key_share(envs[0], owner_pub) + # A forged envelope under another sender is refused by the owner pin. + forged = dict(envs[0]) + forged["envelope"] = KeyManager("0x" + "66" * 32).encrypt_data("00" * 32, envs[0]["holder_public_key"]) + with pytest.raises(CitrateError): + KeyManager(HOLDER_KEYS[0]).unwrap_key_share(forged, owner_pub) + + def test_holder_list_must_name_one_distinct_key_per_share(self) -> None: + km = KeyManager(OWNER) + pubs = _holder_pubs() + with pytest.raises(CitrateError, match="one holder public key per share"): + km.encrypt_model_with_key_shares(MODEL, EncryptionConfig(threshold_shares=2, total_shares=3, + share_holder_public_keys=pubs[:2])) + with pytest.raises(CitrateError, match="distinct"): + km.encrypt_model_with_key_shares(MODEL, EncryptionConfig(threshold_shares=2, total_shares=3, + share_holder_public_keys=[pubs[0], pubs[0], pubs[1]])) + + def test_caller_metadata_cannot_smuggle_share_fields(self, tmp_path: Path) -> None: + client, box, mp = _client(tmp_path) + cfg = ModelConfig(name="m", encrypted=False, + metadata={"nested": [{"key_shares": [{"x": "1", "y": "ab"}]}]}) + with pytest.raises(CitrateError, match="key-share material"): + client.deploy_model(mp, cfg) + assert "raw" not in box + + def test_threshold_zero_still_deploys_without_revealing_the_key(self, tmp_path: Path) -> None: + client, box, mp = _client(tmp_path) + client.deploy_model(mp, _cfg(threshold_shares=0, total_shares=0)) + calldata = _calldata(box["raw"]) + assert tripwire.reveals_key(calldata, _owner_key(calldata)) == (False, "") + + def test_tripwire_flags_the_pre_fix_shape(self) -> None: + key = bytes(range(32)) + shares = [{"x": str(x), "y": y.hex(), "threshold": "2"} for x, y in split_secret_bytes(key, 2, 3)] + leaked = json.dumps({"encryption_metadata": {"key_shares": shares}}).encode() + assert tripwire.reveals_key(leaked, key)[0] + bare = json.dumps({"a": shares[0]["y"], "b": shares[2]["y"]}).encode() + assert tripwire.reveals_key(bare, key)[0] + + def test_example_uses_holder_keys(self) -> None: + src = (Path(__file__).resolve().parents[1] / "examples" / "encrypted_inference.py").read_text() + assert "share_holder_public_keys" in src and "unwrap_key_share" in src + + +class TestShamirValidation: + """Python twin of PBA-L4-005 (found by the variant sweep).""" + + secret = b"ABC" + + def test_x_zero_share_is_rejected(self) -> None: + forged = [(0, b"AB"), (1, b"\x99\x10"), (2, b"\x07\xee")] + with pytest.raises(ValueError, match=r"x must be an integer in 1\.\.255"): + reconstruct_secret_bytes(forged, 3) + + @pytest.mark.parametrize("bad", [256, -1, 1.5, True, "1", None]) + def test_bad_x_values(self, bad: Any) -> None: + s = split_secret_bytes(self.secret, 2, 3) + with pytest.raises(ValueError, match=r"x must be an integer in 1\.\.255"): + reconstruct_secret_bytes([(bad, s[0][1]), s[1]], 2) + + def test_duplicate_x(self) -> None: + s = split_secret_bytes(self.secret, 2, 3) + with pytest.raises(ValueError, match="duplicate share x"): + reconstruct_secret_bytes([s[0], (s[0][0], s[1][1])], 2) + + def test_length_mismatch_anywhere(self) -> None: + s = split_secret_bytes(self.secret, 2, 3) + with pytest.raises(ValueError, match="same length"): + reconstruct_secret_bytes([s[0], s[1], (3, b"\x01")], 2) + + def test_empty_y(self) -> None: + with pytest.raises(ValueError, match="empty"): + reconstruct_secret_bytes([(1, b""), (2, b"")], 2) + + def test_threshold_comes_from_the_caller(self) -> None: + km = KeyManager(OWNER) + shares = [{"x": str(x), "y": y.hex(), "threshold": "1"} for x, y in split_secret_bytes(self.secret, 3, 5)] + with pytest.raises(CitrateError, match="Insufficient shares"): + km.reconstruct_key_from_shares(shares[:2], threshold=3) + assert km.reconstruct_key_from_shares(shares[:3], threshold=3) == self.secret + + @pytest.mark.parametrize("x", [" 1", "1 ", "0x1", "1a", "1000", "٣"]) + def test_share_x_text_is_strict(self, x: str) -> None: + with pytest.raises(CitrateError, match="x must be an integer"): + KeyManager(OWNER).reconstruct_key_from_shares([{"x": x, "y": "aa"}], threshold=1) + + def test_verify_shares_detects_an_off_polynomial_share(self) -> None: + sss = ShamirSecretSharing(2, 4) + s = sss.split_secret(self.secret) + assert sss.verify_shares(s) + tampered = (4, bytes([s[3][1][0] ^ 1]) + s[3][1][1:]) + assert not sss.verify_shares([s[0], s[1], s[2], tampered]) + assert not sss.verify_shares([s[0], s[0], s[1]]) + assert not sss.verify_shares([(0, s[0][1]), s[1], s[2]]) + + def test_valid_shares_round_trip(self) -> None: + s = split_secret_bytes(self.secret, 3, 5) + assert reconstruct_secret_bytes([s[4], s[1], s[2]], 3) == self.secret + + +def test_guard_rejects_each_denylisted_name() -> None: + from citrate_sdk.crypto import assert_no_key_share_material + + for name in ("key_shares", "keyShares", "key_share_envelopes", "keyShareEnvelopes"): + with pytest.raises(CitrateError, match=f"key-share material \\('{name}'\\)"): + assert_no_key_share_material({"a": [{"b": {name: []}}]}) + assert_no_key_share_material({"a": None, "b": "key_shares", "c": [1, (2, {"d": 3})]}) From 4b7a3358cfbaa3c712ce83086dfb39264125ea0b Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:18:33 -0700 Subject: [PATCH 02/12] fix(transport): refuse whitespace/control chars and non-http(s) schemes in the transport gate (PBA-L6b-026) "\u00a0http://remote" parsed with an empty scheme and passed the gate; requests then stripped the whitespace and sent plaintext to the remote host (all 6 gated clients). The gate now refuses whitespace/control/format characters anywhere, allowlists {https, http}, refuses an empty scheme or host and non-string input, and the gated clients use the URL it returns (identity client hunks land with the PBA-L6b-029 commit). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- citrate_sdk/_url_security.py | 35 ++++++++-- citrate_sdk/gateway.py | 3 +- citrate_sdk/memory.py | 5 +- tests/test_pba_r2_l6b_026_transport.py | 93 ++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 tests/test_pba_r2_l6b_026_transport.py diff --git a/citrate_sdk/_url_security.py b/citrate_sdk/_url_security.py index 03e6af4..8cb382f 100644 --- a/citrate_sdk/_url_security.py +++ b/citrate_sdk/_url_security.py @@ -14,8 +14,12 @@ """ import ipaddress +import unicodedata from urllib.parse import urlparse +# PBA-L6b-026: only these schemes are ever accepted. +_ALLOWED_SCHEMES = frozenset({"https", "http"}) + # Hostnames that are always local to the calling machine — plaintext to these # never traverses an untrusted network, so http:// is silently allowed. _LOCAL_HOSTNAMES = frozenset({"localhost", "ip6-localhost", "ip6-loopback"}) @@ -49,6 +53,8 @@ def enforce_transport_security(url: str, *, allow_insecure_http: bool = False) - """ Validate the transport security of ``url`` and return it unchanged. + - Anything but ``https``/``http``, an empty scheme or host, and any URL + containing whitespace or control characters RAISES (PBA-L6b-026). - ``https://`` URLs pass silently. - ``http://`` to a loopback/localhost host passes silently (local traffic). - ``http://`` to a *remote* host RAISES :class:`InsecureTransportError` @@ -64,15 +70,36 @@ def enforce_transport_security(url: str, *, allow_insecure_http: bool = False) - implicitly — an automatic http→https upgrade to a host that doesn't serve TLS would fail confusingly. """ + # PBA-L6b-026: this used to return non-strings, empty strings and any URL + # whose parsed scheme was not "http" unchecked. "\u00a0http://remote" + # parses with an EMPTY scheme, so it passed; requests then strips the + # whitespace and sends plaintext to the remote host. Now: refuse + # whitespace/control/format characters anywhere (they have no business in + # an endpoint URL and different parsers treat them differently), allow + # only https/http, and refuse an empty scheme or host. if not isinstance(url, str) or not url: - return url + raise InsecureTransportError(f"Refusing an empty or non-string endpoint URL: {url!r}") + for ch in url: + if ch.isspace() or unicodedata.category(ch)[0] in ("C", "Z"): + raise InsecureTransportError( + f"Refusing endpoint URL {url!r}: it contains whitespace or a control " + f"character (U+{ord(ch):04X}). Parsers disagree on such URLs, which " + "lets an http:// endpoint slip past this check (PBA-L6b-026)." + ) parsed = urlparse(url) - if parsed.scheme != "http": - # https, or a non-http scheme we don't police here. + scheme = parsed.scheme.lower() + if scheme not in _ALLOWED_SCHEMES: + raise InsecureTransportError( + f"Refusing endpoint URL {url!r}: scheme {parsed.scheme!r} is not https or " + "http (PBA-L6b-026)." + ) + if not parsed.hostname: + raise InsecureTransportError(f"Refusing endpoint URL {url!r}: no host (PBA-L6b-026).") + if scheme == "https": return url - if _is_local_host(parsed.hostname or ""): + if _is_local_host(parsed.hostname): return url if not allow_insecure_http: diff --git a/citrate_sdk/gateway.py b/citrate_sdk/gateway.py index 59b68d3..736ea5f 100644 --- a/citrate_sdk/gateway.py +++ b/citrate_sdk/gateway.py @@ -49,8 +49,7 @@ def __init__(self, api_key: str, base_url: str | None = None, transport: Transpo # SPY-B-004: this client sends `Authorization: Bearer ` on every # call. A remote http:// base_url would ship the key in cleartext — flag it # (localhost http:// stays silent; allow_insecure_http silences remote http). - enforce_transport_security(base, allow_insecure_http=allow_insecure_http) - self._base = base + self._base = enforce_transport_security(base, allow_insecure_http=allow_insecure_http) self._transport = transport or _default_transport def _call(self, method: str, path: str, body: str | None = None) -> Any: diff --git a/citrate_sdk/memory.py b/citrate_sdk/memory.py index 7b64e90..2f305c3 100644 --- a/citrate_sdk/memory.py +++ b/citrate_sdk/memory.py @@ -223,8 +223,7 @@ def __init__( origin = origin.rstrip("/") # SPY-B-004: org routes send `Authorization: Bearer ` — an OIDC # identity assertion. A remote http:// origin would ship it in cleartext. - enforce_transport_security(origin, allow_insecure_http=allow_insecure_http) - self._origin = origin + self._origin = enforce_transport_security(origin, allow_insecure_http=allow_insecure_http) self._id_token = id_token self._transport = transport or _default_transport @@ -271,7 +270,7 @@ def __init__( origin = origin.rstrip("/") # SPY-B-004: the BYOM endpoint carries `Authorization: Bearer `. # A remote http:// origin would ship it in cleartext. - enforce_transport_security(origin, allow_insecure_http=allow_insecure_http) + origin = enforce_transport_security(origin, allow_insecure_http=allow_insecure_http) self._url = "{}/mcp/u/{}".format(origin, quote(sub, safe="")) self._connect_token = connect_token self._transport = transport or _default_transport diff --git a/tests/test_pba_r2_l6b_026_transport.py b/tests/test_pba_r2_l6b_026_transport.py new file mode 100644 index 0000000..d855d4c --- /dev/null +++ b/tests/test_pba_r2_l6b_026_transport.py @@ -0,0 +1,93 @@ +"""PBA-L6b-026: transport gate bypassed by leading Unicode whitespace. + +``"\\u00a0http://remote"`` parsed with an EMPTY scheme, so the gate returned it +unchecked; ``requests`` then stripped the whitespace and sent plaintext http to +the remote host. The gate now rejects whitespace/control characters anywhere in +the URL, allowlists {https, http}, and rejects an empty scheme; every gated +client uses the URL the gate returns. +""" +from __future__ import annotations + +from typing import Any +from unittest import mock + +import pytest +import requests + +from citrate_sdk import CitrateClient +from citrate_sdk._url_security import InsecureTransportError, enforce_transport_security +from citrate_sdk.gateway import GatewayClient +from citrate_sdk.identity.client import IdentityClient +from citrate_sdk.ipfs import IPFSClient +from citrate_sdk.memory import MemoryClient + +REMOTE = "http://rpc.attacker.example:8545" +PREFIXES = [" ", "\u00a0", "\u3000", "\u2003", "\x85", "\t", "\n", "\u200b", "\ufeff"] + + +@pytest.mark.parametrize("prefix", PREFIXES) +def test_leading_whitespace_or_control_is_refused(prefix: str) -> None: + with pytest.raises(InsecureTransportError): + enforce_transport_security(prefix + REMOTE) + + +@pytest.mark.parametrize("suffix", [" ", "\u00a0", "\r\n"]) +def test_trailing_whitespace_is_refused(suffix: str) -> None: + with pytest.raises(InsecureTransportError): + enforce_transport_security("https://rpc.citrate.ai" + suffix) + + +@pytest.mark.parametrize("url", ["rpc.attacker.example:8545", "//rpc.attacker.example", "", "ftp://x.example", + "ws://rpc.attacker.example", "file:///etc/passwd", "HTTP//x"]) +def test_scheme_allowlist(url: str) -> None: + with pytest.raises(InsecureTransportError): + enforce_transport_security(url) + + +@pytest.mark.parametrize("url", ["https://rpc.citrate.ai", "HTTPS://rpc.citrate.ai/x", "http://localhost:8545", + "http://127.0.0.1:5001"]) +def test_good_urls_pass_unchanged(url: str) -> None: + assert enforce_transport_security(url) == url + + +def test_uppercase_http_scheme_to_remote_is_still_refused() -> None: + with pytest.raises(InsecureTransportError): + enforce_transport_security("HTTP://rpc.attacker.example:8545") + + +def test_non_string_is_refused() -> None: + with pytest.raises(InsecureTransportError): + enforce_transport_security(None) # type: ignore[arg-type] + + +@pytest.mark.parametrize("prefix", ["\u00a0", "\u3000"]) +def test_rpc_client_never_sends_plaintext_via_whitespace(prefix: str) -> None: + sent: dict[str, Any] = {} + + def fake_send(self: Any, request: Any, **kw: Any) -> requests.Response: + sent["url"] = request.url + r = requests.Response() + r.status_code = 200 + r._content = b'{"jsonrpc":"2.0","id":1,"result":"0x9d0c"}' + return r + + with mock.patch.object(requests.adapters.HTTPAdapter, "send", fake_send): + with pytest.raises(InsecureTransportError): + CitrateClient(prefix + REMOTE) + assert "url" not in sent + + +def _no_network(method: str, url: str, headers: Any, body: Any) -> Any: + raise AssertionError(f"transport reached for {url!r}") + + +@pytest.mark.parametrize("make", [ + lambda u: IPFSClient(u), + lambda u: GatewayClient("cgk_test", base_url=u), + lambda u: MemoryClient(u), + lambda u: IdentityClient(client_id="c", redirect_uri="http://127.0.0.1/cb", + transport=_no_network)._get_json(u), +]) +def test_every_gated_client_refuses_the_bypass(make: Any) -> None: + with pytest.raises(InsecureTransportError): + make("\u00a0" + REMOTE) From 5c71bd8d059b66a6d6d07a12c4bd758360724c57 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:18:34 -0700 Subject: [PATCH 03/12] fix(identity): gate verify_wallet_address_on_chain and assert chain id + factory code (PBA-L6b-027) The verifier posted to any rpc_url (remote plaintext included) and trusted whatever came back, so a MITM or hostile RPC could echo the publicly computable prediction and "do not fund" passed. It now goes through the transport gate (allow_insecure_http opt-in), requires eth_chainId == the pinned chain (chain_id override), and requires non-empty factory code before comparing predictAddress. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- citrate_sdk/identity/wallet.py | 48 +++++++++-- tests/test_pba_r2_l6b_027_wallet_verify.py | 92 ++++++++++++++++++++++ 2 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 tests/test_pba_r2_l6b_027_wallet_verify.py diff --git a/citrate_sdk/identity/wallet.py b/citrate_sdk/identity/wallet.py index acd087f..bf2df75 100644 --- a/citrate_sdk/identity/wallet.py +++ b/citrate_sdk/identity/wallet.py @@ -11,11 +11,13 @@ from __future__ import annotations import json +from typing import Any import requests from eth_utils import keccak, to_checksum_address from .._generated import contract as _contract +from .._url_security import enforce_transport_security _PREFIX = bytes.fromhex("603d3d8160223d3973") _SEP = bytes.fromhex("6009") @@ -65,30 +67,62 @@ def predict_wallet_address( return to_checksum_address(keccak(packed)[-20:]) +def _rpc(url: str, method: str, params: list[Any], timeout: float) -> Any: + payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params} + resp = requests.post(url, data=json.dumps(payload), + headers={"content-type": "application/json"}, timeout=timeout) + resp.raise_for_status() + body = resp.json() + if not isinstance(body, dict) or "error" in body: + raise WalletPredictionError(f"{method} failed: {body.get('error') if isinstance(body, dict) else body!r}") + return body.get("result") + + def verify_wallet_address_on_chain( user_id: str, rpc_url: str | None = None, factory: str | None = None, implementation: str | None = None, timeout: float = 10.0, + *, + chain_id: int | None = None, + allow_insecure_http: bool = False, ) -> str: """Verify the local prediction against the on-chain factory (ground truth). The factory is the deployer, so its own ``predictAddress`` view is authoritative — this is the check that would have caught the 2026-07-25 stale-authority bug. Raises on mismatch. + + PBA-L6b-027: the RPC is only as trustworthy as the path to it. The URL goes + through the transport gate (remote plaintext refused unless + ``allow_insecure_http``), ``eth_chainId`` must equal ``chain_id`` (default: + the pinned federation chain), and the factory must have code there — so a + MITM or a wrong-chain RPC cannot make "verified" pass by echoing the + publicly computable prediction. """ local = predict_wallet_address(user_id, factory=factory, implementation=implementation) aa = _contract.aa_stack() f = factory or aa["CitrateWalletFactory"] - url = rpc_url or _contract.rpc_url() + url = enforce_transport_security(rpc_url or _contract.rpc_url(), allow_insecure_http=allow_insecure_http) + expected_chain = int(chain_id) if chain_id is not None else _contract.chain_id() + + reported = _rpc(url, "eth_chainId", [], timeout) + try: + reported_chain = int(str(reported), 16) + except ValueError: + raise WalletPredictionError(f"eth_chainId returned {reported!r}") + if reported_chain != expected_chain: + raise WalletPredictionError( + f"RPC is on chain {reported_chain}, expected chain {expected_chain} — refusing to verify" + ) + + code = _rpc(url, "eth_getCode", [f, "latest"], timeout) + if not isinstance(code, str) or code.lower() in ("", "0x", "0x0"): + raise WalletPredictionError(f"factory {f} has no code on chain {expected_chain} — refusing to verify") + selector = keccak(b"predictAddress(bytes32)")[:4].hex() data = "0x" + selector + user_id[2:] - payload = {"jsonrpc": "2.0", "id": 1, "method": "eth_call", - "params": [{"to": f, "data": data}, "latest"]} - resp = requests.post(url, data=json.dumps(payload), - headers={"content-type": "application/json"}, timeout=timeout) - resp.raise_for_status() - result = resp.json().get("result") + result = _rpc(url, "eth_call", [{"to": f, "data": data}, "latest"], timeout) if not isinstance(result, str) or len(result) < 66: raise WalletPredictionError("factory predictAddress returned no address") onchain = to_checksum_address("0x" + result[-40:]) diff --git a/tests/test_pba_r2_l6b_027_wallet_verify.py b/tests/test_pba_r2_l6b_027_wallet_verify.py new file mode 100644 index 0000000..58c5dc9 --- /dev/null +++ b/tests/test_pba_r2_l6b_027_wallet_verify.py @@ -0,0 +1,92 @@ +"""PBA-L6b-027: verify_wallet_address_on_chain was ungated and chain-blind. + +It posted to any rpc_url, including remote plaintext http://, with no +eth_chainId assertion and no check that the factory has code there. A MITM or +hostile RPC could echo the (publicly computable) predicted address and the +"do not fund on mismatch" check passed. It now goes through the transport gate, +asserts eth_chainId against the pinned chain, and requires factory code. +""" +from __future__ import annotations + +import json +from typing import Any +from unittest import mock + +import pytest +import requests + +from citrate_sdk._generated import contract as _contract +from citrate_sdk._url_security import InsecureTransportError +from citrate_sdk.identity import wallet + +UID = "0x" + "42" * 32 + + +def _rpc(chain_id: int = 40204, code: str = "0x6080", echo: bool = True) -> tuple[Any, list[str]]: + methods: list[str] = [] + local = wallet.predict_wallet_address(UID) + + def fake_post(url: str, data: str | None = None, headers: Any = None, timeout: Any = None) -> requests.Response: + body = json.loads(data or "{}") + methods.append(body["method"]) + if body["method"] == "eth_chainId": + result: Any = hex(chain_id) + elif body["method"] == "eth_getCode": + result = code + elif body["method"] == "eth_call": + result = "0x" + "00" * 12 + (local[2:].lower() if echo else "11" * 20) + else: + raise AssertionError(body["method"]) + r = requests.Response() + r.status_code = 200 + r._content = json.dumps({"jsonrpc": "2.0", "id": body["id"], "result": result}).encode() + return r + + return fake_post, methods + + +def test_remote_plaintext_rpc_is_refused_before_any_request() -> None: + post, methods = _rpc() + with mock.patch.object(wallet.requests, "post", post): + with pytest.raises(InsecureTransportError): + wallet.verify_wallet_address_on_chain(UID, rpc_url="http://rpc.attacker.example:8545") + assert methods == [] + + +def test_wrong_chain_is_refused_even_when_the_rpc_echoes_the_address() -> None: + post, methods = _rpc(chain_id=1) + with mock.patch.object(wallet.requests, "post", post): + with pytest.raises(wallet.WalletPredictionError, match="chain"): + wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") + assert "eth_call" not in methods + + +@pytest.mark.parametrize("code", ["0x", "0x0", "", None]) +def test_codeless_factory_is_refused(code: Any) -> None: + post, _ = _rpc(code=code) + with mock.patch.object(wallet.requests, "post", post): + with pytest.raises(wallet.WalletPredictionError, match="no code"): + wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") + + +def test_mismatch_still_refused() -> None: + post, _ = _rpc(echo=False) + with mock.patch.object(wallet.requests, "post", post): + with pytest.raises(wallet.WalletPredictionError, match="do not fund"): + wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") + + +def test_happy_path_checks_chain_then_code_then_prediction() -> None: + post, methods = _rpc() + with mock.patch.object(wallet.requests, "post", post): + assert wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") == \ + wallet.predict_wallet_address(UID) + assert methods == ["eth_chainId", "eth_getCode", "eth_call"] + + +def test_expected_chain_defaults_to_the_artifact_and_is_overridable() -> None: + post, _ = _rpc(chain_id=31337) + with mock.patch.object(wallet.requests, "post", post): + with pytest.raises(wallet.WalletPredictionError, match=str(_contract.chain_id())): + wallet.verify_wallet_address_on_chain(UID, rpc_url="http://127.0.0.1:8545") + assert wallet.verify_wallet_address_on_chain(UID, rpc_url="http://127.0.0.1:8545", chain_id=31337) From cb76e4319d424ba273827fbcf56c8e2d05f111f9 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:18:34 -0700 Subject: [PATCH 04/12] fix(learning): unguessable default invite code; enrollWithCode(bytes) ABI parity (PBA-L6b-028, PBA-L6b-040) L6b-028: the default invite code was classroom-; its keccak is public at createClassroom and the block timestamp bounds the window, so a +/-60 s search recovered it in under a second. Default is now secrets.token_urlsafe(16), exposed to the caller as ClassroomManager.last_invite_code. L6b-040: the SDK still encoded enrollWithCode(bytes32 hash); ClassroomRegistry (CHAIN-B-C009) takes enrollWithCode(bytes rawCode). New parity test pins every ClassroomRegistry selector the SDK encodes to the contract source @ citrate-chain 21726055; it also caught getClassroom, which returns a Classroom struct (one tuple behind an offset) that the SDK decoded as six flat values. Two existing tests that pinned the removed bytes32 ABI are corrected. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- citrate_sdk/learning.py | 37 +++++-- tests/test_learning.py | 13 ++- tests/test_pba_r2_l6b_028_040_classroom.py | 110 +++++++++++++++++++++ 3 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 tests/test_pba_r2_l6b_028_040_classroom.py diff --git a/citrate_sdk/learning.py b/citrate_sdk/learning.py index 10c4c7d..b22454e 100644 --- a/citrate_sdk/learning.py +++ b/citrate_sdk/learning.py @@ -15,9 +15,11 @@ from __future__ import annotations -import time +import secrets from typing import Any, cast +from eth_abi import decode as abi_decode + from .abi import AbiInterface, from_wei, keccak256_text, to_wei from .errors import ConfigurationError from .types import ( @@ -92,9 +94,13 @@ "function nextWithdrawalId() view returns (uint256)", ] +_CLASSROOM_TUPLE = "(address,string,uint256,uint256,uint256,bool)" + CLASSROOM_REGISTRY_ABI = [ "function createClassroom(string name, uint256 maxStudents, bytes32 inviteCodeHash)", - "function enrollWithCode(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)", "function unenroll()", "function removeStudent(address student)", "function whitelistModel(bytes32 modelHash)", @@ -646,6 +652,10 @@ 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. + self.last_invite_code: str | None = None def _require_address(self) -> str: if not self._classroom_address: @@ -660,13 +670,19 @@ def create(self, name: str, max_students: int, invite_code: str | None = None) - Args: name: Classroom display name. max_students: Maximum enrollment capacity. - invite_code: Plain-text invite code (will be hashed on-chain). Auto-generated if omitted. + 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``. Returns: Transaction hash. """ addr = self._require_address() - code = invite_code or f"classroom-{int(time.time() * 1000)}" + # 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:]) data = self._iface.encode_function_data("createClassroom", [ @@ -677,7 +693,8 @@ def create(self, name: str, max_students: int, invite_code: str | None = None) - def enroll(self, invite_code: str) -> str: """Enroll as a student in a classroom using an invite code. - Data source: ClassroomRegistry.enrollWithCode(bytes32) via eth_sendTransaction. + Data source: ClassroomRegistry.enrollWithCode(bytes) via eth_sendTransaction. + The contract hashes the raw code itself (CHAIN-B-C009, PBA-L6b-040). Args: invite_code: Plain-text invite code provided by teacher. @@ -686,9 +703,7 @@ def enroll(self, invite_code: str) -> str: Transaction hash. """ addr = self._require_address() - code_hash = keccak256_text(invite_code) - code_hash_bytes = bytes.fromhex(code_hash[2:]) - data = self._iface.encode_function_data("enrollWithCode", [code_hash_bytes]) + data = self._iface.encode_function_data("enrollWithCode", [invite_code.encode("utf-8")]) return self._send_transaction(addr, data) def unenroll(self) -> str: @@ -762,7 +777,11 @@ def get_classroom(self, teacher_address: str) -> ClassroomInfo: addr = self._require_address() data = self._iface.encode_function_data("getClassroom", [teacher_address]) result = self._eth_call(addr, data) - decoded = self._iface.decode_function_result("getClassroom", result) + # getClassroom returns a ``Classroom`` struct with a dynamic member, which + # the ABI encodes as ONE tuple behind an offset, not six flat values + # (found by the PBA-L6b-040 parity test). + raw = bytes.fromhex(result[2:] if result.startswith("0x") else result) + (decoded,) = abi_decode([_CLASSROOM_TUPLE], raw) teacher_addr, name, max_students, student_count, created_at, exists = decoded return ClassroomInfo( diff --git a/tests/test_learning.py b/tests/test_learning.py index aef520c..ffcb5fb 100644 --- a/tests/test_learning.py +++ b/tests/test_learning.py @@ -289,14 +289,14 @@ def test_missing_classroom_address(self): mgr.unenroll() def test_enroll_calldata(self): - """enroll hashes invite code and sends correct calldata.""" + """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).""" rpc = MagicMock(return_value="0xtx") mgr = self._make_manager(rpc) mgr.enroll("secret-code-123") tx = rpc.call_args_list[0][0][1][0] - expected_hash = keccak256_text("secret-code-123") expected = _classroom_iface.encode_function_data( - "enrollWithCode", [bytes.fromhex(expected_hash[2:])] + "enrollWithCode", [b"secret-code-123"] ) assert tx["data"] == expected assert tx["to"] == FAKE_CLASSROOM_ADDR @@ -409,10 +409,9 @@ def test_deposit_selector(self): assert data[2:10] == expected_selector def test_enroll_with_code_selector(self): - """enrollWithCode(bytes32) selector matches.""" + """enrollWithCode(bytes) selector matches the contract (PBA-L6b-040).""" iface = AbiInterface(CLASSROOM_REGISTRY_ABI) - dummy = bytes(32) - data = iface.encode_function_data("enrollWithCode", [dummy]) + data = iface.encode_function_data("enrollWithCode", [b"code"]) from citrate_sdk.abi import keccak256 - expected_selector = keccak256(b"enrollWithCode(bytes32)")[:4].hex() + expected_selector = keccak256(b"enrollWithCode(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 new file mode 100644 index 0000000..4c220c6 --- /dev/null +++ b/tests/test_pba_r2_l6b_028_040_classroom.py @@ -0,0 +1,110 @@ +"""PBA-L6b-028 and PBA-L6b-040: ClassroomManager. + +L6b-028: the default invite code was ``classroom-``. keccak of it +is published on-chain at createClassroom, and the block timestamp bounds the +millisecond window, so a +/-60 s search (120k hashes, < 1 s) recovered it. +The default is now ``secrets.token_urlsafe(16)`` (128 bits) and is returned to +the caller via ``last_invite_code``. + +L6b-040: the SDK encoded ``enrollWithCode(bytes32 hash)``, the pre-CHAIN-B-C009 +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). +It also caught ``getClassroom``: it returns a ``Classroom`` struct with a +dynamic member (ABI: one tuple, behind an offset), which the SDK decoded flat. +""" +from __future__ import annotations + +import time +from typing import Any + +from eth_abi import encode as abi_encode + +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 +# (external/public functions and public-mapping getters the SDK calls). +CONTRACT_SIGNATURES = { + "createClassroom": "createClassroom(string,uint256,bytes32)", + "enrollWithCode": "enrollWithCode(bytes)", + "unenroll": "unenroll()", + "removeStudent": "removeStudent(address)", + "whitelistModel": "whitelistModel(bytes32)", + "removeModel": "removeModel(bytes32)", + "rotateInviteCode": "rotateInviteCode(bytes32)", + "getClassroom": "getClassroom(address)", + "isStudentEnrolled": "isStudentEnrolled(address,address)", + "getStudentTeacher": "getStudentTeacher(address)", + "isModelWhitelistedFor": "isModelWhitelistedFor(address,bytes32)", + "canStudentAccessModel": "canStudentAccessModel(address,bytes32)", + "classroomExists": "classroomExists(address)", + "activeInviteCode": "activeInviteCode(address)", # public mapping getter +} + +TEACHER = "0x" + "01" * 20 +REGISTRY = "0x" + "02" * 20 + + +def _manager(sent: dict[str, Any], result: str = "0x") -> ClassroomManager: + def rpc(method: str, params: Any) -> Any: + if method == "eth_sendTransaction": + sent["tx"] = params[0] + return "0xhash" + if method == "eth_chainId": + return hex(40204) + if method == "eth_call": + return result + raise AssertionError(method) + return ClassroomManager(rpc, default_account=TEACHER, classroom_address=REGISTRY) + + +def test_every_sdk_selector_matches_the_contract() -> None: + iface = AbiInterface(CLASSROOM_REGISTRY_ABI) + names = {frag.split("function ")[1].split("(")[0] for frag in CLASSROOM_REGISTRY_ABI} + assert names <= set(CONTRACT_SIGNATURES), names - set(CONTRACT_SIGNATURES) + for name in names: + fn = iface._funcs[name] + 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-") + commit = bytes.fromhex(sent["tx"]["data"][2:])[4 + 64: 4 + 96] + assert commit == bytes.fromhex(keccak256_text(code)[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 + mgr.create("Grade 6", 30) + assert mgr.last_invite_code != code + + +def test_explicit_invite_code_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" + commit = bytes.fromhex(sent["tx"]["data"][2:])[4 + 64: 4 + 96] + assert commit == bytes.fromhex(keccak256_text("teacher-chosen")[2:]) + + +def test_get_classroom_decodes_the_struct_return() -> None: + struct = (TEACHER, "Grade 5", 30, 7, 1_700_000_000, True) + ret = "0x" + abi_encode(["(address,string,uint256,uint256,uint256,bool)"], [struct]).hex() + info = _manager({}, result=ret).get_classroom(TEACHER) + assert (info.teacher.lower(), info.name, info.max_students, info.student_count, info.created_at, info.exists) == \ + (TEACHER, "Grade 5", 30, 7, 1_700_000_000, True) From c526318fc4ddc76fa673ca71b69526a402c901b3 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:18:34 -0700 Subject: [PATCH 05/12] fix(identity): require numeric exp/iat and typ JWT; pin sub on refresh; match SIWE routes (PBA-L6b-029, PBA-L3a-011/012 twins) L6b-029: verify_id_token accepted a missing or non-numeric exp (never expired) and ignored typ, so an at+jwt/logout+jwt token from the same key and audience passed as an ID token. exp and iat must be finite numbers (bools rejected) and typ absent or JWT. Existing fixtures gain the iat claim real tokens carry. Variant sweep, Python twins of the JS findings: - PBA-L3a-011: refresh(refresh_token, expected_sub) refuses a refreshed ID token naming a different sub. - PBA-L3a-012: siwe_challenge() is GET /siwe/challenge -> {nonce}; build_siwe_message() builds the EIP-4361 message the authority enforces (40204, Expiration Time <= 24 h, uri host, EIP-55; byte-identical to the JS builder); siwe_verify returns kind=redirect|token (ID token verified) and surfaces the server reason. Contract test transcribes citrate-identity siwe-routes.ts @ 08959bf. Also routes the identity client through the URL the transport gate returns (PBA-L6b-026). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- citrate_sdk/identity/client.py | 132 +++++++++++++-- citrate_sdk/identity/jwt.py | 20 ++- tests/test_identity_client.py | 2 +- tests/test_identity_jwt.py | 2 +- tests/test_pba_r2_l6b_029_identity.py | 229 ++++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 16 deletions(-) create mode 100644 tests/test_pba_r2_l6b_029_identity.py diff --git a/citrate_sdk/identity/client.py b/citrate_sdk/identity/client.py index 7c28754..ae7b7a6 100644 --- a/citrate_sdk/identity/client.py +++ b/citrate_sdk/identity/client.py @@ -11,12 +11,15 @@ from __future__ import annotations import json +import re from collections.abc import Callable from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone from typing import Any, cast -from urllib.parse import urlencode +from urllib.parse import urlencode, urlparse import requests +from eth_utils import to_checksum_address from .._generated import contract as _contract from .._url_security import enforce_transport_security @@ -44,6 +47,64 @@ def _default_transport(method: str, url: str, headers: dict[str, str], body: str return resp.status_code, parsed +#: citrate-identity rejects an Expiration Time more than 24 h out. +_SIWE_MAX_TTL_SECONDS = 24 * 60 * 60 + + +def _iso_ms(t: datetime) -> str: + t = t.astimezone(timezone.utc) + return t.strftime("%Y-%m-%dT%H:%M:%S.") + "%03dZ" % (t.microsecond // 1000) + + +def build_siwe_message( + address: str, + nonce: str, + *, + statement: str | None = None, + uri: str | None = None, + chain_id: int | None = None, + ttl_seconds: int = 600, + issued_at: datetime | None = None, +) -> str: + """Build the EIP-4361 message citrate-identity verifies (PBA-L3a-012 twin). + + Enforces what the authority enforces: chain 40204 (the artifact chain), a + mandatory Expiration Time no more than 24 h out, a URI whose host is the + authority's, and an EIP-55 checksummed address. Same output as the JS SDK's + ``buildSiweMessage``. + """ + issuer = _contract.identity()["issuer"] + authority = urlparse(issuer) + domain = authority.netloc + the_uri = uri or f"{authority.scheme}://{authority.netloc}" + if urlparse(the_uri).netloc != domain: + raise IdentityError("build_siwe_message: uri host must be the authority domain " + domain) + try: + checksummed = to_checksum_address(address) + except (ValueError, TypeError): + raise IdentityError("build_siwe_message: address must be a valid 20-byte hex address") + if not re.fullmatch(r"[A-Za-z0-9]{8,}", nonce or ""): + raise IdentityError("build_siwe_message: nonce must be the alphanumeric value from siwe_challenge") + if isinstance(ttl_seconds, bool) or not isinstance(ttl_seconds, int) or not 0 < ttl_seconds <= _SIWE_MAX_TTL_SECONDS: + raise IdentityError("build_siwe_message: ttl_seconds must be an integer in 1..86400 (the authority caps expiry at 24 h)") + start = issued_at or datetime.now(timezone.utc) + lines = [ + f"{domain} wants you to sign in with your Ethereum account:", + checksummed, + "", + # EIP-4361 ABNF: address LF LF [statement LF] LF "URI: ..." + *([statement] if statement else []), + "", + "URI: " + the_uri, + "Version: 1", + f"Chain ID: {chain_id if chain_id is not None else _contract.chain_id()}", + "Nonce: " + nonce, + "Issued At: " + _iso_ms(start), + "Expiration Time: " + _iso_ms(start + timedelta(seconds=ttl_seconds)), + ] + return "\n".join(lines) + + @dataclass class TokenSet: id_token: str @@ -84,7 +145,7 @@ def _id(self) -> dict[str, Any]: return _contract.identity() def _get_json(self, url: str, bearer: str | None = None) -> Any: - enforce_transport_security(url, allow_insecure_http=self.allow_insecure_http) + url = enforce_transport_security(url, allow_insecure_http=self.allow_insecure_http) headers = {"authorization": "Bearer " + bearer} if bearer else {} status, body = self.transport("GET", url, headers, None) if not (200 <= status < 300): @@ -92,7 +153,7 @@ def _get_json(self, url: str, bearer: str | None = None) -> Any: return body def _post(self, url: str, headers: dict[str, str], body: str) -> Any: - enforce_transport_security(url, allow_insecure_http=self.allow_insecure_http) + url = enforce_transport_security(url, allow_insecure_http=self.allow_insecure_http) status, parsed = self.transport("POST", url, headers, body) if not (200 <= status < 300): raise IdentityError("POST %s failed: %d" % (url, status), status) @@ -170,21 +231,66 @@ def exchange_code(self, code: str, code_verifier: str, nonce: str) -> TokenSet: tok = self._post(disc["token_endpoint"], {"content-type": "application/x-www-form-urlencoded"}, body) return self._finish_tokens(tok, nonce) - def refresh(self, refresh_token: str) -> TokenSet: + def refresh(self, refresh_token: str, expected_sub: str) -> TokenSet: + """Refresh with a rotating refresh token. + + PBA-L3a-011 (Python twin): ``expected_sub`` is REQUIRED — the ``sub`` of + the session being refreshed. OIDC Core 12.2 requires the refreshed ID + token to carry the same ``sub``; the old code adopted a different one. + """ + if not expected_sub: + raise IdentityError("refresh requires expected_sub: the sub of the session being refreshed (PBA-L3a-011).") disc = self.discover() body = urlencode({"grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": self.client_id}) tok = self._post(disc["token_endpoint"], {"content-type": "application/x-www-form-urlencoded"}, body) - return self._finish_tokens(tok) + tokens = self._finish_tokens(tok) + if tokens.claims.get("sub") != expected_sub: + raise IdentityError( + "refresh: ID token sub changed ({} != {}); refusing the refreshed session " + "(PBA-L3a-011).".format(tokens.claims.get("sub"), expected_sub) + ) + return tokens - def siwe_challenge(self, address: str) -> dict[str, Any]: - return cast("dict[str, Any]", self._post(self._id["issuer"] + "/siwe/challenge", - {"content-type": "application/json"}, json.dumps({"address": address}))) + def siwe_challenge(self) -> dict[str, str]: + """GET /siwe/challenge -> ``{"nonce": ...}`` (PBA-L3a-012 twin). - def siwe_verify(self, message: str, signature: str) -> TokenSet: - tok = self._post(self._id["issuer"] + "/siwe/verify", - {"content-type": "application/json"}, - json.dumps({"message": message, "signature": signature})) - return self._finish_tokens(tok) + The authority serves this on GET only and returns just the nonce; build + the message with :func:`build_siwe_message` and have the wallet sign it. + """ + body = self._get_json(self._id["issuer"] + "/siwe/challenge") + nonce = body.get("nonce") if isinstance(body, dict) else None + if not isinstance(nonce, str) or len(nonce) < 8: + raise IdentityError("siwe_challenge: authority returned no nonce") + return {"nonce": nonce} + + def siwe_verify(self, message: str, signature: str) -> dict[str, Any]: + """POST /siwe/verify ``{message, signature}`` (PBA-L3a-012 twin). + + Returns ``{"kind": "redirect", "address", "method", "redirect_to"}`` when + an OIDC interaction was in flight (follow ``redirect_to`` to finish the + code flow), or ``{"kind": "token", "address", "method", "id_token", + "claims"}`` for the headless direct grant (only an ID token; it is + verified against the JWKS first). Raises IdentityError with the + authority's reason otherwise. + """ + url = enforce_transport_security(self._id["issuer"] + "/siwe/verify", + allow_insecure_http=self.allow_insecure_http) + status, parsed = self.transport("POST", url, {"content-type": "application/json"}, + json.dumps({"message": message, "signature": signature})) + body = parsed if isinstance(parsed, dict) else {} + if not (200 <= status < 300): + reason = body.get("reason") if isinstance(body.get("reason"), str) else str(body.get("error") or "") + raise IdentityError("POST %s failed: %d%s" % (url, status, " (%s)" % reason if reason else ""), status) + address = body.get("address") if isinstance(body.get("address"), str) else "" + method = body.get("method") if isinstance(body.get("method"), str) else "" + if isinstance(body.get("redirectTo"), str): + return {"kind": "redirect", "address": address, "method": method, "redirect_to": body["redirectTo"]} + if isinstance(body.get("id_token"), str): + claims = verify_id_token(body["id_token"], issuer=self._id["issuer"], audience=self.client_id, + jwks=self._get_jwks()) + return {"kind": "token", "address": address, "method": method, "id_token": body["id_token"], + "claims": claims} + raise IdentityError("siwe_verify: authority response has neither redirectTo nor id_token") def user_info(self, access_token: str) -> UserInfo: disc = self.discover() diff --git a/citrate_sdk/identity/jwt.py b/citrate_sdk/identity/jwt.py index 6be78bc..a280212 100644 --- a/citrate_sdk/identity/jwt.py +++ b/citrate_sdk/identity/jwt.py @@ -7,6 +7,7 @@ import base64 import json +import math import time from typing import Any, cast @@ -35,6 +36,10 @@ def _decode(seg: str) -> dict[str, Any]: raise IdTokenError("malformed token segment") +def _is_number(v: Any) -> bool: + return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) + + def verify_id_token( token: str, issuer: str, @@ -53,6 +58,12 @@ def verify_id_token( raise IdTokenError("unsupported or unsafe alg: {!r} (only RS256 accepted)".format(header.get("alg"))) if not parts[2]: raise IdTokenError("empty signature") + # PBA-L6b-029: an access token (``at+jwt``, RFC 9068) or logout token + # (``logout+jwt``) signed by the same key for the same audience must not + # pass as an ID token. ID tokens carry no typ or ``JWT``. + typ = header.get("typ") + if typ is not None and (not isinstance(typ, str) or typ.upper() != "JWT"): + raise IdTokenError(f"unexpected token typ: {typ!r} (an ID token has typ JWT or none)") kid = header.get("kid") rsa_keys = [k for k in jwks if k.get("kty") == "RSA" and k.get("n") and k.get("e")] @@ -80,8 +91,15 @@ def verify_id_token( now = (now_ms if now_ms is not None else int(time.time() * 1000)) // 1000 tol = clock_tolerance_sec + # PBA-L6b-029: exp and iat are REQUIRED numeric claims (OIDC Core 2). The + # old check ran only when exp happened to be a number, so a token with no + # exp, or a string or list one, never expired. exp = payload.get("exp") - if isinstance(exp, (int, float)) and now > exp + tol: + if not _is_number(exp): + raise IdTokenError("token has no numeric exp claim") + if not _is_number(payload.get("iat")): + raise IdTokenError("token has no numeric iat claim") + if now > cast(float, exp) + tol: raise IdTokenError("token expired") nbf = payload.get("nbf") if isinstance(nbf, (int, float)) and now + tol < nbf: diff --git a/tests/test_identity_client.py b/tests/test_identity_client.py index 59fd2f7..bdf5875 100644 --- a/tests/test_identity_client.py +++ b/tests/test_identity_client.py @@ -36,7 +36,7 @@ def _b64url(obj) -> str: def _id_token(nonce=NONCE): header = {"alg": "RS256", "kid": KID, "typ": "JWT"} - payload = {"iss": ID["issuer"], "sub": "user-1", "aud": CLIENT_ID, "exp": int(time.time()) + 3600} + payload = {"iss": ID["issuer"], "sub": "user-1", "aud": CLIENT_ID, "iat": int(time.time()), "exp": int(time.time()) + 3600} if nonce is not None: payload["nonce"] = nonce si = _b64url(header) + "." + _b64url(payload) diff --git a/tests/test_identity_jwt.py b/tests/test_identity_jwt.py index 7816102..395265d 100644 --- a/tests/test_identity_jwt.py +++ b/tests/test_identity_jwt.py @@ -45,7 +45,7 @@ def _make_token(payload, header=None): def _valid(): import time - return {"iss": ISSUER, "sub": "user-1", "aud": AUD, "exp": int(time.time()) + 3600} + return {"iss": ISSUER, "sub": "user-1", "aud": AUD, "iat": int(time.time()), "exp": int(time.time()) + 3600} def _raises(exc, fn): diff --git a/tests/test_pba_r2_l6b_029_identity.py b/tests/test_pba_r2_l6b_029_identity.py new file mode 100644 index 0000000..93406fc --- /dev/null +++ b/tests/test_pba_r2_l6b_029_identity.py @@ -0,0 +1,229 @@ +"""PBA-L6b-029 plus the Python twins of PBA-L3a-011 and PBA-L3a-012. + +L6b-029: ``verify_id_token`` accepted a token with no ``exp`` (or a string or +list one), so it never expired, and it did not check ``typ``, so other RS256 +JWTs from the authority with the same key and audience (e.g. an ``at+jwt`` +access token) passed as ID tokens. ``exp`` and ``iat`` must now be numeric and +``typ``, when present, must be ``JWT``. + +L3a-011 twin: ``refresh()`` adopted whatever ``sub`` the refreshed ID token +named. ``refresh(refresh_token, expected_sub)`` now refuses a different sub. + +L3a-012 twin: ``siwe_challenge`` POSTed ``{address}`` to a GET-only route and +``siwe_verify`` expected ``access_token``, which ``/siwe/verify`` never returns. +The fake authority below transcribes citrate-identity src/siwe-routes.ts @ +08959bf (same contract the JS SDK tests use). +""" +from __future__ import annotations + +import base64 +import json +import time +from datetime import datetime, timezone +from typing import Any + +import pytest +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from eth_utils import to_checksum_address + +from citrate_sdk._generated import contract +from citrate_sdk.identity import client as idc +from citrate_sdk.identity.client import IdentityClient, IdentityError +from citrate_sdk.identity.jwt import IdTokenError, verify_id_token + +ID = contract.identity() +CLIENT_ID = "citrate-core" +KID = "kid-r2" +ADDRESS = to_checksum_address("0x" + "ab" * 20) +_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) +_pub = _key.public_key().public_numbers() + + +def _b64(b: bytes) -> str: + return base64.urlsafe_b64encode(b).rstrip(b"=").decode() + + +JWKS = [{"kty": "RSA", "kid": KID, "n": _b64(_pub.n.to_bytes(256, "big")), "e": _b64(_pub.e.to_bytes(3, "big"))}] + + +def _token(payload: dict[str, Any] | str, header: dict[str, Any] | None = None) -> str: + h = _b64(json.dumps(header or {"alg": "RS256", "kid": KID, "typ": "JWT"}).encode()) + p = _b64((payload if isinstance(payload, str) else json.dumps(payload)).encode()) + sig = _key.sign(f"{h}.{p}".encode(), padding.PKCS1v15(), hashes.SHA256()) + return f"{h}.{p}.{_b64(sig)}" + + +def _claims(**extra: Any) -> dict[str, Any]: + now = int(time.time()) + c: dict[str, Any] = {"iss": ID["issuer"], "sub": "user-1", "aud": CLIENT_ID, "iat": now, "exp": now + 3600} + c.update(extra) + return {k: v for k, v in c.items() if v is not _DROP} + + +_DROP = object() + + +def _verify(tok: str, **kw: Any) -> dict[str, Any]: + return verify_id_token(tok, ID["issuer"], CLIENT_ID, JWKS, **kw) + + +class TestVerifyIdToken: + def test_missing_exp(self) -> None: + with pytest.raises(IdTokenError, match="exp"): + _verify(_token(_claims(exp=_DROP))) + + @pytest.mark.parametrize("exp", ["9999999999", [1], None, True, {"a": 1}]) + def test_non_numeric_exp(self, exp: Any) -> None: + with pytest.raises(IdTokenError, match="exp"): + _verify(_token(_claims(exp=exp))) + + def test_infinite_exp(self) -> None: + raw = json.dumps(_claims(exp=0)).replace('"exp": 0', '"exp": 1e400') + with pytest.raises(IdTokenError, match="exp"): + _verify(_token(raw)) + + def test_missing_or_bad_iat(self) -> None: + with pytest.raises(IdTokenError, match="iat"): + _verify(_token(_claims(iat=_DROP))) + with pytest.raises(IdTokenError, match="iat"): + _verify(_token(_claims(iat="1"))) + + @pytest.mark.parametrize("typ", ["at+jwt", "logout+jwt", "JWS", ""]) + def test_wrong_typ(self, typ: str) -> None: + with pytest.raises(IdTokenError, match="typ"): + _verify(_token(_claims(), {"alg": "RS256", "kid": KID, "typ": typ})) + + @pytest.mark.parametrize("header", [{"alg": "RS256", "kid": KID}, {"alg": "RS256", "kid": KID, "typ": "jwt"}]) + def test_absent_or_lowercase_jwt_typ_is_fine(self, header: dict[str, Any]) -> None: + assert _verify(_token(_claims(), header))["sub"] == "user-1" + + def test_still_expires(self) -> None: + with pytest.raises(IdTokenError, match="expired"): + _verify(_token(_claims(exp=int(time.time()) - 3600))) + + +def _server(refreshed_sub: str = "user-1", direct: bool = False, interaction: bool = False) -> tuple[Any, list[Any]]: + calls: list[Any] = [] + issued: set[str] = set() + disc = {"issuer": ID["issuer"], "token_endpoint": ID["issuer"] + "/token", "jwks_uri": ID["issuer"] + "/jwks", + "authorization_endpoint": ID["issuer"] + "/auth", "userinfo_endpoint": ID["issuer"] + "/me"} + + def transport(method: str, url: str, headers: dict[str, str], body: str | None) -> tuple[int, Any]: + calls.append((method, url, headers, body)) + if url == ID["discovery"]: + return 200, disc + if url == disc["jwks_uri"]: + return 200, {"keys": JWKS} + if url == disc["token_endpoint"] and method == "POST": + return 200, {"id_token": _token(_claims(sub=refreshed_sub)), "access_token": "at-2", "refresh_token": "rt-2"} + if url == ID["issuer"] + "/siwe/challenge": + if method != "GET": + return 404, {"error": "invalid_request"} + nonce = f"n{len(issued) + 1}abcdefgh" + issued.add(nonce) + return 200, {"nonce": nonce} + if url == ID["issuer"] + "/siwe/verify" and method == "POST": + b = json.loads(body or "{}") + m = b.get("message") + if not isinstance(m, str) or not isinstance(b.get("signature"), str): + return 400, {"error": "invalid_request", "reason": "message and signature are required strings"} + nonce = m.split("\nNonce: ")[1].split("\n")[0] + if nonce not in issued: + return 401, {"error": "invalid_grant", "reason": "unknown_nonce"} + issued.discard(nonce) + if "\nChain ID: 40204\n" not in m or "\nExpiration Time: " not in m: + return 401, {"error": "invalid_grant", "reason": "wrong_chain_or_expiry"} + if interaction: + return 200, {"address": ADDRESS, "method": "eoa", "redirectTo": ID["issuer"] + "/auth/xyz"} + if not direct: + return 400, {"error": "invalid_request", "reason": "no active OIDC interaction; the direct token grant is disabled"} + return 200, {"address": ADDRESS, "method": "eoa", "token_type": "Bearer", + "id_token": _token(_claims(sub=ADDRESS.lower())), "wallet_address": ADDRESS} + return 404, {} + + return transport, calls + + +def _client(transport: Any) -> IdentityClient: + return IdentityClient(client_id=CLIENT_ID, redirect_uri="http://127.0.0.1:8899/cb", transport=transport) + + +class TestRefreshPinsSub: + def test_same_sub(self) -> None: + t, _ = _server() + assert _client(t).refresh("rt-1", expected_sub="user-1").claims["sub"] == "user-1" + + def test_changed_sub(self) -> None: + t, _ = _server(refreshed_sub="user-2") + with pytest.raises(IdentityError, match=r"sub changed \(user-2 != user-1\)"): + _client(t).refresh("rt-1", expected_sub="user-1") + + def test_expected_sub_required(self) -> None: + t, _ = _server() + with pytest.raises(TypeError): + _client(t).refresh("rt-1") # type: ignore[call-arg] + with pytest.raises(IdentityError, match="expected_sub"): + _client(t).refresh("rt-1", expected_sub="") + + +class TestSiweContract: + def test_challenge_is_get(self) -> None: + t, calls = _server() + assert _client(t).siwe_challenge() == {"nonce": "n1abcdefgh"} + method, url, _, body = [c for c in calls if c[1].endswith("/siwe/challenge")][0] + assert method == "GET" and body is None + + def test_message_matches_eip4361_exactly(self) -> None: + issued = datetime(2026, 9, 25, tzinfo=timezone.utc) + domain = ID["issuer"].split("://", 1)[1].split("/")[0] + assert idc.build_siwe_message(ADDRESS.lower(), "abcdefgh", issued_at=issued) == ( + f"{domain} wants you to sign in with your Ethereum account:\n{ADDRESS}\n\n\n" + f"URI: {ID['issuer']}\nVersion: 1\nChain ID: 40204\nNonce: abcdefgh\n" + "Issued At: 2026-09-25T00:00:00.000Z\nExpiration Time: 2026-09-25T00:10:00.000Z" + ) + assert "\n\nHi\n\nURI: " in idc.build_siwe_message(ADDRESS, "abcdefgh", statement="Hi", issued_at=issued) + + @pytest.mark.parametrize("kw", [{"uri": "https://evil.example/x"}, {"nonce": "short"}, {"nonce": "-abcdefgh"}, + {"ttl_seconds": 0}, {"ttl_seconds": 86401}, {"address": "0x1234"}]) + def test_message_inputs_are_checked(self, kw: dict[str, Any]) -> None: + args: dict[str, Any] = {"address": ADDRESS, "nonce": "abcdefgh"} + args.update(kw) + with pytest.raises(IdentityError): + idc.build_siwe_message(**args) + + def test_direct_token_path(self) -> None: + t, _ = _server(direct=True) + c = _client(t) + nonce = c.siwe_challenge()["nonce"] + r = c.siwe_verify(idc.build_siwe_message(ADDRESS, nonce), "0x" + "11" * 65) + assert r["kind"] == "token" and r["address"] == ADDRESS and r["claims"]["sub"] == ADDRESS.lower() + + def test_interaction_path(self) -> None: + t, _ = _server(interaction=True) + c = _client(t) + nonce = c.siwe_challenge()["nonce"] + r = c.siwe_verify(idc.build_siwe_message(ADDRESS, nonce), "0x" + "11" * 65) + assert r == {"kind": "redirect", "address": ADDRESS, "method": "eoa", "redirect_to": ID["issuer"] + "/auth/xyz"} + + def test_rejection_reason_surfaces(self) -> None: + t, _ = _server() + c = _client(t) + nonce = c.siwe_challenge()["nonce"] + with pytest.raises(IdentityError, match="direct token grant is disabled"): + c.siwe_verify(idc.build_siwe_message(ADDRESS, nonce), "0x" + "11" * 65) + + def test_forged_id_token_rejected(self) -> None: + t, _ = _server(direct=True) + + def forging(method: str, url: str, headers: dict[str, str], body: str | None) -> tuple[int, Any]: + status, parsed = t(method, url, headers, body) + if url.endswith("/siwe/verify") and status == 200: + h, p, _s = parsed["id_token"].split(".") + parsed = dict(parsed, id_token=f"{h}.{p}.AAAA") + return status, parsed + + c = _client(forging) + nonce = c.siwe_challenge()["nonce"] + with pytest.raises(IdTokenError, match="signature"): + c.siwe_verify(idc.build_siwe_message(ADDRESS, nonce), "0x" + "11" * 65) From 0b5df4b255f0542e243d799797523dbf829be439 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:21:11 -0700 Subject: [PATCH 06/12] fix(ipfs): stream downloads under a size cap and verify content (PBA-L6b-030) download_bytes buffered the whole body and returned it unchecked. It now streams with max_bytes (default 1 GiB), verifies expected_sha256 when given, and verifies self-describing addresses with no hint (sha256: pointers and CIDv1 raw/sha2-256). dag-pb CIDs hash a chunked DAG and cannot be recomputed from the bytes; the docstring directs callers to pass expected_sha256 (e.g. the on-chain model_hash) for those. IPFSManager.download and download_from_ipfs thread both parameters. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- citrate_sdk/ipfs.py | 106 ++++++++++++++++++++++++++---- tests/test_pba_r2_l6b_030_ipfs.py | 90 +++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 tests/test_pba_r2_l6b_030_ipfs.py diff --git a/citrate_sdk/ipfs.py b/citrate_sdk/ipfs.py index 34a7297..bb9693c 100644 --- a/citrate_sdk/ipfs.py +++ b/citrate_sdk/ipfs.py @@ -2,6 +2,9 @@ Real IPFS integration for Citrate Python SDK """ +import base64 +import hashlib +import hmac import json from typing import Any, cast @@ -10,6 +13,33 @@ from ._url_security import enforce_transport_security from .errors import IPFSError +#: PBA-L6b-030: default ceiling on a single download (bytes). Pass +#: ``max_bytes`` to raise or lower it per call. +DEFAULT_MAX_DOWNLOAD_BYTES = 1 << 30 # 1 GiB +_CHUNK = 1 << 16 + + +def _expected_digest_from_address(ipfs_hash: str) -> bytes | None: + """sha256 digest a self-describing address commits to, when it can be checked + locally from the bytes alone: ``sha256:`` pointers, and CIDv1 with the + raw codec (0x55) and a sha2-256 multihash (``bafkrei...``). dag-pb CIDs + (``Qm...``, ``bafybei...``) hash a chunked DAG, not the bytes; None.""" + if ipfs_hash.startswith("sha256:"): + try: + digest = bytes.fromhex(ipfs_hash[len("sha256:"):]) + except ValueError: + return None + return digest if len(digest) == 32 else None + if ipfs_hash.startswith("b"): + body = ipfs_hash[1:].upper() + try: + raw = base64.b32decode(body + "=" * (-len(body) % 8)) + except ValueError: + return None + if raw[:4] == bytes([0x01, 0x55, 0x12, 0x20]) and len(raw) == 36: + return raw[4:] + return None + class IPFSClient: """ @@ -84,34 +114,70 @@ def upload_bytes(self, data: bytes) -> str: except KeyError as e: raise IPFSError(f"Missing field in IPFS response: {str(e)}") - def download_bytes(self, ipfs_hash: str) -> bytes: + def download_bytes( + self, + ipfs_hash: str, + *, + expected_sha256: str | None = None, + max_bytes: int | None = None, + ) -> bytes: """ Download bytes from IPFS using hash + PBA-L6b-030: the body is streamed and capped at ``max_bytes`` (default + ``DEFAULT_MAX_DOWNLOAD_BYTES``), and the bytes are checked before they + are returned: + + - ``expected_sha256`` (hex), when given, must match. Pass it whenever + you know it (e.g. the on-chain ``model_hash``): it is the only + end-to-end check for dag-pb CIDs (``Qm...``/``bafybei...``), whose + hash covers a chunked DAG rather than the bytes. + - self-describing addresses are verified with no hint: ``sha256:`` + pointers and CIDv1 raw/sha2-256 (``bafkrei...``). + Args: ipfs_hash: IPFS hash (CID) + expected_sha256: optional hex sha256 the content must match + max_bytes: optional size ceiling for this download Returns: Downloaded bytes Raises: - IPFSError: If download fails + IPFSError: If download fails, exceeds the cap, or fails verification """ + limit = DEFAULT_MAX_DOWNLOAD_BYTES if max_bytes is None else max_bytes try: response = self.session.post( f"{self.api_url}/api/v0/cat", params={'arg': ipfs_hash}, timeout=self.timeout, + stream=True, ) - - if response.status_code != 200: - raise IPFSError(f"IPFS download failed: HTTP {response.status_code}") - - return response.content - + try: + if response.status_code != 200: + raise IPFSError(f"IPFS download failed: HTTP {response.status_code}") + buf = bytearray() + for chunk in response.iter_content(chunk_size=_CHUNK): + buf.extend(chunk) + if len(buf) > limit: + raise IPFSError(f"IPFS download exceeds max_bytes={limit} for {ipfs_hash}") + finally: + response.close() except requests.exceptions.RequestException as e: raise IPFSError(f"IPFS connection error: {str(e)}") + data = bytes(buf) + digest = hashlib.sha256(data).digest() + if expected_sha256 is not None: + want = expected_sha256[2:] if expected_sha256.startswith("0x") else expected_sha256 + if not hmac.compare_digest(digest.hex(), want.lower()): + raise IPFSError(f"IPFS content for {ipfs_hash} does not match expected_sha256") + committed = _expected_digest_from_address(ipfs_hash) + if committed is not None and not hmac.compare_digest(digest, committed): + raise IPFSError(f"IPFS content does not match its address {ipfs_hash}") + return data + def get_file_info(self, ipfs_hash: str) -> dict[str, Any]: """ Get file information from IPFS @@ -286,7 +352,13 @@ def upload(self, data: bytes) -> str: else: raise IPFSError("No IPFS nodes available") - def download(self, ipfs_hash: str) -> bytes: + def download( + self, + ipfs_hash: str, + *, + expected_sha256: str | None = None, + max_bytes: int | None = None, + ) -> bytes: """ Download data with automatic fallback @@ -305,7 +377,8 @@ def download(self, ipfs_hash: str) -> bytes: # Try active client first if available if self.active_client: try: - return self.active_client.download_bytes(ipfs_hash) + return self.active_client.download_bytes( + ipfs_hash, expected_sha256=expected_sha256, max_bytes=max_bytes) except IPFSError: pass @@ -313,7 +386,8 @@ def download(self, ipfs_hash: str) -> bytes: for client in clients: try: if client.is_available(): - return client.download_bytes(ipfs_hash) + return client.download_bytes( + ipfs_hash, expected_sha256=expected_sha256, max_bytes=max_bytes) except IPFSError as e: last_error = e @@ -387,7 +461,13 @@ def upload_to_ipfs(data: bytes, ipfs_urls: list | None = None) -> str: return manager.upload(data) -def download_from_ipfs(ipfs_hash: str, ipfs_urls: list | None = None) -> bytes: +def download_from_ipfs( + ipfs_hash: str, + ipfs_urls: list | None = None, + *, + expected_sha256: str | None = None, + max_bytes: int | None = None, +) -> bytes: """ Convenience function to download data from IPFS @@ -399,4 +479,4 @@ def download_from_ipfs(ipfs_hash: str, ipfs_urls: list | None = None) -> bytes: Downloaded bytes """ manager = get_ipfs_manager(ipfs_urls) - return manager.download(ipfs_hash) + return manager.download(ipfs_hash, expected_sha256=expected_sha256, max_bytes=max_bytes) diff --git a/tests/test_pba_r2_l6b_030_ipfs.py b/tests/test_pba_r2_l6b_030_ipfs.py new file mode 100644 index 0000000..be80635 --- /dev/null +++ b/tests/test_pba_r2_l6b_030_ipfs.py @@ -0,0 +1,90 @@ +"""PBA-L6b-030: IPFS download returned bytes with no content check and no size +bound (the whole body was buffered). + +Now ``download_bytes`` streams with a ``max_bytes`` cap, verifies +``expected_sha256`` when given, and verifies self-describing CIDs it can check +locally (CIDv1 raw + sha2-256, and ``sha256:`` pointers the JS SDK emits). +A dag-pb CID (Qm..., bafybei...) cannot be recomputed from the bytes without +the chunking parameters, so for those the caller must pass ``expected_sha256`` +(e.g. the on-chain model_hash) for end-to-end integrity; the docstring says so. +""" +from __future__ import annotations + +import base64 +import hashlib +from typing import Any +from unittest import mock + +import pytest +import requests + +from citrate_sdk.errors import IPFSError +from citrate_sdk.ipfs import IPFSClient, IPFSManager + +DATA = b"model-bytes" * 100 + + +def _raw_cid(data: bytes) -> str: + mh = bytes([0x12, 0x20]) + hashlib.sha256(data).digest() + cid = bytes([0x01, 0x55]) + mh + return "b" + base64.b32encode(cid).decode().lower().rstrip("=") + + +def _serve(body: bytes, status: int = 200) -> Any: + def post(url: str, params: Any = None, timeout: Any = None, stream: bool = False, **kw: Any) -> requests.Response: + r = requests.Response() + r.status_code = status + r.raw = __import__("io").BytesIO(body) + return r + return post + + +def _client(body: bytes, status: int = 200) -> IPFSClient: + c = IPFSClient("http://127.0.0.1:5001") + c.session.post = _serve(body, status) # type: ignore[method-assign] + return c + + +def test_expected_sha256_mismatch_is_refused() -> None: + with pytest.raises(IPFSError, match="sha256"): + _client(b"tampered").download_bytes("QmWhatever", expected_sha256=hashlib.sha256(DATA).hexdigest()) + + +def test_expected_sha256_match_returns_bytes() -> None: + assert _client(DATA).download_bytes("QmWhatever", expected_sha256=hashlib.sha256(DATA).hexdigest()) == DATA + + +def test_raw_cid_is_verified_without_a_hint() -> None: + cid = _raw_cid(DATA) + assert _client(DATA).download_bytes(cid) == DATA + with pytest.raises(IPFSError, match="does not match"): + _client(b"tampered").download_bytes(cid) + + +def test_sha256_pointer_is_verified() -> None: + ptr = "sha256:" + hashlib.sha256(DATA).hexdigest() + assert _client(DATA).download_bytes(ptr) == DATA + with pytest.raises(IPFSError, match="does not match"): + _client(b"x").download_bytes(ptr) + + +def test_size_cap_stops_the_stream() -> None: + with pytest.raises(IPFSError, match="exceeds max_bytes"): + _client(b"a" * 1025).download_bytes("QmWhatever", max_bytes=1024) + assert _client(b"a" * 1024).download_bytes("QmWhatever", max_bytes=1024) == b"a" * 1024 + + +def test_default_cap_exists() -> None: + c = _client(b"") + with mock.patch("citrate_sdk.ipfs.DEFAULT_MAX_DOWNLOAD_BYTES", 10): + with pytest.raises(IPFSError, match="exceeds max_bytes"): + _client(b"a" * 11).download_bytes("QmWhatever") + assert c.download_bytes("QmWhatever") == b"" + + +def test_manager_threads_the_checks_through() -> None: + m = IPFSManager("http://127.0.0.1:5001") + m.primary.session.post = _serve(b"tampered") # type: ignore[method-assign] + m.primary.is_available = lambda: True # type: ignore[method-assign] + with pytest.raises(IPFSError): + m.download("QmWhatever", expected_sha256=hashlib.sha256(DATA).hexdigest()) From f24db96dfd1aab2c3df27527505499f70f59f4f5 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:21:11 -0700 Subject: [PATCH 07/12] fix(managers): unknown access/tier/mode strings raise instead of defaulting to index 0 (PBA-L6b-031) create_pool(access=...) turned any unrecognised value into an Open pool, post_job(tier='zk') posted a Commitment-tier job while escrowing max_price, and an unknown pool mode became InferencePool. New abi.enum_index accepts the canonical name or a case-insensitive exact match and raises ValueError otherwise, before any transaction. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- citrate_sdk/abi.py | 15 ++++++ citrate_sdk/compute.py | 6 +-- citrate_sdk/learning.py | 4 +- tests/test_pba_r2_l6b_031_enums.py | 84 ++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 tests/test_pba_r2_l6b_031_enums.py diff --git a/citrate_sdk/abi.py b/citrate_sdk/abi.py index 9cd7f99..7380532 100644 --- a/citrate_sdk/abi.py +++ b/citrate_sdk/abi.py @@ -47,6 +47,21 @@ def keccak256_text(text: str) -> str: # Wei / Ether conversion # --------------------------------------------------------------------------- +def enum_index(choices: Sequence[str], value: str, what: str) -> int: + """Index of ``value`` in ``choices`` (exact, else case-insensitive exact). + + PBA-L6b-031: callers used ``choices.index(v) if v in choices else 0``, so an + unknown or mis-cased value silently became option 0 (an Open pool, a + Commitment-tier job). Unknown values now raise ValueError. + """ + if value in choices: + return list(choices).index(value) + folded = [c.lower() for c in choices] + if isinstance(value, str) and value.lower() in folded: + return folded.index(value.lower()) + raise ValueError(f"unknown {what} {value!r}; expected one of {', '.join(choices)}") + + def to_wei(ether: str) -> int: """Convert an ether-denominated decimal string to wei (int).""" return Web3.to_wei(ether, "ether") diff --git a/citrate_sdk/compute.py b/citrate_sdk/compute.py index 10a6f39..6b3ce3d 100644 --- a/citrate_sdk/compute.py +++ b/citrate_sdk/compute.py @@ -23,7 +23,7 @@ from typing import Any, cast -from .abi import AbiInterface, from_wei, to_wei +from .abi import AbiInterface, enum_index, from_wei, to_wei from .errors import ConfigurationError from .types import ComputeJob, ComputePool, Dispute, ProviderInfo @@ -159,7 +159,7 @@ def post_job( addr = self._require_compute_address() h = model_hash if model_hash.startswith("0x") else f"0x{model_hash}" hash_bytes = bytes.fromhex(h[2:]) - tier_num = JOB_TIERS.index(tier) if tier in JOB_TIERS else 0 + tier_num = enum_index(JOB_TIERS, tier, "tier") max_price_wei = to_wei(max_price) input_bytes = input_data.encode("utf-8") data = self._compute_iface.encode_function_data("postJob", [ @@ -363,7 +363,7 @@ def create_pool( Transaction hash (pool ID emitted in PoolCreated event). """ addr = self._require_compute_address() - mode_num = POOL_MODES.index(mode) if mode in POOL_MODES else 0 + mode_num = enum_index(POOL_MODES, mode, "pool mode") price_wei = to_wei(price) data = self._compute_iface.encode_function_data("createPool", [ name, mode_num, min_providers, throughput, price_wei, diff --git a/citrate_sdk/learning.py b/citrate_sdk/learning.py index b22454e..8ae4ce7 100644 --- a/citrate_sdk/learning.py +++ b/citrate_sdk/learning.py @@ -20,7 +20,7 @@ from eth_abi import decode as abi_decode -from .abi import AbiInterface, from_wei, keccak256_text, to_wei +from .abi import AbiInterface, enum_index, from_wei, keccak256_text, to_wei from .errors import ConfigurationError from .types import ( ClassroomInfo, @@ -322,7 +322,7 @@ def create_pool( Transaction hash (pool ID emitted in PoolCreated event). """ addr = self._require_learning_pool() - access_num = ACCESS_TYPES.index(access) if access in ACCESS_TYPES else 0 + access_num = enum_index(ACCESS_TYPES, access, "access type") min_stake_wei = to_wei(min_stake) data = self._iface.encode_function_data("createPool", [ name, description, access_num, min_stake_wei, diff --git a/tests/test_pba_r2_l6b_031_enums.py b/tests/test_pba_r2_l6b_031_enums.py new file mode 100644 index 0000000..12e9797 --- /dev/null +++ b/tests/test_pba_r2_l6b_031_enums.py @@ -0,0 +1,84 @@ +"""PBA-L6b-031: unknown access/tier/mode strings silently became index 0. + +``LearningManager.create_pool(access="invite-only")`` created an OPEN pool; +``ComputeManager.post_job(tier="zk")`` posted a Commitment-tier job while still +escrowing ``max_price``; an unknown pool mode became InferencePool. Unknown +values now raise ValueError before any transaction; the canonical names still +work, and a case-insensitive exact match is accepted. +""" +from __future__ import annotations + +from typing import Any + +import pytest + +from citrate_sdk.compute import ComputeManager +from citrate_sdk.learning import LearningManager + + +def _rpc(sent: list[Any]) -> Any: + def rpc(method: str, params: Any) -> Any: + if method == "eth_chainId": + return hex(40204) + sent.append((method, params)) + return "0xhash" + return rpc + + +ACCT = "0x" + "01" * 20 +ADDRS = {"computePool": "0x" + "02" * 20, "learningPool": "0x" + "03" * 20} + + +@pytest.mark.parametrize("access", ["invite-only", "Private", "", "OPEN "]) +def test_unknown_access_raises_before_sending(access: str) -> None: + sent: list[Any] = [] + mgr = LearningManager(_rpc(sent), default_account=ACCT, contract_addresses=ADDRS) + with pytest.raises(ValueError, match="access"): + mgr.create_pool("p", "d", access, "1") + assert sent == [] + + +@pytest.mark.parametrize("tier", ["zkp", "tee2", "", "Commit"]) +def test_unknown_tier_raises_before_escrow(tier: str) -> None: + sent: list[Any] = [] + mgr = ComputeManager(_rpc(sent), default_account=ACCT, contract_addresses=ADDRS) + with pytest.raises(ValueError, match="tier"): + mgr.post_job("0x" + "aa" * 32, "in", "1", tier) + assert sent == [] + + +def test_unknown_mode_raises() -> None: + sent: list[Any] = [] + mgr = ComputeManager(_rpc(sent), default_account=ACCT, contract_addresses=ADDRS) + with pytest.raises(ValueError, match="mode"): + mgr.create_pool("p", "Sharded", 1, 1, "1") + assert sent == [] + + +@pytest.mark.parametrize(("tier", "index"), [("Commitment", 0), ("ZK", 1), ("zk", 1), ("TEE", 2), ("tee", 2)]) +def test_known_tiers_encode_their_index(tier: str, index: int) -> None: + sent: list[Any] = [] + mgr = ComputeManager(_rpc(sent), default_account=ACCT, contract_addresses=ADDRS) + mgr.post_job("0x" + "aa" * 32, "in", "1", tier) + data = bytes.fromhex(sent[-1][1][0]["data"][2:]) + # postJob(bytes32,bytes,uint256,uint8,...): tier is the 4th head word. + assert int.from_bytes(data[4 + 96: 4 + 128], "big") == index + + +@pytest.mark.parametrize(("access", "index"), [("Open", 0), ("InviteOnly", 1), ("inviteonly", 1), ("ApplicationRequired", 2)]) +def test_known_access_encodes_its_index(access: str, index: int) -> None: + sent: list[Any] = [] + mgr = LearningManager(_rpc(sent), default_account=ACCT, contract_addresses=ADDRS) + mgr.create_pool("p", "d", access, "1") + data = bytes.fromhex(sent[-1][1][0]["data"][2:]) + # createPool(string,string,uint8,uint256): access is the 3rd head word. + assert int.from_bytes(data[4 + 64: 4 + 96], "big") == index + + +@pytest.mark.parametrize(("mode", "index"), [("InferencePool", 0), ("DataParallel", 1), ("pipelineparallel", 2)]) +def test_known_modes_encode_their_index(mode: str, index: int) -> None: + sent: list[Any] = [] + mgr = ComputeManager(_rpc(sent), default_account=ACCT, contract_addresses=ADDRS) + mgr.create_pool("p", mode, 1, 1, "1") + data = bytes.fromhex(sent[-1][1][0]["data"][2:]) + assert int.from_bytes(data[4 + 32: 4 + 64], "big") == index From ae8d74ba364ff8924ecd6d88d2398536b0faa7aa Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:23:07 -0700 Subject: [PATCH 08/12] fix(managers): assert eth_chainId before node-signed writes (PBA-L6b-042) The SPY-B-005 pin covered only CitrateClient. The Learning/Staking/Classroom/Compute/Treasury/Farming managers send writes with node-side eth_sendTransaction and never asked which chain the node is on. New _chain_guard.pinned_send asserts eth_chainId against the pinned chain (artifact 40204 by default; chain_id= keyword to override) once per manager, refuses garbage, and puts chainId in the tx so an honest node also rejects a mismatch. Existing manager tests now answer eth_chainId like a real node (tests/_chain_rpc.py) and read the last rpc call instead of the first. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- citrate_sdk/_chain_guard.py | 39 ++++++++++++ citrate_sdk/compute.py | 8 ++- citrate_sdk/farming.py | 8 ++- citrate_sdk/learning.py | 18 +++++- citrate_sdk/treasury.py | 8 ++- tests/_chain_rpc.py | 15 +++++ tests/test_compute.py | 37 ++++++------ tests/test_farming.py | 9 +-- tests/test_learning.py | 67 ++++++++++----------- tests/test_pba_r2_l6b_042_chain_pin.py | 82 ++++++++++++++++++++++++++ tests/test_treasury.py | 13 ++-- 11 files changed, 239 insertions(+), 65 deletions(-) create mode 100644 citrate_sdk/_chain_guard.py create mode 100644 tests/_chain_rpc.py create mode 100644 tests/test_pba_r2_l6b_042_chain_pin.py diff --git a/citrate_sdk/_chain_guard.py b/citrate_sdk/_chain_guard.py new file mode 100644 index 0000000..edf0e53 --- /dev/null +++ b/citrate_sdk/_chain_guard.py @@ -0,0 +1,39 @@ +"""Chain-id pin for the node-signed manager write paths (PBA-L6b-042). + +SPY-B-005 pinned the chain id for ``CitrateClient`` (which signs locally), but +the Learning/Staking/Classroom/Compute/Treasury/Farming managers send writes +with node-side ``eth_sendTransaction`` and never asked the node which chain it +is on. A manager pointed at another network's node that holds the same account +moved funds there. ``pinned_send`` asserts ``eth_chainId`` against the expected +chain once per manager, then sends with ``chainId`` in the tx so an honest node +also rejects a mismatch itself. +""" +from __future__ import annotations + +from typing import Any, cast + +from ._generated import contract as _contract +from .errors import CitrateError + + +def expected_chain_id(chain_id: int | None) -> int: + return int(chain_id) if chain_id is not None else _contract.chain_id() + + +def pinned_send(manager: Any, tx: dict[str, Any]) -> str: + """Assert the node's chain (cached per manager) and send ``tx``.""" + expected = cast(int, manager._expected_chain_id) + if not getattr(manager, "_chain_verified", False): + raw = manager._rpc_call("eth_chainId", []) + try: + reported = int(raw, 16) if isinstance(raw, str) else int(raw) + except (TypeError, ValueError): + raise CitrateError(f"RPC returned an unparseable eth_chainId {raw!r}; refusing to send (PBA-L6b-042).") + if reported != expected: + raise CitrateError( + f"RPC chain-id mismatch: node reports {reported} but the configured/vendored " + f"chain-id is {expected}. Refusing to send a transaction to another network " + "(PBA-L6b-042)." + ) + manager._chain_verified = True + return cast(str, manager._rpc_call("eth_sendTransaction", [dict(tx, chainId=hex(expected))])) diff --git a/citrate_sdk/compute.py b/citrate_sdk/compute.py index 6b3ce3d..0e13866 100644 --- a/citrate_sdk/compute.py +++ b/citrate_sdk/compute.py @@ -23,6 +23,7 @@ from typing import Any, cast +from ._chain_guard import expected_chain_id, pinned_send from .abi import AbiInterface, enum_index, from_wei, to_wei from .errors import ConfigurationError from .types import ComputeJob, ComputePool, Dispute, ProviderInfo @@ -86,8 +87,13 @@ def __init__( gas_limit: int = 500_000, gas_price: str = "0x3b9aca00", contract_addresses: dict[str, str] | None = None, + *, + chain_id: int | None = None, ) -> None: self._rpc_call = rpc_call + # PBA-L6b-042: writes assert eth_chainId against this before sending. + self._expected_chain_id = expected_chain_id(chain_id) + self._chain_verified = False self._default_account = default_account self._gas_limit = gas_limit self._gas_price = gas_price @@ -130,7 +136,7 @@ def _send_transaction(self, to: str, data: str, value: str = "0x0") -> str: "gas": hex(self._gas_limit), "gasPrice": self._gas_price, } - return cast(str, self._rpc_call("eth_sendTransaction", [tx])) + return pinned_send(self, tx) # ------------------------------------------------------------------- # Job Lifecycle diff --git a/citrate_sdk/farming.py b/citrate_sdk/farming.py index 44271a7..2c05ebc 100644 --- a/citrate_sdk/farming.py +++ b/citrate_sdk/farming.py @@ -22,6 +22,7 @@ from typing import Any, cast +from ._chain_guard import expected_chain_id, pinned_send from .abi import AbiInterface from .errors import ConfigurationError @@ -67,8 +68,13 @@ def __init__( gas_limit: int = 300_000, gas_price: str = "0x3b9aca00", contract_addresses: dict[str, str] | None = None, + *, + chain_id: int | None = None, ) -> None: self._rpc_call = rpc_call + # PBA-L6b-042: writes assert eth_chainId against this before sending. + self._expected_chain_id = expected_chain_id(chain_id) + self._chain_verified = False self._default_account = default_account self._gas_limit = gas_limit self._gas_price = gas_price @@ -102,7 +108,7 @@ def _send_transaction(self, to: str, data: str, value: str = "0x0") -> str: "gas": hex(self._gas_limit), "gasPrice": self._gas_price, } - return cast(str, self._rpc_call("eth_sendTransaction", [tx])) + return pinned_send(self, tx) def _resolve_address(self, address: str | None) -> str: target = address or self._default_account diff --git a/citrate_sdk/learning.py b/citrate_sdk/learning.py index 8ae4ce7..3c13962 100644 --- a/citrate_sdk/learning.py +++ b/citrate_sdk/learning.py @@ -20,6 +20,7 @@ from eth_abi import decode as abi_decode +from ._chain_guard import expected_chain_id, pinned_send from .abi import AbiInterface, enum_index, from_wei, keccak256_text, to_wei from .errors import ConfigurationError from .types import ( @@ -172,7 +173,7 @@ def _send_transaction(self, to: str, data: str, value: str = "0x0") -> str: "gas": hex(self._gas_limit), "gasPrice": self._gas_price, } - return cast(str, self._rpc_call("eth_sendTransaction", [tx])) + return pinned_send(self, tx) # ============================================================================ @@ -195,8 +196,13 @@ def __init__( gas_limit: int = 500_000, gas_price: str = "0x3b9aca00", contract_addresses: dict[str, str] | None = None, + *, + chain_id: int | None = None, ) -> None: self._rpc_call = rpc_call + # PBA-L6b-042: writes assert eth_chainId against this before sending. + self._expected_chain_id = expected_chain_id(chain_id) + self._chain_verified = False self._default_account = default_account self._gas_limit = gas_limit self._gas_price = gas_price @@ -478,8 +484,13 @@ def __init__( gas_limit: int = 500_000, gas_price: str = "0x3b9aca00", staking_address: str | None = None, + *, + chain_id: int | None = None, ) -> None: self._rpc_call = rpc_call + # PBA-L6b-042: writes assert eth_chainId against this before sending. + self._expected_chain_id = expected_chain_id(chain_id) + self._chain_verified = False self._default_account = default_account self._gas_limit = gas_limit self._gas_price = gas_price @@ -645,8 +656,13 @@ def __init__( gas_limit: int = 300_000, gas_price: str = "0x3b9aca00", classroom_address: str | None = None, + *, + chain_id: int | None = None, ) -> None: self._rpc_call = rpc_call + # PBA-L6b-042: writes assert eth_chainId against this before sending. + self._expected_chain_id = expected_chain_id(chain_id) + self._chain_verified = False self._default_account = default_account self._gas_limit = gas_limit self._gas_price = gas_price diff --git a/citrate_sdk/treasury.py b/citrate_sdk/treasury.py index cef1744..12994fd 100644 --- a/citrate_sdk/treasury.py +++ b/citrate_sdk/treasury.py @@ -21,6 +21,7 @@ from typing import Any, cast +from ._chain_guard import expected_chain_id, pinned_send from .abi import AbiInterface from .errors import ConfigurationError @@ -72,8 +73,13 @@ def __init__( gas_limit: int = 500_000, gas_price: str = "0x3b9aca00", contract_addresses: dict[str, str] | None = None, + *, + chain_id: int | None = None, ) -> None: self._rpc_call = rpc_call + # PBA-L6b-042: writes assert eth_chainId against this before sending. + self._expected_chain_id = expected_chain_id(chain_id) + self._chain_verified = False self._default_account = default_account self._gas_limit = gas_limit self._gas_price = gas_price @@ -116,7 +122,7 @@ def _send_transaction(self, to: str, data: str, value: str = "0x0") -> str: "gas": hex(self._gas_limit), "gasPrice": self._gas_price, } - return cast(str, self._rpc_call("eth_sendTransaction", [tx])) + return pinned_send(self, tx) # ------------------------------------------------------------------- # StablecoinTreasury — Deposits diff --git a/tests/_chain_rpc.py b/tests/_chain_rpc.py new file mode 100644 index 0000000..5e315fd --- /dev/null +++ b/tests/_chain_rpc.py @@ -0,0 +1,15 @@ +"""Test helper: an rpc_call mock that answers eth_chainId with the pinned chain. + +PBA-L6b-042 made every manager write assert ``eth_chainId`` before sending, so +write-path mocks must answer it like a real 40204 node does. +""" +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock + +from citrate_sdk._generated import contract as _contract + + +def chain_rpc(result: Any) -> MagicMock: + return MagicMock(side_effect=lambda method, params: hex(_contract.chain_id()) if method == "eth_chainId" else result) diff --git a/tests/test_compute.py b/tests/test_compute.py index aa7210a..621be27 100644 --- a/tests/test_compute.py +++ b/tests/test_compute.py @@ -19,6 +19,7 @@ ComputeManager, ) from citrate_sdk.errors import ConfigurationError +from tests._chain_rpc import chain_rpc # --------------------------------------------------------------------------- # Helpers @@ -67,11 +68,11 @@ def test_missing_dispute_address(self): def test_post_job_calldata(self): """post_job sends correct calldata, value, and contract address.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) model_hash = "0x" + "ab" * 32 mgr.post_job(model_hash, "hello world", "1.5", "ZK") - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] assert tx["to"] == FAKE_COMPUTE_ADDR assert int(tx["value"], 16) == to_wei("1.5") # Verify function selector @@ -80,10 +81,10 @@ def test_post_job_calldata(self): def test_bid_on_job_calldata(self): """bid_on_job sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.bid_on_job(job_id=3, price="0.5", latency=30) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _compute_iface.encode_function_data("bidOnJob", [ 3, to_wei("0.5"), 30, ]) @@ -91,10 +92,10 @@ def test_bid_on_job_calldata(self): def test_submit_result_calldata(self): """submit_result encodes output and proof as bytes.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.submit_result(job_id=7, output="result data", proof="proof data") - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _compute_iface.encode_function_data("submitResult", [ 7, b"result data", b"proof data", ]) @@ -102,14 +103,14 @@ def test_submit_result_calldata(self): def test_register_provider_calldata(self): """register_provider sends correct calldata and stake as value.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.register_provider( stake="100", models=["model-a", "model-b"], endpoint="https://my-gpu.example.com", ) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _compute_iface.encode_function_data("registerProvider", [ ["model-a", "model-b"], "https://my-gpu.example.com", ]) @@ -118,19 +119,19 @@ def test_register_provider_calldata(self): def test_heartbeat_calldata(self): """heartbeat sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.heartbeat() - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _compute_iface.encode_function_data("heartbeat") assert tx["data"] == expected def test_create_pool_calldata(self): """create_pool encodes mode enum and price correctly.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.create_pool("GPU Cluster", "DataParallel", 5, 1000, "2.5") - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _compute_iface.encode_function_data("createPool", [ "GPU Cluster", 1, 5, 1000, to_wei("2.5"), ]) @@ -138,28 +139,28 @@ def test_create_pool_calldata(self): def test_join_pool_calldata(self): """join_pool sends correct poolId and gpuCount.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.join_pool(pool_id=2, gpu_count=4) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _compute_iface.encode_function_data("joinPool", [2, 4]) assert tx["data"] == expected def test_leave_pool_calldata(self): """leave_pool sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.leave_pool(pool_id=9) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _compute_iface.encode_function_data("leavePool", [9]) assert tx["data"] == expected def test_dispute_result_calldata(self): """dispute_result sends to dispute contract with bond as value.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.dispute_result(job_id=5, bond="3") - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _dispute_iface.encode_function_data("disputeResult", [5]) assert tx["data"] == expected assert tx["to"] == FAKE_DISPUTE_ADDR diff --git a/tests/test_farming.py b/tests/test_farming.py index 5dd04e7..4702e15 100644 --- a/tests/test_farming.py +++ b/tests/test_farming.py @@ -19,6 +19,7 @@ TESTNET_FARMING_ABI, FarmingManager, ) +from tests._chain_rpc import chain_rpc # --------------------------------------------------------------------------- # Helpers @@ -112,7 +113,7 @@ def test_get_my_score_uses_default_account(self): rpc = MagicMock(return_value=_encode_uint256(100)) mgr = self._make_manager(rpc) mgr.get_my_score() # no address parameter - call_args = rpc.call_args_list[0][0] + call_args = rpc.call_args_list[-1][0] assert call_args[1][0]["to"] == FAKE_FARMING_ADDR def test_get_my_score_selector(self): @@ -180,7 +181,7 @@ def test_get_leaderboard_default_count(self): rpc = MagicMock(return_value=encoded) mgr = self._make_manager(rpc) mgr.get_leaderboard() # default count - call_args = rpc.call_args_list[0][0] + call_args = rpc.call_args_list[-1][0] expected = _farming_iface.encode_function_data("getTopContributors", [20]) assert call_args[1][0]["data"] == expected @@ -188,10 +189,10 @@ def test_get_leaderboard_default_count(self): def test_claim_calldata(self): """claim sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.claim() - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _farming_iface.encode_function_data("claim") assert tx["data"] == expected assert tx["to"] == FAKE_FARMING_ADDR diff --git a/tests/test_learning.py b/tests/test_learning.py index ffcb5fb..33b5f20 100644 --- a/tests/test_learning.py +++ b/tests/test_learning.py @@ -24,6 +24,7 @@ LearningManager, StakingManager, ) +from tests._chain_rpc import chain_rpc # --------------------------------------------------------------------------- # Helpers @@ -97,17 +98,17 @@ def test_list_pools_empty(self): pools = mgr.list_pools() assert pools == [] # Verify we called eth_call with correct target - call_args = rpc.call_args_list[0] + call_args = rpc.call_args_list[-1] assert call_args[0][0] == "eth_call" assert call_args[0][1][0]["to"] == FAKE_POOL_ADDR def test_join_pool_calldata(self): """join_pool sends correct calldata and value.""" - rpc = MagicMock(return_value="0xfake_tx_hash") + rpc = chain_rpc("0xfake_tx_hash") mgr = self._make_manager(rpc) result = mgr.join_pool(pool_id=5, stake_amount="10") assert result == "0xfake_tx_hash" - call_args = rpc.call_args_list[0] + call_args = rpc.call_args_list[-1] assert call_args[0][0] == "eth_sendTransaction" tx = call_args[0][1][0] assert tx["to"] == FAKE_POOL_ADDR @@ -117,19 +118,19 @@ def test_join_pool_calldata(self): def test_leave_pool_calldata(self): """leave_pool sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.leave_pool(pool_id=3) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _pool_iface.encode_function_data("leavePool", [3]) assert tx["data"] == expected def test_create_pool_calldata(self): """create_pool sends correct ABI-encoded data with access enum.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.create_pool("My Pool", "A test pool", "InviteOnly", "5") - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _pool_iface.encode_function_data("createPool", [ "My Pool", "A test pool", 1, to_wei("5"), ]) @@ -138,29 +139,29 @@ def test_create_pool_calldata(self): def test_register_for_cycle_calldata(self): """register_for_cycle sends correct calldata to cycle contract.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.register_for_cycle(cycle_id=7) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _cycle_iface.encode_function_data("registerParticipant", [7]) assert tx["data"] == expected assert tx["to"] == FAKE_CYCLE_ADDR def test_claim_cycle_reward_calldata(self): """claim_cycle_reward sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.claim_cycle_reward(cycle_id=2) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _cycle_iface.encode_function_data("claimCycleReward", [2]) assert tx["data"] == expected def test_claim_contribution_rewards_calldata(self): """claim_contribution_rewards sends correct calldata to contrib contract.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.claim_contribution_rewards() - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _contrib_iface.encode_function_data("claimRewards") assert tx["data"] == expected assert tx["to"] == FAKE_CONTRIB_ADDR @@ -207,10 +208,10 @@ def test_missing_staking_address(self): def test_deposit_calldata(self): """deposit sends correct calldata and value.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.deposit("50") - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _staking_iface.encode_function_data("deposit") assert tx["data"] == expected assert int(tx["value"], 16) == to_wei("50") @@ -218,19 +219,19 @@ def test_deposit_calldata(self): def test_withdraw_calldata(self): """withdraw encodes requestWithdrawal with share amount.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.withdraw("25") - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _staking_iface.encode_function_data("requestWithdrawal", [to_wei("25")]) assert tx["data"] == expected def test_claim_withdrawal_calldata(self): """claim_withdrawal sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.claim_withdrawal(request_id=42) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _staking_iface.encode_function_data("claimWithdrawal", [42]) assert tx["data"] == expected @@ -239,7 +240,7 @@ def test_preview_deposit_calldata(self): rpc = MagicMock(return_value=_encode_uint256(to_wei("49"))) mgr = self._make_manager(rpc) result = mgr.preview_deposit("50") - call_args = rpc.call_args_list[0] + call_args = rpc.call_args_list[-1] assert call_args[0][0] == "eth_call" tx = call_args[0][1][0] expected = _staking_iface.encode_function_data("previewDeposit", [to_wei("50")]) @@ -252,7 +253,7 @@ def test_preview_withdraw_calldata(self): rpc = MagicMock(return_value=_encode_uint256(to_wei("24"))) mgr = self._make_manager(rpc) mgr.preview_withdraw("25") - call_args = rpc.call_args_list[0] + call_args = rpc.call_args_list[-1] tx = call_args[0][1][0] expected = _staking_iface.encode_function_data("previewWithdraw", [to_wei("25")]) assert tx["data"] == expected @@ -291,10 +292,10 @@ def test_missing_classroom_address(self): 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).""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.enroll("secret-code-123") - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _classroom_iface.encode_function_data( "enrollWithCode", [b"secret-code-123"] ) @@ -303,20 +304,20 @@ def test_enroll_calldata(self): def test_unenroll_calldata(self): """unenroll sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.unenroll() - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _classroom_iface.encode_function_data("unenroll") assert tx["data"] == expected def test_deploy_model_calldata(self): """deploy_model sends whitelistModel with correct bytes32.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) model_hash = "0x" + "ab" * 32 mgr.deploy_model(model_hash) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _classroom_iface.encode_function_data( "whitelistModel", [bytes.fromhex("ab" * 32)] ) @@ -324,11 +325,11 @@ def test_deploy_model_calldata(self): def test_remove_model_calldata(self): """remove_model sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) model_hash = "cd" * 32 # no 0x prefix mgr.remove_model(model_hash) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] expected = _classroom_iface.encode_function_data( "removeModel", [bytes.fromhex("cd" * 32)] ) @@ -336,10 +337,10 @@ def test_remove_model_calldata(self): def test_rotate_invite_code_calldata(self): """rotate_invite_code hashes new code and sends correct calldata.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.rotate_invite_code("new-secret") - tx = rpc.call_args_list[0][0][1][0] + 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:])] @@ -354,7 +355,7 @@ def test_can_student_access_model_calldata(self): model = "0x" + "ff" * 32 result = mgr.can_student_access_model(student, model) assert result is True - call_args = rpc.call_args_list[0] + call_args = rpc.call_args_list[-1] assert call_args[0][0] == "eth_call" assert call_args[0][1][0]["to"] == FAKE_CLASSROOM_ADDR @@ -367,7 +368,7 @@ def test_get_student_teacher_calldata(self): mgr = self._make_manager(rpc) student = "0x" + "44" * 20 mgr.get_student_teacher(student) - call_args = rpc.call_args_list[0] + call_args = rpc.call_args_list[-1] assert call_args[0][0] == "eth_call" tx = call_args[0][1][0] expected = _classroom_iface.encode_function_data("getStudentTeacher", [student]) diff --git a/tests/test_pba_r2_l6b_042_chain_pin.py b/tests/test_pba_r2_l6b_042_chain_pin.py new file mode 100644 index 0000000..54acce5 --- /dev/null +++ b/tests/test_pba_r2_l6b_042_chain_pin.py @@ -0,0 +1,82 @@ +"""PBA-L6b-042: the SPY-B-005 chain-id pin applied only to CitrateClient. + +The Learning/Staking/Classroom/Compute/Treasury/Farming managers send writes +with node-side ``eth_sendTransaction`` and never asked the node which chain it +is on, so a manager pointed at the wrong network's node (holding the same +account) moved funds there. Every manager write now asserts ``eth_chainId`` +against the pinned chain (default: the federation artifact, 40204; overridable +with ``chain_id=``) before the first send, and puts ``chainId`` in the tx. +""" +from __future__ import annotations + +from typing import Any + +import pytest + +from citrate_sdk.compute import ComputeManager +from citrate_sdk.errors import CitrateError +from citrate_sdk.farming import FarmingManager +from citrate_sdk.learning import ClassroomManager, LearningManager, StakingManager +from citrate_sdk.treasury import TreasuryManager + +ACCT = "0x" + "01" * 20 +A = "0x" + "02" * 20 +ADDRS = dict.fromkeys(("computePool", "learningPool", "stablecoinTreasury", "bulkComputeGateway", "testnetFarmingAccounting", "learningCycleManager", "contributionAccounting", "disputeResolution"), A) + + +def _rpc(chain: int) -> tuple[Any, list[str]]: + methods: list[str] = [] + + def rpc(method: str, params: Any) -> Any: + methods.append(method) + if method == "eth_chainId": + return hex(chain) + return "0xhash" + return rpc, methods + + +WRITES = [ + ("learning.join_pool", lambda r, **k: LearningManager(r, default_account=ACCT, contract_addresses=ADDRS, **k).join_pool(1, "1")), + ("staking.deposit", lambda r, **k: StakingManager(r, default_account=ACCT, staking_address=A, **k).deposit("1")), + ("classroom.unenroll", lambda r, **k: ClassroomManager(r, default_account=ACCT, classroom_address=A, **k).unenroll()), + ("compute.heartbeat", lambda r, **k: ComputeManager(r, default_account=ACCT, contract_addresses=ADDRS, **k).heartbeat()), + ("treasury.deposit", lambda r, **k: TreasuryManager(r, default_account=ACCT, contract_addresses=ADDRS, **k).deposit_stablecoin(A, 1)), + ("farming.claim", lambda r, **k: FarmingManager(r, default_account=ACCT, contract_addresses=ADDRS, **k).claim()), +] + + +@pytest.mark.parametrize(("name", "write"), WRITES) +def test_wrong_chain_refused_before_send(name: str, write: Any) -> None: + rpc, methods = _rpc(1) + with pytest.raises(CitrateError, match="chain-id mismatch"): + write(rpc) + assert "eth_sendTransaction" not in methods, name + + +@pytest.mark.parametrize(("name", "write"), WRITES) +def test_right_chain_sends_with_chain_id_field(name: str, write: Any) -> None: + sent: list[Any] = [] + + def rpc(method: str, params: Any) -> Any: + if method == "eth_chainId": + return hex(40204) + sent.append(params[0]) + return "0xhash" + + write(rpc) + assert sent and sent[-1]["chainId"] == hex(40204), name + + +def test_chain_is_checked_once_per_manager_and_override_works() -> None: + rpc, methods = _rpc(31337) + mgr = ComputeManager(rpc, default_account=ACCT, contract_addresses=ADDRS, chain_id=31337) + mgr.heartbeat() + mgr.heartbeat() + assert methods.count("eth_chainId") == 1 and methods.count("eth_sendTransaction") == 2 + + +def test_garbage_chain_id_is_refused() -> None: + def rpc(method: str, params: Any) -> Any: + return "0xnope" if method == "eth_chainId" else "0xhash" + with pytest.raises(CitrateError, match="chain"): + ComputeManager(rpc, default_account=ACCT, contract_addresses=ADDRS).heartbeat() diff --git a/tests/test_treasury.py b/tests/test_treasury.py index c556596..ca7b931 100644 --- a/tests/test_treasury.py +++ b/tests/test_treasury.py @@ -20,6 +20,7 @@ STABLECOIN_TREASURY_ABI, TreasuryManager, ) +from tests._chain_rpc import chain_rpc # --------------------------------------------------------------------------- # Helpers @@ -112,10 +113,10 @@ def test_no_address_for_estimate_calls(self): def test_deposit_stablecoin_calldata(self): """deposit_stablecoin sends correct calldata to treasury.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.deposit_stablecoin(FAKE_STABLECOIN, 100_000_000) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] assert tx["to"] == FAKE_TREASURY_ADDR expected = _treasury_iface.encode_function_data("deposit", [FAKE_STABLECOIN, 100_000_000]) assert tx["data"] == expected @@ -131,10 +132,10 @@ def test_deposit_stablecoin_selector(self): def test_purchase_compute_credits_calldata(self): """purchase_compute_credits sends correct calldata to gateway.""" - rpc = MagicMock(return_value="0xtx") + rpc = chain_rpc("0xtx") mgr = self._make_manager(rpc) mgr.purchase_compute_credits(FAKE_STABLECOIN, 50_000_000) - tx = rpc.call_args_list[0][0][1][0] + tx = rpc.call_args_list[-1][0][1][0] assert tx["to"] == FAKE_GATEWAY_ADDR expected = _gateway_iface.encode_function_data("purchaseComputeCredits", [ FAKE_STABLECOIN, 50_000_000, @@ -163,7 +164,7 @@ def test_get_credit_balance_uses_default_account(self): rpc = MagicMock(return_value=_encode_uint256(100)) mgr = self._make_manager(rpc) mgr.get_credit_balance() # no address parameter - call_args = rpc.call_args_list[0][0] + call_args = rpc.call_args_list[-1][0] # Verify the call went to the correct contract assert call_args[1][0]["to"] == FAKE_GATEWAY_ADDR @@ -189,7 +190,7 @@ def test_estimate_calls_remaining_default_tokens(self): rpc = MagicMock(return_value=_encode_uint256(100)) mgr = self._make_manager(rpc) mgr.estimate_calls_remaining(DEFAULT_ACCOUNT) - call_args = rpc.call_args_list[0][0] + call_args = rpc.call_args_list[-1][0] expected = _gateway_iface.encode_function_data("estimateCallsRemaining", [ DEFAULT_ACCOUNT, 1000, ]) From a25cc43ebb0aa35fd2e3f3797bfb0da1e1892181 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:24:06 -0700 Subject: [PATCH 09/12] chore(release): 0.6.2 with security advisory for PBA-L6b-003 0.6.1 exists only in the repo (PyPI has 0.6.0 alone) and already names another change set, so the fix ships as the next free version, 0.6.2. CHANGELOG carries the advisory (rotate models deployed with threshold_shares > 0; yank 0.6.0) and the breaking-change notes. Not published: release is the owner's call via the publish workflow. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- CHANGELOG.md | 68 ++++++++++++++++++++++++++++++++++++++++- citrate_sdk/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9555d4..4b28627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,73 @@ All notable changes to `citrate-labs-sdk` are documented here. This project adheres to [Semantic Versioning](https://semver.org/). -## [0.6.1] - 2026-08-02 — SECREM-02 K3 envelope hardening +## [0.6.2] - 2026-09-25 — Pre-bounty audit remediation (SECURITY) + +> **Security advisory: upgrade from 0.6.0 (and any unreleased 0.6.1 build).** +> `CitrateClient.deploy_model` with `encrypted=True` and +> `EncryptionConfig(threshold_shares > 0)` wrote **every Shamir share of the +> model AES key** into the public deploy calldata (`encryption_metadata.key_shares`). +> Anyone reading the chain could rebuild the key and decrypt the uploaded model +> with no private key (PBA-L6b-003, CRITICAL; same design flaw as the JS SDK's +> PBA-L4-001). Models deployed that way should be treated as disclosed: rotate +> (re-encrypt under a new key) and redeploy. Deploys with `threshold_shares=0` +> (the default) were not affected. 0.6.0 is to be yanked on PyPI. + +### Security + +- **PBA-L6b-003 (CRITICAL):** key shares never enter metadata or calldata. + `encrypt_model` refuses `threshold_shares > 0`; the new + `KeyManager.encrypt_model_with_key_shares` requires + `EncryptionConfig.share_holder_public_keys` (one distinct secp256k1 key per + share), ECDH-wraps each share to its holder (V2 envelope) and returns the + envelopes separately. `deploy_model` returns them on + `ModelDeployment.key_share_envelopes` for **off-chain** delivery; holders open + theirs with `KeyManager.unwrap_key_share(envelope, owner_public_key)`. + `deploy_model` also refuses any payload that carries a key-share field. + Tripwires: a calldata decoder asserts no subset of the deploy calldata + rebuilds the key, in the test suite and against the built wheel in CI and in + the publish workflow. +- Shamir hardening (variant of the JS PBA-L4-005): every share is validated + (integer x in 1..255, distinct, equal non-empty y); + `reconstruct_key_from_shares(shares, threshold)` takes the threshold from the + caller; `verify_shares` now checks consistency. +- **PBA-L6b-026:** the transport gate refused plain remote `http://` but passed + `"\u00a0http://..."` (empty parsed scheme). It now refuses whitespace and + control characters anywhere, allows only https/http, and refuses an empty + scheme or host. +- **PBA-L6b-027:** `verify_wallet_address_on_chain` goes through the transport + gate, asserts `eth_chainId`, and requires factory code. +- **PBA-L6b-028:** the default classroom invite code is + `secrets.token_urlsafe(16)` (was a millisecond timestamp); it is available as + `ClassroomManager.last_invite_code`. +- **PBA-L6b-029:** `verify_id_token` requires numeric `exp` and `iat` and + refuses a `typ` other than `JWT`. +- **PBA-L6b-030:** IPFS downloads stream under `max_bytes` (default 1 GiB) and + verify `expected_sha256` and self-describing CIDs. +- **PBA-L6b-042:** manager writes assert `eth_chainId` against the pinned chain. + +### Changed (breaking) + +- `IdentityClient.refresh(refresh_token, expected_sub)`: `expected_sub` is + required; a refreshed token naming another `sub` is refused. +- `IdentityClient.siwe_challenge()` takes no argument and returns `{"nonce"}` + (GET, as the authority serves it); build the message with + `build_siwe_message(...)`. `siwe_verify` returns + `{"kind": "redirect" | "token", ...}` (the authority never returned the + access/refresh tokens the old client expected). +- `reconstruct_key_from_shares(shares, threshold)`: threshold is required. +- Unknown `access` / `tier` / `mode` strings raise `ValueError` (PBA-L6b-031) + instead of silently becoming option 0. + +### Fixed + +- **PBA-L6b-040:** `ClassroomManager.enroll` encodes `enrollWithCode(bytes)` + (the contract hashes the raw code); `get_classroom` decodes the struct + return. A parity test pins every ClassroomRegistry selector. +- **PBA-L6b-041:** `uv.lock` regenerated (cryptography 50.0.1) and checked in + CI with `uv lock --check`. + +## [0.6.1] - 2026-08-02 (never published) — SECREM-02 K3 envelope hardening **Package metadata (2026-09-24):** Repository, Bug Tracker and Changelog now point at `github.com/CitrateNetwork/citrate-sdk-python`. The published 0.6.0 pointed at a private diff --git a/citrate_sdk/__init__.py b/citrate_sdk/__init__.py index ad265a6..829b162 100644 --- a/citrate_sdk/__init__.py +++ b/citrate_sdk/__init__.py @@ -48,7 +48,7 @@ StakingInfo, ) -__version__ = "0.6.1" +__version__ = "0.6.2" __author__ = "Citrate Team" __all__ = [ diff --git a/pyproject.toml b/pyproject.toml index 9e1c9b5..c48b24c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta" [project] name = "citrate-labs-sdk" -version = "0.6.1" +version = "0.6.2" description = "Python SDK for the Citrate distributed AI network (chain 40204). The canonical TypeScript SDK is @citratelabs/sdk on npm; this Python client is opt-in and may lag it." readme = "README.md" requires-python = ">=3.10" From 409eea734131180548d6e889f8f4bd5c3828f46d Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Thu, 24 Sep 2026 23:24:07 -0700 Subject: [PATCH 10/12] fix(deps): regenerate uv.lock and gate it in CI (PBA-L6b-041) uv.lock locked cryptography 46.0.7 although pyproject requires >=48.0.1, and was missing packages the dev extra pulls in; uv lock --check failed on main. Regenerated with uv 0.12.19 (cryptography now 50.0.1; existing pins kept where still compatible) and added a uv-lock-check CI job (uv lock --check) so the lock cannot drift silently again. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- .github/workflows/ci.yml | 14 + uv.lock | 1341 +++++++++++++++++++++++++++++++++----- 2 files changed, 1196 insertions(+), 159 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85ad896..b14fd71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,20 @@ jobs: python: uses: CitrateNetwork/.github/.github/workflows/reusable-python-ci.yml@db5f1a6ddaec28275708265b56762eb1dfc3571b # main + # PBA-L6b-041: uv.lock drifted from pyproject (it locked cryptography 46.0.7 + # while pyproject requires >=48.0.1) because nothing checked it. Fail when the + # lock no longer matches pyproject.toml. + uv-lock-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install "uv==0.12.19" + - name: uv.lock matches pyproject.toml + run: uv lock --check + # 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 diff --git a/uv.lock b/uv.lock index 83426f0..ef090b4 100644 --- a/uv.lock +++ b/uv.lock @@ -530,13 +530,15 @@ wheels = [ ] [[package]] -name = "citrate-ai-sdk" -version = "0.5.0" +name = "citrate-labs-sdk" +version = "0.6.2" source = { editable = "." } dependencies = [ { name = "cryptography" }, { name = "eth-account" }, - { name = "numpy" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "requests" }, { name = "typing-extensions" }, { name = "web3" }, @@ -547,10 +549,14 @@ dev = [ { name = "black" }, { name = "coverage" }, { name = "isort" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mypy" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, + { name = "setuptools" }, + { name = "tomli" }, ] docs = [ { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -568,19 +574,22 @@ docs = [ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0" }, { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.0" }, - { name = "cryptography", specifier = "~=46.0" }, + { name = "cryptography", specifier = ">=48.0.1,<51" }, { name = "eth-account", specifier = "~=0.9" }, - { name = "isort", marker = "extra == 'dev'", specifier = ">=5.0" }, + { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13,<7" }, + { name = "matplotlib", marker = "extra == 'dev'", specifier = ">=3.7" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0" }, { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=2.0" }, - { name = "numpy", specifier = "~=1.21" }, + { name = "numpy", specifier = "~=2.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21" }, - { name = "requests", specifier = "~=2.28" }, + { name = "requests", specifier = "~=2.33" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, - { name = "sphinx", marker = "extra == 'docs'", specifier = ">=6.0" }, + { name = "setuptools", marker = "extra == 'dev'", specifier = ">=83" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=8.1.3" }, { name = "sphinx-autodoc-typehints", marker = "extra == 'docs'", specifier = ">=1.20" }, { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=1.0" }, + { name = "tomli", marker = "extra == 'dev'", specifier = ">=2.0" }, { name = "typing-extensions", specifier = "~=4.0" }, { name = "web3", specifier = "~=7.15" }, ] @@ -663,6 +672,244 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "contourpy" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/5a/a55177dd22553a277388e8a1b3220e92de91bacb28356cdc73caa240121d/contourpy-1.4.0.tar.gz", hash = "sha256:20156f5a1ac4f8ce02656e39a61e82164a3d359796dc8026f75b062783d500e1", size = 13323726, upload-time = "2026-09-11T19:05:05.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ba/01bde9753bdbba04a6da9d2bff3881f021bc708f654655e95fae26d3b3a3/contourpy-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:186ba929df36d61b6127da2e89cd1357e4cb79aca647381ab1a6feb0b152877b", size = 297302, upload-time = "2026-09-11T19:02:45.103Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f5/c5bf67522d49a2222f3154fe46879451d26f0e35ece41770758a64ea78cb/contourpy-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c76f3a5318164db7d9401132fc1b5364b784c613c93fa506e3b5e6d1bf353ec8", size = 284921, upload-time = "2026-09-11T19:02:48.049Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cc/cb989599eec12fda312e127eb8e04a8b21a7a6c94bf7ff1bc68273334a42/contourpy-1.4.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43c3ccbb32c6294b183dcc8e8c46dacf5ecef809497e3d48a5be298eef185ad0", size = 356332, upload-time = "2026-09-11T19:02:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/47/fb/6f620d7602507817b0b4c21dd790ed68688f1f4ae9c29aacf21f08db5cf1/contourpy-1.4.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86d05cec773c9507a3950122e0e40ce77c23c75ceeb2fc189514e71893cbb34b", size = 405878, upload-time = "2026-09-11T19:02:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/693c6d6bdece679f0953775eef3a1b66d56be55478d936e7deb1f3004422/contourpy-1.4.0-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2deb580178ca19437a84bd77e4bc2cd91a8ccad212413a83c273900868b5978c", size = 408296, upload-time = "2026-09-11T19:02:56.528Z" }, + { url = "https://files.pythonhosted.org/packages/78/f9/b6831508960d559581c448532ba4df312217072975486dbf2b24fcc5b76f/contourpy-1.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:875f42444c9cf48d56f724f2637e60d0f73b3b12c9041e1484580a233edf9591", size = 383243, upload-time = "2026-09-11T19:02:58.638Z" }, + { url = "https://files.pythonhosted.org/packages/f2/21/52903825a0ae7bb625e8bd30a09816d4c3c5d6174a469c10a616166cf780/contourpy-1.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f863c6100bf926cf47d13f3cd75f9bb8ebd98aaab230eeb4468b91f98f39a6b3", size = 1357306, upload-time = "2026-09-11T19:03:01.036Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/d68b1ce8539e4071518fed9523f558398f34dcd078b8927b109c72dad2ef/contourpy-1.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3863ef2e2b13fe93f8c0ebb08ee400cb07153b8b0e91c5acb26e4537f283634f", size = 1427243, upload-time = "2026-09-11T19:03:03.866Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/b75a10e9d0616b97a30de277b6dfe83f1879880854c7330337309530eb48/contourpy-1.4.0-cp312-cp312-win32.whl", hash = "sha256:5450f091ac1be0be3ad3a2a3b3f23b5e443e78c670ced4fd347d626f92a28fd2", size = 347345, upload-time = "2026-09-11T19:03:06.192Z" }, + { url = "https://files.pythonhosted.org/packages/50/a9/dab08786bb4d77ef9a046b3c1be923be4e73c5d07d91f5a54e8ea9b09e41/contourpy-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e697d94e69f499ff6bebb899cae97a58d5d14f0e1fe9568b43a0248d2f9af8c", size = 234083, upload-time = "2026-09-11T19:03:08.122Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/3ba509755a970dbde5948e7142ac21f266be72f38017cc4087a05fdceee1/contourpy-1.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:0c7a4c2716a4e98342221954416a836cca77c996c14ddbf22b5f01d5d93ca09c", size = 559521, upload-time = "2026-09-11T19:03:10.047Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8b/62a6eacea08ea0b6ab0159a7783df7ed421ff9c1ff58fef7c31f58fa5e52/contourpy-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e21d1d4a9db5b3a793649c7ab0ddb4697f2818929eec871a7b9c45fce7b2a1ea", size = 297330, upload-time = "2026-09-11T19:03:11.809Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2f/103cdc5b7077cd569989d3ddb99ee3d4e96ee31bde8df2a6e426b2dc6195/contourpy-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:196759a3e4db60e1546167e361090ebb6d199aa3aafc9e34ea17a5ac3b23e814", size = 285037, upload-time = "2026-09-11T19:03:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/bf/25/97001d1fdf940e6f5adfc592d4f5899fff95daf7dceead3988fe09987d2d/contourpy-1.4.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c0e07c691f3b3321913ed9b8161c50ea5f77fadd006f52f5e755359b0dcbfc5", size = 356675, upload-time = "2026-09-11T19:03:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5e/9a7241a89e2f74402aefa2569e1349ad2750ff77823c04c788eaaedb7fdd/contourpy-1.4.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f861af508fca2384eef392bc1d8377cfa349b2b854cb11b45d89c75780e2561", size = 406990, upload-time = "2026-09-11T19:03:17.44Z" }, + { url = "https://files.pythonhosted.org/packages/36/81/3a4fbbf6a806eada5453a6d16f6c7d96b9f4b3e57a3127368fb545986b8a/contourpy-1.4.0-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a03b32c2e7eda8b17757c022162c13c86f5c3d6f937dddf9ba3c3ff7b513953b", size = 407271, upload-time = "2026-09-11T19:03:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/68/77/8f93ecbde1a4d11dfd460cd19a0c159f5487352e751fa87f5d78cc9db08e/contourpy-1.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1219a8898523cba821085f2da8a1b962cc696325763f09d7f93538ba43b2d70", size = 384707, upload-time = "2026-09-11T19:03:21.208Z" }, + { url = "https://files.pythonhosted.org/packages/05/07/e5cdf94d68961ee464de6b4fedd3d2f666584f59ccd124bb1290597122ab/contourpy-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20da2f86bfaed60dbba729fe738c8241b1dd22b6cf7e8cbbf30df290bd04ba28", size = 1356741, upload-time = "2026-09-11T19:03:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8a/2301d07accb257ac08daeb631875fada06fedd019c20ff7b00ee07e9d211/contourpy-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ced1670fafee703b8277ab1645072a226e74f167166eae146fb743916119b67", size = 1426793, upload-time = "2026-09-11T19:03:25.955Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5d/a62648f06259d15c139b14eb8cf3c58fe53cb970a55f2bb77d1070e80a41/contourpy-1.4.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:abd0f7e51ecc52bf8c95713bd80b1c17c516a71fa391c54475308a4377903c2b", size = 117961, upload-time = "2026-09-11T19:03:27.713Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/c5b7771ba6982f319ec90c0cf5c425b36891adfeee2d9f667f90b2d0c166/contourpy-1.4.0-cp313-cp313-win32.whl", hash = "sha256:510f7d93d94cf6ebf4e2cd0640bea16ab8affac7547230175b6745b596ce0599", size = 347438, upload-time = "2026-09-11T19:03:29.829Z" }, + { url = "https://files.pythonhosted.org/packages/68/c1/a59df0754bf83a6f3e153cc19d550a5b01f607e389b1c4d57f436f9139e0/contourpy-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:6507a016976a75a15aee47809a34605de6244ccc056e6164bf9fb14b27eecad4", size = 234084, upload-time = "2026-09-11T19:03:31.657Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/97224f5decb1efa5a610f912195deb45321f73ec9b9dac485cd6fe2d5a6a/contourpy-1.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:423bd5f4b3f11d54a8c597234e513382e3454bb106b8ec7ae32ba4ed63f8d99b", size = 559559, upload-time = "2026-09-11T19:03:33.528Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/b548628c8dddf6e5e0a981b7fa8e4c008024df09c7d17c6a9e271edfa0b3/contourpy-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a3e67bc1a6a4618a1dac7c3053a9ffece5ddfa2046b2670b342cf430bb0b87d1", size = 297354, upload-time = "2026-09-11T19:03:35.017Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/513484208742f65af2648c519f0b7ab034616e183c4403538024c136a1ab/contourpy-1.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a702e2e178f4fcf96f04775d0c059e4c917925abf0d3208ebb6865df3c23d", size = 283729, upload-time = "2026-09-11T19:03:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/35/da/5a6562febc994b2c4bf9d01e57a50458ef8a2051bb3e7b35a00333e0fc26/contourpy-1.4.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:404dbcd9233513dfd1323b66ea593fef90e899f658f6b8a9ed8e93bd0ca669db", size = 357819, upload-time = "2026-09-11T19:03:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/6d/99/7b358ce888aaa426f755daa096c99c59d17c352037344e836f20e5d3b957/contourpy-1.4.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61624f6722480aa7e168fd746164e7bfdf48f8ccb6542f1613741200dc37e2b1", size = 407795, upload-time = "2026-09-11T19:03:41.293Z" }, + { url = "https://files.pythonhosted.org/packages/be/02/34d7548adf08c60967435d79d7a90882eacc84f5093435287974c7e99943/contourpy-1.4.0-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e741a39dfc96babe1722561e81351aeec104526c57dbe91c167c1db606c2d55", size = 408552, upload-time = "2026-09-11T19:03:42.728Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8c/9677dad226e5aaac5b823d5b09d881ebc32ff1648af417597649841e516c/contourpy-1.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e439ab450c93455feb0218532ded3e946ec7a9b5f459069f122cfa44b89229f6", size = 384850, upload-time = "2026-09-11T19:03:44.402Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/e3fbeaa489c223629fbb434737cb3de4ecaca9cae4825e77b92490f640f8/contourpy-1.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:417be048f7e122cefbe2a34c51a1f2b0410f8eb35796f794d45475ddb7872d9d", size = 1357601, upload-time = "2026-09-11T19:03:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55fec293d342c222ed58a5d4056df4362e192af2e59527a3d5d822de8828/contourpy-1.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43d80072a32299bf945de0a6dd4e034ea0162e832261de2e07a274a5adbba9c9", size = 1427340, upload-time = "2026-09-11T19:03:48.745Z" }, + { url = "https://files.pythonhosted.org/packages/86/40/0faabf453edf59b0e3633381d7ca72c3414fb9b72b5cbb08b06ed792d3e5/contourpy-1.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:fa1b787362a3856e63dd2b89449f6c384d55ee1beb8f94f4bcc871b40f02462c", size = 118355, upload-time = "2026-09-11T19:03:50.047Z" }, + { url = "https://files.pythonhosted.org/packages/38/0c/7ea75aa3559ae8a6805015e15a19c92b6480752ecc92526361e5776e88c5/contourpy-1.4.0-cp314-cp314-win32.whl", hash = "sha256:738c44fa71735a617f36da58e32810512407d283d5d1bb5b1cecb00d7eeba7cf", size = 356977, upload-time = "2026-09-11T19:04:11.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/3df8cc14bb572c8b447f9dfbc876703f0f2f2896058285b5c87a1d4a638d/contourpy-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:181bea01bc742734ae672fa00c717d855dd35e1f029536406538c52e7b0cd73d", size = 240101, upload-time = "2026-09-11T19:04:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/66/01/ee6830a5aa4565662a345172f6f35eeeed625d20f1c9d4ecd35318fe4ade/contourpy-1.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:64039341e2d8804f1a13bda8c69083eab935167bbd7e856cf096241f17d5bd45", size = 576359, upload-time = "2026-09-11T19:04:14.983Z" }, + { url = "https://files.pythonhosted.org/packages/51/af/49ee9c9cf012699e505c1b63c531cc99f19df50f205173092c4d541e3ec7/contourpy-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ef47fef9a912c77e3d84b014f701e5a9340f25703e9ed3bcebe37f91fb69dc49", size = 313601, upload-time = "2026-09-11T19:03:51.452Z" }, + { url = "https://files.pythonhosted.org/packages/ad/59/ce411ab2be0f805038626d8f4ca9ce563487ff51ff6c363e7512271564ed/contourpy-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:96019f059bcb3774acc0104be3360a57b36d33eaaa2e1d7f77c800ca8e07618b", size = 302812, upload-time = "2026-09-11T19:03:53.022Z" }, + { url = "https://files.pythonhosted.org/packages/95/b0/390915f9af14c1e2d3b2a9ad99d85b98df4352346a51e368cbb825f813c3/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddf5a2596d716fd3793434844caf31abc7a40b1e8718431420c89858268fb909", size = 348996, upload-time = "2026-09-11T19:03:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/c7/85/e0952576d54322f3e9983b1e049a7a31f42ce82c5c29d01821e6dd3780d7/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd59df9e2fb0aff8bd7fd1adb9f349b7c835245a9d0f18c2f2deeb537190f2b1", size = 400272, upload-time = "2026-09-11T19:03:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/f13543a5e1598e4a4ad01d9d5e2d65f9c09c37660616429913d606c5977d/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7794ea07cab575633daad8d6e963b662053391022294d3aacc1d9ba9ef54114", size = 412895, upload-time = "2026-09-11T19:03:58.487Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a8/6e5eaff53507dfd2dfc428240eecda32453067277d4b3e7ca71210b2d611/contourpy-1.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c8a74744fa746eeaa12f0f9ed7f8b4add9d8f1b14d95e3b5e133675eac888f", size = 377060, upload-time = "2026-09-11T19:04:00.122Z" }, + { url = "https://files.pythonhosted.org/packages/16/a3/d00e44d35511b3cead5bd794ee672d4eecf30d9de4ceac57a49d6204064f/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:107ec46f7aa1664266d69b1181aadc953d58d39f7d8d648fa5b795d4a060b0de", size = 1345688, upload-time = "2026-09-11T19:04:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/1e/01/1e533b0cae32c8edd63fe40435ec3b29345dab87321afcd28722d7f957d1/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a4c89c7f38a0d7a94356d74e3391073c4325c4d47ad2fd065c3fe7dbae16c0c", size = 1416140, upload-time = "2026-09-11T19:04:04.77Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/4ee66759f00d6a5d6909782f14f441d639fa18a4a43142e334351efc47f8/contourpy-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:912c6afaa106e2f74ba22b30416b77ef7eb94e3bdc5a4df51ab147845dba9b6d", size = 371982, upload-time = "2026-09-11T19:04:06.233Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/b3e5324138c3e3741f526663f4f265f4f69dfdf26415a39428940864a5f6/contourpy-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ef9440f6f8506246269a82734f5ff9e2e4c5e775b3996fc883cc491c5257eca6", size = 262907, upload-time = "2026-09-11T19:04:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/19/37/c9aa45e47819dc15a38fc5c81a2fb987fde55e9d3b991fbde514e3b6b5f5/contourpy-1.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:fc9feef8f1f001c5b87decadc67c4a5d1eebb62ca39c4763d1237ff62cf2b707", size = 587071, upload-time = "2026-09-11T19:04:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/cc/74/66d4e6fbea3bcb3fd4e5ae21c039ba31b8fc408b35884b4bc09b88016dce/contourpy-1.4.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:79b06c60d5e569ce12c5da8f0c51217cdc484386e2349fa717c30fafb56d2871", size = 297358, upload-time = "2026-09-11T19:04:16.718Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8d/8f7f25e71d73f15d7859943189900d2d348e740388c9ef52a0180d284762/contourpy-1.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f963ea9f0d68d2b6a02d861e3c0aae09ae25ba21f243fcf281e9fed468b078d", size = 283732, upload-time = "2026-09-11T19:04:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/f0/90/957549e3b03c407c670e3e191db63f4aef60dab6605bc967af3a806b543e/contourpy-1.4.0-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5545ff38229c15d11d822fc6f7415b932da9f4975f53e305b7b96ebfb01f889c", size = 357820, upload-time = "2026-09-11T19:04:20.077Z" }, + { url = "https://files.pythonhosted.org/packages/56/4c/fa8de87d907b41b05d03f4945a51675cd67319650d01b4c927d209ffa6ee/contourpy-1.4.0-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630d32f06cedf37b7f1dd2b6a12e4492826bd477a4c9c7b2e2fee6ce3a2cea76", size = 407804, upload-time = "2026-09-11T19:04:21.781Z" }, + { url = "https://files.pythonhosted.org/packages/04/e7/96bd4f4a4ae285727befbdb4b332d53a2654cc5da4585d2525b47a063e1f/contourpy-1.4.0-cp315-cp315-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be85d160e7c795113c2a93254dde2fc7e86b375322570eee78b420969426973c", size = 408559, upload-time = "2026-09-11T19:04:23.432Z" }, + { url = "https://files.pythonhosted.org/packages/ae/94/39660e8dc309f96bf7971eb4ec5659f272f84e42b34c9990a96ecce8442c/contourpy-1.4.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:018227e134b73090b06f911e773a526c77c9af426be29065500ce096a5accd4c", size = 384861, upload-time = "2026-09-11T19:04:25.198Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7b/411dccbf73d5d19da49bf7735786b3baf4ddb1456412d48ae4d4ab1fc761/contourpy-1.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4bffcb2e8cc5c0e837631acab59dd04265fdac376550e7c3e310523877c68cb7", size = 1357612, upload-time = "2026-09-11T19:04:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/39/bf2442130e7d5aae75a4de4504f0c8aca3ef3da3feac3940ee27ca985d83/contourpy-1.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc87f2ffa84a49a77d76cb792f28a48ff3b05a222aa0b2bddbd839ba78e1d5ca", size = 1427345, upload-time = "2026-09-11T19:04:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/96/60/e5d9196dd83ccd7dd103762982b10ef19a4f72e8ef67ae7157699b9be0bb/contourpy-1.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:c8be3392b84cf43373a488874dc1ebf46f60784a70977f92586e18019d71c7ed", size = 118157, upload-time = "2026-09-11T19:04:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b2/ae3d90f08ba2ed82189d93b55ffbd41e559760e89af4f9ef54d51869f41d/contourpy-1.4.0-cp315-cp315-win32.whl", hash = "sha256:618137ec5778fb76d494c9ec854b104a377fddba0d128bef7136cce2c3bf0df8", size = 356982, upload-time = "2026-09-11T19:04:52.913Z" }, + { url = "https://files.pythonhosted.org/packages/02/a0/36648fff407dcf8b8482985d33367ffc0fe90be72383869312cd0bd00a1b/contourpy-1.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:ea09dc704029930cbd097f75cb0cc1db9852adbcf1a151fac7e9559f7bcbfd19", size = 240101, upload-time = "2026-09-11T19:04:54.587Z" }, + { url = "https://files.pythonhosted.org/packages/07/09/49e987a96706b8f877596cee00d558d7880793a833addc6b33f80805e5dc/contourpy-1.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:a5e2bb871b90cd7a52bdee63bf80ef7acb46398d27efc66d7679240016efc5aa", size = 576375, upload-time = "2026-09-11T19:04:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/e3/18/820ba24a070fe2a834af73e52245c501960746f5c1777ebc64ec0c0dec12/contourpy-1.4.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:432e89f835cf01f8d2123a130a3126b17ee6e0348e4d62c0973872b30aa873bf", size = 313543, upload-time = "2026-09-11T19:04:32.582Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4c/f61a63d8107e0c1c3fa47d30ce45abad90b2ad48a1a9a4510332db7e0176/contourpy-1.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e0a2392533e5e2abdf277c951047516acc8d2f3c42b2dd9dc34c907c9ebabf2", size = 302702, upload-time = "2026-09-11T19:04:34.257Z" }, + { url = "https://files.pythonhosted.org/packages/9d/93/5af95cb5113ecc6e780ca0c6c79756a2cea8833f971bbae112dd4ad73ded/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:758e7496cec3195fc28a28945eab3ceef348c47fb95c7afc2eca2f3901209c8c", size = 354370, upload-time = "2026-09-11T19:04:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/47/75/5a9942446647202beb7b4d150891bfcb78fb112c73862484bbe4cc031092/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bee6ff96653e0807cd0f8eaef00174a031e3a2effa5c20f49a5a8d574b05af23", size = 402340, upload-time = "2026-09-11T19:04:37.914Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4f/bfdb0c67775a584f957211936661289789c4d0ec3e988bca2c8c02f8090e/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a58b5f130afec61a093d743cdda435cc4047748b6791627e34a68f41ec3a9b07", size = 410721, upload-time = "2026-09-11T19:04:39.925Z" }, + { url = "https://files.pythonhosted.org/packages/65/d3/dddf13d6bb26e145c99debd1f2695018b5f3e24452dd9bfc9904c9d2ec89/contourpy-1.4.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9dc493e7924e5aa32d89d0b8e50986280f3d4b284968a792787bd7bcffd3cad", size = 382604, upload-time = "2026-09-11T19:04:41.793Z" }, + { url = "https://files.pythonhosted.org/packages/37/b4/89f0a5f6c6ccd322eae2704c7e933c82a4e594a61773933b4b0a18646866/contourpy-1.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c927ec747633e68c9920bcaad48b0bcefe648812b822f18c34e2d0e2270a3d2a", size = 1354996, upload-time = "2026-09-11T19:04:43.856Z" }, + { url = "https://files.pythonhosted.org/packages/5d/29/fc91cc1ac6f3b591a269f4357b656d6ed0c39eb17c148f41f6da2f9729b9/contourpy-1.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:80c7fc8e7ac217777ce7ba2db3cf8478652b5a4b335283111012ff9eaac9d842", size = 1425030, upload-time = "2026-09-11T19:04:45.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b8/bb129f524210c5d3eb8616876e02af280fba8b5554e0ac1afad80b51e06b/contourpy-1.4.0-cp315-cp315t-win32.whl", hash = "sha256:90feb8da006803a95ba573fddc9536357d0c3faddc3a4c9e22816ca49af52878", size = 371854, upload-time = "2026-09-11T19:04:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b1/bc774d9d200cfc4f073bba78b1bb1cb3b55bf1c8c345eae41fc2db510459/contourpy-1.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:de503609fdb71f597634ca64fb4bd2f13d27c97f140e912b1e8ed93544840df0", size = 262450, upload-time = "2026-09-11T19:04:49.502Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/eb4ff492e1c3d027617b5a20e4e6d63a035b41b8cf77a95950159556a3a5/contourpy-1.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:78ed5c5f962b3e156109f0531b174a65ba80d13cc681a70227b12524c162e184", size = 587064, upload-time = "2026-09-11T19:04:51.148Z" }, +] + [[package]] name = "coverage" version = "7.13.5" @@ -778,62 +1025,68 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.7" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, - { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, - { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, - { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] @@ -1148,13 +1401,158 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "fonttools" +version = "4.65.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/77/51/d63c7e52163ac14393a35bd14bd7c0da95f8f74be5d7cc988092f9965129/fonttools-4.65.0.tar.gz", hash = "sha256:762ba5431358d0dbd4a01982484a1d494fb267e91f974cdcf20b80eab8560f6f", size = 3674467, upload-time = "2026-09-10T15:35:54.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d9/1caaa015dd207da7ccd3feba87289997bd88334ea3e74a367cdc7b0e50a3/fonttools-4.65.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93a73af2075036d36d7fbf856779c56a1b3b86ffcdae6abede7596604c42c156", size = 3095449, upload-time = "2026-09-10T15:33:04.814Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/988cd9b33ae2d92b15a51d73eb77c991f7a0fe90a7bc74526e9669247789/fonttools-4.65.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c130be2232e3caf8d2b476854ea78421ec1642917ff5ab695284bac31bbb072b", size = 2587697, upload-time = "2026-09-10T15:33:07.653Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6a/36f465a1c277131f9569f6a56fe99cf135391b4860b9c4f4b23e5b1cd5df/fonttools-4.65.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3944e0bdba42effb71959e43d91b599326b02b59c78310d5675e8a75525e7d8", size = 5313308, upload-time = "2026-09-10T15:33:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/ee/56/151b5e81d20c63834f48ad37a0cbbbe2f9b248e38f8d10387f0cf5219244/fonttools-4.65.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb53892b570f7f1f0055e75fc4de32673e32f749c4c8a606b63d5c436650e634", size = 5253649, upload-time = "2026-09-10T15:33:12.225Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/6389215da9d4f98623aacdca9479c1030bbd516cb658e893bc54b61098ce/fonttools-4.65.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a6c8d184e523580a7c55d21cde37176a3c91cb539cf06c2aa36ffc634fd75296", size = 5277745, upload-time = "2026-09-10T15:33:14.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/e3/c1037a1dfb7c8efe6f2a7d1951ebdde40cbbf82e9c5d796fcb03077e4790/fonttools-4.65.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5ceccaf2e57d83b753a2b5db5d94aa0a8071886d4afebd2d520c9683e6bef0e", size = 5413259, upload-time = "2026-09-10T15:33:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/44/b9/7dd72330168d39635c329f23a98279393b1e825e42a6db362e09897aad7b/fonttools-4.65.0-cp310-cp310-win32.whl", hash = "sha256:aff640a4fcb021fa83f9879d5bfa115b6931522dae991a24faa75888bd6aeff6", size = 1577124, upload-time = "2026-09-10T15:33:19.864Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c2/e959385b4626989b25b82b9f4f99e7c3ac68377d6846b376239b6f126966/fonttools-4.65.0-cp310-cp310-win_amd64.whl", hash = "sha256:5c1700a60e4ff23a0425d5a64abf43d092e6b55071354825781faf255904dcb4", size = 1633354, upload-time = "2026-09-10T15:33:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/62/9e/58250cdc54d96fcfacb544e12997a6390fa4e6b71ae2241cfcfe5b341803/fonttools-4.65.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06273c71e692caf5989c0437ca50875a5e49e216ddf653228fe9bb35bdc82c0f", size = 3089141, upload-time = "2026-09-10T15:33:24.509Z" }, + { url = "https://files.pythonhosted.org/packages/3e/67/0f0416069e38da0a1327a847a2e8dd1edb425d0043d8a3e63eb940070209/fonttools-4.65.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ca2b02d74e9ad7e21a1d11e4701425800a4b0c63cf90486e60258262feccbcbf", size = 2584298, upload-time = "2026-09-10T15:33:26.598Z" }, + { url = "https://files.pythonhosted.org/packages/99/0d/7e40e9957359afc0bab081131c215370a6d2d203361bb6f945ab595e924c/fonttools-4.65.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7830e9fa3bebc44dbc27ff44d8201def30ea5c48a773696d58e69e6bcd9cd5d4", size = 5498163, upload-time = "2026-09-10T15:33:29.071Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e6/e48cf0a272a5d4d17a09d44f92727e67f975ddfa94acc8464763d19a654d/fonttools-4.65.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3991732c87b3f054a2a8cf86dd0d602833fa8cb37c911503173771646e1013d", size = 5457456, upload-time = "2026-09-10T15:33:31.918Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8a/a5c67ddeda82ee5e4ec3bc52ac1cbb685f0bb7a0516badc55643b454ab0d/fonttools-4.65.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6031e77b3fb8c765055ba2b8bd8dcb17030f3bf2484c448b472fdedf4460ba80", size = 5464334, upload-time = "2026-09-10T15:33:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/d7/16/294e77383b2d39c9f8f25144a7ba23fe1cbbc05227cc72545097785ff07c/fonttools-4.65.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6813cc1e2e883bd6c15b3e04f72c78dc65fdc4ca861063adf5f341fbaec2ca62", size = 5595597, upload-time = "2026-09-10T15:33:37.591Z" }, + { url = "https://files.pythonhosted.org/packages/80/01/8e74ce8626c734959c782f2d89af8e9f14d078fd3d4ddf8b5a51401ae475/fonttools-4.65.0-cp311-cp311-win32.whl", hash = "sha256:4a5db8442453da4b6f43ad325879381b726bf2238a2253efd9584be21a2cefc2", size = 2441114, upload-time = "2026-09-10T15:33:40.592Z" }, + { url = "https://files.pythonhosted.org/packages/37/3e/835dc6c658426e2670b7f38c38295492fcbaeb06080e9dce89ce8105993c/fonttools-4.65.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f201796c8e24e657be77c16fa664e798a46122144217f90838982937a964f0a", size = 2498823, upload-time = "2026-09-10T15:33:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/242fa4fce7f632c5f7ab15585343393b25792510c0c32bd218ad24d59f1c/fonttools-4.65.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e844a45c9e5ced6536f184cf1a65b5d65e8f7e711993b413e10500a8223622e5", size = 3097120, upload-time = "2026-09-10T15:33:46Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/42fa4d373416675f74446421cf0b2badb82a4245c60745f05f424f75c649/fonttools-4.65.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b30e953de049bf43fc0a63c7d0c44d205c923e4bbf24716aae1518c0e65f977c", size = 2584658, upload-time = "2026-09-10T15:33:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/6f/d589b9d62280a846c77a2c383d852c6dcb79ae8aa02bf0fa46c8577af145/fonttools-4.65.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09c34bdeed8915bfb53bee0c8ed2254dbd8ec69c0014b7f3702f347c049bf358", size = 5425737, upload-time = "2026-09-10T15:33:51.473Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/de2c0c20a42c18e565a2617932beb08c06697bbdd0d3f62b108262e11583/fonttools-4.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05595385ae99f4b9626cebb973bf171b8fe38a8f40708e6e42abba0ed7537778", size = 5402463, upload-time = "2026-09-10T15:33:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/bc0f5c9dce21821454bb5812d3b23410bca33c8bbd5468386d0327aa0cff/fonttools-4.65.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d95b34dd68fbfc0e4a1740c421597656117f979ed8dc85de66e08f9f9981806e", size = 5362168, upload-time = "2026-09-10T15:33:57.481Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0d/2116763ade7e71e0e5d421babe1785d745be9b3d605bf914792ce1c97f79/fonttools-4.65.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:924d06e6130429168318db71c40174a765ad016fc4b56ca811287e3d7373b3a6", size = 5524117, upload-time = "2026-09-10T15:34:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/f9600553b9f645e3068553831685ff1dab6259536a23b38d2e048de38f17/fonttools-4.65.0-cp312-cp312-win32.whl", hash = "sha256:04f73dd01005752a6e75cf4a8dc6b70dc724d1d4bc34cc89522153f4a2f07680", size = 2432089, upload-time = "2026-09-10T15:34:02.803Z" }, + { url = "https://files.pythonhosted.org/packages/3a/02/e436a6a1863b9862bab9f82d6da33055dd7aa3738017edd902a163525dc2/fonttools-4.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:3b5d9ba89edf778b376e669b879ae33a198bf45cf5a23c3f6514f935cf9d0d9d", size = 2483475, upload-time = "2026-09-10T15:34:05.09Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5c/343a4225e83eb06f82c1d8bf41fc5e5f71eab2f62bc7ca215c764722c0a9/fonttools-4.65.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b7bb52817a24731d2e4f4df0e71fdde05e6c806c8f8f1517b015d142fdacfa5", size = 3094663, upload-time = "2026-09-10T15:34:07.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c9/49b2401be932741d9218181c08948c96db32eab21ecaaf79283b752e13e7/fonttools-4.65.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e2c21772fcf70325189707b19f346812690bb1b0bd7e207e6ac205244806b303", size = 2584521, upload-time = "2026-09-10T15:34:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/c9/48b07e6c5cf44fa56f758a04c3772a66c0e9ba82bbe22077e4076746a62b/fonttools-4.65.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c9b26816415b5e3d899e9077109d327b22140fe3c4066644d8cdbad5bb1569", size = 5395399, upload-time = "2026-09-10T15:34:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2d/5cc5a10c8ed56079d6c2e9e3e5920622529a2ae045c9f47a6055ccb1a319/fonttools-4.65.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dd6243f60e2d6160c2966e1e14020dc261ffd741b69a2e4ca8bfd051592e4b7", size = 5376659, upload-time = "2026-09-10T15:34:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6f/ccf33739d936bb3afa1655a225be7ee5d63d6d3c8b7d0e570bc5a2a7b899/fonttools-4.65.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:834962fd7cf21c58e81ac50a59e6ed2306f9df5e3dd481dad1cd7d2c4c60b773", size = 5336795, upload-time = "2026-09-10T15:34:17.817Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ffd17483f2094f0f4118974295b514b5b82afd4f6e3c80c23f01684e97a6/fonttools-4.65.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:580eb68ff7bd6954a7a76afddd864bfc66eaaf5f5c20dd6ead9186d0055a4ffe", size = 5496530, upload-time = "2026-09-10T15:34:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/b1/19/9aca7712d0676ba5f8d1530ce20478a9bb09cccd0153d56693337379cdcf/fonttools-4.65.0-cp313-cp313-win32.whl", hash = "sha256:7a18b2ffd44249fe84289253197aa65ad4f2de554c0d381f18b1f5939bc6bc60", size = 2430392, upload-time = "2026-09-10T15:34:22.906Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6f/f015dea0f4354e0798751b657cea2dee482914737f88bf1a078349ce92cf/fonttools-4.65.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ae1846b0f192fd485d26a455af19b8f5cf05aff08f9836f533913d8fcea133c", size = 2481620, upload-time = "2026-09-10T15:34:25.816Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/606365ef601668bfebed14cfe3dc72bb7fcd1e23011bbb2833f17fea3065/fonttools-4.65.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:dc87a9f846bec83c3795804f62b4632716d46e3522869a3dd9cd44a5d245b006", size = 3098393, upload-time = "2026-09-10T15:34:28.472Z" }, + { url = "https://files.pythonhosted.org/packages/c7/61/11412939d6b7abf5ac7ce0d61d7f94a0a4fbabc9f1ab0a04fa622e0fc11c/fonttools-4.65.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa50dd7b9baf75e2bbd43401fc0d237f7a94a8ad2e0c57ea97160fc631af5eb0", size = 2585766, upload-time = "2026-09-10T15:34:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/db/17/734921d8aee8309801da42590375d32d4d46f771b73373ec9520d5d4220b/fonttools-4.65.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d2a9892fdb3b7e2d0f4174e3b907d226ff83698249762eeefce08ec5b2de1dd", size = 5380679, upload-time = "2026-09-10T15:34:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/2a4d78d60d978e694cfa04c98e4d8ddbf7f028fd768ddef470bb9da5d69e/fonttools-4.65.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d815734e7fede0ad1f233f23f0f191cbe8fc64762ff041e589bc0f78e0b2397", size = 5321833, upload-time = "2026-09-10T15:34:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ca/1cd48b5c11ef9658732787bf2362e1bf3871dad5945d2f6cc8f675ca769c/fonttools-4.65.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:71e4c67b6196a2f447f46476fd2302604721617f5e0a21b0988bdd87b6bb9687", size = 5320938, upload-time = "2026-09-10T15:34:38.992Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/90e6051bded926cccabe9dd0bce3b6ca012f4d5779d61167afbf4989ceb6/fonttools-4.65.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b11d8a4a0c3ca74bbd4c105b7ef82501945c939e6096d9934ec7d288cdf5aaa9", size = 5451747, upload-time = "2026-09-10T15:34:41.387Z" }, + { url = "https://files.pythonhosted.org/packages/18/74/23e0268e48029ff0752083f69312c49b738163d5af919add9dc4ac81e907/fonttools-4.65.0-cp314-cp314-win32.whl", hash = "sha256:8e44a34d91b3c793879767eb115867ced74d2eb94974e64e72fe9e2eea71cf1a", size = 2434246, upload-time = "2026-09-10T15:34:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2d/ee69affecd4bc81cb932a213438d4199fb48bf8ca6d664438ccc7f623c2a/fonttools-4.65.0-cp314-cp314-win_amd64.whl", hash = "sha256:0aa8901db22875c831d6a91796549590d7e747da37438f38b69d771b668be445", size = 2486706, upload-time = "2026-09-10T15:34:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9c/edee5f785198ce3327e1ddeace91c773122d47f086a67e0a84b800f4a940/fonttools-4.65.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2e4a380ca40d3a5372e31b340f0da0d53b4583aadbb8e41f6a516afa69c509a4", size = 3172027, upload-time = "2026-09-10T15:34:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c6/252ec9884381089bc30da75978b072593920249de60219817f16cbc9145f/fonttools-4.65.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:661bd91c4be13721408b2d4b67a9b3fa7736713adc9a6c9780c9c60fc7959f90", size = 2619020, upload-time = "2026-09-10T15:34:51.453Z" }, + { url = "https://files.pythonhosted.org/packages/42/79/f71b0d202b8473bb45b07876c08a474de9fe560ed2c0cde642812a81e22a/fonttools-4.65.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62c5e42c79449def957adf8a9a65a43018efa7e2a6bc6baa3afe955e0d5fb2ab", size = 5545942, upload-time = "2026-09-10T15:34:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/4a/bd/52e1bf33e0aebfe22ecc9a85c634db707c1dd9f6b1b438efeed98c55b959/fonttools-4.65.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:36fca8efc46b5adfca327c666e739fc05b7a7a6ef17840230f81b22f53230f61", size = 5351477, upload-time = "2026-09-10T15:34:57.514Z" }, + { url = "https://files.pythonhosted.org/packages/5c/76/8c6b2ad20beec95cd446f3a8bdc753c7e4a4fd69ef3e66704c0f7c8cb0b5/fonttools-4.65.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa1291e4c767abf1b0b79ca2d6895f7c0b661d9d95d03b5791c883a9d1e1f08", size = 5412271, upload-time = "2026-09-10T15:35:00.895Z" }, + { url = "https://files.pythonhosted.org/packages/79/49/fadbf11bbbd2d699d88a5498a0634280206e01e3bd5da9a4e0c504953ce9/fonttools-4.65.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fcf39949f56911348514b466714efa9118bec3d2be249e1c487263f7cda6edab", size = 5446450, upload-time = "2026-09-10T15:35:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/df/77/5fda646d3a6d5ee26465865c00319cc0925cf484a3b42dd8232d5b39f973/fonttools-4.65.0-cp314-cp314t-win32.whl", hash = "sha256:ffc918702661f1d74d2fbb2f5551036b64f6d2d743139e105289b694bcd16f54", size = 2467858, upload-time = "2026-09-10T15:35:05.744Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0f/afa0f3de70ebe02bba46b32cccb30b1de52624472b14ac2e7cd403d08db9/fonttools-4.65.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5a977e3645dbffaee924209828aa702a215f7ff68bc08010740c10c723787e62", size = 2518248, upload-time = "2026-09-10T15:35:07.846Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/b273cf5712b36a381c13284fbf244e811e1e1d7081aed20f200f0a191ffa/fonttools-4.65.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:7aa0518b45ff5286ad56f063938db3add3816e899aab58d782b3f9a252523caa", size = 3092770, upload-time = "2026-09-10T15:35:10.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/d3e4511475954d4ec4f3c254e81f0fcc1ade504cb7381eeca48b8c44b8a6/fonttools-4.65.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:673e2b3ac4ac8e4f3607d390ecc5a606e5db5c4e88fb4cb2999593efb65afea2", size = 2584364, upload-time = "2026-09-10T15:35:13.54Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3f/7cfaba467bdd1d3d04be21ba7b5bd75c6ca58ba280e0519707ca96a43c4d/fonttools-4.65.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a03cff943b204a90bf3d1c04c97b9509a8aa0ee99e2e544084ca43ad995975b", size = 5377819, upload-time = "2026-09-10T15:35:15.839Z" }, + { url = "https://files.pythonhosted.org/packages/25/17/a68d9b19a97bb2ee37e8098fea50657073df97c7e5392afbe1b9d7c0c581/fonttools-4.65.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41f684ee6212e411196ab054f8308faf6605f154950e6f4686fb8f2103d624b0", size = 5341296, upload-time = "2026-09-10T15:35:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8d/d744653ed607a241339015d4af6743ca3d85a74a2045a02cc06ea0383c71/fonttools-4.65.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:52ea9d2a8385075770db74d5e5718fa80b2222bb4fc62856a377dd2865ca8848", size = 5315310, upload-time = "2026-09-10T15:35:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c8/1ca6dc69cbaa0e394ff70d9e266777124d3ce3c6433026a6b4de50b890ae/fonttools-4.65.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d0d25027ade65ec46b13c0436e51bcb7c5171a4ea255a5e7a8d0d1d3ab4cffd7", size = 5463840, upload-time = "2026-09-10T15:35:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/07/97/d374df38a14f2ac04ba9ed96bca89d4988e7d63aa5ed85c54fd8f2badd7d/fonttools-4.65.0-cp315-cp315-win32.whl", hash = "sha256:22cb846d35d278235ef3b7e947c6040b2057d72e8305a314f21d5342eca49040", size = 2433149, upload-time = "2026-09-10T15:35:26.59Z" }, + { url = "https://files.pythonhosted.org/packages/62/c5/eb8f7506faf6a70c5a8f2eccbeea3cd826c748a6fd78c7b4b3effb5a3a11/fonttools-4.65.0-cp315-cp315-win_amd64.whl", hash = "sha256:aecc899fdbf9ecbf728f8977977e2e1043ee4d70c257124c8fa4cbcf796fcd83", size = 2485725, upload-time = "2026-09-10T15:35:28.945Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/2df0d97514feb8d857d5de854cd8d660d9a7ee1fd081bc98497124f515a8/fonttools-4.65.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:6275863dad195ee34b6e0ca3fc61c74096bc37e5d6fb8e049f4d68d65865a2b7", size = 3164065, upload-time = "2026-09-10T15:35:31.197Z" }, + { url = "https://files.pythonhosted.org/packages/15/33/e09661c09e6c8a3b0bd50da0e72918df7576dede31f39c948cc245011434/fonttools-4.65.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d8ffd2f62b402180b0edae8f86a071f583970e2177143117db5cf4c52da60079", size = 2615195, upload-time = "2026-09-10T15:35:33.569Z" }, + { url = "https://files.pythonhosted.org/packages/64/d7/114b05f4193679d0935272220083ec43de7f918b5a777018c5ac5c1ae39e/fonttools-4.65.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:830f91327ca83bfc1278e7060068a498938f84d05dc4869675486f84f55d4fe1", size = 5521239, upload-time = "2026-09-10T15:35:36.291Z" }, + { url = "https://files.pythonhosted.org/packages/ba/10/67d615939f859ffe75663a67bca3593966febbcd0109ec72f20f73cbb4cc/fonttools-4.65.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9db2cb95847c18eef74a4ef0fe257a893ae3f4b0395f4866e2f426ab07f3d804", size = 5343802, upload-time = "2026-09-10T15:35:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/faa793a806da03acce862fbed353f76026b28103d43860d69be9a6a1f0fd/fonttools-4.65.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:be9b9a95ed0af03375e99020e921c4bc6b41fad10e053dea7acad370521a3c46", size = 5388482, upload-time = "2026-09-10T15:35:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/53/d2/eb7258df60e634db60c9a8cd72bc9eaf8dea409d1b1ceec3b9fb49531ad2/fonttools-4.65.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:bbd9faf777a9deb6790df4f2b0be611857c45fe86605e840d7154a028d828af7", size = 5435088, upload-time = "2026-09-10T15:35:44.639Z" }, + { url = "https://files.pythonhosted.org/packages/00/6a/58597f16e1265fe9205de3069e338a339e3eef0b1daf049ee08269458091/fonttools-4.65.0-cp315-cp315t-win32.whl", hash = "sha256:c779d838815b91889c95ed64c9be5950ad5a683279f91aeb23384cb757ddc6a3", size = 2464925, upload-time = "2026-09-10T15:35:47.224Z" }, + { url = "https://files.pythonhosted.org/packages/4b/95/122fc172006db747f4968e08c710f52a94f55eb007173b859b3f80b5b810/fonttools-4.65.0-cp315-cp315t-win_amd64.whl", hash = "sha256:d9484b7ee1b49b6b8a0231c849f3983723dec29e3a7366d9b1b02f4036f71944", size = 2513945, upload-time = "2026-09-10T15:35:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/e6/35/f894ceb867118c0261d0f69a9bd516b045a3754238f76c88a49513ac7a83/fonttools-4.65.0-py3-none-any.whl", hash = "sha256:3060b8c1fc2329fa20265b7c138614143ea7c1624e26c5c180c76aeb74deae6f", size = 1196441, upload-time = "2026-09-10T15:35:52.347Z" }, +] + +[[package]] +name = "fonttools" +version = "4.66.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/24/86f9930b930b97fc82266083320f3c34643ef67261c32651e893db525aca/fonttools-4.66.0.tar.gz", hash = "sha256:ef0610dfe7bb5bf574d9bdad6f597403ebc9807d124ac6f148d7604b2609be98", size = 3692007, upload-time = "2026-09-23T17:59:14.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/61/ea35d7faff1430f875d0b0b80ea043f5b17d4155e62704985af415df8f31/fonttools-4.66.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b432b6b54a0f3be118c6243312c826aa086aa9bcb150160be4f0bc8a21ea3aaa", size = 3088761, upload-time = "2026-09-23T17:56:49.236Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cb/0b243306434748a071d12fb6757f6494534de7fbfe4e05d78ec1d5b1c1b0/fonttools-4.66.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8009e736e1491c569392b2e9782c30dcb6ac879def102c8c2e06add8d2adc10f", size = 2586088, upload-time = "2026-09-23T17:56:51.916Z" }, + { url = "https://files.pythonhosted.org/packages/25/57/9499457eec5e40a1e9eb44fc81ab3451e4590ea767377e55ecfd9931c316/fonttools-4.66.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba65eb3e2c46ad0922d84ef78e287c2f48b7a8cae4420728bf3b8e583282d6d7", size = 5483789, upload-time = "2026-09-23T17:56:53.945Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d2/43fa30bb42476a587381c4a5df8d3f305ad787ef6e5f203800b891ff074a/fonttools-4.66.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ea59aaec135c96c4cff20d79fdf1d10623993712a77990585a42cfd6a42bc5c", size = 5447057, upload-time = "2026-09-23T17:56:56.466Z" }, + { url = "https://files.pythonhosted.org/packages/3b/58/748a43b023707b1b409e8c773635064428c4383bd048206eff446db935bd/fonttools-4.66.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3a66c749b69e9abd92e519ccbdea7210522b0fffdba3f6492a8fab461d4708a", size = 5449597, upload-time = "2026-09-23T17:56:58.802Z" }, + { url = "https://files.pythonhosted.org/packages/94/31/b881d8374e482b4fcb3874ba4317222dea099e880b6054f0bab76a45197e/fonttools-4.66.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fd3dcb69d65c05a2fffa5d9b743b77e0cdc13832665ba435065493dad2b57f83", size = 5587418, upload-time = "2026-09-23T17:57:01.707Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a5/941a7e7033026a9942121bdd01a4d595c7b9b1ec76e7278c82d319184419/fonttools-4.66.0-cp311-cp311-win32.whl", hash = "sha256:4513753259f06316bdc2039c9bf3129e74923c1cfad0722b4728a9ce19beeeb2", size = 1577034, upload-time = "2026-09-23T17:57:04.03Z" }, + { url = "https://files.pythonhosted.org/packages/4d/26/440338213c9257d0deb72be98ddde8f4da900a07a694e157474c7621af2c/fonttools-4.66.0-cp311-cp311-win_amd64.whl", hash = "sha256:6371e7ed43cc9e23550fe4a1b72af3205275f28dcecac8c09b80a4582e326802", size = 1634644, upload-time = "2026-09-23T17:57:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c1/25821e3bddd0644df229cfdc2e8b4101d28e4da15ffbe6e6b563ea6f3acd/fonttools-4.66.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:67ea5af3ca60e1e5c9b2841b1f6dba5ef16aab581bf463abb68515447154ab61", size = 3096788, upload-time = "2026-09-23T17:57:08.302Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3d/d16b4cb63710abd91f1aaa482f2848d8b0c402502de5b8a72ba9d2613cb0/fonttools-4.66.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbee639a23c4e067aedfbcf84d2e466a71a1a1156ad04d6d09023791ed167495", size = 2586926, upload-time = "2026-09-23T17:57:10.678Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f0/dffa4fa83ec6780464e8484fa2f30ccc252d480137c8e4d3fc6ac510b3eb/fonttools-4.66.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35684b562df7154d7a0ebfa512db9199c7fb0a93b5abfb59dac9d022dbee7aaa", size = 5415821, upload-time = "2026-09-23T17:57:13.174Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/3c862bdd297a272a37e2ba1149da01446fd3f387ee80855699bb939bf5b2/fonttools-4.66.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aea0cc5609a5f2a2500f91bd6e1989fdd22da0f225be00dc1c4cec775838a7d0", size = 5396689, upload-time = "2026-09-23T17:57:15.987Z" }, + { url = "https://files.pythonhosted.org/packages/93/d7/f4b7c81a7a6b316b7226d11f8e8659f023babf1e0957f8e7ddcc4e155a82/fonttools-4.66.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8af85dfc2ad564f95b3e650024578bb2268bc4d827d418aab9650bbde4b6926a", size = 5353218, upload-time = "2026-09-23T17:57:18.803Z" }, + { url = "https://files.pythonhosted.org/packages/70/1e/f12bc535f3da359a4d29d5e7442ab2d7d67579d1516b9c7e1a38890168bd/fonttools-4.66.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b278e2abe596872b3c9fe611f956ded85588537974e48e2adbe2d6ee159e099", size = 5516965, upload-time = "2026-09-23T17:57:21.077Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/a1df66888c8b5f63c14e089400f2353da0c1ff9d09683cc0bf85afea348e/fonttools-4.66.0-cp312-cp312-win32.whl", hash = "sha256:ee7b9f6c835ea1a9beb5a35c8eb0c62b6a3290f8105d3d0c29a0a18c90f9b8ba", size = 2434425, upload-time = "2026-09-23T17:57:23.463Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f2/fd490f1ddf2e430289c13059982771e6b02a2bf37504401d2a5c53307892/fonttools-4.66.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d120ea0f5260b04e9b5ac0d9239efde4c23d990347d5d5889cdd37221726fec", size = 2486335, upload-time = "2026-09-23T17:57:25.561Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/1cf0a4f54c8cf351a4d9f8fcad5201dc0c5fb8676efdb1ba7cc3136ca523/fonttools-4.66.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c144e68572af1dfcb05fe065827d0920d4118b265504928e1d2262e3701a25a1", size = 3094615, upload-time = "2026-09-23T17:57:27.942Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ed/89baf1795841eaad6fcf0ee6f319c236f4379dd98653b4bff294b83d4172/fonttools-4.66.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ada87643b20a8fa5763e6dd4fa0f5e2901cbdbe0833deb6d5c5a9c3186782b94", size = 2586645, upload-time = "2026-09-23T17:57:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/56/f9/982609322f92532bdd769a225c9b3e2bbac0a077c37f7780710db9a8e018/fonttools-4.66.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700c448f190cc7bbd9f059bd96131f4770e90938c9692550b142153bea73b14c", size = 5389194, upload-time = "2026-09-23T17:57:33.07Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/12177031dd8be493d5afda574323da19f6ea5488c85202f3a5708c94757c/fonttools-4.66.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c519433e8632284dbe64ee4c81b609a17e205bc1afff285553723964880d1c89", size = 5371422, upload-time = "2026-09-23T17:57:35.915Z" }, + { url = "https://files.pythonhosted.org/packages/19/e1/e8ed0950eb846f4a404ff84aea89e4c93393c2475cdfdf1c03a1b3f4e2ef/fonttools-4.66.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8739d77810aeb6d2ecd55cc2e4864d98a00c317fb6131a6d22ca58b3f50cf22a", size = 5332183, upload-time = "2026-09-23T17:57:39.041Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fb/c0310cf9838859d74130530ef351a903735c6ecf47418d7d0dc4cf459a9b/fonttools-4.66.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cffa168522a0e057d3a5628c8ebd43ddf66a34613f42e116bffb85b0b32ce5a0", size = 5490019, upload-time = "2026-09-23T17:57:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7b/928f334aea18c77f6892895bb1e28388c9e2466e04bc924f93043024c780/fonttools-4.66.0-cp313-cp313-win32.whl", hash = "sha256:379b94fe10651d26caf398eb646b3ecde886a9c504d3cce5e46164d58a78e32d", size = 2432785, upload-time = "2026-09-23T17:57:44.484Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/576072a97256f06fe6d0112c522c118fe72390482baf52ad9bfa3b0d1db6/fonttools-4.66.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee4f85d1341af630d787514eef1a0e1ad883f9d6c11a4c1465faeb9cd0bb5ced", size = 2484475, upload-time = "2026-09-23T17:57:46.625Z" }, + { url = "https://files.pythonhosted.org/packages/38/e6/2ea45d294ce967a8f76b5dec041abdd71c7abb652c9a81b24592edc00579/fonttools-4.66.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ce818581070527883e3678b92b1ac7ed3569e48c80f51b89b264f97d84be2608", size = 3098619, upload-time = "2026-09-23T17:57:48.843Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d5/225d95df6b88df277d86c3d86fdaa73833cd628dc2918fcf7b73133d6f67/fonttools-4.66.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cf0033f343cb1592fb24ae3eaf8b58156f208884ac16991094293ee94c585654", size = 2588128, upload-time = "2026-09-23T17:57:52.041Z" }, + { url = "https://files.pythonhosted.org/packages/15/e6/5892522733dc50b0577693bb164032b47d3504f33ee44d92a1a740d2e89b/fonttools-4.66.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3889fdbe63e85dbf2a0d9d7c9d90746827ce60af2cdd24f0bb4738380a9540eb", size = 5373639, upload-time = "2026-09-23T17:57:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/72/72ec66154babfd53a1c705a9f973c49d12fa81084aa72d939e062242ada1/fonttools-4.66.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:850d50b347f31667d3a277a6e66a4219236db9d98afadb2993b8addf9bd4f593", size = 5317190, upload-time = "2026-09-23T17:57:57.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c6/a8ca0334c73d1fa4fae1d423ff92e0b0be0885ed98e866e3478a00ad1a47/fonttools-4.66.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:221defad3b1949c1fdde2558d1fe780d42f926b683a71a46a4495a0e6a6614bd", size = 5315603, upload-time = "2026-09-23T17:57:59.842Z" }, + { url = "https://files.pythonhosted.org/packages/d2/34/d3d4cd925d1a934e841d39a0540539b21e952c40dfac27874bac1392f3d2/fonttools-4.66.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b02686b2e47115b2378ab6156abd984c776c6faa4f56898fbcc1c30ee387937d", size = 5448784, upload-time = "2026-09-23T17:58:02.669Z" }, + { url = "https://files.pythonhosted.org/packages/61/b9/269bebce24f590d28f2e59fd8808f3282b5f5511a4a95c5f6413f0507993/fonttools-4.66.0-cp314-cp314-win32.whl", hash = "sha256:b46500e4d6f5708a9127ea12902ba54cb0ec594fc6bec649afc53154a1125b2b", size = 2437006, upload-time = "2026-09-23T17:58:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bd/9c10c9a62a274451f10295c17a01286c7777f44da87cd9f77d08c5a21d4e/fonttools-4.66.0-cp314-cp314-win_amd64.whl", hash = "sha256:0cf33d1041364ebb7a9958db757e213c6d2a238eb41755fbf935db028c2db20f", size = 2489086, upload-time = "2026-09-23T17:58:07.516Z" }, + { url = "https://files.pythonhosted.org/packages/1f/45/c9bc900dca5ed7760c1692a6b1161c380515071da9d855eddedd4bb21fb5/fonttools-4.66.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d2d10f89c4bf7cd42b84695da617f9130fa594647ee74673bf8c8d001fa04ef9", size = 3172290, upload-time = "2026-09-23T17:58:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/bb/82/1017a312eeb373f30d99d032d04bf55a8ea80858f1670b2e12ed90732574/fonttools-4.66.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:135ce8738a6341fbefeedc061e20c2889350e84405abea7e61b1c9d87e1a1f2f", size = 2621293, upload-time = "2026-09-23T17:58:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/af/c3/b54678b08a6637d5a222b68e1f133c58096c664403b9ead1cce4eab53fd1/fonttools-4.66.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:107aae7c38f05561fef5103baa98a44400b5f4c69d87839c9a5f8125405e7d9a", size = 5546296, upload-time = "2026-09-23T17:58:15.041Z" }, + { url = "https://files.pythonhosted.org/packages/98/2c/42e1c39e4025323bf65e042ea7ed709912aebfa22d422a2937da12268331/fonttools-4.66.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4677025ee40b3b1420387ca85f55545b8095f7596c3d5d59552dbee19c91f07", size = 5352908, upload-time = "2026-09-23T17:58:17.77Z" }, + { url = "https://files.pythonhosted.org/packages/f1/66/07a6b9e634d7e0edb3e8f23ebb483479599842093624f13bde44e3c7a4bc/fonttools-4.66.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:03922992966a1830b94a750961d1ccfe1a7375d792ca61077a5afb719811788a", size = 5413677, upload-time = "2026-09-23T17:58:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c6/d649d4d09ebbfdb0eb13cdeea731bf9685d184fef716e0c18f7779eb44fb/fonttools-4.66.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5dda4087a7f8f0b985995489b3c0115bd117f18c0fc236013a0e78e837bb9ac2", size = 5449024, upload-time = "2026-09-23T17:58:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/04/4c/60c6c9a2bc4cc0ae1012df9084bfe4ba69a1a60df06f30830787a4e6c061/fonttools-4.66.0-cp314-cp314t-win32.whl", hash = "sha256:85e2960a29e6882567b981ba408b5a5ca2d8865b8f67240e1b420348997a13be", size = 2470760, upload-time = "2026-09-23T17:58:25.739Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5e/fd8593bbe3da06d2cbf9c096e17becbb48ace6e7a33e0e5e21f82a55c32c/fonttools-4.66.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8ea6265535b25f8254868f5a212a1c9dbd88c2c4050f3df18d9174949df0d6d9", size = 2520614, upload-time = "2026-09-23T17:58:28.078Z" }, + { url = "https://files.pythonhosted.org/packages/a0/23/f9f265526c6a4096bbe67f45d6ed853519d49ad7e07667bd0c8edb33f7fd/fonttools-4.66.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b0198962bbba64a88bf15dc5d95e4bab8c5208dede22a10fb06280406ed9874", size = 3092996, upload-time = "2026-09-23T17:58:30.828Z" }, + { url = "https://files.pythonhosted.org/packages/a9/25/44d594d7fd703b2c4914b0df728012b8f2e69578de2d9238742c1754243a/fonttools-4.66.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:15315302c620e6ce8582934e07932dcf442fc3f59ab208d30564200b9a2f7ea4", size = 2586696, upload-time = "2026-09-23T17:58:33.159Z" }, + { url = "https://files.pythonhosted.org/packages/b5/73/b33888692f06471222a66a79bd7f3e7b9aa9e3d87be6adb794a2ba3dc467/fonttools-4.66.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:056641fcedeaad24b92e343e35e9fd1337200c102450678894b17928482d010d", size = 5370868, upload-time = "2026-09-23T17:58:35.93Z" }, + { url = "https://files.pythonhosted.org/packages/34/22/b7fa275e29642ab706bdffc5d02564bf6ade4168bd4c8b98b6ac2a78a653/fonttools-4.66.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa995d96ced196388d8ed7a67ee001b181459d230e24bb68b193ae859f5609da", size = 5334214, upload-time = "2026-09-23T17:58:38.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/69/417f550986631f9b443666b6152c9396830c0bb7a241b2aa235d1faa2500/fonttools-4.66.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:f014bbf05f30731bf8b9f5c7d2b4c0e4f28f375926f95b187f85e0bbceb16029", size = 5311067, upload-time = "2026-09-23T17:58:41.686Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f5/80b8b3b55306975a69995b188d640664b53891509a47e6e19e5496adc529/fonttools-4.66.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:886f3029ea10362c592d2e1a5326a44645b85a2a0be6fdb7c9edbdb60cedb0ab", size = 5458838, upload-time = "2026-09-23T17:58:44.145Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1a/7f5e8b4e0e876a94424faf039e99e09833c3ffb105978e3617a6cba58e92/fonttools-4.66.0-cp315-cp315-win32.whl", hash = "sha256:0640e69b00e089d6b82e0a54e3a03af84b775f1f0b2cbecc1b0d7fe3980a194a", size = 2435840, upload-time = "2026-09-23T17:58:46.482Z" }, + { url = "https://files.pythonhosted.org/packages/e0/8a/91ecd6ed7140e0589538298f941934eaccaf7ab067a60585de3a60ddcc32/fonttools-4.66.0-cp315-cp315-win_amd64.whl", hash = "sha256:a5a0624aaefb0a29e4ed7500a98732a1b0ec2d3040fe86cc8837f82e3fe4f446", size = 2488078, upload-time = "2026-09-23T17:58:49.027Z" }, + { url = "https://files.pythonhosted.org/packages/a5/16/3a82202c98d55aeb53dd1fe5cd295c53b1057a45e6be943a347dba5bc92b/fonttools-4.66.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:4bdaf80590fcccb1b9d8e3b8ca72225defb14d03b79788f59781ce1bb66049b9", size = 3163820, upload-time = "2026-09-23T17:58:51.661Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/597c587ec929f18622d0f6ad4aac15a698e5d4e42d291cf051e63fe0d1b0/fonttools-4.66.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:398bfc5ce9055e0b91a6189f8cf462d4230b38f522b2ceaecb4f34cbf4fb5480", size = 2617537, upload-time = "2026-09-23T17:58:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/cc/08/6477bf19e923deb834c4c5866c66f47a679cc0a31028e2240ea58f9c1244/fonttools-4.66.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd9297a5f670f3e59ae697289ddad5902d3c4a86188e83e7823bde06334a560c", size = 5524223, upload-time = "2026-09-23T17:58:57.078Z" }, + { url = "https://files.pythonhosted.org/packages/69/2a/3e893827e4d6f8858784f71f0484046626a43e21492d3683b7ea5c7262bb/fonttools-4.66.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2032ac005e14c56e774fd19f2bc1c22c796c0c3b39d91741c0e3f9575944d38", size = 5345920, upload-time = "2026-09-23T17:58:59.848Z" }, + { url = "https://files.pythonhosted.org/packages/22/7a/574cfd9f567231006152694a8904e1cfa2e7ddd2b7476e8e9d1b079278b4/fonttools-4.66.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:02117d05dddb51e39b5c5a6eed0702ed8e0e1c341cb138101b2446eb479858f8", size = 5392188, upload-time = "2026-09-23T17:59:02.461Z" }, + { url = "https://files.pythonhosted.org/packages/17/15/656c907ef5063eeb9a0f6e36a253c14ffe5d5605009d6c86140234cb0968/fonttools-4.66.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b11180c4166a4fc353bb02b147322b7c454927bba4f5040ccdef109a73f9051", size = 5438594, upload-time = "2026-09-23T17:59:04.967Z" }, + { url = "https://files.pythonhosted.org/packages/5d/82/5df8cfb6c16035a9f0baef4c7b37ae1691e5d9280195459f1eb6f0633014/fonttools-4.66.0-cp315-cp315t-win32.whl", hash = "sha256:6627f48130cdbe04c7e1189bff4192886ecda757f575626ee8468f6580eaecf3", size = 2467910, upload-time = "2026-09-23T17:59:07.332Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cd/3359c485efaed245663bf6f840e1bb45fffa1230b1e9af1880d7d9003e56/fonttools-4.66.0-cp315-cp315t-win_amd64.whl", hash = "sha256:a9a45a23b020e667499fb2dbce0f1373b1b1b8cf88e47f3031e69ef3ea21f3ee", size = 2516298, upload-time = "2026-09-23T17:59:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/83/65/826290863c9df6041f2e36a5ae5d604cd8247fffc8ec7b45581fd2473e4d/fonttools-4.66.0-py3-none-any.whl", hash = "sha256:bc7b7ddc1a1f46c363354304e9a8dd93722e4a6a24f785015650898dacf40df9", size = 1201583, upload-time = "2026-09-23T17:59:12.104Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1314,11 +1712,11 @@ wheels = [ [[package]] name = "isort" -version = "8.0.1" +version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" }, ] [[package]] @@ -1333,6 +1731,154 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "kiwisolver" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/54/1b8d2bc580414cc75b3a5b3d195981a6facacd8a8d986e589d0a3b51a709/kiwisolver-1.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1acc7e5b7ef05e9da8bb70cd6c7c4513090213d2e1ad9720f599f0bf6c52aec5", size = 123362, upload-time = "2026-08-28T10:24:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/57/19/92c30f540dcfff86ff625389427c39b53a9aaea16420ddcc09b2ab8d1073/kiwisolver-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bad20d4c69c851c982a1e3606f4c293edfd5a87885786c50082412240c4b1ffd", size = 66551, upload-time = "2026-08-28T10:24:44.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ae/30b94088722e7bce8b821cd5ee935a87f80023e7243125f1accf98e39bf7/kiwisolver-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0a4faea5c6db201c6a21391d2ac926ea97acf7dacdbc3c417189e1adb1a00837", size = 64076, upload-time = "2026-08-28T10:24:46.184Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/9f394d693be549fa3fab62498c2778294595991047adc3dcf10aa99b91c0/kiwisolver-1.5.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e05c2f7925f1d88778e53cb44f14e0223204a3bdd09a41664750363acfb1f2ef", size = 1628782, upload-time = "2026-08-28T10:24:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/64/96/26efc04348c0f332b6e6c471dff132bb8b18b2df494a1b65826601674e62/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3a4e41e3096bf1f0f1b76e2ffd6d828d6547f574f702d59bdbef7acfa59db9c", size = 1228112, upload-time = "2026-08-28T10:24:49.503Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ac/df9ddc19ec972cd97b60b262dd2c4be28c6192b5a779cad429c6657ee680/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1d56ec54d257d05e0b50f5780d967540cd07beeaf9e5f645b26d50cce79f4d8", size = 1246756, upload-time = "2026-08-28T10:24:51.225Z" }, + { url = "https://files.pythonhosted.org/packages/cf/de/c38a246b8a4f4b293c7291aa650917b05c5549b0d532749bc523296e90c4/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8de6f2a4ce7e7bd27d23dd94abf0ccafe0e0e5cc9c764b0577191f2c25f08f26", size = 1295555, upload-time = "2026-08-28T10:24:52.953Z" }, + { url = "https://files.pythonhosted.org/packages/b6/48/3c95a5232d8b5783050d8c8089b58bf1f57018f9135232d72c5aa6b0c01b/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16895f553ee6620a827d2da56b871f835fb70b9216cca5d188e885caf6e3bd23", size = 2179476, upload-time = "2026-08-28T10:24:54.568Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e9/b9e999336afc561b154e93682f597832bdc7446f2ee18b467c5b0e924867/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b92f60017dda7d877fdc546438b5e28f31c523264f49cf5a48c1d0ce1a0dfbc", size = 2275258, upload-time = "2026-08-28T10:24:56.186Z" }, + { url = "https://files.pythonhosted.org/packages/76/cd/c1c550796ada4a59644b8264b425fe332686d2a086c4729fcc640ececdfe/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7824b5e8bdbf0bccb4ccd37bbb115849a1dc45437fb4de8351385ed07c437ee0", size = 2443424, upload-time = "2026-08-28T10:24:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/30f641eb8e10f6ad5bb9af8a15ec8eea386162a6ef491e68995b8680ef4e/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aa7d00b1700966d2917e54d278aba86897890ca9276dd8b76cf6446b6c181b92", size = 2249457, upload-time = "2026-08-28T10:25:00.223Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d9/211e016f8aa5b5bf496dd3bd7ebab4b73b22d91f025f25e20156a44a77fc/kiwisolver-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:96c30002424670b5e1e46495c2b8cbffef39cf77c1d79e76462029d50339785b", size = 70643, upload-time = "2026-08-28T10:25:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/ae7007266dea1fd93fcc48541042c1f6583958055de3b424a59ec16def25/kiwisolver-1.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:f0f4a42db92d6ec7677ab9d12830a2a8ec145a9c6d15db2b593466bc875c78d7", size = 68215, upload-time = "2026-08-28T10:25:03.136Z" }, + { url = "https://files.pythonhosted.org/packages/94/7b/2de6908edc668427c149af5f93112e931f87e1fa4cab80bac32c5844dccc/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6", size = 123364, upload-time = "2026-08-28T10:25:04.359Z" }, + { url = "https://files.pythonhosted.org/packages/8a/24/e70914415c77c97be7e22c80a0740869cb7428768cc380fdcdf6703e7084/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55", size = 66558, upload-time = "2026-08-28T10:25:05.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2b/8b08b11833db4d475b8ef1f36174f8d8a7abd31bedd7e794be78e8814b48/kiwisolver-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839", size = 64071, upload-time = "2026-08-28T10:25:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720", size = 1438206, upload-time = "2026-08-28T10:25:08.254Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/c941a139f27438c1910d630fdc3ccfdab7c8407c72052299ead12ece086e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:719a35fa1156db3640555f95ebb94f60a444e64d1c69626b0edef5df78eba225", size = 1248975, upload-time = "2026-08-28T10:25:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/a1/2669ee5512e39b9d4de25faacaedf788c957f93730c5f7c63993ec4f5933/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febcce10f2bcdbb80b4ea919238a6a4ac13dbc4c7cadbe8d5d75c3682f8b5404", size = 1266301, upload-time = "2026-08-28T10:25:11.754Z" }, + { url = "https://files.pythonhosted.org/packages/28/b8/353f52f2c7f861a9e90cd2e8f90f85b3ad03060835f823e08298d094c463/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1d852545c4d0e35a72728d072cbaa59e2fa7dd84bdf01e068d670dd0ceb58eb6", size = 1319708, upload-time = "2026-08-28T10:25:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/21/0e/14b83200eadc2c1d63b76bac01c1813bf072aecf567429f303e00b70258e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:2e10ae1bba1899188b33557c10d73affcc12033edd18adddb57d209039976a4c", size = 971720, upload-time = "2026-08-28T10:25:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/86/91/9d43d84d23b1cbff72a142d387ead1ea03db0cba8ff86ed5335addad3cc9/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b69602970994a2ed8bbfa78c2f0394a7435226c6040489702d9f0a0ad0c07052", size = 2200119, upload-time = "2026-08-28T10:25:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/f2bd9616f27ffb5e17cecc0baa5d0bbcee7e55aeddc0ccc871d69e2fc3ee/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d50de98e8d807dc31822fff96f50293163a62418eb65487a21b42713d72ed0b7", size = 2295005, upload-time = "2026-08-28T10:25:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/ba/17/ee671b72bf8f46a08379d4392c65582541759a542428197562f2898294ad/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3221f78211074f561c44ca42eac0619828171bec15a2c4cf6f7747d07df76e8e", size = 1960982, upload-time = "2026-08-28T10:25:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/0e26b4c05bee3bdb0f048dfa305e4fe701999ea17b51e9c616ef91035bbe/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0ba9527afc80ae3d7814ed98b6572d02bf85eaf48065678342c5f0c6dab7a8c7", size = 2464918, upload-time = "2026-08-28T10:25:21.65Z" }, + { url = "https://files.pythonhosted.org/packages/ae/62/6eb431133d30ce656ac1e5ff72fac70dd34d54c3984f4011b9ac8bf77d54/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12dfea7f5fc2a34a9080efbf79c4c44eb380ec5b9c6fea09407e08f0d1e941d", size = 2270967, upload-time = "2026-08-28T10:25:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9f/6f9e489c188200e6fb3193935501894811e8c97577c8ffe9033589bf3521/kiwisolver-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071", size = 70744, upload-time = "2026-08-28T10:25:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6d/dfc430d1d43957061599adea3f08ea982bb6f4ab601a8c974bedcf2ba850/kiwisolver-1.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3", size = 68404, upload-time = "2026-08-28T10:25:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/65b302742389c6f96f2956bef5decf26011309feb2fc5d79613af18adea4/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", size = 123876, upload-time = "2026-08-28T10:25:27.44Z" }, + { url = "https://files.pythonhosted.org/packages/71/74/c21f339956f6f691b2ed7e31d5f3ae767304df6c460192739fc830853051/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", size = 66487, upload-time = "2026-08-28T10:25:28.728Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/bdb34e21523e01dceda064d63713f3bdec91388af24fba1eca7ea5e85864/kiwisolver-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", size = 64660, upload-time = "2026-08-28T10:25:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", size = 1477929, upload-time = "2026-08-28T10:25:31.495Z" }, + { url = "https://files.pythonhosted.org/packages/6f/35/09c58daac34e6f6ea5c6dee0094b422118e5a7c265586008a95fd135ac5f/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", size = 1278499, upload-time = "2026-08-28T10:25:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/19/32/739765e24fbad29d13f83e546ea4abc215a78cea9d677ca09025b027724d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", size = 1296677, upload-time = "2026-08-28T10:25:35.059Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/03304d1010e2cc45e5b3b52cef7e43fed3a2a5cd6c87a89b4a88e1d85b5d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", size = 1346037, upload-time = "2026-08-28T10:25:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/3e/57/4c49377bfd274450dd72ecaa13eaac32ea804a03363e4d1db0c5aa999ceb/kiwisolver-1.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", size = 988248, upload-time = "2026-08-28T10:25:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/38df144a08b6c5d75ca4504e5cc3141bb3bfef64c04f4ef48204f42711b6/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", size = 2228722, upload-time = "2026-08-28T10:25:40.038Z" }, + { url = "https://files.pythonhosted.org/packages/e7/11/3221838a89cd64d9b386353e000cd8a296069a20fbe3584507fdfd5bebae/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", size = 2325216, upload-time = "2026-08-28T10:25:41.699Z" }, + { url = "https://files.pythonhosted.org/packages/83/d4/075c219230697bb5db910d37262b9bacf880f92b4811a02ab81ed073a253/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", size = 1977689, upload-time = "2026-08-28T10:25:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/1d219c3c2dd960983d0d4da623d916e9de6385df2b0bab3d1af0e9b8fccc/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", size = 2491443, upload-time = "2026-08-28T10:25:45.242Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/024208ec1079d273f1047468d1bdffbf38bb75b7b268090fd3a0301b9d9a/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", size = 2295200, upload-time = "2026-08-28T10:25:46.984Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/7b210498f9f92e1cd7855f260fa69ef056881087b199ee20c208f0e4189a/kiwisolver-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", size = 70748, upload-time = "2026-08-28T10:25:48.444Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/ef0daa157c8bb23672f7423e0d14c39db1dc6ef8ed47e6bc54c9c1bef3bf/kiwisolver-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", size = 68324, upload-time = "2026-08-28T10:25:49.81Z" }, + { url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" }, + { url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" }, + { url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" }, + { url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" }, + { url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" }, + { url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" }, + { url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" }, + { url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" }, + { url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" }, + { url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/8fbecaf4fc18c02f31f05e47a84c010a80e3ec391ed2f0bdade1d62b5954/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", size = 124031, upload-time = "2026-08-28T10:27:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c6/bf3090d2983b4204347cbdbe952116e7c3b2abf62b4e33e50167a13e75ee/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", size = 66489, upload-time = "2026-08-28T10:27:17.506Z" }, + { url = "https://files.pythonhosted.org/packages/85/de/562dddef55fdd7c291da8626d6619e72b5fc0870e6ccca0e149a5731e7f3/kiwisolver-1.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", size = 64806, upload-time = "2026-08-28T10:27:18.673Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/ba7b9c0164ce1cf62bf2872db63b8483289cf0f3110d6f9390eb09e409ed/kiwisolver-1.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", size = 1482211, upload-time = "2026-08-28T10:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/34da54efb4976616d45c20aae32d70e89d6e7395ed908029154d1609ef22/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", size = 1283739, upload-time = "2026-08-28T10:27:21.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3a/30ffb62bee646e266e98a1b5cd276d9c75b6116fbfcb87c1190838c1b6df/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", size = 1301681, upload-time = "2026-08-28T10:27:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8d/13c70be22a8506880b35fdc38dca36629613bc493405c79f4037f2cd2bb9/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", size = 1349159, upload-time = "2026-08-28T10:27:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/0e/993972b8ec6767f47cd69818fb3a5ff14510557d29f7d1a839be7574fa1b/kiwisolver-1.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", size = 997613, upload-time = "2026-08-28T10:27:27.507Z" }, + { url = "https://files.pythonhosted.org/packages/36/82/ca26eddd2eda2420dfc56693449c1f821f78b485da9cbde9904c03af3f93/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", size = 2235109, upload-time = "2026-08-28T10:27:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c7/84/97d920881e10840b8d7c7185620298e3e4c88820b05514e3a15a258b08a6/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", size = 2331207, upload-time = "2026-08-28T10:27:32.795Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ce/34d74b8f25acc58800f4c09268371e8d6159cf0f1206f1e4dc7835629b48/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", size = 1986696, upload-time = "2026-08-28T10:27:34.628Z" }, + { url = "https://files.pythonhosted.org/packages/0e/01/f892644014612527aef7031d3306a2ffc60b3cb044f802c1561f8e5e14f3/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", size = 2496400, upload-time = "2026-08-28T10:27:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/32/6d/d8284e66e697026536e5f418b9cfe56567bffd3c775e3ecfbae373605854/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", size = 2302967, upload-time = "2026-08-28T10:27:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/46/0a/69a355e27f32ba50d5b6369949b6a1702e122f5277c89bc76d452b81c1c4/kiwisolver-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", size = 72283, upload-time = "2026-08-28T10:27:40.491Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/7a95be90c33dcdd52204d4aa6384d731443225b887283bbd8b61e7931f6c/kiwisolver-1.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", size = 69860, upload-time = "2026-08-28T10:27:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/31/f8/9bc493e7f5707788ba7f621902c68f82dc3a7ba03c78fbd337b026cef1ed/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", size = 126336, upload-time = "2026-08-28T10:27:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d1/82/3aea86b3f99712db825e9ac5631bf99571e818a8b8961ff98cebd798413e/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", size = 67698, upload-time = "2026-08-28T10:27:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/84/7d/8daafc5d2e7f9c47a4f78f8865d86d2a9cf399c2a85f86c44a993594410c/kiwisolver-1.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", size = 65945, upload-time = "2026-08-28T10:27:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c9/51dc974d9130da70a8c47a96160123443d387ffe1b6b833d6f91d9429339/kiwisolver-1.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", size = 1588030, upload-time = "2026-08-28T10:27:47.678Z" }, + { url = "https://files.pythonhosted.org/packages/e6/86/f3e1a730e7a995149d8d3ff9e313b6d8a17b2cf1d98a8eff139dc30463fb/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", size = 1390760, upload-time = "2026-08-28T10:27:49.676Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/8ba25a2b5a0d2375e046f1b72de5179513f0be95aba6e7b094c89303929f/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", size = 1403279, upload-time = "2026-08-28T10:27:51.242Z" }, + { url = "https://files.pythonhosted.org/packages/18/d0/278d5cb8be812740027d5ca0a7eda0c375488a88d6dce0fa60fcc2591ad2/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", size = 1454429, upload-time = "2026-08-28T10:27:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/bcbab41063ec284c1d200efe5087cf087798c2f8916960aa8a20dd303290/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", size = 1073225, upload-time = "2026-08-28T10:27:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c5/ab79dcdf5ae28909a51210ae0a1c579e97ff997b3466414f0d04c0994583/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", size = 2334335, upload-time = "2026-08-28T10:27:56.78Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/71d047468a189041d9c93f3b76844b924f9793b188c44bd149fa258912da/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", size = 2424982, upload-time = "2026-08-28T10:27:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/1347461bbea6d0e1f0580b94ef603b18e72c2be5f667fa1653867361a00b/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", size = 2062857, upload-time = "2026-08-28T10:28:00.407Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/c7f0976c9cd0d127643351bc0c9929e0b8899d7f49d4ec238cd909e39c42/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", size = 2596022, upload-time = "2026-08-28T10:28:02.183Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/59a6f6ae6f938c15076be2c21b6cedea973d71bb1349ec84fa485fab82cf/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", size = 2395634, upload-time = "2026-08-28T10:28:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/8982c1fb7926da5bb7ed60318c3665b5c3f941447271ac982960a11b8637/kiwisolver-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", size = 75379, upload-time = "2026-08-28T10:28:06.307Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/ba7b6dfa1708b82b373ac056928a30c545d5c1a627df9839dcec3c6c1881/kiwisolver-1.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", size = 73011, upload-time = "2026-08-28T10:28:07.578Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/1407df7512a5b36cc79840e01710dc575733c461b13ab866cae77eaf87f3/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", size = 134002, upload-time = "2026-08-28T10:28:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/16/45/c37a21ad5c0ab581a93c55ad544721aaa1f0ae94edb29c6a678a23d013e6/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", size = 194292, upload-time = "2026-08-28T10:28:11.06Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/69f00d627949580e43d57af0aa465df46868d7c29801c137a55374101294/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", size = 73362, upload-time = "2026-08-28T10:28:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/8bae9fac0f1837679ed59a5dd7e97e7eb943b738defa7cc0117ea0d107dc/kiwisolver-1.5.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a5716a33bfabb2c6ce27b6cf03253467b3804f83e215f4d202685cf93c6c9874", size = 59543, upload-time = "2026-08-28T10:28:13.794Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3e/42e3639d32ef3ac6ede80c39dc1d1df6224927409960d8f762b6cb504efe/kiwisolver-1.5.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:685929988b208a911f1285e2f8ed54210b0d681a3dc0f03e00d599d291986e7e", size = 57504, upload-time = "2026-08-28T10:28:15.074Z" }, + { url = "https://files.pythonhosted.org/packages/e1/12/f4beaaceb740b96c363a8fa6f34dbdf37a58d9e5f15416427d7bf89c552a/kiwisolver-1.5.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e49f7e1a4e7191bdf9dc67a974db714501b1fc52c24324103d06a86abd5c08", size = 79885, upload-time = "2026-08-28T10:28:16.345Z" }, + { url = "https://files.pythonhosted.org/packages/02/24/18d8a755acdae79c37ca2f1a925795aab67b9456c30c8569c377ea3abc77/kiwisolver-1.5.1-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a887b6565bbfe80efde2b7f6e8890d7d9bbdb11bdb17028a3690c32fe0621f", size = 77582, upload-time = "2026-08-28T10:28:17.719Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/b86dde4f553ef74db0f9c32398614e24b2e8ec1d29a6ca04ae080d7dd29b/kiwisolver-1.5.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1983f0974a750a6f6556f368ba11105d1d8369c735b944747c9f12ae5aea7aae", size = 98321, upload-time = "2026-08-28T10:28:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1d/59ba570b1774e95e97fde3a0981b2e22118a7a495f73bf74cedc538566a0/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1", size = 59450, upload-time = "2026-08-28T10:28:20.383Z" }, + { url = "https://files.pythonhosted.org/packages/22/98/a6849f04dc18b5400e8b98affa2cd8fd86ed583085f036e57b32e571f4fa/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957", size = 57400, upload-time = "2026-08-28T10:28:21.632Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/a7dc71353dd06a4cbe02222773f52d4a28c81e5a452a75797f8ed113dc99/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5daa1f19e097050b9c4d9a78fcc9263cb96c9dfae08037ddc1b7c4ad1889f2a2", size = 79891, upload-time = "2026-08-28T10:28:22.936Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/d61c61a84ff85d1a36a99df2c152b59ffedb1d356c598902aba44abcdb60/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdaeeb6c350106df6bf9d873395973e5f066a9713200b72cd64f55d0a3eafab6", size = 77605, upload-time = "2026-08-28T10:28:24.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/52/5aef56f21a460a6e43ab3cdfc7697d59d7b87deb0ec97a0f7b91aa4a521b/kiwisolver-1.5.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f", size = 98465, upload-time = "2026-08-28T10:28:25.696Z" }, +] + [[package]] name = "librt" version = "0.8.1" @@ -1426,7 +1972,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version < '3.11'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ @@ -1442,7 +1988,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.11'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ @@ -1534,6 +2080,159 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, + { name = "cycler" }, + { name = "fonttools", version = "4.65.0", source = { registry = "https://pypi.org/simple" } }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.11.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "contourpy", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "cycler" }, + { name = "fonttools", version = "4.66.0", source = { registry = "https://pypi.org/simple" } }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/c8/9aa712a0afb882649424dd8de8ad9aa6235e796e84c6052e8f6dc1598d0d/matplotlib-3.11.2.tar.gz", hash = "sha256:cec596316640f2b394b8f0daa0ea61a8eae82d017b620b9f202befb972a59ea4", size = 32660610, upload-time = "2026-09-11T19:05:31.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/a9/a7fc5a63c427aeb9a4b99d6a0449f891be0952d3aee2b85229099f7b9f54/matplotlib-3.11.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5e1e923a3fc3326b99ec0a6ff1ab1338ac6c6cc62ad9d8a9c944197c7f8c6221", size = 9461046, upload-time = "2026-09-11T19:02:59.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/59d8ee63e4dee845d953515c50d67e6b8cefa953b453c6b803f197ae556e/matplotlib-3.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c27e577ece613ea12a0790e00b4eb80d901c59a3e16cad474f31d8b1529690b9", size = 9290872, upload-time = "2026-09-11T19:03:02.295Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a4/1ca765c63a60e3096ae2ad132eb25f6f7eab0377418cd0472808befd9665/matplotlib-3.11.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:07d9b9fa60cd4c393692f50d0bb03123242ddf61c99bb0e95e75feb354e7c1a8", size = 9854405, upload-time = "2026-09-11T19:03:04.796Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3d/f28ca971a4ad0ed83ebdd60230b09d99de8b91aba47964fac3a81eb2e2ba/matplotlib-3.11.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30ec15d7eefee71de16b7c689b42ba49715a4644650b76a6c7c70d79daf24e91", size = 10660507, upload-time = "2026-09-11T19:03:07.402Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0c/a743556bc64a9b57d2246b14d91b713078adfd2df4199f542df1861737a9/matplotlib-3.11.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:116cdb0eb0eb5644fc98eb2975d4b4dd4ad35e5c8e6b851c22e6976f371f7ab5", size = 10801141, upload-time = "2026-09-11T19:03:11.357Z" }, + { url = "https://files.pythonhosted.org/packages/66/54/91fe3fdfa29100b4668cbf9721577a10acde6e009694618230e4e7a97527/matplotlib-3.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:a24d5fd36e4f0e742c3851dcd20810e56a95633a342e4bf6cb591c678e8fe61f", size = 9329982, upload-time = "2026-09-11T19:03:14.25Z" }, + { url = "https://files.pythonhosted.org/packages/49/c1/13b70871c98d7e93f0d2ed71f226b8d6f873f1372124046068a6b4902fb5/matplotlib-3.11.2-cp311-cp311-win_arm64.whl", hash = "sha256:57b9ea60a835937c2012861923cbb91f47db8565775d326d1c42fc926aa10351", size = 9023542, upload-time = "2026-09-11T19:03:16.878Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/1bfcc4873b121597791ad74032943b123218c0613af0b97e8dd05e916fb1/matplotlib-3.11.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef752769cd962f39ea0b6ffc82d1ea43a0012c5a6157c7a075212fa509cfcff2", size = 9476466, upload-time = "2026-09-11T19:03:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c4/7f5f3601ee69baf072c0c7d3ce60c03e0618621c5a56c34a62a460e29d11/matplotlib-3.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ef31985c4dedb5f1424e1aec6849a47dd37689cb7fa3c20b1b82187f26806261", size = 9305583, upload-time = "2026-09-11T19:03:22.39Z" }, + { url = "https://files.pythonhosted.org/packages/b8/90/2b3fd67ee273163faeda6d514be70b5596eaa0fd77b60ffc294ad0b34f5f/matplotlib-3.11.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df4f7784aca81a94f254c0a2767d592ee25f407e488f5fa7203e51093fb6ca27", size = 9860353, upload-time = "2026-09-11T19:03:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/84/32549e7a462dc311aed2ab62e5d2538028840b5c53a8be0195c789937c3d/matplotlib-3.11.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b9a7ad579856284135e401ecc918c5f8a017ee30539298862a109f51b971710", size = 10670348, upload-time = "2026-09-11T19:03:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/84/39/02e21b74f7439bd643d717ea846006d46e309e6d29b632b57751265311d6/matplotlib-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3aa4b8516fd26659e4363abbf317c703d9116496c5db2e9d0609a2866dd39dd2", size = 10810580, upload-time = "2026-09-11T19:03:31.402Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/0d239bde12b308262265c2d98909b2f0ecad2b5deeae96241642e822e9c7/matplotlib-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:c5c1c68ee401fc98271263410f0e5ce88285abacf7627132914e8adf3d70ff43", size = 9349409, upload-time = "2026-09-11T19:03:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/49/a8/06baf901c02246c8a222b655cc4540ef9c15e1549a04e07927f9cde716c3/matplotlib-3.11.2-cp312-cp312-win_arm64.whl", hash = "sha256:643ff850d8e0f5b8319337f87ed3cb59506afb3df3cc48de777d85871233be7b", size = 9028084, upload-time = "2026-09-11T19:03:37.032Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ce/fc58855a3d17f6cb8a87b6f9ccd100f65432a3a7ffed57309cbf14c78987/matplotlib-3.11.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d43ff8cebb50840648cb6429b2228621dbd709010f3117b3740abb20abf21c0", size = 9476987, upload-time = "2026-09-11T19:03:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/2a47b700545aca050e0ed73c89ba21e08407e11f5f42a567421b220de6c1/matplotlib-3.11.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a8285cea8ef4d92aa041d1c33788bcae82248503300f93f1ca2136b9049452f", size = 9306050, upload-time = "2026-09-11T19:03:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/a9/df/e7c10292f348cf55178a5b3c0092a38ea68891a3945f199ae8b7b8ac9dc1/matplotlib-3.11.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1944895967f87c84c9b4bad29a707b31f5b36df4a3a2339ea1f8ea3ce5105539", size = 9861060, upload-time = "2026-09-11T19:03:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/b978de6d43d47f1d4d8e653fac32c16d8d2e5f46bbbb822e26d7cb1adece/matplotlib-3.11.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af2661f6ac6bbd1d081996f54fdd9385715c625ace8cec0869055c0cfbf38981", size = 10670885, upload-time = "2026-09-11T19:03:48.942Z" }, + { url = "https://files.pythonhosted.org/packages/a3/80/d72f61bb2631ac15d17fada1f17e0f99fecba69d8376d34f17cf93b7eddd/matplotlib-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e5a90f8a707ebb6004a713b1cff091a40cc5df4c7c1cb165e5a505ebc11c292", size = 10811222, upload-time = "2026-09-11T19:03:51.665Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/6d1d33f242c705b5c6c6fb3f0fc141add51cb1abb62912e59de0835d28c4/matplotlib-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:bc067c462a86f0e57bf52fc6e058d90171a5420007dac00c46e90153050c69a7", size = 9349539, upload-time = "2026-09-11T19:03:54.534Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bb/c0aa86a7f4c0e62f1c3c0711e361f6f23f87eb9018732a5e11994bd1b7ec/matplotlib-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:3e8576f7c47e02fd4f21f44171302d2d1d58d4471d46da3d71fe8899d19539d9", size = 9028295, upload-time = "2026-09-11T19:03:57.261Z" }, + { url = "https://files.pythonhosted.org/packages/54/b4/4facdb700bc236e4c676495ca3798d8134ab8668cbfe0f6b2e9ebf962a38/matplotlib-3.11.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c9721f81275499da1feeb36a2cf8192ea086283b3bd16b7dc4c9d7aedb7396d6", size = 9478976, upload-time = "2026-09-11T19:04:00.12Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a0/bba05f0a25bfeb0475c2f2a87958b31103aa6b7a5386017afb1a96212825/matplotlib-3.11.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a3040b209f3968b4e84161df7b174f07a9fad33b0f2d7e48ea3bbd3075e2863", size = 9307932, upload-time = "2026-09-11T19:04:02.976Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/81d5cba4bb39b41b0998efc880e129f2d7cdb94256ca49678fdcd9c21be5/matplotlib-3.11.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3fe71fbfb8ec0e310e0bc8537c3405a01f38f25f9394ed2135e6202fed542b", size = 10674370, upload-time = "2026-09-11T19:04:06.912Z" }, + { url = "https://files.pythonhosted.org/packages/29/5e/52f56f93d5b20815ee0ae352f7a38eff25580edb180b4fa7df87cfeba68f/matplotlib-3.11.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c8255de28f986d935a64c9ca71c0ec2d2f41d355691f5ea684725dc91413f71", size = 10954597, upload-time = "2026-09-11T19:04:09.948Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f8918a1b564db49c53cd603eb3c8748c5767e4ce0fb2cf1d7c38653e4c5d/matplotlib-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a8756cc73d9af9a7fe0deb54ea2e75ef73b01d9e575877e72acad5458e660943", size = 10812583, upload-time = "2026-09-11T19:04:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/4315ef0d63937fe56f451887b4a0be3b8e8b22cf1d92a8d157caac54570d/matplotlib-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:ecea603dd2fbf8242fd31a305a8b12a4ece2de28096870c65fdd0d1e35b8d9a6", size = 9505972, upload-time = "2026-09-11T19:04:15.788Z" }, + { url = "https://files.pythonhosted.org/packages/d5/00/02398b0a1a62ef73d9af0a957fe1773363eec9a6ca7e433c72ae2790fbce/matplotlib-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:01dc8eaaab5a9fce9ff615eca82345728f289e4715b186ee10c6d85272fc26bb", size = 9184534, upload-time = "2026-09-11T19:04:18.691Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c5/1e6ca10baa7e04f113c37856aadb4adc3dbb1c55fc7da6dc830bba1292d7/matplotlib-3.11.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79a258f58253dfa025af80a9e9bb228d75fced007f0e93ae7423fefbde81a74d", size = 9526415, upload-time = "2026-09-11T19:04:21.39Z" }, + { url = "https://files.pythonhosted.org/packages/e0/93/7561727af07ccc84747953c6a33eee3981cc436d04f465032e707ca0e429/matplotlib-3.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c0b83f044ce10a98027b105b3931548719a6e8c7ef986b4362651e0b5367c8dc", size = 9361064, upload-time = "2026-09-11T19:04:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/c1/40/ce270ef2a6794d94a409fe28b8dabedffc07c07f9e5dbb89e0f605bd157d/matplotlib-3.11.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e43b188f0a5b75447bcc197728258166aa64365770ed1caa36595a5e1ca4bbba", size = 10684626, upload-time = "2026-09-11T19:04:27.201Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/396e0307dbf1c1f2d0a3654caa6dbc780dd5379f5f11257369cf8d21d9fe/matplotlib-3.11.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3304eb5a59442a8867f6920d484591c0fa09ffc29e9260be2feec3351e25869", size = 10960602, upload-time = "2026-09-11T19:04:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1c/97/4defb84695477340aa88b27c9482cc1ca4de11f36f66b6c8c0ddd685def1/matplotlib-3.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:02329432ae5c6af87cf208ee575b701d698bdf0b1a3bb28cc6d53c36e967e575", size = 10822603, upload-time = "2026-09-11T19:04:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e7/eab54551c1ec6b72dcd678a078b6c6da8d4c85ec1ff0016e81fe52866304/matplotlib-3.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:f2ac30cf5eb5dff1b584627ae0b0e1186551a4f69ae3c75073911da497a29170", size = 9551206, upload-time = "2026-09-11T19:04:35.811Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/04698032cb4d8fb1e30d8ffec5ded20f6f582b03c8c58b2385c95245f158/matplotlib-3.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:cf41ecd1b0c0b6f7177ed965a54c2afbe888715c7cf6054dc12d53bc1494002c", size = 9234977, upload-time = "2026-09-11T19:04:38.972Z" }, + { url = "https://files.pythonhosted.org/packages/44/a9/ca827c184b87364378964e36e7c0ae23651d146fdbc4000635205490cf82/matplotlib-3.11.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:894a9cbbecbe30ae6787d464df2e8fc7a8d475cfc68f87c3029f7c11152899b3", size = 9478997, upload-time = "2026-09-11T19:04:41.914Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/01aaafc9c3159f3fae9a65bbcddaa4a6d4d72b0aeffdd636d7f6d00a5c8d/matplotlib-3.11.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:bbf1062991d826ed27e2144f3afa4461afbb8ff56e8f703043e191a8163b1ee9", size = 9307947, upload-time = "2026-09-11T19:04:44.834Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a0/6aa72c7cd7cf4dfac652744d7a3310007cec3ce8e5ab9be607bec7096471/matplotlib-3.11.2-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:399fef672f7046ef7d6a57572b2a6f9845f3f5afcff04e7b2df7a363a9f42190", size = 10674370, upload-time = "2026-09-11T19:04:47.745Z" }, + { url = "https://files.pythonhosted.org/packages/a1/97/d7e90225ac145046787a2ee0fa4cdc7e4d5cb6772ea232130b10f8a4095a/matplotlib-3.11.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc82dde2a0d3e3ad472edce04897ad7146b8d8bfd1df8a32992eebb81af18fdc", size = 10954581, upload-time = "2026-09-11T19:04:50.712Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/89cf056a9d2cae01f4b8baf01c7e809d613244fcdfa2a2acf28c1e0d79fa/matplotlib-3.11.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:930efb28f59fda124e39265d177bab302625297bb147d2910de725a2fc2aef54", size = 10812605, upload-time = "2026-09-11T19:04:53.552Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/c61b9f4c34af802d4d84e58803e009013d626b5f468bafe1416131863b5b/matplotlib-3.11.2-cp315-cp315-win_amd64.whl", hash = "sha256:3da3bc0cbf7245e7db72cc6d29d12c5abef72cb73059c73b945f14e3545f3eb2", size = 9506068, upload-time = "2026-09-11T19:04:56.519Z" }, + { url = "https://files.pythonhosted.org/packages/03/7b/047f2fa8708d47430d8e7fa45e9fffb0aee5d4e300f978e087be2ee11fbc/matplotlib-3.11.2-cp315-cp315-win_arm64.whl", hash = "sha256:f25446b2981717dca9786bac841cb3fd7efb568e3e3c755dd980481c5cb9228d", size = 9184576, upload-time = "2026-09-11T19:04:59.318Z" }, + { url = "https://files.pythonhosted.org/packages/03/6e/ad9c5989950785a98642667df265a47c6637e924cb69b5dcf68c3299ae7c/matplotlib-3.11.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fea03cf56568cc1cba08b470be6a0559e71c3a5b688d54b7179bb35ba23d0821", size = 9525988, upload-time = "2026-09-11T19:05:02.304Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c3/25c61701fb655dd412428b15eee2591ce554cdd593e39a045a153373f428/matplotlib-3.11.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:254d4ddb2fa8df3b4c689c0c306063aee10521df82cfb438185e499c75fe37c1", size = 9360774, upload-time = "2026-09-11T19:05:05.092Z" }, + { url = "https://files.pythonhosted.org/packages/58/5a/4552878fce8f9d249a5e264c16f5c267c27eac08fb039fa3e207a06feff5/matplotlib-3.11.2-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60cf3047a51edecdc4535a9196bbd6a732936b8e9f8184aabf4d16165884aaf", size = 10685892, upload-time = "2026-09-11T19:05:07.686Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/b3b19ec0124fc4aafd8a4b3bae859c562a7ff0fd8ae3b2099947f78bde19/matplotlib-3.11.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb3712dc9b464793de0a4e42a7313d50293c751f94bddaf7332a1bc71bccdda9", size = 10964946, upload-time = "2026-09-11T19:05:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/7b/30/687ccce22f66a57fed85942450e88062773bcbf1a42914e70c7148ec71dc/matplotlib-3.11.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:75b6d88402770e181b5d06a67dda4129c31da7d05004d63a21c310ec78d1b83c", size = 10824960, upload-time = "2026-09-11T19:05:13.9Z" }, + { url = "https://files.pythonhosted.org/packages/d3/34/117fe35a8d05054b4b34d2d29287099927c24c3841d36e06d9fa2f20c1cf/matplotlib-3.11.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a6939df7567114b6bac7f4c5e06c84a67f197c1b2f2e4b234d4eecb3bfec9482", size = 9551204, upload-time = "2026-09-11T19:05:16.48Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/9f440e7bec8b80d0af2a65d1165b2f4e718982dad785531f62255d9ff19b/matplotlib-3.11.2-cp315-cp315t-win_arm64.whl", hash = "sha256:d480038c83691532ed52ff3147db51fa902fc78cb2d8349993a1cdb684435bff", size = 9233946, upload-time = "2026-09-11T19:05:19.063Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d9/a1218a31fa2d8082eb893548990ad09a2005ebda1b2b89dad88a8308e51a/matplotlib-3.11.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:854df8d7dfe9fdffcbaa6f39e44a6c24b409cb4d7561fbc09213b157d833f6a6", size = 9457702, upload-time = "2026-09-11T19:05:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/68/d7/a9fde05d87c3c853f233ec20c5b5c3a1c86035e1ed4bfa397cd54d46ec90/matplotlib-3.11.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eac4b07d4e3743b172451e122ea964f72f152879d4f9adcf3f3d33e518f12ead", size = 9287753, upload-time = "2026-09-11T19:05:24.677Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/ea2ae4eab84f011c1dc8145864f78e5a63adda8efb95557ef36cc38e6da2/matplotlib-3.11.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ef7a53b66780e5d942923724f08577fbc5be1f7322da0f0f3f9dcaa45dd803d", size = 9848259, upload-time = "2026-09-11T19:05:27.455Z" }, +] + [[package]] name = "mdit-py-plugins" version = "0.5.0" @@ -1769,12 +2468,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "mdit-py-plugins", marker = "python_full_version < '3.11'" }, - { name = "pyyaml", marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, + { name = "jinja2" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } wheels = [ @@ -1790,12 +2489,12 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" }, - { name = "pyyaml", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "jinja2" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } @@ -1805,34 +2504,225 @@ wheels = [ [[package]] name = "numpy" -version = "1.26.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, + { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, + { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, ] [[package]] @@ -1865,6 +2755,100 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -2183,6 +3167,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/11/b213bebff182584360cb8d17c72c1677fec5c5c228de439e63bcf8ab1c8f/pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36", size = 1050487, upload-time = "2026-09-20T20:59:05.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4", size = 126420, upload-time = "2026-09-20T20:59:04.025Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -2215,6 +3208,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "pytokens" version = "0.4.1" @@ -2531,6 +3536,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, ] +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "snowballstemmer" version = "3.0.1" @@ -2548,23 +3571,23 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.11'" }, - { name = "babel", marker = "python_full_version < '3.11'" }, - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "imagesize", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -2579,23 +3602,23 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version == '3.11.*'" }, - { name = "babel", marker = "python_full_version == '3.11.*'" }, - { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "imagesize", marker = "python_full_version == '3.11.*'" }, - { name = "jinja2", marker = "python_full_version == '3.11.*'" }, - { name = "packaging", marker = "python_full_version == '3.11.*'" }, - { name = "pygments", marker = "python_full_version == '3.11.*'" }, - { name = "requests", marker = "python_full_version == '3.11.*'" }, - { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -2610,23 +3633,23 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -2641,7 +3664,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282, upload-time = "2025-01-16T18:25:30.958Z" } wheels = [ @@ -2656,7 +3679,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/f6/bdd93582b2aaad2cfe9eb5695a44883c8bc44572dd3c351a947acbb13789/sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe", size = 37563, upload-time = "2026-01-02T15:23:46.543Z" } wheels = [ @@ -2671,7 +3694,7 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/e9/d29ae58dd12971d2cbb872884676a70d1a5e4719b4d82e197264cdf0431a/sphinx_autodoc_typehints-3.9.11.tar.gz", hash = "sha256:28516c916b41fa83271ee2ab9191b73807e4113d3bfb94222ac87d8d9795b6e7", size = 70261, upload-time = "2026-03-24T16:57:28.462Z" } wheels = [ From 6a3af9486d1b2a7e772a0df266f30575faa3827d Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 00:15:28 -0700 Subject: [PATCH 11/12] ci: SHA-pin the actions in the new keyshare-wheel and uv-lock jobs (PBA-L6-003 policy) Rebased onto the merge-gate PR (#4), which pins every action to a full commit SHA; the two jobs this branch adds now use the same pins. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b14fd71..326c406 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: uv-lock-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - 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" @@ -32,8 +32,8 @@ jobs: keyshare-wheel-tripwire: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: '3.11' - run: python -m pip install --upgrade pip && pip install -e . From 29c5182999dcf0a2ab03da127b429e8b6425cf06 Mon Sep 17 00:00:00 2001 From: SaulBuilds Date: Fri, 25 Sep 2026 00:23:47 -0700 Subject: [PATCH 12/12] test: mutation-harden the R2 fixes; refuse malformed sha256 pointers (PBA-L6b-030) mutmut over the functions this branch changed left survivors on boundaries (exp/nbf tolerance, share-count and threshold edges, guard depth), parameter hand-offs (IPFS manager, wallet RPC, deploy plumbing) and request shapes. tests/test_pba_r2_mutation_hardening.py pins them. One behaviour change: a sha256: pointer that is not 32 bytes of hex is now refused instead of being treated as unverifiable. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01FYQkdsk54yob6FD24jAT8P --- citrate_sdk/ipfs.py | 8 +- tests/test_pba_r2_mutation_hardening.py | 700 ++++++++++++++++++++++++ 2 files changed, 706 insertions(+), 2 deletions(-) create mode 100644 tests/test_pba_r2_mutation_hardening.py diff --git a/citrate_sdk/ipfs.py b/citrate_sdk/ipfs.py index bb9693c..679cfea 100644 --- a/citrate_sdk/ipfs.py +++ b/citrate_sdk/ipfs.py @@ -25,11 +25,15 @@ def _expected_digest_from_address(ipfs_hash: str) -> bytes | None: raw codec (0x55) and a sha2-256 multihash (``bafkrei...``). dag-pb CIDs (``Qm...``, ``bafybei...``) hash a chunked DAG, not the bytes; None.""" if ipfs_hash.startswith("sha256:"): + # A pointer that claims to be a sha256 must be one: a malformed one is + # refused rather than silently downgraded to "unverifiable". try: digest = bytes.fromhex(ipfs_hash[len("sha256:"):]) except ValueError: - return None - return digest if len(digest) == 32 else None + raise IPFSError(f"malformed sha256 content pointer: {ipfs_hash!r}") + if len(digest) != 32: + raise IPFSError(f"malformed sha256 content pointer: {ipfs_hash!r}") + return digest if ipfs_hash.startswith("b"): body = ipfs_hash[1:].upper() try: diff --git a/tests/test_pba_r2_mutation_hardening.py b/tests/test_pba_r2_mutation_hardening.py new file mode 100644 index 0000000..97b77ce --- /dev/null +++ b/tests/test_pba_r2_mutation_hardening.py @@ -0,0 +1,700 @@ +"""Mutation hardening for the R2 fixes (mutmut survivors in the changed functions). + +Each test pins a boundary, a parameter hand-off or a branch that a surviving +mutant flipped without any test noticing. Grouped by module; the finding each +function belongs to is noted on the class. +""" +from __future__ import annotations + +import base64 +import hashlib +import io +import json +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any +from unittest import mock + +import pytest +import requests +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from eth_abi import encode as abi_encode +from eth_utils import to_checksum_address + +from citrate_sdk import CitrateClient, KeyManager +from citrate_sdk._chain_guard import pinned_send +from citrate_sdk._generated import contract +from citrate_sdk._url_security import InsecureTransportError, enforce_transport_security +from citrate_sdk.crypto import ( + EncryptionConfig, + assert_no_key_share_material, + canonical_public_key_hex, +) +from citrate_sdk.errors import CitrateError, IPFSError +from citrate_sdk.finite_field import ShamirSecretSharing, split_secret_bytes +from citrate_sdk.identity import client as idc +from citrate_sdk.identity import wallet +from citrate_sdk.identity.client import IdentityClient, IdentityError +from citrate_sdk.identity.jwt import IdTokenError, verify_id_token +from citrate_sdk.ipfs import IPFSClient, IPFSManager +from citrate_sdk.learning import ClassroomManager, LearningManager +from citrate_sdk.models import ModelConfig + +OWNER = "0x" + "11" * 32 +ACCT = "0x" + "01" * 20 +ADDR = "0x" + "02" * 20 + + +# ── PBA-L6b-026: transport gate ───────────────────────────────────────────── +class TestTransportGate: + def test_non_string_truthy_input_is_refused(self) -> None: + with pytest.raises(InsecureTransportError): + enforce_transport_security(123) # type: ignore[arg-type] + + @pytest.mark.parametrize("url", ["https://rpc​.citrate.ai", "https://rpc.citrate.ai/‎", "https://a\x7fb.example"]) + def test_format_or_control_char_inside_the_url_is_refused(self, url: str) -> None: + with pytest.raises(InsecureTransportError, match="control"): + enforce_transport_security(url) + + def test_https_without_host_is_refused(self) -> None: + with pytest.raises(InsecureTransportError, match="no host"): + enforce_transport_security("https:///path") + + +# ── PBA-L6b-003: deploy guard, share plan, holder API ─────────────────────── +def _nest(n: int) -> dict[str, Any]: + return {"leaf": 1} if n == 0 else {"n": _nest(n - 1)} + + +class TestShareGuardAndPlan: + def test_guard_depth_boundary(self) -> None: + assert_no_key_share_material(_nest(32)) + with pytest.raises(CitrateError, match="nested more than 32 levels"): + assert_no_key_share_material(_nest(33)) + + def test_canonical_key_accepts_0x_and_compressed(self) -> None: + km = KeyManager("0x" + "22" * 32) + comp = km.get_public_key() + unc = km.ecdh_manager.get_public_key_uncompressed().hex() + assert canonical_public_key_hex(comp) == unc + assert canonical_public_key_hex("0x" + comp) == unc + assert canonical_public_key_hex("0x" + unc) == unc + with pytest.raises(CitrateError, match="invalid secp256k1"): + canonical_public_key_hex("0x" + "00" * 33) + + def _pubs(self, n: int) -> list[str]: + return [KeyManager("0x" + (i + 100).to_bytes(32, "big").hex()).get_public_key() for i in range(n)] + + @pytest.mark.parametrize(("t", "n"), [(True, 3), (2, True), ("2", 3), (2, "3"), (0, 3), (4, 3), (2, 256), (-1, 3)]) + def test_invalid_share_parameters(self, t: Any, n: Any) -> None: + cfg = EncryptionConfig(threshold_shares=t, total_shares=n, share_holder_public_keys=self._pubs(3)) + with pytest.raises(CitrateError): + KeyManager(OWNER).encrypt_model_with_key_shares(b"m", cfg) + + @pytest.mark.parametrize(("t", "n"), [(1, 1), (3, 3), (2, 255)]) + def test_boundary_share_parameters_accepted(self, t: int, n: int) -> None: + cfg = EncryptionConfig(threshold_shares=t, total_shares=n, share_holder_public_keys=self._pubs(n)) + _, meta, envs = KeyManager(OWNER).encrypt_model_with_key_shares(b"m", cfg) + assert meta["key_sharing"] == {"threshold": t, "total_shares": n} + assert [e["x"] for e in envs] == list(range(1, n + 1)) + + def test_nonce_is_96_bits_and_zero_threshold_is_refused_by_the_shared_api(self) -> None: + _, meta = KeyManager(OWNER).encrypt_model(b"m", EncryptionConfig()) + assert len(bytes.fromhex(meta["nonce"])) == 12 + with pytest.raises(CitrateError, match="threshold_shares must be > 0"): + KeyManager(OWNER).encrypt_model_with_key_shares(b"m", EncryptionConfig()) + + def test_unwrap_returns_the_share_threshold(self) -> None: + owner = KeyManager(OWNER) + holder = KeyManager("0x" + "22" * 32) + envs = owner._create_key_shares(b"k" * 32, 2, 2, [holder.get_public_key(), self._pubs(1)[0]]) + share = holder.unwrap_key_share(envs[0], owner.get_public_key()) + assert share["threshold"] == "2" and share["x"] == "1" + + def test_create_key_shares_needs_one_holder_per_share(self) -> None: + with pytest.raises(CitrateError, match="exactly one holder"): + KeyManager(OWNER)._create_key_shares(b"k" * 32, 2, 3, self._pubs(2)) + + +class TestReconstructKey: + @pytest.mark.parametrize("t", [True, 0, 256, "2"]) + def test_bad_threshold(self, t: Any) -> None: + with pytest.raises(CitrateError, match="threshold must be an integer"): + KeyManager(OWNER).reconstruct_key_from_shares([{"x": "1", "y": "aa"}], t) + + def test_255_of_255_and_three_digit_x(self) -> None: + shares = [{"x": str(x), "y": y.hex()} for x, y in split_secret_bytes(b"s", 255, 255)] + assert KeyManager(OWNER).reconstruct_key_from_shares(shares, 255) == b"s" + assert shares[-1]["x"] == "255" + + def test_validation_detail_propagates(self) -> None: + dup = [{"x": "1", "y": "aa"}, {"x": "1", "y": "bb"}] + with pytest.raises(CitrateError, match="duplicate share x = 1"): + KeyManager(OWNER).reconstruct_key_from_shares(dup, 2) + + def test_empty_and_non_hex(self) -> None: + with pytest.raises(CitrateError, match="No shares provided"): + KeyManager(OWNER).reconstruct_key_from_shares([], 1) + with pytest.raises(CitrateError, match="y is not hex"): + KeyManager(OWNER).reconstruct_key_from_shares([{"x": "1", "y": "zz"}], 1) + + +class TestShamirBoundaries: + @pytest.mark.parametrize(("t", "n"), [(True, 3), (2, True), (0, 3), (2, 256), (1.0, 2)]) + def test_constructor_refuses(self, t: Any, n: Any) -> None: + with pytest.raises(ValueError): + ShamirSecretSharing(t, n) + + def test_constructor_accepts_edges(self) -> None: + ShamirSecretSharing(1, 1) + ShamirSecretSharing(255, 255) + + def test_list_shaped_share_and_x_255(self) -> None: + with pytest.raises(ValueError, match="tuple"): + ShamirSecretSharing.validate_shares([[1, b"a"]]) # type: ignore[list-item] + ShamirSecretSharing.validate_shares([(255, b"a")]) + with pytest.raises(ValueError, match="y must be bytes"): + ShamirSecretSharing.validate_shares([(1, "a")]) # type: ignore[list-item] + + def test_verify_shares_threshold_edges(self) -> None: + sss = ShamirSecretSharing(2, 3) + s = sss.split_secret(b"ab") + assert sss.verify_shares(s[:2]) is True + assert sss.verify_shares(s[:1]) is False + + +# ── deploy_model calldata shape ───────────────────────────────────────────── +def test_unencrypted_deploy_sends_an_empty_access_list(tmp_path: Path) -> None: + import rlp # type: ignore[import-untyped] + + mp = tmp_path / "m.onnx" + mp.write_bytes(b"w") + client = CitrateClient("http://localhost:8545", private_key=OWNER) + box: dict[str, Any] = {} + + def rpc(method: str, params: Any = None) -> Any: + if method == "eth_sendRawTransaction": + box["raw"] = params[0] + return "0x" + "ab" * 32 + return {"eth_getTransactionCount": "0x0", "eth_chainId": hex(40204), "eth_gasPrice": "0x1"}[method] + + stubs: dict[str, Any] = {"_rpc_call": rpc, "_upload_to_ipfs": lambda d: "bafy", + "_wait_for_receipt": lambda h: {"logs": []}, + "_extract_model_id_from_receipt": lambda r: "0x01"} + for name, fn in stubs.items(): + setattr(client, name, fn) + client.deploy_model(mp, ModelConfig(name="m")) + payload = json.loads(bytes(rlp.decode(bytes.fromhex(box["raw"][2:]))[5])) + assert payload["access_list"] == [] and "encryption_metadata" not in payload + + +# ── PBA-L6b-042: pinned_send ──────────────────────────────────────────────── +class TestPinnedSend: + def test_integer_chain_id_answer_is_accepted(self) -> None: + sent: list[Any] = [] + + class M: + _expected_chain_id = 40204 + _chain_verified = False + + def _rpc_call(self, method: str, params: Any) -> Any: + if method == "eth_chainId": + return 40204 + sent.append(params[0]) + return "0xh" + + assert pinned_send(M(), {"to": ADDR}) == "0xh" + assert sent == [{"to": ADDR, "chainId": hex(40204)}] + + +# ── PBA-L6b-028/040/031: learning managers ────────────────────────────────── +def _rpc_capture(result: str = "0xhash") -> tuple[Any, list[Any]]: + sent: list[Any] = [] + + def rpc(method: str, params: Any) -> Any: + if method == "eth_chainId": + return hex(40204) + if method == "eth_sendTransaction": + sent.append(params[0]) + return "0xhash" + return result + return rpc, sent + + +class TestLearningManagers: + def test_create_targets_the_registry_with_a_22_char_code(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 + + def test_create_pool_targets_the_pool(self) -> None: + rpc, sent = _rpc_capture() + LearningManager(rpc, default_account=ACCT, contract_addresses={"learningPool": ADDR}).create_pool("p", "d", "Open", "1") + assert sent[-1]["to"] == ADDR + + def test_get_classroom_accepts_unprefixed_hex(self) -> None: + ret = abi_encode(["(address,string,uint256,uint256,uint256,bool)"], [(ADDR, "n", 1, 0, 2, True)]).hex() + rpc, _ = _rpc_capture(result=ret) + info = ClassroomManager(rpc, default_account=ACCT, classroom_address=ADDR).get_classroom(ADDR) + assert info.name == "n" and info.exists is True + + +# ── PBA-L6b-029 / L3a twins: identity ─────────────────────────────────────── +_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) +_pub = _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(_pub.n.to_bytes(256, "big")), "e": _b64(_pub.e.to_bytes(3, "big"))}] +ISS = contract.identity()["issuer"] + + +def _tok(payload: dict[str, Any], kid: str | None = "k") -> str: + header: dict[str, Any] = {"alg": "RS256"} + if kid: + header["kid"] = kid + h, p = _b64(json.dumps(header).encode()), _b64(json.dumps(payload).encode()) + return f"{h}.{p}.{_b64(_key.sign(f'{h}.{p}'.encode(), padding.PKCS1v15(), hashes.SHA256()))}" + + +class TestVerifyIdTokenEdges: + now = 1_900_000_000 + + def _p(self, **kw: Any) -> dict[str, Any]: + base: dict[str, Any] = {"iss": ISS, "aud": "c", "sub": "u", "iat": self.now, "exp": self.now + 600} + base.update(kw) + return base + + def _v(self, tok: str, **kw: Any) -> dict[str, Any]: + return verify_id_token(tok, ISS, "c", JWKS, now_ms=kw.pop("now_ms", self.now * 1000), **kw) + + def test_exp_boundary_with_default_tolerance(self) -> None: + exp = self.now - 60 + assert self._v(_tok(self._p(exp=exp)))["sub"] == "u" + with pytest.raises(IdTokenError, match="expired"): + self._v(_tok(self._p(exp=exp - 1))) + + def test_nbf_boundary(self) -> None: + assert self._v(_tok(self._p(nbf=self.now + 60)))["sub"] == "u" + with pytest.raises(IdTokenError, match="not yet valid"): + self._v(_tok(self._p(nbf=self.now + 61))) + + def test_audience_list(self) -> None: + assert self._v(_tok(self._p(aud=["x", "c"])))["sub"] == "u" + with pytest.raises(IdTokenError, match="aud"): + self._v(_tok(self._p(aud=["x", "y"]))) + + def test_jwks_filter_and_kid_selection(self) -> None: + jwks = [{"kty": "EC", "kid": "k", "n": "x", "e": "y"}, *JWKS] + assert verify_id_token(_tok(self._p()), ISS, "c", jwks, now_ms=self.now * 1000)["sub"] == "u" + with pytest.raises(IdTokenError, match="no RSA JWK for kid zz"): + verify_id_token(_tok(self._p(), kid="zz"), ISS, "c", JWKS, now_ms=self.now * 1000) + two = [*JWKS, dict(JWKS[0], kid="other")] + with pytest.raises(IdTokenError, match="ambiguous or missing RSA JWK"): + verify_id_token(_tok(self._p(), kid=None), ISS, "c", two, now_ms=self.now * 1000) + + def test_iss_mismatch_names_the_issuer(self) -> None: + with pytest.raises(IdTokenError, match="iss mismatch: https://evil"): + self._v(_tok(self._p(iss="https://evil"))) + + +class TestSiweHelpers: + def test_iso_ms_truncates_microseconds(self) -> None: + t = datetime(2026, 9, 25, 0, 0, 0, 999_999, tzinfo=timezone.utc) + assert idc._iso_ms(t) == "2026-09-25T00:00:00.999Z" + + @pytest.mark.parametrize("ttl", [1, 86400]) + def test_ttl_edges_accepted(self, ttl: int) -> None: + addr = to_checksum_address("0x" + "ab" * 20) + assert "Expiration Time" in idc.build_siwe_message(addr, "abcdefgh", ttl_seconds=ttl) + + def test_bool_ttl_refused(self) -> None: + with pytest.raises(IdentityError, match="ttl_seconds"): + idc.build_siwe_message(to_checksum_address("0x" + "ab" * 20), "abcdefgh", ttl_seconds=True) + + @pytest.mark.parametrize(("body", "ok"), [({"nonce": "abcdefgh"}, True), ({"nonce": "abcdefg"}, False), + ({"nonce": 12345678}, False), ({}, False), ([], False)]) + def test_challenge_nonce_rules(self, body: Any, ok: bool) -> None: + c = IdentityClient(client_id="c", redirect_uri="http://127.0.0.1/cb", transport=lambda m, u, h, b: (200, body)) + if ok: + assert c.siwe_challenge() == {"nonce": "abcdefgh"} + else: + with pytest.raises(IdentityError, match="no nonce"): + c.siwe_challenge() + + @pytest.mark.parametrize(("status", "body", "msg"), [ + (300, {"redirectTo": "https://x"}, r"failed: 300$"), + (401, {"reason": 5, "error": "invalid_grant"}, r"failed: 401 \(invalid_grant\)$"), + (401, {"reason": "unknown_nonce"}, r"failed: 401 \(unknown_nonce\)$"), + (500, [], r"failed: 500$"), + (200, ["redirectTo"], "neither redirectTo nor id_token"), + ]) + def test_verify_errors(self, status: int, body: Any, msg: str) -> None: + c = IdentityClient(client_id="c", redirect_uri="http://127.0.0.1/cb", transport=lambda m, u, h, b: (status, body)) + with pytest.raises(IdentityError, match=msg): + c.siwe_verify("m", "s") + + def test_verify_redirect_with_bad_types(self) -> None: + c = IdentityClient(client_id="c", redirect_uri="http://127.0.0.1/cb", + transport=lambda m, u, h, b: (299, {"address": 1, "method": 2, "redirectTo": "https://x"})) + assert c.siwe_verify("m", "s") == {"kind": "redirect", "address": "", "method": "", "redirect_to": "https://x"} + + +# ── PBA-L6b-027: wallet verification plumbing ─────────────────────────────── +UID = "0x" + "42" * 32 + + +def _wallet_post(chain: int = 40204, code: str = "0x60", result: Any = "echo", error: bool = False, + factory: str | None = None) -> tuple[Any, list[Any]]: + calls: list[Any] = [] + + def post(url: str, data: str | None = None, headers: Any = None, timeout: Any = None) -> requests.Response: + body = json.loads(data or "{}") + calls.append((url, body, headers, timeout)) + r = requests.Response() + r.status_code = 200 + if error: + r._content = json.dumps({"jsonrpc": "2.0", "id": 1, "error": {"code": -1}}).encode() + return r + m = body["method"] + if m == "eth_chainId": + res: Any = hex(chain) + elif m == "eth_getCode": + res = code + else: + local = wallet.predict_wallet_address(UID, factory=factory) + res = ("0x" + "00" * 12 + local[2:].lower()) if result == "echo" else result + r._content = json.dumps({"jsonrpc": "2.0", "id": 1, "result": res}).encode() + return r + return post, calls + + +class TestWalletPlumbing: + def test_request_shape(self) -> None: + post, calls = _wallet_post() + with mock.patch.object(wallet.requests, "post", post): + wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") + factory = contract.aa_stack()["CitrateWalletFactory"] + url, body, headers, timeout = calls[-1] + assert url == "https://rpc.example" and headers == {"content-type": "application/json"} and timeout == 10.0 + from eth_utils import keccak + assert body["params"] == [{"to": factory, "data": "0x" + keccak(b"predictAddress(bytes32)")[:4].hex() + "42" * 32}, "latest"] + assert calls[1][1]["params"] == [factory, "latest"] + + def test_rpc_error_object_is_refused(self) -> None: + post, _ = _wallet_post(error=True) + with mock.patch.object(wallet.requests, "post", post): + with pytest.raises(wallet.WalletPredictionError, match="eth_chainId failed"): + wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") + + @pytest.mark.parametrize("result", [None, "0x1234"]) + def test_missing_prediction_is_refused(self, result: Any) -> None: + post, _ = _wallet_post(result=result) + with mock.patch.object(wallet.requests, "post", post): + with pytest.raises(wallet.WalletPredictionError, match="returned no address"): + wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") + + def test_custom_factory_and_insecure_opt_in(self) -> None: + f = to_checksum_address("0x" + "fe" * 20) + post, calls = _wallet_post(factory=f) + with mock.patch.object(wallet.requests, "post", post): + out = wallet.verify_wallet_address_on_chain(UID, rpc_url="http://rpc.remote.example", factory=f, + allow_insecure_http=True) + assert out == wallet.predict_wallet_address(UID, factory=f) + assert calls[-1][1]["params"][0]["to"] == f + + def test_garbage_chain_id(self) -> None: + post, _ = _wallet_post() + + def bad(url: str, data: str | None = None, headers: Any = None, timeout: Any = None) -> requests.Response: + r: requests.Response = post(url, data, headers, timeout) + if json.loads(data or "{}")["method"] == "eth_chainId": + r._content = b'{"jsonrpc":"2.0","id":1,"result":"0xzz"}' + return r + with mock.patch.object(wallet.requests, "post", bad): + with pytest.raises(wallet.WalletPredictionError, match="eth_chainId returned"): + wallet.verify_wallet_address_on_chain(UID, rpc_url="https://rpc.example") + + +# ── PBA-L6b-030: IPFS plumbing ────────────────────────────────────────────── +DATA = b"bytes" * 10 + + +def _session(body: bytes, calls: list[Any], status: int = 200) -> Any: + def post(url: str, params: Any = None, timeout: Any = None, stream: bool = False) -> requests.Response: + calls.append((url, params, timeout, stream)) + r = requests.Response() + r.status_code = status + r.raw = io.BytesIO(body) + return r + return post + + +class TestIpfsPlumbing: + def test_request_shape(self) -> None: + calls: list[Any] = [] + c = IPFSClient("http://127.0.0.1:5001", timeout=7.0) + c.session.post = _session(DATA, calls) # type: ignore[method-assign] + assert c.download_bytes("QmX") == DATA + assert calls == [("http://127.0.0.1:5001/api/v0/cat", {"arg": "QmX"}, 7.0, True)] + + def test_0x_prefixed_expected_hash(self) -> None: + c = IPFSClient("http://127.0.0.1:5001") + c.session.post = _session(DATA, []) # type: ignore[method-assign] + assert c.download_bytes("QmX", expected_sha256="0x" + hashlib.sha256(DATA).hexdigest()) == DATA + + def test_http_error_and_connection_error(self) -> None: + c = IPFSClient("http://127.0.0.1:5001") + c.session.post = _session(b"", [], status=500) # type: ignore[method-assign] + with pytest.raises(IPFSError, match="HTTP 500"): + c.download_bytes("QmX") + + def boom(*a: Any, **k: Any) -> Any: + raise requests.exceptions.ConnectionError("refused") + c.session.post = boom # type: ignore[method-assign] + with pytest.raises(IPFSError, match="connection error"): + c.download_bytes("QmX") + + def test_non_raw_cidv1_is_not_misverified(self) -> None: + mh = bytes([0x12, 0x20]) + hashlib.sha256(b"other").digest() + cid = "b" + base64.b32encode(bytes([0x01, 0x70]) + mh).decode().lower().rstrip("=") + c = IPFSClient("http://127.0.0.1:5001") + c.session.post = _session(DATA, []) # type: ignore[method-assign] + assert c.download_bytes(cid) == DATA + + def test_manager_falls_back_and_threads_parameters(self) -> None: + m = IPFSManager("http://127.0.0.1:5001", fallback_urls=["http://127.0.0.1:5002"]) + good = hashlib.sha256(DATA).hexdigest() + m.primary.session.post = _session(b"tampered", []) # type: ignore[method-assign] + m.fallbacks[0].session.post = _session(DATA, []) # type: ignore[method-assign] + for cl in (m.primary, m.fallbacks[0]): + cl.is_available = lambda: True # type: ignore[method-assign] + assert m.download("QmX", expected_sha256=good) == DATA + + def test_manager_active_client_gets_the_parameters(self) -> None: + m = _single(DATA) + m.active_client = m.primary + with pytest.raises(IPFSError, match="exceeds max_bytes"): + m.download("QmX", max_bytes=3) + with pytest.raises(IPFSError, match="expected_sha256"): + m.download("QmX", expected_sha256="00" * 32) + + def test_download_from_ipfs_threads_parameters(self) -> None: + m = _single(DATA) + with mock.patch("citrate_sdk.ipfs.get_ipfs_manager", return_value=m): + from citrate_sdk.ipfs import download_from_ipfs + assert download_from_ipfs("QmX", expected_sha256=hashlib.sha256(DATA).hexdigest()) == DATA + with pytest.raises(IPFSError, match="expected_sha256"): + download_from_ipfs("QmX", expected_sha256="00" * 32) + with pytest.raises(IPFSError, match="exceeds max_bytes"): + download_from_ipfs("QmX", max_bytes=3) + + +def _single(body: bytes) -> IPFSManager: + m = IPFSManager("http://127.0.0.1:5001") + m.primary.session.post = _session(body, []) # type: ignore[method-assign] + m.primary.is_available = lambda: True # type: ignore[method-assign] + return m + + +def test_time_is_not_frozen() -> None: + # Guard for the boundary tests above, which pass now_ms explicitly. + assert time.time() > 1_700_000_000 + + +# ── second round: survivors from the targeted mutmut rerun ────────────────── +class TestSecondRound: + def test_malformed_sha256_pointer_is_refused(self) -> None: + c = IPFSClient("http://127.0.0.1:5001") + c.session.post = _session(DATA, []) # type: ignore[method-assign] + for ptr in ("sha256:" + "ab" * 16, "sha256:zz"): + with pytest.raises(IPFSError, match="malformed sha256"): + c.download_bytes(ptr) + + def test_active_client_receives_the_hash(self) -> None: + calls: list[Any] = [] + m = IPFSManager("http://127.0.0.1:5001") + m.primary.session.post = _session(DATA, calls) # type: ignore[method-assign] + m.active_client = m.primary + assert m.download("QmY") == DATA + assert calls[0][1] == {"arg": "QmY"} + + def test_download_from_ipfs_passes_the_urls(self) -> None: + with mock.patch("citrate_sdk.ipfs.get_ipfs_manager", return_value=_single(DATA)) as g: + from citrate_sdk.ipfs import download_from_ipfs + download_from_ipfs("QmX", ["http://127.0.0.1:5009"]) + g.assert_called_once_with(["http://127.0.0.1:5009"]) + + def test_pinned_send_verifies_when_the_flag_is_absent(self) -> None: + seen: list[Any] = [] + + class Bare: + _expected_chain_id = 40204 + + def _rpc_call(self, method: str, params: Any) -> Any: + seen.append((method, params)) + return hex(1) if method == "eth_chainId" else "0xh" + + with pytest.raises(CitrateError, match="chain-id mismatch"): + pinned_send(Bare(), {"to": ADDR}) + assert seen == [("eth_chainId", [])] + + def test_learning_tx_shape(self) -> None: + rpc, sent = _rpc_capture() + ClassroomManager(rpc, default_account=ACCT, classroom_address=ADDR, gas_limit=123).unenroll() + tx = sent[-1] + assert set(tx) == {"from", "to", "data", "value", "gas", "gasPrice", "chainId"} + assert (tx["from"], tx["value"], tx["gas"], tx["gasPrice"]) == (ACCT, "0x0", hex(123), "0x3b9aca00") + + def test_get_classroom_calls_the_registry(self) -> None: + ret = abi_encode(["(address,string,uint256,uint256,uint256,bool)"], [(ADDR, "n", 1, 0, 2, True)]).hex() + calls: list[Any] = [] + + def rpc(method: str, params: Any) -> Any: + calls.append((method, params)) + return ret + ClassroomManager(rpc, default_account=ACCT, classroom_address=ADDR).get_classroom(ACCT) + method, params = calls[-1] + assert method == "eth_call" and params[0]["to"] == ADDR and params[0]["data"].startswith("0x") + assert params[0]["data"][10:].endswith(ACCT[2:].lower()) + + def test_iso_ms_millisecond_rounding(self) -> None: + assert idc._iso_ms(datetime(2026, 1, 1, 0, 0, 0, 1000, tzinfo=timezone.utc)).endswith(".001Z") + + def test_siwe_uppercase_nonce_and_chain_override(self) -> None: + addr = to_checksum_address("0x" + "ab" * 20) + msg = idc.build_siwe_message(addr, "ABCDEFGH", chain_id=31337) + assert "\nNonce: ABCDEFGH\n" in msg and "\nChain ID: 31337\n" in msg + + def test_refresh_request_shape(self) -> None: + now = int(time.time()) + tok = _tok({"iss": ISS, "aud": "c", "sub": "u", "iat": now, "exp": now + 600}) + seen: list[Any] = [] + + def transport(method: str, url: str, headers: dict[str, str], body: str | None) -> Any: + seen.append((method, url, headers, body)) + if url.endswith("/.well-known/openid-configuration") or url == contract.identity()["discovery"]: + return 200, {"issuer": ISS, "token_endpoint": ISS + "/token", "jwks_uri": ISS + "/jwks"} + if url.endswith("/jwks"): + return 200, {"keys": JWKS} + return 200, {"id_token": tok, "access_token": "a"} + IdentityClient(client_id="c", redirect_uri="http://127.0.0.1/cb", transport=transport).refresh("rt", expected_sub="u") + method, url, headers, body = [s for s in seen if s[1].endswith("/token")][0] + assert method == "POST" and headers == {"content-type": "application/x-www-form-urlencoded"} + assert body == "grant_type=refresh_token&refresh_token=rt&client_id=c" + + def test_siwe_verify_request_and_token_shape(self) -> None: + now = int(time.time()) + tok = _tok({"iss": ISS, "aud": "c", "sub": "u", "iat": now, "exp": now + 600}) + seen: list[Any] = [] + + def transport(method: str, url: str, headers: dict[str, str], body: str | None) -> Any: + seen.append((method, url, headers, body)) + if url.endswith("/siwe/verify"): + return 200, {"address": "0xa", "method": "eoa", "id_token": tok} + if url == contract.identity()["discovery"]: + return 200, {"issuer": ISS, "jwks_uri": ISS + "/jwks"} + return 200, {"keys": JWKS} + r = IdentityClient(client_id="c", redirect_uri="http://127.0.0.1/cb", transport=transport).siwe_verify("m", "s") + assert set(r) == {"kind", "address", "method", "id_token", "claims"} and r["method"] == "eoa" and r["id_token"] == tok + assert seen[0][2] == {"content-type": "application/json"} + c = IdentityClient(client_id="c", redirect_uri="http://127.0.0.1/cb", transport=lambda *a: (401, {"reason": "x"})) + with pytest.raises(IdentityError) as ei: + c.siwe_verify("m", "s") + assert ei.value.status == 401 + + +class TestIdTokenMore: + now = 1_900_000_000 + + def test_single_key_without_kid(self) -> None: + p = {"iss": ISS, "aud": "c", "sub": "u", "iat": self.now, "exp": self.now + 600} + assert verify_id_token(_tok(p, kid=None), ISS, "c", JWKS, now_ms=self.now * 1000)["sub"] == "u" + + def test_string_aud_is_compared_exactly(self) -> None: + p = {"iss": ISS, "aud": "xcx", "sub": "u", "iat": self.now, "exp": self.now + 600} + with pytest.raises(IdTokenError, match="aud"): + verify_id_token(_tok(p), ISS, "c", JWKS, now_ms=self.now * 1000) + + def test_now_is_whole_seconds(self) -> None: + p = {"iss": ISS, "aud": "c", "sub": "u", "iat": self.now, "exp": self.now - 60} + assert verify_id_token(_tok(p), ISS, "c", JWKS, now_ms=self.now * 1000 + 999)["sub"] == "u" + + def test_alg_message_names_the_alg(self) -> None: + h = _b64(json.dumps({"alg": "HS256"}).encode()) + with pytest.raises(IdTokenError, match="'HS256'"): + verify_id_token(f"{h}.e30.sig", ISS, "c", JWKS) + + +class TestWalletMore: + def test_rpc_call_shapes_and_custom_implementation(self) -> None: + impl = to_checksum_address("0x" + "ee" * 20) + f = contract.aa_stack()["CitrateWalletFactory"] + calls: list[Any] = [] + local = wallet.predict_wallet_address(UID, implementation=impl) + + def post(url: str, data: str | None = None, headers: Any = None, timeout: Any = None) -> requests.Response: + body = json.loads(data or "{}") + calls.append((url, body, timeout)) + res = {"eth_chainId": hex(40204), "eth_getCode": "0x60"}.get(body["method"], "0x" + "00" * 12 + local[2:].lower()) + r = requests.Response() + r.status_code = 200 + r._content = json.dumps({"jsonrpc": "2.0", "id": 1, "result": res}).encode() + return r + with mock.patch.object(wallet.requests, "post", post): + assert wallet.verify_wallet_address_on_chain(UID, rpc_url="https://r.example", implementation=impl, timeout=3.0) == local + assert [(c[0], c[1]["method"], c[1]["params"], c[2]) for c in calls[:2]] == [ + ("https://r.example", "eth_chainId", [], 3.0), ("https://r.example", "eth_getCode", [f, "latest"], 3.0)] + assert all(c[1]["jsonrpc"] == "2.0" for c in calls) + + +class TestDeployMore: + def test_deploy_shape_and_plumbing(self, tmp_path: Path) -> None: + import rlp + + mp = tmp_path / "m.onnx" + mp.write_bytes(b"weights") + client = CitrateClient("http://localhost:8545", private_key=OWNER) + box: dict[str, Any] = {} + + def rpc(method: str, params: Any = None) -> Any: + if method == "eth_sendRawTransaction": + box["raw"] = params[0] + return "0x" + "cd" * 32 + return {"eth_getTransactionCount": "0x0", "eth_chainId": hex(40204), "eth_gasPrice": "0x1"}[method] + + def wait(h: str) -> Any: + assert h == "0x" + "cd" * 32 + return {"logs": ["L"]} + + def extract(r: Any) -> str: + assert r == {"logs": ["L"]} + return "0x77" + stubs: dict[str, Any] = {"_rpc_call": rpc, "_upload_to_ipfs": lambda d: "bafy", + "_wait_for_receipt": wait, "_extract_model_id_from_receipt": extract} + for name, fn in stubs.items(): + setattr(client, name, fn) + cfg = ModelConfig(name="m", encrypted=True, access_price=5) + dep = client.deploy_model(mp, cfg) + fields = rlp.decode(bytes.fromhex(box["raw"][2:])) + payload = json.loads(bytes(fields[5])) + assert set(payload) == {"model_hash", "ipfs_hash", "encrypted", "access_price", "access_list", "metadata", + "encryption_metadata"} + assert payload["model_hash"] == hashlib.sha256(b"weights").hexdigest() and payload["ipfs_hash"] == "bafy" + assert payload["encrypted"] is True and payload["access_price"] == 5 and payload["metadata"] == {} + assert set(payload["encryption_metadata"]) == {"algorithm", "nonce", "key_derivation", "encrypted_key", "access_control"} + assert "0x" + bytes(fields[3]).hex() == client._precompile("ModelDeploy").lower() + assert (dep.model_id, dep.tx_hash, dep.encrypted, dep.access_price) == ("0x77", "0x" + "cd" * 32, True, 5) + assert isinstance(dep.deployment_time, int) and dep.deployment_time > 1_700_000_000 + + def test_total_256_is_a_parameter_error(self) -> None: + pubs = [KeyManager("0x" + (i + 1).to_bytes(32, "big").hex()).get_public_key() for i in range(3)] + with pytest.raises(CitrateError, match="invalid share parameters"): + KeyManager(OWNER).encrypt_model_with_key_shares( + b"m", EncryptionConfig(threshold_shares=2, total_shares=256, share_holder_public_keys=pubs))