diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..72c83c2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,15 @@ +[run] +source = toploc +omit = + toploc/__init__.py + toploc/C/csrc/__init__.py + +[report] +exclude_lines = + pragma: no cover + def __repr__ + raise NotImplementedError + if __name__ == .__main__.: + pass + raise ImportError + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49d39b1..c3334c6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,11 @@ Run single test: uv run pytest tests/test_utils.py::test_get_fp32_parts ``` +Run coverage: +```bash +uv run pytest --cov=toploc --cov-report=term-missing --cov-report=html +``` + ## Code Quality Install pre-commit hooks: diff --git a/README.md b/README.md index 97b8f5c..7ae0920 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ The feature set includes: For code used by experiments in our paper, check out: https://github.com/PrimeIntellect-ai/toploc-experiments -### Installation +## Installation ```bash pip install -U toploc ``` -### Usage +## Usage -#### Build proofs from activations: +### Build proofs from activations: As bytes (more compact when stored in binary formats): ```python import torch @@ -53,38 +53,30 @@ Activation shapes: [torch.Size([1, 5, 16]), torch.Size([1, 16]), torch.Size([1, Proofs: ['/9kbQitnukt1bQ==', '/9nLuJrxhiVUoA==', '/9m0aNrm5KtBtg==', '/9mAZNZYMOKvcw==', '/9nSBGTqkZH21w=='] ``` -#### Verify proofs: +### Verify proofs: ```python import torch -from toploc import ProofPoly -from toploc.poly import batch_activations -from toploc.C.csrc.utils import get_fp_parts -from statistics import mean, median +from toploc import verify_proofs_base64 torch.manual_seed(42) activations = [torch.randn(1, 5, 16, dtype=torch.bfloat16), *(torch.randn(1, 16, dtype=torch.bfloat16) for _ in range(10))] proofs = ['/9kbQitnukt1bQ==', '/9nLuJrxhiVUoA==', '/9m0aNrm5KtBtg==', '/9mAZNZYMOKvcw==', '/9nSBGTqkZH21w=='] -proofs = [ProofPoly.from_base64(proof) for proof in proofs] - # apply some jitter to the activations activations = [i * 1.01 for i in activations] -for index, (proof, chunk) in enumerate(zip(proofs, batch_activations(activations, decode_batching_size=3))): - chunk = chunk.view(-1).cpu() - topk_indices = chunk.abs().topk(k=4).indices.tolist() - topk_values = chunk[topk_indices] - proof_topk_values = torch.tensor([proof(i) for i in topk_indices], dtype=torch.uint16).view(dtype=torch.bfloat16) - exps, mants = get_fp_parts(proof_topk_values) - proof_exps, proof_mants = get_fp_parts(topk_values) - - exp_intersections = [i == j for i, j in zip(exps, proof_exps)] - mant_errs = [abs(i - j) for i, j, k in zip(mants, proof_mants, exp_intersections) if k] - print(f"=== Proof {index}") - print(f"Exp intersections: {sum(exp_intersections)}") - print(f"Mean mantissa error: {mean(mant_errs)}") - print(f"Median mantissa error: {median(mant_errs)}") -``` +results = verify_proofs_base64(activations, proofs, decode_batching_size=3, topk=4, skip_prefill=False) +print("Results:") +print(*results, sep="\n") +``` +```python +Results: +VerificationResult(exp_intersections=4, mant_err_mean=1.75, mant_err_median=2.0) +VerificationResult(exp_intersections=4, mant_err_mean=2, mant_err_median=2.0) +VerificationResult(exp_intersections=4, mant_err_mean=1.25, mant_err_median=1.0) +VerificationResult(exp_intersections=4, mant_err_mean=1, mant_err_median=1.0) +VerificationResult(exp_intersections=4, mant_err_mean=2, mant_err_median=2.0) +``` # Citing diff --git a/pyproject.toml b/pyproject.toml index bd47c8c..a727a95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dev-dependencies = [ "ruff", "pre-commit", "pytest", + "pytest-cov" ] [tool.setuptools.dynamic] diff --git a/tests/test_poly.py b/tests/test_poly.py index 366a5d9..135274c 100644 --- a/tests/test_poly.py +++ b/tests/test_poly.py @@ -6,6 +6,9 @@ build_proofs_bytes, build_proofs_base64, ProofPoly, + VerificationResult, + verify_proofs_bytes, + verify_proofs_base64, ) @@ -123,6 +126,7 @@ def test_proof_poly_repr(sample_poly): @pytest.fixture def sample_activations(): + torch.manual_seed(42) DIM = 16 a = [torch.randn(3, DIM, dtype=torch.bfloat16)] for _ in range(3 * 2 + 1): @@ -196,3 +200,66 @@ def test_build_proofs_edge_cases(sample_activations): sample_activations[:1], decode_batching_size=2, topk=5, skip_prefill=True ) assert len(proofs_one_skip) == 0 + + +def test_verify_proofs_bytes(sample_activations): + """Test verification of proofs in bytes format""" + # Generate proofs in bytes format + proofs_bytes = build_proofs_bytes( + sample_activations, decode_batching_size=3, topk=4 + ) + + results = verify_proofs_bytes( + [i * 1.01 for i in sample_activations], + proofs_bytes, + decode_batching_size=3, + topk=4, + ) + + assert isinstance(results, list) + assert all(isinstance(r, VerificationResult) for r in results) + assert len(results) == len(proofs_bytes) + assert all(r.exp_intersections == 4 for r in results) + assert all(r.mant_err_mean > 0 and r.mant_err_mean <= 2 for r in results) + assert all(r.mant_err_median > 0 and r.mant_err_median <= 2 for r in results) + + +def test_verify_proofs_base64(sample_activations): + """Test verification of proofs in base64 format""" + # Generate proofs in base64 format + proofs_base64 = build_proofs_base64( + sample_activations, decode_batching_size=2, topk=5 + ) + + results = verify_proofs_base64( + sample_activations, proofs_base64, decode_batching_size=2, topk=5 + ) + + assert isinstance(results, list) + assert all(isinstance(r, VerificationResult) for r in results) + assert len(results) == len(proofs_base64) + assert all(r.exp_intersections == 5 for r in results) + assert all(r.mant_err_mean == 0 for r in results) + assert all(r.mant_err_median == 0 for r in results) + + +def test_verify_proofs_bytes_invalid(sample_activations): + # Generate proofs in bytes format + proofs_bytes = build_proofs_bytes( + sample_activations, decode_batching_size=3, topk=4 + ) + + results = verify_proofs_bytes( + [i * 1.10 for i in sample_activations], + proofs_bytes, + decode_batching_size=3, + topk=4, + ) + + print(results) + assert isinstance(results, list) + assert all(isinstance(r, VerificationResult) for r in results) + assert len(results) == len(proofs_bytes) + assert all(r.exp_intersections >= 3 for r in results) + assert all(r.mant_err_mean > 10 for r in results) + assert all(r.mant_err_median > 10 for r in results) diff --git a/tests/test_utils.py b/tests/test_utils.py index e8bf8e8..3bcede9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,6 +2,8 @@ import torch import time import pytest +import tempfile +from toploc.utils import sha256sum @pytest.mark.parametrize( @@ -114,3 +116,13 @@ def py_get_fp_parts(tensor: torch.FloatTensor) -> tuple[list[int], list[int]]: assert exps == ref_exps assert mantissas == ref_mants assert new_time < old_time + + +def test_sha256sum(): + with tempfile.NamedTemporaryFile() as f: + f.write(b"Hello, world!" * 1000) + f.flush() + assert ( + sha256sum(f.name) + == "a8f764e70df94be2c911fb51b3d0c56c03882078dbdb215de8b7bd0374b0fb10" + ) diff --git a/toploc/__init__.py b/toploc/__init__.py index fbd8688..6037aa9 100644 --- a/toploc/__init__.py +++ b/toploc/__init__.py @@ -1,4 +1,13 @@ -from toploc.poly import ProofPoly, build_proofs, build_proofs_bytes, build_proofs_base64 # noqa: F401 -from toploc.utils import sha256sum # noqa: F401 +# ruff: noqa: F401 +from toploc.poly import ( + ProofPoly, + build_proofs, + build_proofs_bytes, + build_proofs_base64, + verify_proofs, + verify_proofs_bytes, + verify_proofs_base64, +) +from toploc.utils import sha256sum __version__ = "0.0.0.dev1" diff --git a/toploc/commits.py b/toploc/commits.py deleted file mode 100644 index 8ebd1a3..0000000 --- a/toploc/commits.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import List -import dataclasses -from dataclasses import dataclass -import json -from pathlib import Path -import base64 -from .ndd import compute_newton_coefficients, evaluate_polynomial -import torch - - -@dataclass -class Commit: - model_name: str - device: str - dtype: str - engine: str - hashes: List[str] - completion: List[int] | None - input_tokens: int - generation_config: dict - - def __repr__(self): - _json = dataclasses.asdict(self) - return json.dumps(_json) - - def to_file(self, path: str | Path): - with open(path, "w") as f: - f.write(repr(self)) - - @classmethod - def from_file(cls, path: str | Path): - with open(path, "r") as f: - data = json.load(f) - return cls(**data) - - -def find_injective_modulus(x: list[int]) -> int: - for i in range(65497, 2**15, -1): - if len(set([j % i for j in x])) == len(x): - return i - raise ValueError("No injective modulus found!") - - -class ProofPoly: - def __init__(self, coeffs: list[int], modulus: int): - self.coeffs = coeffs - self.modulus = modulus - - def __call__(self, x: int): - return evaluate_polynomial(self.coeffs, x % self.modulus) - - def __len__(self): - return len(self.coeffs) - - @classmethod - def from_points( - cls, x: list[int] | torch.Tensor, y: list[int] | torch.Tensor - ) -> "ProofPoly": - if isinstance(x, torch.Tensor): - x = x.tolist() - if isinstance(y, torch.Tensor): - if y.dtype == torch.bfloat16: - y = y.view(dtype=torch.uint16).tolist() - elif y.dtype == torch.float32: - raise NotImplementedError( - "float32 not supported yet because interpolate has hardcode prime" - ) - else: - y = y.tolist() - modulus = find_injective_modulus(x) - x = [i % modulus for i in x] - return cls(compute_newton_coefficients(x, y), modulus) - - def to_base64(self): - base64_encoded = base64.b64encode(self.to_bytes()).decode("utf-8") - return base64_encoded - - def to_bytes(self): - return self.modulus.to_bytes(2, byteorder="big", signed=False) + b"".join( - coeff.to_bytes(2, byteorder="big", signed=False) for coeff in self.coeffs - ) - - @classmethod - def from_bytes(cls, byte_data: bytes) -> "ProofPoly": - modulus = int.from_bytes(byte_data[:2], byteorder="big", signed=False) - coeffs = [ - int.from_bytes(byte_data[i : i + 2], byteorder="big", signed=False) - for i in range(2, len(byte_data), 2) - ] - return cls(coeffs, modulus) - - @classmethod - def from_base64(cls, base64_encoded: str) -> "ProofPoly": - byte_data = base64.b64decode(base64_encoded) - return cls.from_bytes(byte_data) - - def __repr__(self) -> str: - return f"ProofPoly[{self.modulus}]({self.coeffs})" diff --git a/toploc/ndd.py b/toploc/ndd.py deleted file mode 100644 index c44101c..0000000 --- a/toploc/ndd.py +++ /dev/null @@ -1,105 +0,0 @@ -# TODO: Deprecate this file and move to C -MOD_N = 65497 - - -def extended_gcd(a, b): - """ - Returns (gcd, x, y) where gcd is the greatest common divisor of a and b - and x, y are coefficients where ax + by = gcd - """ - if a == 0: - return (b, 0, 1) - else: - gcd, x, y = extended_gcd(b % a, a) - return (gcd, y - (b // a) * x, x) - - -def mod_inverse(a, m=MOD_N): - """ - Returns the modular multiplicative inverse of a under modulo m - """ - gcd, x, _ = extended_gcd(a, m) - if gcd != 1: - raise ValueError(f"Modular inverse does not exist for {a} (mod {m})") - else: - return x % m - - -def multiply_polynomials_mod(poly1, poly2, mod=MOD_N): - """ - Multiply two polynomials with coefficients under given modulus. - Polynomials are represented as lists of coefficients from lowest to highest degree. - """ - result = [0] * (len(poly1) + len(poly2) - 1) - for i, coef1 in enumerate(poly1): - for j, coef2 in enumerate(poly2): - result[i + j] = (result[i + j] + coef1 * coef2) % mod - return result - - -def compute_newton_coefficients(x, y, mod=MOD_N): - """ - Calculate coefficients for the interpolation polynomial in standard form - (powers of x) using modular arithmetic. - - Args: - x (list): x coordinates of points - y (list): y coordinates of points - mod (int): modulus for arithmetic operations - - Returns: - list: Coefficients of polynomial in standard form (ascending powers of x) - """ - n = len(x) - # First calculate Newton coefficients - newton_coef = [[0] * n for _ in range(n)] - - # First column is y values - for i in range(n): - newton_coef[i][0] = y[i] % mod - - # Calculate divided differences - for j in range(1, n): - for i in range(n - j): - numerator = (newton_coef[i + 1][j - 1] - newton_coef[i][j - 1]) % mod - denominator = (x[i + j] - x[i]) % mod - inv_denominator = mod_inverse(denominator, mod) - newton_coef[i][j] = (numerator * inv_denominator) % mod - - newton_coeffs = [newton_coef[0][j] for j in range(n)] - - # Convert Newton form to standard form - result = [newton_coeffs[0]] # constant term - current_term = [1] # start with 1 - - for i in range(n - 1): - # Multiply current_term by (x - x[i]) - next_term = [(-x[i] % mod)] + [1] # represents (x - x[i]) - current_term = multiply_polynomials_mod(current_term, next_term, mod) - - # Add newton_coeffs[i+1] * current_term to result - while len(result) < len(current_term): - result.append(0) - for j, coef in enumerate(current_term): - result[j] = (result[j] + (newton_coeffs[i + 1] * coef)) % mod - - return result - - -def evaluate_polynomial(coeffs, x_eval, mod=MOD_N): - """ - Evaluate polynomial at x_eval using Horner's method. - Coefficients are in ascending power order. - - Args: - coeffs (list): coefficients in standard form (ascending powers of x) - x_eval (int): point at which to evaluate the polynomial - mod (int): modulus for arithmetic operations - - Returns: - int: Value of polynomial at x_eval (mod MOD_N) - """ - result = coeffs[-1] - for coef in reversed(coeffs[:-1]): - result = (result * x_eval + coef) % mod - return result diff --git a/toploc/poly.py b/toploc/poly.py index b6f4616..63db2a5 100644 --- a/toploc/poly.py +++ b/toploc/poly.py @@ -1,8 +1,11 @@ from typing import Union import base64 from toploc.C.csrc.ndd import compute_newton_coefficients, evaluate_polynomial +from toploc.C.csrc.utils import get_fp_parts import torch import logging +from dataclasses import dataclass +from statistics import mean, median logger = logging.getLogger(__name__) @@ -11,7 +14,7 @@ def find_injective_modulus(x: list[int]) -> int: for i in range(65497, 2**15, -1): if len(set([j % i for j in x])) == len(x): return i - raise ValueError("No injective modulus found!") + raise ValueError("No injective modulus found!") # pragma: no cover class ProofPoly: @@ -138,12 +141,14 @@ def build_proofs_base64( def batch_activations( activations: list[torch.Tensor], decode_batching_size: int, + skip_prefill: bool = False, ) -> list[torch.Tensor]: batches = [] # Prefill - flat_view = activations[0].view(-1) - batches.append(flat_view) + if not skip_prefill: + flat_view = activations[0].view(-1) + batches.append(flat_view) # Batched Decode for i in range(1, len(activations), decode_batching_size): @@ -153,3 +158,79 @@ def batch_activations( batches.append(flat_view) return batches + + +@dataclass +class VerificationResult: + exp_intersections: int + mant_err_mean: float + mant_err_median: float + + +def verify_proofs( + activations: list[torch.Tensor], + proofs: list[ProofPoly], + decode_batching_size: int, + topk: int, + skip_prefill: bool = False, +) -> list[VerificationResult]: + results = [] + for proof, chunk in zip( + proofs, + batch_activations( + activations, + decode_batching_size=decode_batching_size, + skip_prefill=skip_prefill, + ), + ): + chunk = chunk.view(-1).cpu() + topk_indices = chunk.abs().topk(k=topk).indices.tolist() + topk_values = chunk[topk_indices] + proof_topk_values = torch.tensor( + [proof(i) for i in topk_indices], dtype=torch.uint16 + ).view(dtype=torch.bfloat16) + exps, mants = get_fp_parts(proof_topk_values) + proof_exps, proof_mants = get_fp_parts(topk_values) + + exp_intersections = [i == j for i, j in zip(exps, proof_exps)] + mant_errs = [ + abs(i - j) for i, j, k in zip(mants, proof_mants, exp_intersections) if k + ] + results.append( + VerificationResult( + sum(exp_intersections), mean(mant_errs), median(mant_errs) + ) + ) + return results + + +def verify_proofs_bytes( + activations: list[torch.Tensor], + proofs: list[bytes], + decode_batching_size: int, + topk: int, + skip_prefill: bool = False, +) -> list[VerificationResult]: + return verify_proofs( + activations, + [ProofPoly.from_bytes(proof) for proof in proofs], + decode_batching_size, + topk, + skip_prefill, + ) + + +def verify_proofs_base64( + activations: list[torch.Tensor], + proofs: list[str], + decode_batching_size: int, + topk: int, + skip_prefill: bool = False, +) -> list[VerificationResult]: + return verify_proofs( + activations, + [ProofPoly.from_base64(proof) for proof in proofs], + decode_batching_size, + topk, + skip_prefill, + ) diff --git a/uv.lock b/uv.lock index 7d042cf..b6b4afd 100644 --- a/uv.lock +++ b/uv.lock @@ -25,6 +25,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "coverage" +version = "7.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/67/81dc41ec8f548c365d04a29f1afd492d3176b372c33e47fa2a45a01dc13a/coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8", size = 208345 }, + { url = "https://files.pythonhosted.org/packages/33/43/17f71676016c8829bde69e24c852fef6bd9ed39f774a245d9ec98f689fa0/coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879", size = 208775 }, + { url = "https://files.pythonhosted.org/packages/86/25/c6ff0775f8960e8c0840845b723eed978d22a3cd9babd2b996e4a7c502c6/coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe", size = 237925 }, + { url = "https://files.pythonhosted.org/packages/b0/3d/5f5bd37046243cb9d15fff2c69e498c2f4fe4f9b42a96018d4579ed3506f/coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674", size = 235835 }, + { url = "https://files.pythonhosted.org/packages/b5/f1/9e6b75531fe33490b910d251b0bf709142e73a40e4e38a3899e6986fe088/coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb", size = 236966 }, + { url = "https://files.pythonhosted.org/packages/4f/bc/aef5a98f9133851bd1aacf130e754063719345d2fb776a117d5a8d516971/coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c", size = 236080 }, + { url = "https://files.pythonhosted.org/packages/eb/d0/56b4ab77f9b12aea4d4c11dc11cdcaa7c29130b837eb610639cf3400c9c3/coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c", size = 234393 }, + { url = "https://files.pythonhosted.org/packages/0d/77/28ef95c5d23fe3dd191a0b7d89c82fea2c2d904aef9315daf7c890e96557/coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e", size = 235536 }, + { url = "https://files.pythonhosted.org/packages/29/62/18791d3632ee3ff3f95bc8599115707d05229c72db9539f208bb878a3d88/coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425", size = 211063 }, + { url = "https://files.pythonhosted.org/packages/fc/57/b3878006cedfd573c963e5c751b8587154eb10a61cc0f47a84f85c88a355/coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa", size = 211955 }, + { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, + { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, + { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, + { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, + { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, + { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, + { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, + { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, + { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, + { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, + { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, + { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, + { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, + { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, + { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, + { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, + { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, + { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, + { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, + { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, + { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, + { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, + { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, + { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, + { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, + { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, + { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, + { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, + { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, + { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, + { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, + { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, + { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, + { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, + { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, + { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, + { url = "https://files.pythonhosted.org/packages/6c/eb/cf062b1c3dbdcafd64a2a154beea2e4aa8e9886c34e41f53fa04925c8b35/coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d", size = 208343 }, + { url = "https://files.pythonhosted.org/packages/95/42/4ebad0ab065228e29869a060644712ab1b0821d8c29bfefa20c2118c9e19/coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929", size = 208769 }, + { url = "https://files.pythonhosted.org/packages/44/9f/421e84f7f9455eca85ff85546f26cbc144034bb2587e08bfc214dd6e9c8f/coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87", size = 237553 }, + { url = "https://files.pythonhosted.org/packages/c9/c4/a2c4f274bcb711ed5db2ccc1b851ca1c45f35ed6077aec9d6c61845d80e3/coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c", size = 235473 }, + { url = "https://files.pythonhosted.org/packages/e0/10/a3d317e38e5627b06debe861d6c511b1611dd9dc0e2a47afbe6257ffd341/coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2", size = 236575 }, + { url = "https://files.pythonhosted.org/packages/4d/49/51cd991b56257d2e07e3d5cb053411e9de5b0f4e98047167ec05e4e19b55/coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd", size = 235690 }, + { url = "https://files.pythonhosted.org/packages/f7/87/631e5883fe0a80683a1f20dadbd0f99b79e17a9d8ea9aff3a9b4cfe50b93/coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73", size = 234040 }, + { url = "https://files.pythonhosted.org/packages/7c/34/edd03f6933f766ec97dddd178a7295855f8207bb708dbac03777107ace5b/coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86", size = 235048 }, + { url = "https://files.pythonhosted.org/packages/ee/1e/d45045b7d3012fe518c617a57b9f9396cdaebe6455f1b404858b32c38cdd/coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31", size = 211085 }, + { url = "https://files.pythonhosted.org/packages/df/ea/086cb06af14a84fe773b86aa140892006a906c5ec947e609ceb6a93f6257/coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57", size = 211965 }, + { url = "https://files.pythonhosted.org/packages/7a/7f/05818c62c7afe75df11e0233bd670948d68b36cdbf2a339a095bc02624a8/coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf", size = 200558 }, + { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "distlib" version = "0.3.9" @@ -501,6 +576,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -652,6 +740,7 @@ dependencies = [ dev = [ { name = "pre-commit" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, ] @@ -665,6 +754,7 @@ requires-dist = [ dev = [ { name = "pre-commit" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "ruff" }, ]