diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..52b0b0a Binary files /dev/null and b/.coverage differ diff --git a/pyproject.toml b/pyproject.toml index c5fb13c..e23e2ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "mypy>=2.4.0", "pydantic>=2.13.5", "pytest>=9.1.1", + "pytest-cov>=7.1.0", "python-dotenv>=1.2.3", "sqlalchemy>=2.0.52", ] diff --git a/src/models/__init__.py b/src/models/__init__.py index 3ae1afe..6816475 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -4,7 +4,9 @@ from typing import TYPE_CHECKING, Any _EXPORTS = { "B4wExport": ".b4w", "Category": ".category", - "CategoryBudgetDb": ".category_budget", + "CategoryBudget": ".category", + "CategoryBudgetDb": ".category", + "CategoryDb": ".category", "ComdirectCreditCard": ".comdirect", "ComdirectDepot": ".comdirect", "ComdirectGiro": ".comdirect", @@ -33,7 +35,9 @@ def __dir__() -> list[str]: if TYPE_CHECKING: from src.models.b4w import B4wExport as B4wExport from src.models.category import Category as Category + from src.models.category import CategoryBudget as CategoryBudget from src.models.category import CategoryBudgetDb as CategoryBudgetDb + from src.models.category import CategoryDb as CategoryDb from src.models.comdirect import ComdirectCreditCard as ComdirectCreditCard from src.models.comdirect import ComdirectDepot as ComdirectDepot from src.models.comdirect import ComdirectGiro as ComdirectGiro diff --git a/src/models/category.py b/src/models/category.py index cdb17e7..12b457d 100644 --- a/src/models/category.py +++ b/src/models/category.py @@ -83,9 +83,13 @@ class CategoryBudget(BaseModel): category: Category amount_eur: Decimal = Field(gt=0, decimal_places=2) valid_from: date | None = Field( + default=None, description="Start of validity. None means unbounded" ) - valid_to: date | None = Field(description="End of validity. None means unbounded") + valid_to: date | None = Field( + default=None, + description="End of validity. None means unbounded" + ) @model_validator(mode="after") def validate_date_range(self) -> Self: diff --git a/tests/test_category_edge_cases.py b/tests/test_category_edge_cases.py new file mode 100644 index 0000000..ccd8c6f --- /dev/null +++ b/tests/test_category_edge_cases.py @@ -0,0 +1,151 @@ +import pytest +from datetime import date + +from src.models.category import Category, CategoryBudget + + +class TestCategoryComputedFields: + """Tests for Category's computed fields and special methods.""" + + def test_hash_id_computed_field_simple(self): + """Test hash_id for single-level category.""" + c = Category.from_str("single") + assert hasattr(c, "hash_id") + assert isinstance(c.hash_id, str) + # Hash should be deterministic for same inputs + c2 = Category.from_str("single") + assert c.hash_id == c2.hash_id + + def test_hash_id_computed_field_nested(self): + """Test hash_id for multi-level category hierarchy.""" + c1 = Category.from_str("level1:level2:level3") + assert hasattr(c1, "hash_id") + + c2 = Category.from_str("level1:level2:level3") + assert c1.hash_id == c2.hash_id + + def test_hash_id_different_hierarchy(self): + """Test that different hierarchies have different hash_ids.""" + c1 = Category.from_str("A:B") + c2 = Category.from_str("A:C") + # Different last level -> different hash_id + assert c1.hash_id != c2.hash_id + + def test_hash_id_same_name_different_parent(self): + """Test that same name but different parent gives different hash.""" + root = Category(name="Budget") + cat1 = Category(name="Budget", parent=root) + + # Create another with same name but no parent + orphan = Category.from_str("Budget") + + assert cat1.hash_id != orphan.hash_id + + def test_to_str_single_level(self): + """Test to_str() for single-level category.""" + c = Category.from_str("single") + assert c.to_str() == "single" + + def test_to_str_nested(self): + """Test to_str() for nested category.""" + c = Category.from_str("parent:child:grandchild") + assert c.to_str() == "parent:child:grandchild" + + def test_str_uses_to_str(self): + """Test that __str__ delegates to to_str().""" + c = Category.from_str("A:B:C") + assert str(c) == c.to_str() == "A:B:C" + + def test_repr_simple(self): + """Test repr() for single-level category.""" + c = Category.from_str("single") + expected = 'Category.from_str("single")' + assert repr(c) == expected + + def test_repr_nested(self): + """Test repr() for nested category.""" + c = Category.from_str("parent:child") + expected = 'Category.from_str("parent:child")' + assert repr(c) == expected + + +class TestCategoryBudgetValidation: + """Tests for CategoryBudget's date range validation.""" + + def test_valid_budget_no_dates(self): + """Budget with None dates is valid.""" + cat = Category.from_str("test") + budget = CategoryBudget( + category=cat, + amount_eur=100.00, + ) + assert budget.valid_from is None + assert budget.valid_to is None + + def test_valid_budget_with_dates(self): + """Budget with valid date range is accepted.""" + from datetime import date + cat = Category.from_str("test") + budget = CategoryBudget( + category=cat, + amount_eur=100.00, + valid_from=date(2026, 10, 1), + valid_to=date(2026, 10, 31), + ) + assert budget.valid_from == date(2026, 10, 1) + assert budget.valid_to == date(2026, 10, 31) + + def test_invalid_date_range_raises(self): + """Budget with valid_from > valid_to raises ValueError.""" + from datetime import date + cat = Category.from_str("test") + + with pytest.raises(ValueError) as exc_info: + CategoryBudget( + category=cat, + amount_eur=100.00, + valid_from=date(2026, 10, 31), + valid_to=date(2026, 10, 1), + ) + + assert "valid_from" in str(exc_info.value) + assert "valid_to" in str(exc_info.value) + assert "nor in order" in str(exc_info.value) + + def test_valid_budget_exact_boundary(self): + """Budget with exact same from and to is valid (single day).""" + from datetime import date + cat = Category.from_str("test") + budget = CategoryBudget( + category=cat, + amount_eur=100.00, + valid_from=date(2026, 10, 1), + valid_to=date(2026, 10, 1), + ) + assert budget.valid_from == date(2026, 10, 1) + assert budget.valid_to == date(2026, 10, 1) + + def test_validation_preserves_original_values(self): + """Validation doesn't change the model's original values.""" + from datetime import date + cat = Category.from_str("test") + budget = CategoryBudget( + category=cat, + amount_eur=150.75, + valid_from=date(2026, 9, 1), + valid_to=date(2026, 9, 30), + ) + + # Create an invalid one and verify validation catches it + try: + bad_budget = CategoryBudget( + category=cat, + amount_eur=200.00, + valid_from=date(2026, 12, 31), + valid_to=date(2026, 12, 30), + ) + except ValueError: + pass # Expected + + # Valid one should still have original values + assert budget.amount_eur == 150.75 diff --git a/tests/test_db_utils_update_path.py b/tests/test_db_utils_update_path.py new file mode 100644 index 0000000..8033734 --- /dev/null +++ b/tests/test_db_utils_update_path.py @@ -0,0 +1,201 @@ +"""Tests for db_utils.py update path in upsert function. + +The upsert function has two paths: +1. Row exists -> either return existing (do_update=False) or update it (do_update=True) +2. Row doesn't exist -> insert new row + +These tests cover the update path (lines 25-26). +""" + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from src.db_utils import upsert + + +class TestUpsertUpdatePath: + """Tests for the update path in db_utils.upsert.""" + + def test_upsert_updates_existing_row(self, db_session: Session): + """Test that upsert updates an existing row when do_update=True.""" + from sqlalchemy.orm import declarative_base + from sqlalchemy import Column, Integer, String + + Base = declarative_base() + + class TestModel(Base): + __tablename__ = "test_model" + id: int = Column(Integer, primary_key=True, autoincrement=True) + name: str = Column(String(100), nullable=False) + value: str = Column(String(50), nullable=False) + + Base.metadata.create_all(db_session.get_bind()) + + # Create and insert a row + initial = TestModel(name="initial", value="old_value") + db_session.add(initial) + db_session.commit() + + # Verify it was inserted + result = db_session.query(TestModel).filter_by(name="initial").one() # type: ignore + assert result.value == "old_value" + + def test_upsert_skips_update_when_do_update_false(self, db_session: Session): + """Test that upsert doesn't modify existing row when do_update=False.""" + from sqlalchemy.orm import declarative_base + from sqlalchemy import Column, Integer, String + + Base = declarative_base() + + class TestModel(Base): + __tablename__ = "test_model_2" + id: int = Column(Integer, primary_key=True, autoincrement=True) + name: str = Column(String(100), nullable=False) + value: str = Column(String(50), nullable=False) + + Base.metadata.create_all(db_session.get_bind()) + + # Create and insert a row + initial = TestModel(name="skip_update", value="original") + db_session.add(initial) + db_session.commit() + + # Upsert with do_update=False should not modify existing row + same_row = TestModel(name="skip_update", value="should_not_change") + + result = upsert(db_session, TestModel, {"name": "skip_update"}, same_row.__dict__, + do_update=False, do_commit=True) + + # The row should still have original values + assert result.value == "original" + + def test_upsert_updates_and_returns_existing(self, db_session: Session): + """Test that upsert returns the existing (now updated) row when do_update=True.""" + from sqlalchemy.orm import declarative_base + from sqlalchemy import Column, Integer, String + + Base = declarative_base() + + class TestModel(Base): + __tablename__ = "test_model_3" + id: int = Column(Integer, primary_key=True, autoincrement=True) + name: str = Column(String(100), nullable=False) + value: str = Column(String(50), nullable=False) + + Base.metadata.create_all(db_session.get_bind()) + + # Create and insert a row + initial = TestModel(name="update_test", value="old") + db_session.add(initial) + db_session.commit() + + # Upsert with do_update=True should update and return the same object + new_values = TestModel(name="update_test", value="new_value") + + result = upsert(db_session, TestModel, {"name": "update_test"}, new_values.__dict__, + do_update=True, do_commit=True) + + # Result should be the same row (not a new one) + assert result is not None + # Should have updated values + assert result.value == "new_value" + + def test_upsert_returns_updated_row_by_value_when_not_same_object(self, db_session: Session): + """Test update path when the returned row is same by identity (session cache).""" + from sqlalchemy.orm import declarative_base + from sqlalchemy import Column, Integer, String + + Base = declarative_base() + + class TestModel(Base): + __tablename__ = "test_model_4" + id: int = Column(Integer, primary_key=True, autoincrement=True) + name: str = Column(String(100), nullable=False) + value: str = Column(String(50), nullable=False) + + Base.metadata.create_all(db_session.get_bind()) + + # Create and insert a row + initial = TestModel(name="identity_test", value="initial") + db_session.add(initial) + db_session.commit() + + # Get the row from DB again (fresh load) + before_update = db_session.query(TestModel).filter_by(name="identity_test").one() # type: ignore + + # Now upsert should return same object due to session cache + identity map + new_values = TestModel(name="identity_test", value="updated") + + result = upsert(db_session, TestModel, {"name": "identity_test"}, new_values.__dict__, + do_update=True, do_commit=False) # don't commit yet + + # Should be same identity due to session cache + assert result is before_update + assert result.value == "updated" + + +class TestUpsertUpdateEdgeCases: + """Test edge cases for the update path.""" + + def test_upsert_partial_update(self, db_session: Session): + """Test that only specified fields are updated.""" + from sqlalchemy.orm import declarative_base + from sqlalchemy import Column, Integer, String + + Base = declarative_base() + + class TestModel(Base): + __tablename__ = "test_model_5" + id: int = Column(Integer, primary_key=True, autoincrement=True) + name: str = Column(String(100), nullable=False) + value: str = Column(String(50), nullable=False) + extra: str = Column(String(50), nullable=False) + + Base.metadata.create_all(db_session.get_bind()) + + # Create and insert a row with all fields + initial = TestModel(name="partial", value="original_value", extra="extra_original") + db_session.add(initial) + db_session.commit() + + # Create new values with only some fields set (note: we skip 'extra' field) + partial_update = TestModel(name="partial", value="new_value", extra=None) + + result = upsert(db_session, TestModel, {"name": "partial"}, partial_update.__dict__, + do_update=True, do_commit=True) + + # value should be updated since it was in the dict + assert result.value == "new_value" + # extra field handling depends on what's in the __dict__ + + +class TestUpsertUpdateEmptyDict: + """Test edge case of empty update dictionary.""" + + def test_upsert_with_empty_update_dict(self, db_session: Session): + """Test upsert with empty parameters_new dict - should still work if existing found.""" + from sqlalchemy.orm import declarative_base + from sqlalchemy import Column, Integer, String + + Base = declarative_base() + + class TestModel(Base): + __tablename__ = "test_model_6" + id: int = Column(Integer, primary_key=True, autoincrement=True) + name: str = Column(String(100), nullable=False) + value: str = Column(String(50), nullable=False) + + Base.metadata.create_all(db_session.get_bind()) + + # Create and insert a row + initial = TestModel(name="empty_dict", value="test") + db_session.add(initial) + db_session.commit() + + # Try to upsert with empty new parameters (edge case) + result = upsert(db_session, TestModel, {"name": "empty_dict"}, {}, + do_update=True, do_commit=False) + + # Should return the existing row unchanged + assert result is not None + assert result.name == "empty_dict" diff --git a/tests/test_model_category.py b/tests/test_model_category.py index 587bd65..992abe0 100644 --- a/tests/test_model_category.py +++ b/tests/test_model_category.py @@ -43,3 +43,51 @@ def test_upsert_category_skips_existing(db_session: Session): select(CategoryDb).filter_by(name="try_again_and_again") ).all() assert len(result) == 1 + + +def test_category_hash_id_computed_field(): + """Test hash_id computed field is present and deterministic.""" + c1 = Category.from_str("single") + c2 = Category.from_str("single") + assert hasattr(c1, "hash_id") + assert c1.hash_id == c2.hash_id + + +def test_category_hash_id_different_inputs(): + """Test that different category hierarchies have different hash_ids.""" + c1 = Category.from_str("A:B") + c2 = Category.from_str("A:C") + # Different last level -> different hash_id + assert c1.hash_id != c2.hash_id + + +def test_category_to_str_single_level(): + """Test to_str() for single-level category.""" + c = Category.from_str("single") + assert c.to_str() == "single" + + +def test_category_to_str_nested(): + """Test to_str() for nested category.""" + c = Category.from_str("parent:child:grandchild") + assert c.to_str() == "parent:child:grandchild" + + +def test_category_str_delegates_to_to_str(): + """Test that __str__ delegates to to_str().""" + c = Category.from_str("A:B:C") + assert str(c) == c.to_str() == "A:B:C" + + +def test_category_repr_simple(): + """Test repr() for single-level category.""" + c = Category.from_str("single") + expected = 'Category.from_str("single")' + assert repr(c) == expected + + +def test_category_repr_nested(): + """Test repr() for nested category.""" + c = Category.from_str("parent:child") + expected = 'Category.from_str("parent:child")' + assert repr(c) == expected diff --git a/tests/test_models_common.py b/tests/test_models_common.py new file mode 100644 index 0000000..91de7a9 --- /dev/null +++ b/tests/test_models_common.py @@ -0,0 +1,143 @@ +"""Tests for common model utilities.""" + +from datetime import date + +import pytest + +from src.domain_utils import set_category_budget, find_category_db +from src.models.common import Sign, date2str, ExportBaseModel +from src.models.category import Category, CategoryBudget, CategoryBudgetDb +from src.models.b4w import B4wExport + + +class TestSignEnum: + """Test Sign enum from common module.""" + + def test_sign_str(self): + """Test Sign.__str__ method.""" + assert str(Sign.DEBIT) == "DBIT" + assert str(Sign.CREDIT) == "CRDT" + + def test_sign_repr(self): + """Test Sign.__repr__ method (line 24).""" + repr_debit = repr(Sign.DEBIT) + assert repr_debit == "Sign.DEBIT" + + repr_credit = repr(Sign.CREDIT) + assert repr_credit == "Sign.CREDIT" + + def test_sign_to_sign(self): + """Test Sign.to_sign method.""" + assert Sign.DEBIT.to_sign() == -1 + assert Sign.CREDIT.to_sign() == 1 + + +class TestDate2Str: + """Test date2str utility function from common module.""" + + def test_with_date(self): + """date2str with a valid date.""" + test_date = date(2026, 10, 15) + result = date2str(test_date) + assert result == "2026-10-15" + + def test_with_none_default(self): + """date2str with None value (line 40).""" + result = date2str(None) + assert result == "--" + + def test_with_none_custom_default(self): + """date2str with None and custom default.""" + result = date2str(None, default="N/A") + assert result == "N/A" + + def test_with_date_custom_format(self): + """date2str with custom format.""" + test_date = date(2026, 10, 15) + result = date2str(test_date, date_format="%d.%m.%Y") + assert result == "15.10.2026" + + def test_with_none_custom_default_and_format(self): + """date2str with None, custom default and format.""" + result = date2str(None, default="--", date_format="%d.%m.%Y") + assert result == "--" + + +class TestExportBaseModel: + """Test ExportBaseModel methods from common module.""" + + def test_parameters_to_dict_empty(self): + """parameters_to_dict returns empty dict (line 70).""" + class TestModel(ExportBaseModel): + pass + + model = TestModel() + result = model.parameters_to_dict() + assert result == {} + + def test_parameters_to_str_no_params(self): + """parameters_to_str with no parameters.""" + class TestModel(ExportBaseModel): + pass + + model = TestModel() + result = model.parameters_to_str() + assert result == "" + + def test_repr_empty_params(self): + """__repr__ with empty parameters (line 79).""" + class TestModel(ExportBaseModel): + pass + + model = TestModel() + + # __repr__ should return string representation with empty params + repr_result = repr(model) + assert "TestModel" in repr_result + + def test_str_empty_params(self): + """__str__ with empty parameters.""" + class TestModel(ExportBaseModel): + pass + + model = TestModel() + + # __str__ should return string representation + str_result = str(model) + assert "TestModel" in str_result + + def test_repr_with_params(self): + """Test repr with actual parameters.""" + class ConfigurableModel(ExportBaseModel): + def parameters_to_dict(self) -> dict[str, str]: + return {"key": "value"} + + model = ConfigurableModel() + + # Test that repr works correctly with params + repr_result = repr(model) + assert "key" in repr_result or "value" in repr_result + + def test_str_with_params(self): + """Test str with actual parameters.""" + class ConfigurableModel(ExportBaseModel): + def parameters_to_dict(self) -> dict[str, str]: + return {"name": "test"} + + model = ConfigurableModel() + + # Test that str works correctly with params + str_result = str(model) + assert "name" in str_result or "test" in str_result + + +class TestMyNameMethod: + """Test ExportBaseModel._my_name classmethod.""" + + def test_my_name_returns_classname(self): + """_my_name should return the class name.""" + class SubModel(ExportBaseModel): + pass + + result = SubModel._my_name() + assert result == "SubModel" diff --git a/tests/test_models_init.py b/tests/test_models_init.py new file mode 100644 index 0000000..0733071 --- /dev/null +++ b/tests/test_models_init.py @@ -0,0 +1,101 @@ +"""Tests for src.models.__init__.py lazy loading and __dir__.""" + +import pytest +from src import models + + +class TestLazyLoading: + """Tests for lazy attribute loading via __getattr__.""" + + def test_load_b4wexport(self): + """Test loading B4wExport model.""" + from src.models.b4w import B4wExport as DirectB4wExport + b4w = models.B4wExport + assert b4w is DirectB4wExport + + def test_load_category(self): + """Test loading Category model.""" + from src.models.category import Category as DirectCategory + cat = models.Category + assert cat is DirectCategory + + def test_load_categorybudgetdb(self): + """Test loading CategoryBudgetDb model.""" + from src.models.category import CategoryBudgetDb as DirectCategoryBudgetDb + cbdb = models.CategoryBudgetDb + assert cbdb is DirectCategoryBudgetDb + + def test_load_comdirectgiro(self): + """Test loading ComdirectGiro model.""" + from src.models.comdirect import ComdirectGiro as DirectComdirectGiro + giro = models.ComdirectGiro + assert giro is DirectComdirectGiro + + def test_load_transaction(self): + """Test loading Transaction model.""" + from src.models.transactions import Transaction as DirectTransaction + tx = models.Transaction + assert tx is DirectTransaction + + def test_load_account(self): + """Test loading Account model.""" + from src.models.settings import Account as DirectAccount + acc = models.Account + assert acc is DirectAccount + + def test_load_source(self): + """Test loading Source model.""" + from src.models.settings import Source as DirectSource + src = models.Source + assert src is DirectSource + + def test_nonexistent_attribute_raises(self): + """Test that non-existent attributes raise AttributeError.""" + with pytest.raises(AttributeError) as exc_info: + models.NonExistentModel + + expected_msg = "module 'src.models' has no attribute 'NonExistentModel'" + assert expected_msg in str(exc_info.value) + + +class TestDirMethod: + """Tests for __dir__() method.""" + + def test_dir_returns_sorted_list(self): + """Test that __dir__() returns a sorted list.""" + result = models.__dir__() + assert isinstance(result, list) + # Should be alphabetically sorted + assert result == sorted(result) + + def test_dir_includes_all_exports(self): + """Test that __dir__() includes all exported models.""" + result = models.__dir__() + + expected_exports = { + "B4wExport", + "Category", + "CategoryBudgetDb", + "ComdirectCreditCard", + "ComdirectDepot", + "ComdirectGiro", + "Account", + "Source", + "Transaction", + "TransactionDb", + } + + # All exports should be in dir() + for export_name in expected_exports: + assert export_name in result + + def test_dir_includes_globals(self): + """Test that __dir__() includes module-level globals.""" + result = models.__dir__() + assert "_EXPORTS" in result + + def test_dir_is_alphabetically_sorted(self): + """Verify the list is truly sorted alphabetically.""" + result = models.__dir__() + # Double-check: if we sort it again, it should be identical + assert result == sorted(result) diff --git a/uv.lock b/uv.lock index 8c6ffec..67c5412 100644 --- a/uv.lock +++ b/uv.lock @@ -142,6 +142,75 @@ 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 = "coverage" +version = "7.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/55/d1eaf3e73781174340a00dc1ba2aee8a65f82fadb18e2797b192b6b3925b/coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa", size = 971999, upload-time = "2026-09-27T12:29:01.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/4c/577fc0803dab4155dcf808faffbdd7b159256781c0874a8586e17b81b149/coverage-7.16.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4ee546b9e4872ffa194bf07ac87bfa1202ebb824d0795dc1ef22f175545ca90a", size = 224138, upload-time = "2026-09-27T12:27:05.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/9e/e3785ba3ecba2bd11efc74bfe2801ca4b78c4480b15a375648d809a59da3/coverage-7.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a2fac6895eb299a2e52d7bbb8fb3903502b9da8d3f5309ceb16ec40c646b58ee", size = 224445, upload-time = "2026-09-27T12:27:06.805Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/963ff22d3fd27117da3b8cc442f5bdc91196f783321e1a8ff0ec43476772/coverage-7.16.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:57ff3783f99d75a1e81dd56a9737eb5665e6736a5d93258ba596b6dcad8fd05b", size = 256112, upload-time = "2026-09-27T12:27:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d4/a306940c81c6ae759e82fff27d20b7fdc6896e422b821f51313cce212b6c/coverage-7.16.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f37886699cb9abd29958247d718628d5bc6f39e623dff66a09e546c42a7e03", size = 258727, upload-time = "2026-09-27T12:27:09.927Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/d3d99d93b02517087aa05bc0cf2d04d372956b849e5443e059079901429b/coverage-7.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd7a86fdda7cb6d616d178654bd0ad6bc0f3f33c2e478aa598500a1a9e34eda", size = 259909, upload-time = "2026-09-27T12:27:11.55Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/39dd599181726758dd185ae4dc0c0ab3aeabf7ca70e68e145060feeaaa16/coverage-7.16.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac0f3b379c94acc2f7dce5f5f0b24d44fa1cc6a509717ef83dfee07450c2117c", size = 262481, upload-time = "2026-09-27T12:27:13.17Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/91ee43f6ded411460c359d7e1aebde4d6fd8f00a2e5394182d9d212eb23c/coverage-7.16.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d0732c83746bc24123c581a85d9dd96b70ddb538c9076020aa1a041790361e9", size = 256012, upload-time = "2026-09-27T12:27:14.91Z" }, + { url = "https://files.pythonhosted.org/packages/11/8c/e9499ddc33197bd7eabcb1118ca81756fc874457b324e2b479a4804b2ad2/coverage-7.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b451c68218c150f616bc9649783ec8de76a59792c759b43aa0c9c0466a465e4", size = 257898, upload-time = "2026-09-27T12:27:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6e/c081cb5991a0afba99f9c4ad6c74a5fce9513a38ddc64e3e6680c6fed9af/coverage-7.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a56ac4fa5a75c7e182e8f62600cfb4aff43c5ed7356a034f3557659c3bec1d90", size = 255938, upload-time = "2026-09-27T12:27:18.19Z" }, + { url = "https://files.pythonhosted.org/packages/b2/42/1c3d819e8f9b6eb01c2fe90874d67a8882adb9507e0bbb09361ed131ea89/coverage-7.16.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc4f73aa3fabc36e32046d6cd2971405948d8a903636508a3d3b2f9128b3a95", size = 260368, upload-time = "2026-09-27T12:27:19.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/4f/d70eac07901fd587b6ab05e659b52afe13959992aa5113bf6cce059cc572/coverage-7.16.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:723dcdab91357159b722935b500ee8abc0a66c8c432e1e9fabf4cc7598952de8", size = 255620, upload-time = "2026-09-27T12:27:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/6d87af88317d3d9a9b18a9ca1bc1673eb516917f296e579d0d4a55cb3490/coverage-7.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5397e21a90dde0e9c6896b77ded8f0be26b66f8b22b33aed41f6043ed95d55e6", size = 257521, upload-time = "2026-09-27T12:27:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/79/bb/90c2641170d2fa1a6757b3f8450ba2740197317b0ddd749e9604b914e886/coverage-7.16.2-cp314-cp314-win32.whl", hash = "sha256:848893e1d361448c113dc2f0913503522a6f7be231d0e38333d2a22d9698a011", size = 226248, upload-time = "2026-09-27T12:27:25.153Z" }, + { url = "https://files.pythonhosted.org/packages/30/08/d8d0478bb02c8eb0ae20a496fc80c40fcf4d3450bd184300d682ba2d28a6/coverage-7.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a27b731c171e43dc8b5f32b76a5051dde2ec9b9366c87028f08a7088ebc2c7b", size = 226732, upload-time = "2026-09-27T12:27:26.907Z" }, + { url = "https://files.pythonhosted.org/packages/32/3f/0001da22155b0a8ce063ec0f7e64ecbe17b373f306e7a74435f6d6accb72/coverage-7.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c569a9fd25505f1cd6bea90588818f90373ce90e2632e2cacf19ddbd6e14fdb", size = 226645, upload-time = "2026-09-27T12:27:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/d7/85/6d8813aff9b8b8586691a9d33c43c5604f7227622574da7cdc3d91a86861/coverage-7.16.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d93db87adb6b1c1b408dce4763314b55d76a9f589e96783a84ac9e7689e48bdf", size = 224871, upload-time = "2026-09-27T12:27:30.32Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/444f3a4981ac2cda40fdcf4cc9b56a4e1a33c222abeb33e51ed3e3eb2a6b/coverage-7.16.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aa62c85046473959c13ba9edca9dc90a77d5c1095b1ba313556314d77fe5b036", size = 225123, upload-time = "2026-09-27T12:27:32.33Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c1/980681cd7b33eb66ac835044116ef0a92e11fcc7bdd866cc89d10b1130b9/coverage-7.16.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db76506aa5416081f3e8974ae0f7965c58ada0bb0ef7339ac86099588dbb20d3", size = 265049, upload-time = "2026-09-27T12:27:34.085Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/87679875c33bb2191f0f05544a1cc9adcc940fe0c35443a10f2df753dde5/coverage-7.16.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a0f2285329dac10ab08f79cb11f5692c497018e6c7c511f95e6fd63a70b8f831", size = 267717, upload-time = "2026-09-27T12:27:36.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/64/5d372776d6eb523d4e93bafba2253f96984e3b18261c4cc56a50863c6d0d/coverage-7.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:382d3346d56b0eec1b793d53a4c88799c8053f516aa3a8d7c44315696954bacf", size = 270047, upload-time = "2026-09-27T12:27:37.96Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/44082ff0cbf9f97d0043f57970a71204097ec7ba606361a9fd2065393669/coverage-7.16.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:648352b94507179d82637292e7ae8802508d95f78e2f00a705a50b6c48011681", size = 271360, upload-time = "2026-09-27T12:27:39.766Z" }, + { url = "https://files.pythonhosted.org/packages/b8/17/9a215efe25b5e0ecc87c89dbe525c4a87d14d87c8c0c7316ef140a5f6f3e/coverage-7.16.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb2bde05838fffae1a1bf75e5d411a6cac3e4e9bb97e6640fed8cd47888b33f0", size = 264736, upload-time = "2026-09-27T12:27:42.072Z" }, + { url = "https://files.pythonhosted.org/packages/a2/da/7f0a31af8e448107d4d32844bd684757f51ea907bc0c68c8fd537b2123ff/coverage-7.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6a75180829efb8ae62b4aded25be6ddca1c888d138d2d82e21d93bfbd88f41cb", size = 267600, upload-time = "2026-09-27T12:27:43.85Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/3bfecbd3366b775bacdcb3330394d356cf384b5d8f5b2146ac4b14b252b5/coverage-7.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99704f73721e23859112072d522076e11c31744fc96b5652e5dd2018aa4359f7", size = 264767, upload-time = "2026-09-27T12:27:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3f/5d62163732d87e4a0c4710a0eab30f0fd6a2d480112abe2029f014fe8c9d/coverage-7.16.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:29309ccc86b7f33df7db12813c299f215bbbc470ed6292d0bedd63ffae1ebf64", size = 269026, upload-time = "2026-09-27T12:27:47.787Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/8e4579f225426535085a9be371cc75e3b026d058d679b80affbdfb4c3ef0/coverage-7.16.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:30c1b65d529e46569899fadca59e4a87c1faf2886923f1307ba61e654d4f3c20", size = 264147, upload-time = "2026-09-27T12:27:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/d1/36/ef1f77e2c3f7bb03c2b13b9a2006f88700fdd75535ef158d70049f425c1c/coverage-7.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcf4bc2aab4e16b1c4c0c2005918f23a7dd5d7821ddae82caed9e3342dc2fcce", size = 266376, upload-time = "2026-09-27T12:27:51.551Z" }, + { url = "https://files.pythonhosted.org/packages/be/79/0cb2bf4428830dec971c718c2c841a039c084415c99e67281f5a72841aab/coverage-7.16.2-cp314-cp314t-win32.whl", hash = "sha256:a9cd3de0a5bfe7b0e21ee10e1a14e3d61bf52efc88217ab1d95d6ace6970bd46", size = 226585, upload-time = "2026-09-27T12:27:53.945Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f9/da17121c16667fd84998e972200ae226a41540f6ea4795776c6d99e8976f/coverage-7.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:611a44e5229a59d7483ce830160e1a0e85f700562c7a5651c7c63fb8f4eb528c", size = 227378, upload-time = "2026-09-27T12:27:55.778Z" }, + { url = "https://files.pythonhosted.org/packages/74/89/01179c62d1b7e6e33bd5001566b02d7f778cf33d3ec1e81e94ca170c517f/coverage-7.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:22957cef43ce038641de78ba995de7568d2d6a37c6ddbf7fa0fd7d1ae2344d91", size = 227064, upload-time = "2026-09-27T12:27:57.496Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/52935003c3f627ba6e5203d7179aad32448c10899663a30336aba8e81a2c/coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8", size = 224140, upload-time = "2026-09-27T12:27:59.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/38/df472520f3e626524d7e2fc9d6da0afe7895a2f1489d36b48af8ca40bb41/coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24", size = 224449, upload-time = "2026-09-27T12:28:01.299Z" }, + { url = "https://files.pythonhosted.org/packages/0c/aa/3be084d5b82e63ccdad4ed751e4acbae294673573e30481d29f8b7402eec/coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09", size = 256060, upload-time = "2026-09-27T12:28:03.185Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/48fca82a7ebf7ff7b2e35019cc9537e7f65e4d2aa1215cc5a8792c989251/coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843", size = 259106, upload-time = "2026-09-27T12:28:05.15Z" }, + { url = "https://files.pythonhosted.org/packages/06/3d/b2d5986f2dd53fe201aa1be2e4ab204fa1aed5101e67c0dbbb419b850aee/coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1", size = 260674, upload-time = "2026-09-27T12:28:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7e/b50160be3506ead12e6480d14279af7f0f17627694300a2d1fd2c42d2ff5/coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982", size = 263139, upload-time = "2026-09-27T12:28:08.771Z" }, + { url = "https://files.pythonhosted.org/packages/14/5e/7c805ac9a32606de1399bd7e9bd375aa2f973dc61b12680d9e6403c2e891/coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34", size = 256591, upload-time = "2026-09-27T12:28:10.842Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9e/76f1ed129a2daf658a3ea17122824cf2e3b91fea0460d8d3664fc5a61018/coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6", size = 258708, upload-time = "2026-09-27T12:28:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b7/8d62e75f48b527619239a65294f842d4b7fd02a0839d43ae1de80184e2df/coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4", size = 256562, upload-time = "2026-09-27T12:28:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/0a15f95c3afb78e947c52644786ba4bc9de259905687dd720d5e6fae2e76/coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0", size = 261077, upload-time = "2026-09-27T12:28:17.215Z" }, + { url = "https://files.pythonhosted.org/packages/25/00/88389987305a47d732866c07c8a500000ab574df9505e3114ac69c8d027f/coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a", size = 256034, upload-time = "2026-09-27T12:28:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/34d079d4952ad461bde037d353f9a6e037a7edc45fe0f9ee8781ff73f028/coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63", size = 258034, upload-time = "2026-09-27T12:28:21.242Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d8/3e59a62879285b464ec1b10fd824fbc1af9ce66e842cd39974f80a0becc4/coverage-7.16.2-cp315-cp315-win32.whl", hash = "sha256:893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e", size = 226252, upload-time = "2026-09-27T12:28:23.102Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e1/128026e1b2836e9ad6b219207ba9edf1c5e0088a7869e23088aee7fbbe7a/coverage-7.16.2-cp315-cp315-win_amd64.whl", hash = "sha256:01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae", size = 226728, upload-time = "2026-09-27T12:28:25.21Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/c9fa8e7cf525ca7748ac52b0ee89331d13fe09808e45c679830708782e90/coverage-7.16.2-cp315-cp315-win_arm64.whl", hash = "sha256:967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495", size = 226648, upload-time = "2026-09-27T12:28:27.136Z" }, + { url = "https://files.pythonhosted.org/packages/a2/13/e96b045447a856666f36f9c653e2a80bdaa732aaaf72412b19aa2c26a473/coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75", size = 224858, upload-time = "2026-09-27T12:28:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/90/087f6ad1bd3df059632ca3407a4e6552ed1053ee35354de0a771acf35423/coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55", size = 225139, upload-time = "2026-09-27T12:28:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8e/285dcef0184358044e7cbcd810a1bdc9566bc620f54702d605477155df4a/coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2", size = 265090, upload-time = "2026-09-27T12:28:33.04Z" }, + { url = "https://files.pythonhosted.org/packages/06/b2/cc83f3a6e5789a4e89059c69555bc641c2efcde568405a1c06fc702951ab/coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2", size = 268254, upload-time = "2026-09-27T12:28:35.135Z" }, + { url = "https://files.pythonhosted.org/packages/ac/41/f548c19530f5d66ac6e3c92bbcbc49da7261de3a458b9f3e54a3efb1a0b2/coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1", size = 270695, upload-time = "2026-09-27T12:28:36.959Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/4dc27cf82ef96434d2874110ad0cc10ea4621025705dc5049862bd3bd181/coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f", size = 271853, upload-time = "2026-09-27T12:28:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/38/29/bf8072b1b8bd5f2de8b21460a404460b1a2b97e80a9464c78ec0271f6199/coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d", size = 265644, upload-time = "2026-09-27T12:28:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/0aecb8721be5cdeb8afd9d6d9f6b463f074e4d8d37f00f4c42442522709f/coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0", size = 268530, upload-time = "2026-09-27T12:28:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0b/92b4b7628268ee711249958e68fc0328779bd3d9a7ab4715379465aedb84/coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283", size = 265154, upload-time = "2026-09-27T12:28:44.684Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/41c95c1ab29b3dcd357cd1227181d1c98185632aca41ce670ce671b23a43/coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec", size = 269799, upload-time = "2026-09-27T12:28:46.672Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ebeb259aa5362b033a137b86d7274ff4b109d59be8cc9913889b783bf75a/coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5", size = 265196, upload-time = "2026-09-27T12:28:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/b2/18/8437620f90d023680a072eee02f968055f3658bbfb7d386d0ea34cfb7f30/coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c", size = 267266, upload-time = "2026-09-27T12:28:51.361Z" }, + { url = "https://files.pythonhosted.org/packages/28/6c/f08e8ee4293e6434035424180bef4d45e028e8ecc006c61bf9453e74405e/coverage-7.16.2-cp315-cp315t-win32.whl", hash = "sha256:251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35", size = 226583, upload-time = "2026-09-27T12:28:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fd/3f939c2847f4a72c20cff8b1ac33da78ea91a2d38d9b43336e60db719103/coverage-7.16.2-cp315-cp315t-win_amd64.whl", hash = "sha256:2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5", size = 227374, upload-time = "2026-09-27T12:28:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/5a/35/b98cdc354c952402132e675a87f2cc3227fb68f959c84aaa491fbe15933d/coverage-7.16.2-cp315-cp315t-win_arm64.whl", hash = "sha256:b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8", size = 227064, upload-time = "2026-09-27T12:28:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/7a64e1ac90541a8edf50daef0914848011fb057a5bf55284a4811e21939a/coverage-7.16.2-py3-none-any.whl", hash = "sha256:11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f", size = 215754, upload-time = "2026-09-27T12:28:59.075Z" }, +] + [[package]] name = "debugpy" version = "1.8.21" @@ -189,6 +258,7 @@ dependencies = [ { name = "mypy" }, { name = "pydantic" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "python-dotenv" }, { name = "sqlalchemy" }, ] @@ -210,6 +280,7 @@ requires-dist = [ { name = "mypy", specifier = ">=2.4.0" }, { name = "pydantic", specifier = ">=2.13.5" }, { name = "pytest", specifier = ">=9.1.1" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "python-dotenv", specifier = ">=1.2.3" }, { name = "sqlalchemy", specifier = ">=2.0.52" }, ] @@ -564,6 +635,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.3"