Compare commits

3 Commits
Author SHA1 Message Date
mathias2784 d2f47a7787 mypy in pipeline
Tests / lint (push) Successful in 11s
Tests / test (push) Successful in 9s
2026-10-10 17:48:20 +01:00
mathias2784 7038d8546b mypy init 2026-10-10 17:46:16 +01:00
mathias2784 3fab51fe70 central transactions table 2026-10-10 17:25:00 +01:00
17 changed files with 729 additions and 67 deletions
+2
View File
@@ -38,3 +38,5 @@ jobs:
run: uv run black --check . --extend-exclude .venv run: uv run black --check . --extend-exclude .venv
- name: Lint (ruff) - name: Lint (ruff)
run: uv run ruff check . run: uv run ruff check .
- name: Typing (mypy)
run: uv run mypy .
+1
View File
@@ -69,6 +69,7 @@ Two sources with shared patterns:
- **b4w**: `B4wExport`, `Category`, transactional import to `import_b4w` table + `category` table - **b4w**: `B4wExport`, `Category`, transactional import to `import_b4w` table + `category` table
- **comdirect**: `ComdirectGiro`, `ComdirectCreditCard`, `ComdirectDepot`, import to `comdirect_giro` table - **comdirect**: `ComdirectGiro`, `ComdirectCreditCard`, `ComdirectDepot`, import to `comdirect_giro` table
- **unified**: `Transaction`, `TransactionDb` – both ingest pipelines project their staging rows (`import_b4w`, `comdirect_giro`) into one `transactions` table via `src/ingest/transactions.py` (`map_*` mappers + `consolidate_transactions`). Dedup uses a business-key `hash_id` (source + IBAN + value date + amount + text + counterparty) with a UNIQUE constraint and SQLite `INSERT OR IGNORE`, so re-imports are idempotent. Amounts are canonical signed (negative = money out). Depot/credit-card/tagesgeld are not projected yet.
All models inherit from common base in `src/models/_common.py`: All models inherit from common base in `src/models/_common.py`:
- `_hash()`: SHA256-based deduplication key - `_hash()`: SHA256-based deduplication key
+2 -3
View File
@@ -65,9 +65,8 @@ def run_migrations_online() -> None:
and associate a connection with the context. and associate a connection with the context.
""" """
if get_set_engine() is not None: connectable = get_set_engine()
connectable = get_set_engine() if connectable is None:
else:
connectable = engine_from_config( connectable = engine_from_config(
config.get_section(config.config_ini_section, {}), config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.", prefix="sqlalchemy.",
@@ -0,0 +1,60 @@
"""create transactions table
Revision ID: 78ec55b9e1d1
Revises: 8822a28b01ca
Create Date: 2026-10-10 16:32:10.691919
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "78ec55b9e1d1"
down_revision: str | Sequence[str] | None = "8822a28b01ca"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
op.create_table(
"transactions",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("hash_id", sa.String(), nullable=False),
sa.Column("source", sa.String(), nullable=False),
sa.Column("source_row_id", sa.Integer(), nullable=False),
sa.Column("account_iban", sa.String(), nullable=False),
sa.Column("account_currency", sa.String(length=3), nullable=False),
sa.Column("date_valid", sa.Date(), nullable=False),
sa.Column("date_book", sa.Date(), nullable=True),
sa.Column("amount", sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column("amount_currency", sa.String(length=3), nullable=False),
sa.Column("text", sa.String(), nullable=True),
sa.Column("other_name", sa.String(), nullable=True),
sa.Column("other_iban", sa.String(), nullable=True),
sa.Column("category_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["category_id"],
["category.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("hash_id", name="uq_transactions_hash"),
)
op.create_index(
"ix_transactions_source_row_id",
"transactions",
["source", "source_row_id"],
unique=False,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
op.drop_index("ix_transactions_source_row_id", table_name="transactions")
op.drop_table("transactions")
# ### end Alembic commands ###
+8
View File
@@ -11,6 +11,7 @@ dependencies = [
"alembic>=1.19.1", "alembic>=1.19.1",
"argparse>=1.4.0", "argparse>=1.4.0",
"dotenv>=0.9.9", "dotenv>=0.9.9",
"mypy>=2.4.0",
"pydantic>=2.13.5", "pydantic>=2.13.5",
"pytest>=9.1.1", "pytest>=9.1.1",
"python-dotenv>=1.2.3", "python-dotenv>=1.2.3",
@@ -28,9 +29,16 @@ module-root = ""
requires = ["uv_build>=0.12.9,<0.13.0"] requires = ["uv_build>=0.12.9,<0.13.0"]
build-backend = "uv_build" build-backend = "uv_build"
[tool.mypy]
explicit_package_bases = true
mypy_path = "."
[tool.ruff] [tool.ruff]
target-version = "py314" target-version = "py314"
[tool.black]
target-version = [ "py314" ]
[tool.ruff.lint] [tool.ruff.lint]
ignore = ["EXE"] ignore = ["EXE"]
+8
View File
@@ -3,6 +3,7 @@ from pathlib import Path
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.ingest._common import _reader_raw, _upsert from src.ingest._common import _reader_raw, _upsert
from src.ingest.transactions import consolidate_transactions, map_b4w_to_transaction
from src.models.b4w import B4wDb, B4wExport, Category, CategoryDb from src.models.b4w import B4wDb, B4wExport, Category, CategoryDb
@@ -67,4 +68,11 @@ def ingest_b4w(path: Path, session: Session):
data = reader_b4w(path) data = reader_b4w(path)
for row in data: for row in data:
upsert_b4w(session, row, do_commit=False) upsert_b4w(session, row, do_commit=False)
consolidate_transactions(
session,
source="b4w",
StagingDb=B4wDb,
mapper=map_b4w_to_transaction,
do_commit=False,
)
session.commit() session.commit()
+38 -23
View File
@@ -1,11 +1,16 @@
import re import re
from enum import Enum, auto from enum import Enum, auto
from pathlib import Path from pathlib import Path
from typing import cast
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from settings import BANK_ACCOUNTS from settings import BANK_ACCOUNTS
from src.ingest._common import _upsert from src.ingest._common import _upsert
from src.ingest.transactions import (
consolidate_transactions,
map_comdirect_giro_to_transaction,
)
from src.models._common import Csv2Dict, CsV2DictMulti from src.models._common import Csv2Dict, CsV2DictMulti
from src.models.comdirect import ( from src.models.comdirect import (
ComdirectCreditCard, ComdirectCreditCard,
@@ -40,18 +45,18 @@ COMDIRECT_ACCOUNT_PARSERS = {a.name: a.model.model_validate for a in ACCOUNTS}
def reader_comdirect(path: Path) -> dict[str, list[ComdirectExportModel]]: def reader_comdirect(path: Path) -> dict[str, list[ComdirectExportModel]]:
result = {} result: dict[str, list[ComdirectExportModel]] = {}
raw = _reader_raw_comdirect(path) raw = _reader_raw_comdirect(path)
for account in ACCOUNTS: for account in ACCOUNTS:
assert account.name in raw assert account.name in raw
data_raw: Csv2Dict = raw.pop(account.name) data_raw: Csv2Dict = raw.pop(account.name)
data: list = list(map(account.model.model_validate, data_raw)) data = [account.model.model_validate(row) for row in data_raw]
result[account.name] = data result[account.name] = cast(list[ComdirectExportModel], data)
assert not raw assert not raw
return result return result
def _open_and_readlines(path: Path, encoding: str) -> list: def _open_and_readlines(path: Path, encoding: str) -> list[str]:
with path.open("r", encoding=encoding) as fid: with path.open("r", encoding=encoding) as fid:
lines = fid.readlines() lines = fid.readlines()
return lines return lines
@@ -65,14 +70,15 @@ def _reader_raw_comdirect(path: Path) -> CsV2DictMulti:
lines = _open_and_readlines(path, encoding="latin-1") lines = _open_and_readlines(path, encoding="latin-1")
expect_next = EXPECT_LINE.ACCOUNT_NAME expect_next = EXPECT_LINE.ACCOUNT_NAME
account_name = None account_name: str | None = None
result = {} header: list[str] = []
result: CsV2DictMulti = {}
for line in lines: for line in lines:
if expect_next == EXPECT_LINE.ACCOUNT_NAME: if expect_next == EXPECT_LINE.ACCOUNT_NAME:
account_name = re.findall(PATTERN_ACCOUNT_NAME, line) matches = re.findall(PATTERN_ACCOUNT_NAME, line)
if account_name: if matches:
account_name = account_name[0] account_name = matches[0]
result[account_name] = {} result[account_name] = []
expect_next = EXPECT_LINE.BLANK expect_next = EXPECT_LINE.BLANK
elif expect_next == EXPECT_LINE.BLANK: elif expect_next == EXPECT_LINE.BLANK:
if line == "\n": if line == "\n":
@@ -80,11 +86,13 @@ def _reader_raw_comdirect(path: Path) -> CsV2DictMulti:
elif expect_next == EXPECT_LINE.HEADER: elif expect_next == EXPECT_LINE.HEADER:
header = re.findall(PATTERN_HEADER, line) header = re.findall(PATTERN_HEADER, line)
if header: if header:
assert account_name is not None
result[account_name] = [] result[account_name] = []
expect_next = EXPECT_LINE.DATA expect_next = EXPECT_LINE.DATA
elif expect_next == EXPECT_LINE.DATA: elif expect_next == EXPECT_LINE.DATA:
data = re.findall(PATTERN_DATA, line) data = re.findall(PATTERN_DATA, line)
if data: if data:
assert account_name is not None
result[account_name].append( result[account_name].append(
{column: value for column, value in zip(header, data)} {column: value for column, value in zip(header, data)}
) )
@@ -106,7 +114,7 @@ def upsert_comdirect_giro(
parameters_existing = {"hash_id": data.hash_id} parameters_existing = {"hash_id": data.hash_id}
if not data.date_val: if not data.date_val:
return return None
parameters_new = { parameters_new = {
"hash_id": data.hash_id, "hash_id": data.hash_id,
@@ -138,18 +146,25 @@ def ingest_comdirect(
for account_name, data in data_collection.items(): for account_name, data in data_collection.items():
account = SOURCE_COMDIRECT.get_account(account_name) account = SOURCE_COMDIRECT.get_account(account_name)
assert isinstance(account, Account) assert isinstance(account, Account)
if account_name == "Girokonto": if account_name != "Girokonto":
db_load_function = lambda session_, row_, account_: upsert_comdirect_giro( # Aktuell speist nur das Girokonto die Tabelle `comdirect_giro`
session=session_, # (Kreditkarte, Tagesgeld und Depot sind vorerst ausgeklammert).
data=row_,
account_iban=account_.iban,
account_currency=account_.currency,
do_commit=False,
)
else:
continue continue
for row in data: for row in data:
data_validated = account.model.model_validate(row) data_validated = account.model.model_validate(row)
assert isinstance(data_validated, account.model) assert isinstance(data_validated, ComdirectGiro)
db_load_function(session, row, account) upsert_comdirect_giro(
session.commit() session=session,
data=data_validated,
account_iban=account_iban,
account_currency=account_currency,
do_commit=False,
)
consolidate_transactions(
session,
source="comdirect_giro",
StagingDb=ComdirectGiroDb,
mapper=map_comdirect_giro_to_transaction,
do_commit=False,
)
session.commit()
+97
View File
@@ -0,0 +1,97 @@
from collections.abc import Callable
from sqlalchemy import select
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.orm import Session
from src.models._common import BaseModelDb
from src.models.b4w import B4wDb
from src.models.comdirect import ComdirectGiroDb
from src.models.transactions import Transaction, TransactionDb
def map_b4w_to_transaction(row: B4wDb) -> Transaction:
"""Project one staging row of ``import_b4w`` onto a canonical Transaction."""
return Transaction(
source="b4w",
source_row_id=row.id,
account_iban=row.account_iban,
account_currency=row.account_currency,
date_valid=row.date_valid,
date_book=row.date_book,
amount=row.amount,
amount_currency=row.amount_currency,
text=row.text,
other_name=row.other_name,
other_iban=row.other_iban,
category_id=row.category_id,
)
def map_comdirect_giro_to_transaction(row: ComdirectGiroDb) -> Transaction:
"""Project one staging row of ``comdirect_giro`` onto a canonical Transaction."""
return Transaction(
source="comdirect_giro",
source_row_id=row.id,
account_iban=row.account_iban,
account_currency=row.account_currency,
date_valid=row.date_valid,
date_book=row.date_book,
amount=row.amount,
amount_currency=row.amount_currency,
text=row.text,
)
def upsert_transaction(
session: Session, transaction: Transaction, do_commit: bool = True
) -> None:
"""Insert one canonical transaction, ignoring duplicates on ``hash_id``.
Uses a DB-native ``INSERT OR IGNORE`` (SQLite). For PostgreSQL, swap for
``postgresql_insert(TransactionDb).values(...).on_conflict_do_nothing(
constraint="uq_transactions_hash")``.
"""
parameters = {
"hash_id": transaction.hash_id,
"source": transaction.source,
"source_row_id": transaction.source_row_id,
"account_iban": transaction.account_iban,
"account_currency": transaction.account_currency,
"date_valid": transaction.date_valid,
"date_book": transaction.date_book,
"amount": transaction.amount,
"amount_currency": transaction.amount_currency,
"text": transaction.text,
"other_name": transaction.other_name,
"other_iban": transaction.other_iban,
"category_id": transaction.category_id,
}
stmt = sqlite_insert(TransactionDb).values(parameters)
_ = session.execute(stmt.prefix_with("OR IGNORE"))
if do_commit:
session.commit()
def consolidate_transactions[StagingDbT: BaseModelDb](
session: Session,
source: str,
StagingDb: type[StagingDbT],
mapper: Callable[[StagingDbT], Transaction],
do_commit: bool = True,
) -> None:
"""Project staging rows of one source into the unified ``transactions`` table.
Idempotent: rows whose business-key hash already exists for this source
are skipped, and ``INSERT OR IGNORE`` protects against in-batch duplicates.
"""
existing_hashes = set(
session.scalars(select(TransactionDb.hash_id).filter_by(source=source))
)
for staging_row in session.query(StagingDb).all():
transaction = mapper(staging_row)
if transaction.hash_id in existing_hashes:
continue
upsert_transaction(session, transaction, do_commit=False)
if do_commit:
session.commit()
+12 -6
View File
@@ -1,6 +1,7 @@
import argparse import argparse
from pathlib import Path from pathlib import Path
from src.db import get_session
from src.ingest.b4w import ingest_b4w from src.ingest.b4w import ingest_b4w
from src.ingest.comdirect import ingest_comdirect from src.ingest.comdirect import ingest_comdirect
from src.models.settings import Account from src.models.settings import Account
@@ -19,17 +20,22 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if args.input_b4w: if args.input_b4w:
ingest_b4w(Path(args.input_b4w)) with get_session() as session:
ingest_b4w(Path(args.input_b4w), session)
if args.input_comdirect: if args.input_comdirect:
import settings import settings
giro_account = settings.BANK_ACCOUNTS["comdirect"].get_account("Girokonto") giro_account = settings.BANK_ACCOUNTS["comdirect"].get_account("Girokonto")
assert isinstance(giro_account, Account) assert isinstance(giro_account, Account)
ingest_comdirect( with get_session() as session:
Path(args.input_comdirect), ingest_comdirect(
account_iban=giro_account.iban or "", session,
account_currency=giro_account.currency or "", Path(args.input_comdirect),
) account_iban=giro_account.iban or "",
account_currency=(
giro_account.currency.value if giro_account.currency else ""
),
)
if __name__ == "__main__": if __name__ == "__main__":
+2
View File
@@ -5,3 +5,5 @@ from src.models.comdirect import ComdirectDepot as ComdirectDepot
from src.models.comdirect import ComdirectGiro as ComdirectGiro from src.models.comdirect import ComdirectGiro as ComdirectGiro
from src.models.settings import Account as Account from src.models.settings import Account as Account
from src.models.settings import Source as Source from src.models.settings import Source as Source
from src.models.transactions import Transaction as Transaction
from src.models.transactions import TransactionDb as TransactionDb
+10 -10
View File
@@ -4,8 +4,8 @@ from decimal import Decimal
from enum import Enum from enum import Enum
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, Date, Integer, Numeric, String from sqlalchemy import Date, Numeric, String
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
def _hash(*args): def _hash(*args):
@@ -89,11 +89,11 @@ class BaseModelDb(DeclarativeBase):
class IngestdbBase(BaseModelDb): class IngestdbBase(BaseModelDb):
__abstract__ = True __abstract__ = True
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
hash_id = Column(String, nullable=False) hash_id: Mapped[str] = mapped_column(String, nullable=False)
account_iban = Column(String, nullable=False) account_iban: Mapped[str] = mapped_column(String, nullable=False)
account_currency = Column(String(3), nullable=False) account_currency: Mapped[str] = mapped_column(String(3), nullable=False)
date_valid = Column(Date, nullable=False) date_valid: Mapped[date] = mapped_column(Date, nullable=False)
amount = Column(Numeric(12, 2), nullable=False) amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False)
amount_currency = Column(String(3), nullable=False) amount_currency: Mapped[str] = mapped_column(String(3), nullable=False)
text = Column(String, nullable=True) text: Mapped[str | None] = mapped_column(String, nullable=True)
+18 -16
View File
@@ -2,8 +2,8 @@ from datetime import date
from decimal import Decimal from decimal import Decimal
from pydantic import Field, computed_field, field_validator from pydantic import Field, computed_field, field_validator
from sqlalchemy import Column, Date, ForeignKey, Integer, String, UniqueConstraint from sqlalchemy import Date, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.constants import IBAN_PATTERN, Currency from src.constants import IBAN_PATTERN, Currency
from src.models._common import ( from src.models._common import (
@@ -56,7 +56,7 @@ class B4wExport(ExportBaseModel):
"Notes": f"{self.notes}", "Notes": f"{self.notes}",
} }
@computed_field @computed_field # type: ignore[prop-decorator]
@property @property
def hash_id(self) -> str: def hash_id(self) -> str:
return _hash(self.id) return _hash(self.id)
@@ -91,7 +91,7 @@ class Category(ExportBaseModel):
name: str = Field(pattern=r"[^:\r\n]+") name: str = Field(pattern=r"[^:\r\n]+")
parent: Category | None = None parent: Category | None = None
@computed_field @computed_field # type: ignore[prop-decorator]
@property @property
def hash_id(self) -> str: def hash_id(self) -> str:
return _hash(self.name, self.parent) return _hash(self.name, self.parent)
@@ -99,7 +99,7 @@ class Category(ExportBaseModel):
@classmethod @classmethod
def from_str(cls, s: str) -> Category | None: def from_str(cls, s: str) -> Category | None:
if not s: if not s:
return return None
components = s.split(":") components = s.split(":")
name = components[-1] name = components[-1]
rest = ":".join(components[:-1]) rest = ":".join(components[:-1])
@@ -124,9 +124,11 @@ class CategoryDb(BaseModelDb):
UniqueConstraint("name", "parent_id", name="uq_category_name_parent"), UniqueConstraint("name", "parent_id", name="uq_category_name_parent"),
) )
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name = Column(String, nullable=False) name: Mapped[str] = mapped_column(String, nullable=False)
parent_id = Column(Integer, ForeignKey("category.id"), nullable=True) parent_id: Mapped[int | None] = mapped_column(
ForeignKey("category.id"), nullable=True
)
# Relationships # Relationships
parent = relationship( parent = relationship(
@@ -134,18 +136,18 @@ class CategoryDb(BaseModelDb):
) )
children = relationship("CategoryDb", back_populates="parent") children = relationship("CategoryDb", back_populates="parent")
UniqueConstraint("name", "parent_id", name="uq_category_name_parent")
class B4wDb(IngestdbBase): class B4wDb(IngestdbBase):
__tablename__ = "import_b4w" __tablename__ = "import_b4w"
id_b4w = Column(Integer, nullable=False, unique=True) id_b4w: Mapped[int] = mapped_column(Integer, nullable=False, unique=True)
date_book = Column(Date, nullable=True) date_book: Mapped[date | None] = mapped_column(Date, nullable=True)
other_name = Column(String, nullable=True) other_name: Mapped[str | None] = mapped_column(String, nullable=True)
other_iban = Column(String, nullable=True) other_iban: Mapped[str | None] = mapped_column(String, nullable=True)
category_id = Column(Integer, ForeignKey("category.id"), nullable=True) category_id: Mapped[int | None] = mapped_column(
notes = Column(String, nullable=True) ForeignKey("category.id"), nullable=True
)
notes: Mapped[str | None] = mapped_column(String, nullable=True)
# relationship # relationship
category = relationship("CategoryDb") category = relationship("CategoryDb")
+6 -8
View File
@@ -2,7 +2,8 @@ from datetime import date
from decimal import Decimal from decimal import Decimal
from pydantic import ConfigDict, Field, computed_field, field_validator from pydantic import ConfigDict, Field, computed_field, field_validator
from sqlalchemy import Column, Date, String from sqlalchemy import Date, String
from sqlalchemy.orm import Mapped, mapped_column
from src.models._common import ( from src.models._common import (
ExportBaseModel, ExportBaseModel,
@@ -26,7 +27,7 @@ class ComdirectBase(ExportBaseModel):
@classmethod @classmethod
def parse_date_val(cls, date_str: str | None) -> date | None: def parse_date_val(cls, date_str: str | None) -> date | None:
if date_str is None: if date_str is None:
return return None
return _parse_date_german(date_str) return _parse_date_german(date_str)
@field_validator("amount_eur", mode="before") @field_validator("amount_eur", mode="before")
@@ -68,7 +69,7 @@ class ComdirectGiro(ComdirectBase):
) )
text: str = Field(alias="Buchungstext") text: str = Field(alias="Buchungstext")
@computed_field @computed_field # type: ignore[prop-decorator]
@property @property
def hash_id(self) -> str: def hash_id(self) -> str:
return _hash(self.date_val, self.date_book, self.text) return _hash(self.date_val, self.date_book, self.text)
@@ -103,9 +104,6 @@ class ComdirectCreditCard(ComdirectGiro):
) )
reference: str = Field(alias="Referenz", default="") reference: str = Field(alias="Referenz", default="")
text: str = Field(alias="Buchungstext", default="") text: str = Field(alias="Buchungstext", default="")
amount_eur: Decimal | None = Field(
alias="Umsatz in EUR", examples=["32,43"], default=None
)
def parameters_to_dict(self, by_name: bool = True) -> dict[str, str]: def parameters_to_dict(self, by_name: bool = True) -> dict[str, str]:
parameters = dict(**super().parameters_to_dict(by_name=by_name)) parameters = dict(**super().parameters_to_dict(by_name=by_name))
@@ -155,5 +153,5 @@ class ComdirectBaseDb(IngestdbBase):
class ComdirectGiroDb(ComdirectBaseDb): class ComdirectGiroDb(ComdirectBaseDb):
__tablename__ = "comdirect_giro" __tablename__ = "comdirect_giro"
type = Column(String, nullable=True) type: Mapped[str | None] = mapped_column(String, nullable=True)
date_book = Column(Date, nullable=True) date_book: Mapped[date | None] = mapped_column(Date, nullable=True)
+78
View File
@@ -0,0 +1,78 @@
from datetime import date
from decimal import Decimal
from pydantic import Field, computed_field
from sqlalchemy import Date, ForeignKey, Index, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.models._common import ExportBaseModel, IngestdbBase, _hash, date2str
class Transaction(ExportBaseModel):
"""Canonical, source-agnostic representation of a bank transaction.
Both input sources (b4w, comdirect giro) are projected into this model.
The deduplication key is a business-key hash of the transaction
attributes, so re-importing an export is idempotent even if
source-internal ids change between exports.
"""
source: str = Field(description="Source name", examples=["b4w", "comdirect_giro"])
source_row_id: int = Field(description="Primary key of the staging table row")
account_iban: str
account_currency: str
date_valid: date
amount: Decimal = Field(description="Signed amount; negative = money out")
amount_currency: str
date_book: date | None = None
text: str | None = None
other_name: str | None = None
other_iban: str | None = None
category_id: int | None = None
@computed_field # type: ignore[prop-decorator]
@property
def hash_id(self) -> str:
return _hash(
self.source,
self.account_iban,
self.date_valid,
f"{self.amount:.2f}",
self.text,
self.other_name,
)
def parameters_to_dict(self) -> dict[str, str]:
return {
"source": f"{self.source}",
"source_row_id": f"{self.source_row_id}",
"account_iban": f"{self.account_iban}",
"account_currency": f"{self.account_currency}",
"date_valid": date2str(self.date_valid),
"amount": f"{self.amount}",
"amount_currency": f"{self.amount_currency}",
"date_book": date2str(self.date_book),
"text": f"{self.text or ''}",
"other_name": f"{self.other_name or ''}",
"other_iban": f"{self.other_iban or ''}",
"category_id": f"{self.category_id}",
}
class TransactionDb(IngestdbBase):
__tablename__ = "transactions"
__table_args__ = (
UniqueConstraint("hash_id", name="uq_transactions_hash"),
Index("ix_transactions_source_row_id", "source", "source_row_id"),
)
source: Mapped[str] = mapped_column(String, nullable=False)
source_row_id: Mapped[int] = mapped_column(Integer, nullable=False)
date_book: Mapped[date | None] = mapped_column(Date, nullable=True)
other_name: Mapped[str | None] = mapped_column(String, nullable=True)
other_iban: Mapped[str | None] = mapped_column(String, nullable=True)
category_id: Mapped[int | None] = mapped_column(
ForeignKey("category.id"), nullable=True
)
category = relationship("CategoryDb")
+11
View File
@@ -7,6 +7,7 @@ from polyfactory.fields import Use
from src.models.b4w import B4wExport from src.models.b4w import B4wExport
from src.models.comdirect import ComdirectCreditCard, ComdirectDepot, ComdirectGiro from src.models.comdirect import ComdirectCreditCard, ComdirectDepot, ComdirectGiro
from src.models.transactions import Transaction
DEFAULT_CATEGORY = "root_category:parent_category:leaf_category" DEFAULT_CATEGORY = "root_category:parent_category:leaf_category"
@@ -98,3 +99,13 @@ class ComdirectDepotFactory(ModelFactory[ComdirectDepot]):
date_val = Use(_german_date) date_val = Use(_german_date)
num = Use(_amount_eur) num = Use(_amount_eur)
spot_price = Use(_amount_eur) spot_price = Use(_amount_eur)
class TransactionFactory(ModelFactory[Transaction]):
__faker__ = Faker("de_DE")
__use_examples__ = True
account_iban = Use(_iban)
text = Use(_sentence)
other_name = Use(_names)
other_iban = Use(_iban_or_none)
+193
View File
@@ -0,0 +1,193 @@
from datetime import date
from decimal import Decimal
from pathlib import Path
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from src.ingest.b4w import ingest_b4w, upsert_b4w
from src.ingest.comdirect import ingest_comdirect
from src.ingest.transactions import (
consolidate_transactions,
map_b4w_to_transaction,
map_comdirect_giro_to_transaction,
)
from src.models.b4w import B4wDb
from src.models.comdirect import ComdirectGiroDb
from src.models.transactions import Transaction, TransactionDb
from tests.factories import B4wExportFactory
B4W_PATH = Path("tests/test_data/b4w_export_dummy_01.csv")
COMDIRECT_PATH = Path("tests/test_data/comdirect_export_dummy_01.txt")
def test_transaction_hash_is_business_key():
first = Transaction(
source="b4w",
source_row_id=1,
account_iban="DE89370400440532013000",
account_currency="EUR",
date_valid=date(2026, 9, 7),
amount=Decimal("-50.00"),
amount_currency="EUR",
date_book=date(2026, 9, 8),
text="Supermarket",
other_name="Aldi",
)
second = Transaction(
source="b4w",
source_row_id=999,
account_iban="DE89370400440532013000",
account_currency="EUR",
date_valid=date(2026, 9, 7),
amount=Decimal("-50.00"),
amount_currency="EUR",
date_book=date(2026, 9, 8),
text="Supermarket",
other_name="Aldi",
)
# Stable across source-row ids (re-imports) ...
assert second.hash_id == first.hash_id
# ... but source is part of the key, so no cross-source collision.
third = second.model_copy(update={"source": "comdirect_giro"})
assert third.hash_id != first.hash_id
def test_map_b4w_to_transaction():
row = B4wDb(
id=7,
hash_id="unused",
id_b4w=1,
account_iban="DE89370400440532013000",
account_currency="EUR",
date_valid=date(2026, 9, 7),
date_book=date(2026, 9, 8),
amount=Decimal("-50.00"),
amount_currency="EUR",
text="Supermarket",
other_name="Aldi",
other_iban="DE02120300000000202051",
category_id=None,
notes="",
)
transaction = map_b4w_to_transaction(row)
assert isinstance(transaction, Transaction)
assert transaction.source == "b4w"
assert transaction.source_row_id == 7
assert transaction.amount == Decimal("-50.00")
assert transaction.date_book == date(2026, 9, 8)
assert transaction.text == "Supermarket"
assert transaction.other_name == "Aldi"
assert transaction.other_iban == "DE02120300000000202051"
def test_map_comdirect_giro_to_transaction():
row = ComdirectGiroDb(
id=3,
hash_id="unused",
account_iban="DE89370400440532013000",
account_currency="EUR",
amount_currency="EUR",
date_valid=date(2026, 9, 7),
amount=Decimal("32.43"),
date_book=date(2026, 9, 8),
type="Kartenumsatz",
text="Supermarkt",
)
transaction = map_comdirect_giro_to_transaction(row)
assert isinstance(transaction, Transaction)
assert transaction.source == "comdirect_giro"
assert transaction.source_row_id == 3
assert transaction.amount == Decimal("32.43")
assert transaction.other_name is None
assert transaction.other_iban is None
assert transaction.category_id is None
def test_consolidate_b4w_signed_amount_and_category(db_session: Session):
export = B4wExportFactory.build(
Amt="50.00",
RmtInf="Supermarket",
Category="Lebenshaltung:Lebensmittel",
CdtDbtInd="DBIT",
)
staging = upsert_b4w(db_session, export, do_commit=False)
assert staging is not None
consolidate_transactions(
db_session, "b4w", B4wDb, map_b4w_to_transaction, do_commit=False
)
db_session.commit()
transaction = db_session.execute(select(TransactionDb)).scalars().one()
assert transaction.source == "b4w"
assert transaction.source_row_id == staging.id
# b4w amount_signed: DBIT -> money out
assert transaction.amount == Decimal("-50.00")
assert transaction.category_id == staging.category_id
assert transaction.text == "Supermarket"
def test_ingest_b4w_e2e_idempotent(db_session: Session):
for _ in range(2):
ingest_b4w(B4W_PATH, db_session)
staging_count = db_session.execute(
select(func.count()).select_from(B4wDb)
).scalar_one()
transaction_count = db_session.execute(
select(func.count()).select_from(TransactionDb)
).scalar_one()
assert staging_count > 0
assert transaction_count == staging_count
transactions = db_session.execute(select(TransactionDb)).scalars().all()
assert all(t.source == "b4w" for t in transactions)
assert all(t.source_row_id is not None for t in transactions)
def test_ingest_comdirect_e2e_idempotent(db_session: Session):
for _ in range(2):
ingest_comdirect(
session=db_session,
path=COMDIRECT_PATH,
account_iban="DE89370400440532013000",
account_currency="EUR",
)
staging_count = db_session.execute(
select(func.count()).select_from(ComdirectGiroDb)
).scalar_one()
transaction_count = db_session.execute(
select(func.count()).select_from(TransactionDb)
).scalar_one()
assert staging_count > 0
assert transaction_count == staging_count
transactions = db_session.execute(select(TransactionDb)).scalars().all()
assert all(t.source == "comdirect_giro" for t in transactions)
assert all(t.account_iban == "DE89370400440532013000" for t in transactions)
def test_ingest_mixed_sources_into_one_table(db_session: Session):
ingest_b4w(B4W_PATH, db_session)
ingest_comdirect(
session=db_session,
path=COMDIRECT_PATH,
account_iban="DE89370400440532013000",
account_currency="EUR",
)
b4w_count = db_session.execute(select(func.count()).select_from(B4wDb)).scalar_one()
giro_count = db_session.execute(
select(func.count()).select_from(ComdirectGiroDb)
).scalar_one()
transaction_count = db_session.execute(
select(func.count()).select_from(TransactionDb)
).scalar_one()
assert b4w_count > 0
assert giro_count > 0
assert transaction_count == b4w_count + giro_count
sources = set(db_session.execute(select(TransactionDb.source).distinct()).scalars())
assert sources == {"b4w", "comdirect_giro"}
Generated
+183 -1
View File
@@ -1,6 +1,10 @@
version = 1 version = 1
revision = 3 revision = 5
requires-python = ">=3.14" requires-python = ">=3.14"
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version < '3.15'",
]
[[package]] [[package]]
name = "alembic" name = "alembic"
@@ -34,6 +38,70 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/94/3af39d34be01a24a6e65433d19e107099374224905f1e0cc6bbe1fd22a2f/argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", size = 23000, upload-time = "2015-09-14T16:03:16.137Z" }, { url = "https://files.pythonhosted.org/packages/f2/94/3af39d34be01a24a6e65433d19e107099374224905f1e0cc6bbe1fd22a2f/argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", size = 23000, upload-time = "2015-09-14T16:03:16.137Z" },
] ]
[[package]]
name = "ast-serialize"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" },
{ url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" },
{ url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" },
{ url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" },
{ url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" },
{ url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" },
{ url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" },
{ url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" },
{ url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" },
{ url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" },
{ url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" },
{ url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" },
{ url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" },
{ url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" },
{ url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" },
{ url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" },
{ url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" },
{ url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" },
{ url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" },
{ url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" },
{ url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" },
{ url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" },
{ url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" },
{ url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" },
{ url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" },
{ url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" },
{ url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" },
{ url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" },
{ url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" },
{ url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" },
{ url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" },
{ url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" },
{ url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" },
{ url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" },
{ url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" },
{ url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" },
{ url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" },
{ url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" },
{ url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" },
{ url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" },
{ url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" },
{ url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" },
{ url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" },
{ url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" },
{ url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" },
{ url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" },
{ url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" },
{ url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" },
{ url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" },
]
[[package]] [[package]]
name = "black" name = "black"
version = "26.5.1" version = "26.5.1"
@@ -118,6 +186,7 @@ dependencies = [
{ name = "alembic" }, { name = "alembic" },
{ name = "argparse" }, { name = "argparse" },
{ name = "dotenv" }, { name = "dotenv" },
{ name = "mypy" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pytest" }, { name = "pytest" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
@@ -138,6 +207,7 @@ requires-dist = [
{ name = "alembic", specifier = ">=1.19.1" }, { name = "alembic", specifier = ">=1.19.1" },
{ name = "argparse", specifier = ">=1.4.0" }, { name = "argparse", specifier = ">=1.4.0" },
{ name = "dotenv", specifier = ">=0.9.9" }, { name = "dotenv", specifier = ">=0.9.9" },
{ name = "mypy", specifier = ">=2.4.0" },
{ name = "pydantic", specifier = ">=2.13.5" }, { name = "pydantic", specifier = ">=2.13.5" },
{ name = "pytest", specifier = ">=9.1.1" }, { name = "pytest", specifier = ">=9.1.1" },
{ name = "python-dotenv", specifier = ">=1.2.3" }, { name = "python-dotenv", specifier = ">=1.2.3" },
@@ -201,6 +271,76 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
] ]
[[package]]
name = "librt"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" },
{ url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" },
{ url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" },
{ url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" },
{ url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" },
{ url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" },
{ url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" },
{ url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" },
{ url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" },
{ url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" },
{ url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" },
{ url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" },
{ url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" },
{ url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" },
{ url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" },
{ url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" },
{ url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" },
{ url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" },
{ url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" },
{ url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" },
{ url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" },
{ url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" },
{ url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" },
{ url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" },
{ url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" },
{ url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" },
{ url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" },
{ url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" },
{ url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" },
{ url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" },
{ url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" },
{ url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" },
{ url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" },
{ url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" },
{ url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" },
{ url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" },
{ url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" },
{ url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" },
{ url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" },
{ url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" },
{ url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" },
{ url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" },
{ url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" },
{ url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" },
{ url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" },
{ url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" },
{ url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" },
{ url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" },
{ url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" },
{ url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" },
{ url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" },
{ url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" },
{ url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" },
{ url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" },
{ url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" },
]
[[package]] [[package]]
name = "mako" name = "mako"
version = "1.4.1" version = "1.4.1"
@@ -243,6 +383,48 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
] ]
[[package]]
name = "mypy"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ast-serialize" },
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" },
{ url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" },
{ url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" },
{ url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" },
{ url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" },
{ url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" },
{ url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" },
{ url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" },
{ url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" },
{ url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" },
{ url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" },
{ url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" },
{ url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" },
{ url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" },
{ url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" },
{ url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" },
{ url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" },
{ url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" },
{ url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" },
{ url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" },
{ url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" },
{ url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" },
{ url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" },
{ url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" },
{ url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" },
{ url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" },
]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.1.0" version = "1.1.0"