Category overhaul #20

Merged
mathias2784 merged 18 commits from category-overhaul into master 2026-10-27 18:08:10 +00:00
10 changed files with 740 additions and 2 deletions
Showing only changes of commit 2c7f8f4a67 - Show all commits
BIN
View File
Binary file not shown.
+1
View File
@@ -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",
]
+5 -1
View File
@@ -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
+5 -1
View File
@@ -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:
+151
View File
@@ -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
+201
View File
@@ -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"
+48
View File
@@ -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
+143
View File
@@ -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"
+101
View File
@@ -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)
Generated
+85
View File
@@ -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"