Merge pull request 'Ingest comdirect' (#5) from ingest-comdirect into master
Tests / test (push) Successful in 8s
Tests / lint (push) Successful in 7s

Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
2026-09-13 20:27:15 +00:00
34 changed files with 1565 additions and 275 deletions
+40
View File
@@ -0,0 +1,40 @@
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install uv
run: pip install uv
- name: Install dependencies
run: uv sync --frozen
- name: Copy default settings
run: cp settings.template.py settings.py
- name: Run pytest
run: uv run pytest
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install uv
run: pip install uv
- name: Install dependencies
run: uv sync --frozen
- name: Copy default settings
run: cp settings.template.py settings.py
- name: Check formatting (black)
run: uv run black --check . --extend-exclude .venv
- name: Lint (ruff)
run: uv run ruff check .
+4
View File
@@ -6,11 +6,15 @@ dist/
wheels/
*.egg-info
# IDE
.idea
# Virtual environments
.venv
# Environment variables
.env
/settings.py
# Database instances
*.db
+107
View File
@@ -0,0 +1,107 @@
# finanz-cockpit Agent Instructions
## Setup Commands
```bash
# Install dependencies
uv sync
# Run migrations
alembic upgrade head
# Run all tests
pytest
# Run a single test file
pytest tests/test_model_b4wexport.py -v
```
## Project Structure
- **`src/`**: Application code
- `main.py`: CLI entrypoint for CSV ingestion
- `db.py`: Database connection management (uses global `_engine` pattern)
- `models/`: Pydantic models + SQLAlchemy tables for b4w and comdirect imports
- `ingest/`: Data ingestion logic per source
- **`tests/`**: Tests with fixtures in `conftest.py`
- **`alembic/versions/`**: Database migrations
## Key Patterns & Gotchas
### Database Configuration
```python
from src.db import get_engine, get_session, set_engine
# Development: use sqlite://
os.environ["DATABASE_URL"] = "sqlite://"
# Production/default: sqlite:///finanz.db
engine = get_engine() # reads DATABASE_URL or uses default
with get_session() as session:
session.query(...).all()
```
**Critical**: Tests override engine via `conftest.py`. Never assume global state - always use `get_session()` context manager.
### CSV Format Expectations
- **b4w**: German bank export (CSV) from `/mnt/anbau/Software/Python/finanz-cockpit/scripts/dummy_data/b4w_export_dummy_01.csv`
- **comdirect**: German bank export (CSV) with fields like `Buchungstag`, `Umsatz in EUR`, `Vorgang`
### Testing Workflow
```bash
# Tests automatically run migrations via conftest fixture
pytest tests/ -v --tb=short
# To test without migrations (in-memory SQLite), use DATABASE_URL="sqlite://"
export DATABASE_URL="sqlite://" pytest tests/test_db.py
```
### Environment Variables
- `DATABASE_URL`: Database connection string (default: `sqlite:///finanz.db`)
## Data Models
Two sources with shared patterns:
- **b4w**: `B4wExport`, `Category`, transactional import to `import_b4w` table + `category` table
- **comdirect**: `ComdirectGiro`, `ComdirectCreditCard`, `ComdirectDepot`, import to `comdirect_giro` table
All models inherit from common base in `src/models/_common.py`:
- `_hash()`: SHA256-based deduplication key
- `_parse_date_german()`: Handles "07.09.2026" format and values like "offen", "--"
- `_parse_decimal_german()`: Converts "1,23" to Decimal
## Factory Pattern for Testing
Import from `tests/factories.py`:
```python
from tests.factories import B4wExportFactory, ComdirectGiroFactory
from src.constants import Currency
B4wExportFactory.__faker__ = Faker("de_DE")
ComdirectGiroFactory.__faker__ = Faker("de_DE")
# Use in tests:
item = B4wExportFactory.build( # build() for partial, create() for full
Amt="50.00",
RmtInf="Supermarket",
)
```
## Common Mistakes to Avoid
1. **Assuming global engine state**: Always use `get_session()` context manager
2. **Ignoring German date formats**: Use `_parse_date_german()` for CSV parsing
3. **Forgetting migration setup in tests**: Tests auto-migrate, but integration tests with real db need explicit upgrade
4. **Wrong Faker locale**: Use `Faker("de_DE")` for realistic names/IBANs
## Testing Tips
- Run single test: `pytest tests/test_model_b4wexport.py -v`
- View migrations: `ls alembic/versions/`
- Test data location: `/scripts/dummy_data/`
+1 -1
View File
@@ -6,7 +6,7 @@ from sqlalchemy import engine_from_config, pool
from alembic import context
from src.db import get_set_engine
from src.models_db import BaseModelDb
from src.models._common import BaseModelDb
load_dotenv() # reads .env into os.environ
+52
View File
@@ -0,0 +1,52 @@
"""empty message
Revision ID: 8822a28b01ca
Revises: 605e46acc08e
Create Date: 2026-09-11 11:13:48.650768
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "8822a28b01ca"
down_revision: str | Sequence[str] | None = "605e46acc08e"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"comdirect_giro",
sa.Column("type", sa.String(), nullable=True),
sa.Column("date_book", sa.Date(), nullable=True),
sa.Column("account_iban", sa.String(), nullable=False),
sa.Column("account_currency", sa.String(length=3), nullable=False),
sa.Column("amount_currency", sa.String(length=3), nullable=False),
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("hash_id", sa.String(), nullable=False),
sa.Column("date_valid", sa.Date(), nullable=False),
sa.Column("amount", sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column("text", sa.String(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.add_column("import_b4w", sa.Column("hash_id", sa.String(), nullable=False))
op.add_column("import_b4w", sa.Column("text", sa.String(), nullable=True))
op.drop_column("import_b4w", "reference")
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("import_b4w", sa.Column("reference", sa.VARCHAR(), nullable=True))
op.drop_column("import_b4w", "text")
op.drop_column("import_b4w", "hash_id")
op.drop_table("comdirect_giro")
# ### end Alembic commands ###
+9
View File
@@ -28,8 +28,17 @@ module-root = ""
requires = ["uv_build>=0.12.9,<0.13.0"]
build-backend = "uv_build"
[tool.ruff]
target-version = "py314"
[tool.ruff.lint]
ignore = ["EXE"]
[dependency-groups]
dev = [
"black>=26.5.1",
"debugpy>=1.8.21",
"faker>=40.38.0",
"polyfactory>=3.3.0",
"ruff>=0.16.7",
]
@@ -0,0 +1,25 @@
;
"Umsätze Girokonto";"Zeitraum: 180 Tage";
"Neuer Kontostand";"2.363,30 EUR";
{table_giro}
"Alter Kontostand";"1.632,48 EUR";
"Umsätze Visa-Karte (Kreditkarte)";"Zeitraum: 180 Tage";
"Neuer Kontostand";"0,00 EUR";
{table_credit_card}
"Alter Kontostand";"0,00 EUR";
"Umsätze Tagesgeld PLUS-Konto";"Zeitraum: 180 Tage";
"Neuer Kontostand";"1.673,90 EUR";
{table_tagesgeld}
"Alter Kontostand";"6.686,20 EUR";
"Umsätze Depot";"Zeitraum: 180 Tage";
{table_depot}
@@ -0,0 +1,22 @@
import csv
from pathlib import Path
from tests.factories import B4wExportFactory
NUMBER_OF_ROWS = 50
DEFAULT_PATH = Path("tests/test_data/b4w_export_dummy_01.csv")
def save_csv(path: Path = DEFAULT_PATH):
data = B4wExportFactory.batch(NUMBER_OF_ROWS)
row_fields = list(data[0].parameters_to_dict())
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", newline="") as fid:
writer = csv.DictWriter(fid, fieldnames=row_fields)
writer.writeheader()
for export in data:
writer.writerow(export.parameters_to_dict())
if __name__ == "__main__":
save_csv()
@@ -0,0 +1,61 @@
from pathlib import Path
from tests.factories import (
ComdirectCreditCardFactory,
ComdirectDepotFactory,
ComdirectGiroFactory,
)
DEFAULT_PATH = Path("tests/test_data/comdirect_export_dummy_01.txt")
TEMPLATE_PATH = Path("scripts/dummy_data/comdirect_export_template.txt")
NUMBER_OF_ROWS_GIRO = 50
NUMBER_OF_ROWS_CREDIT = 20
NUMBER_OF_ROWS_TAGESGELD = 10
NUMBER_OF_ROWS_DEPOT = 10
def save(path: Path = DEFAULT_PATH):
with TEMPLATE_PATH.open("r", encoding="latin-1") as fid:
txt_temp = fid.read()
assert isinstance(txt_temp, str)
batches = {
"giro": {
"data": ComdirectGiroFactory.batch(NUMBER_OF_ROWS_GIRO),
"table_txt": "",
},
"credit": {
"data": ComdirectCreditCardFactory.batch(NUMBER_OF_ROWS_CREDIT),
"table_txt": "",
},
"tagesgled": {
"data": ComdirectGiroFactory.batch(NUMBER_OF_ROWS_TAGESGELD),
"table_txt": "",
},
"depot": {
"data": ComdirectDepotFactory.batch(NUMBER_OF_ROWS_DEPOT),
"table_txt": "",
},
}
for batch in batches.values():
data = batch.get("data")
assert isinstance(data, list)
lines = [data[0].get_orig_headers()]
lines = lines + [row.get_orig_row() for row in data]
batch["table_txt"] = "\n".join(lines)
txt = txt_temp.format(
table_giro=batches["giro"]["table_txt"],
table_credit_card=batches["credit"]["table_txt"],
table_tagesgeld=batches["tagesgled"]["table_txt"],
table_depot=batches["depot"]["table_txt"],
)
with path.open("w", newline="") as fid:
fid.write(txt)
if __name__ == "__main__":
save()
+59
View File
@@ -0,0 +1,59 @@
from pathlib import Path
from faker import Faker
from src.constants import Currency
from src.models import (
ComdirectCreditCard,
ComdirectDepot,
ComdirectGiro,
)
from src.models.b4w import B4wExport
from src.models.settings import Account, Source
fake = Faker()
BANK_ACCOUNTS = {
"b4w": Source(
name="b4w",
source_path=Path(""),
accounts=[
Account(
name="b4w",
model=B4wExport,
iban=fake.iban(),
currency=Currency.EUR,
)
],
),
"comdirect": Source(
name="comdirect",
source_path=Path(""),
accounts=[
Account(
name="Girokonto",
model=ComdirectGiro,
iban=fake.iban(),
currency=Currency.EUR,
),
Account(
name="Visa-Karte (Kreditkarte)",
model=ComdirectCreditCard,
iban=fake.iban(),
currency=Currency.EUR,
),
Account(
name="Tagesgeld PLUS-Konto",
model=ComdirectGiro,
iban=fake.iban(),
currency=Currency.EUR,
),
Account(
name="Depot",
model=ComdirectDepot,
iban=fake.iban(),
currency=Currency.EUR,
),
],
),
}
+16
View File
@@ -0,0 +1,16 @@
from enum import Enum
IBAN_PATTERN = r"^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$"
class Currency(str, Enum):
EUR = "EUR"
USD = "USD"
GBP = "GBP"
CHF = "CHF"
def __str__(self):
return f"{self.value}"
def __repr__(self):
return f"Currency.{self.name}"
View File
+44
View File
@@ -0,0 +1,44 @@
import csv
from pathlib import Path
from typing import Any
from sqlalchemy.orm import Session
from src.models._common import BaseModelDb
def _query_existing(
session: Session, DbModel: type[BaseModelDb], query_parameters: dict[str, Any]
) -> BaseModelDb | None:
query = session.query(DbModel).filter_by(**query_parameters)
return query.first()
def _upsert[DbModelT: BaseModelDb](
session: Session,
DbModel: type[DbModelT],
parameters_existing: dict[str, Any],
parameters_new: dict[str, Any],
do_update: bool = False,
do_commit: bool = True,
) -> DbModelT:
existing_db_row = _query_existing(session, DbModel, parameters_existing)
if existing_db_row:
if do_update:
for key, value in parameters_new.items():
setattr(existing_db_row, key, value)
assert isinstance(existing_db_row, DbModel)
return existing_db_row
new_db_row = DbModel(**parameters_new)
session.add(new_db_row)
session.flush()
if do_commit:
session.commit()
return new_db_row
def _reader_raw(path: Path):
with path.open() as fid:
return list(csv.DictReader(fid))
+16 -35
View File
@@ -1,41 +1,14 @@
from typing import Any
from pathlib import Path
from sqlalchemy.orm import Session
from src.models import B4wExport, Category
from src.models_db import B4wDb, BaseModelDb, CategoryDb
from src.ingest._common import _reader_raw, _upsert
from src.models.b4w import B4wDb, B4wExport, Category, CategoryDb
def _query_existing(
session: Session, DbModel: type[BaseModelDb], query_parameters: dict[str, Any]
) -> BaseModelDb | None:
query = session.query(DbModel).filter_by(**query_parameters)
return query.first()
def _upsert[DbModelT: BaseModelDb](
session: Session,
DbModel: type[DbModelT],
parameters_existing: dict[str, Any],
parameters_new: dict[str, Any],
do_update: bool = False,
do_commit: bool = True,
) -> DbModelT:
existing_db_row = _query_existing(session, DbModel, parameters_existing)
if existing_db_row:
if do_update:
for key, value in parameters_new.items():
setattr(existing_db_row, key, value)
assert isinstance(existing_db_row, DbModel)
return existing_db_row
new_db_row = DbModel(**parameters_new)
session.add(new_db_row)
session.flush()
if do_commit:
session.commit()
return new_db_row
def reader_b4w(path: Path) -> list[B4wExport]:
raw = _reader_raw(path)
return list(map(B4wExport.model_validate, raw))
def upsert_category(
@@ -62,17 +35,18 @@ def upsert_b4w(
) -> B4wDb | None:
category_db = upsert_category(session, data.category)
parameters_existing = {"id_b4w": data.id}
parameters_existing = {"hash_id": data.hash_id}
parameters_new = {
"id_b4w": data.id,
"hash_id": data.hash_id,
"account_iban": data.account_iban,
"account_currency": data.account_currency.value,
"date_valid": data.date_valid,
"date_book": data.date_book,
"amount": data.amount_signed,
"amount_currency": data.amount_currency.value,
"reference": data.reference,
"text": data.reference,
"other_name": data.other_name,
"other_iban": data.other_iban,
"category": category_db,
@@ -87,3 +61,10 @@ def upsert_b4w(
do_update=do_update,
do_commit=do_commit,
)
def ingest_b4w(path: Path, session: Session):
data = reader_b4w(path)
for row in data:
upsert_b4w(session, row, do_commit=False)
session.commit()
+155
View File
@@ -0,0 +1,155 @@
import re
from enum import Enum, auto
from pathlib import Path
from sqlalchemy.orm import Session
from settings import BANK_ACCOUNTS
from src.ingest._common import _upsert
from src.models._common import Csv2Dict, CsV2DictMulti
from src.models.comdirect import (
ComdirectCreditCard,
ComdirectDepot,
ComdirectGiro,
ComdirectGiroDb,
)
from src.models.settings import Account, Source
PATTERN_ACCOUNT_NAME = r'^"Umsätze ([^"]+)";"Zeitraum: .*$'
PATTERN_HEADER = r'"([^"]+)"'
PATTERN_DATA = r'"([^"]+)"'
class EXPECT_LINE(Enum):
ACCOUNT_NAME = auto()
BLANK = auto()
HEADER = auto()
DATA = auto()
type ComdirectExportModel = ComdirectGiro | ComdirectDepot | ComdirectCreditCard
SOURCE_COMDIRECT = BANK_ACCOUNTS.get("comdirect")
assert isinstance(SOURCE_COMDIRECT, Source)
SOURCE_PATH = SOURCE_COMDIRECT.source_path
ACCOUNTS = SOURCE_COMDIRECT.accounts
COMDIRECT_ACCOUNT_PARSERS = {a.name: a.model.model_validate for a in ACCOUNTS}
def reader_comdirect(path: Path) -> dict[str, list[ComdirectExportModel]]:
result = {}
raw = _reader_raw_comdirect(path)
for account in ACCOUNTS:
assert account.name in raw
data_raw: Csv2Dict = raw.pop(account.name)
data: list = list(map(account.model.model_validate, data_raw))
result[account.name] = data
assert not raw
return result
def _open_and_readlines(path: Path, encoding: str) -> list:
with path.open("r", encoding=encoding) as fid:
lines = fid.readlines()
return lines
def _reader_raw_comdirect(path: Path) -> CsV2DictMulti:
try:
lines = _open_and_readlines(path, encoding="utf-8")
except UnicodeDecodeError:
lines = _open_and_readlines(path, encoding="latin-1")
expect_next = EXPECT_LINE.ACCOUNT_NAME
account_name = None
result = {}
for line in lines:
if expect_next == EXPECT_LINE.ACCOUNT_NAME:
account_name = re.findall(PATTERN_ACCOUNT_NAME, line)
if account_name:
account_name = account_name[0]
result[account_name] = {}
expect_next = EXPECT_LINE.BLANK
elif expect_next == EXPECT_LINE.BLANK:
if line == "\n":
expect_next = EXPECT_LINE.HEADER
elif expect_next == EXPECT_LINE.HEADER:
header = re.findall(PATTERN_HEADER, line)
if header:
result[account_name] = []
expect_next = EXPECT_LINE.DATA
elif expect_next == EXPECT_LINE.DATA:
data = re.findall(PATTERN_DATA, line)
if data:
result[account_name].append(
{column: value for column, value in zip(header, data)}
)
else:
expect_next = EXPECT_LINE.ACCOUNT_NAME
else:
expect_next = EXPECT_LINE.ACCOUNT_NAME
return result
def upsert_comdirect_giro(
session: Session,
data: ComdirectGiro,
account_iban: str,
account_currency: str,
do_update=False,
do_commit=True,
) -> ComdirectGiroDb | None:
parameters_existing = {"hash_id": data.hash_id}
if not data.date_val:
return
parameters_new = {
"hash_id": data.hash_id,
"account_iban": account_iban,
"account_currency": account_currency,
"amount_currency": account_currency,
"date_valid": data.date_val,
"amount": data.amount_eur,
"date_book": data.date_book,
"type": data.type,
"text": data.text,
}
return _upsert(
session,
ComdirectGiroDb,
parameters_existing,
parameters_new,
do_update=do_update,
do_commit=do_commit,
)
def ingest_comdirect(
session: Session, path: Path, account_iban: str, account_currency: str
):
assert SOURCE_COMDIRECT
data_collection = reader_comdirect(path)
for account_name, data in data_collection.items():
account = SOURCE_COMDIRECT.get_account(account_name)
assert isinstance(account, Account)
if account_name == "Girokonto":
db_load_function = lambda session_, row_, account_: upsert_comdirect_giro(
session=session_,
data=row_,
account_iban=account_.iban,
account_currency=account_.currency,
do_commit=False,
)
else:
continue
for row in data:
data_validated = account.model.model_validate(row)
assert isinstance(data_validated, account.model)
db_load_function(session, row, account)
session.commit()
+20 -14
View File
@@ -1,29 +1,35 @@
import argparse
from pathlib import Path
from src.db import get_session
from src.helper_db import upsert_b4w
from src.reader import reader_b4w
def ingest_b4w(path: Path):
data = reader_b4w(path)
with get_session() as session:
for row in data:
upsert_b4w(session, row, do_commit=False)
session.commit()
from src.ingest.b4w import ingest_b4w
from src.ingest.comdirect import ingest_comdirect
from src.models.settings import Account
def main():
parser = argparse.ArgumentParser(
description="Eine App um den Überblick über Finanzen zu behalten"
)
parser.add_argument("input", help="Pfad einer einzulesenden Umsatzdatei (CSV)")
parser.add_argument(
"--input_b4w", help="Pfad einer einzulesenden Umsatzdatei (CSV)"
)
parser.add_argument(
"--input_comdirect", help="Pfad einer einzulesenden Umsatzdatei (CSV)"
)
args = parser.parse_args()
path = Path(args.input)
if args.input_b4w:
ingest_b4w(Path(args.input_b4w))
if args.input_comdirect:
import settings
ingest_b4w(path)
giro_account = settings.BANK_ACCOUNTS["comdirect"].get_account("Girokonto")
assert isinstance(giro_account, Account)
ingest_comdirect(
Path(args.input_comdirect),
account_iban=giro_account.iban or "",
account_currency=giro_account.currency or "",
)
if __name__ == "__main__":
-102
View File
@@ -1,102 +0,0 @@
from datetime import date
from decimal import Decimal
from enum import Enum
from pydantic import BaseModel, Field, field_validator
IBAN_PATTERN = r"^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$"
class Currency(str, Enum):
EUR = "EUR"
USD = "USD"
GBP = "GBP"
CHF = "CHF"
def __str__(self):
return self.value
def __repr__(self):
return f"Currency.{self.name}"
class Sign(str, Enum):
DEBIT = "DBIT"
CREDIT = "CRDT"
def __str__(self):
return self.value
def __repr__(self):
return f"Sign.{self.name}"
def to_sign(self) -> int:
return -1 if self is Sign.DEBIT else 1
class B4wExport(BaseModel):
id: int = Field(alias="Id1", description="internal B4w ID")
account_iban: str = Field(alias="OwnrAcctIBAN", pattern=IBAN_PATTERN)
account_currency: Currency = Field(alias="OwnrAcctCcy")
date_valid: date = Field(alias="ValDt")
date_book: date = Field(alias="BookgDt")
amount: Decimal = Field(alias="Amt")
amount_currency: Currency = Field(alias="AmtCcy")
sign: Sign = Field(alias="CdtDbtInd")
reference: str = Field(alias="RmtInf")
other_name: str = Field(alias="RmtdNm")
other_iban: str = Field(alias="RmtdAcctIBAN")
category: Category | None = Field(
alias="Category",
examples=[
"Freizeit",
"Beruf:Gehalt:super Firma",
"Lebenshaltung:Lebensmitteleinkäuft",
"",
],
)
notes: str = Field(alias="Notes")
@property
def amount_signed(self) -> Decimal:
return self.amount * self.sign.to_sign()
@field_validator("category", mode="before")
@classmethod
def category_str_is_parsable(
cls, category_in: str | Category | None
) -> Category | None:
if category_in is None or category_in == "":
return None
if isinstance(category_in, Category):
return category_in
category = Category.from_str(category_in)
if category is None or category.to_str() != category_in:
raise ValueError(f"Category string cannot be parsed: {category_in!r}")
return category
class Category(BaseModel):
name: str = Field(pattern=r"[^:\r\n]+")
parent: Category | None = None
@classmethod
def from_str(cls, s: str) -> Category | None:
if not s:
return
components = s.split(":")
name = components[-1]
rest = ":".join(components[:-1])
return cls(name=name, parent=cls.from_str(rest))
def to_str(self) -> str | None:
if not self.parent:
return self.name
else:
return f"{self.parent.to_str()}:{self.name}"
def __str__(self):
return self.to_str()
def __repr__(self):
return f'Category.from_str("{self.__str__()}")'
+7
View File
@@ -0,0 +1,7 @@
from src.models.b4w import B4wExport as B4wExport
from src.models.b4w import Category as Category
from src.models.comdirect import ComdirectCreditCard as ComdirectCreditCard
from src.models.comdirect import ComdirectDepot as ComdirectDepot
from src.models.comdirect import ComdirectGiro as ComdirectGiro
from src.models.settings import Account as Account
from src.models.settings import Source as Source
+99
View File
@@ -0,0 +1,99 @@
import hashlib
from datetime import date
from decimal import Decimal
from enum import Enum
from pydantic import BaseModel
from sqlalchemy import Column, Date, Integer, Numeric, String
from sqlalchemy.orm import DeclarativeBase
def _hash(*args):
raw = "".join(str(args))
return hashlib.sha256(raw.encode()).hexdigest()
class Sign(str, Enum):
DEBIT = "DBIT"
CREDIT = "CRDT"
def __str__(self):
return self.value
def __repr__(self):
return f"Sign.{self.name}"
def to_sign(self) -> int:
return -1 if self is Sign.DEBIT else 1
def _parse_date_german(date_str: str) -> date | None:
if date_str in ["offen", "neu", "--"]:
return None
day, month, year = list(map(int, date_str.split(".")))
return date(day=day, month=month, year=year)
def date2str(value: date | None, default="--", date_format="%Y-%m-%d") -> str:
if value:
return value.strftime(date_format)
return default
def _parse_decimal_german(decimal_str: str) -> Decimal:
return Decimal(decimal_str.replace(".", "").replace(",", "."))
def decimal2str(value: Decimal) -> str:
value = Decimal(f"{value:.2f}")
int_part, _, frac = str(value).partition(".")
groups: list[str] = []
while len(int_part) > 3:
groups.insert(0, int_part[-3:])
int_part = int_part[:-3]
groups.insert(0, int_part or "0")
return f"{'.'.join(groups)},{frac or '00'}"
type Csv2DictRow = dict[str, str]
type Csv2Dict = list[Csv2DictRow]
type CsV2DictMulti = dict[str, Csv2Dict]
class ExportBaseModel(BaseModel):
@classmethod
def _my_name(cls):
return cls.__name__
def parameters_to_dict(self) -> dict[str, str]:
return {}
def parameters_to_str(self):
parameters = ", ".join(
f"{key}='{value}'" for key, value in self.parameters_to_dict().items()
)
return parameters
def __repr__(self):
return f"{self._my_name()}({self.parameters_to_str()})"
def __str__(self):
return f"{self._my_name()}({self.parameters_to_str()})"
class BaseModelDb(DeclarativeBase):
pass
class IngestdbBase(BaseModelDb):
__abstract__ = True
id = Column(Integer, primary_key=True, autoincrement=True)
hash_id = Column(String, nullable=False)
account_iban = Column(String, nullable=False)
account_currency = Column(String(3), nullable=False)
date_valid = Column(Date, nullable=False)
amount = Column(Numeric(12, 2), nullable=False)
amount_currency = Column(String(3), nullable=False)
text = Column(String, nullable=True)
+151
View File
@@ -0,0 +1,151 @@
from datetime import date
from decimal import Decimal
from pydantic import Field, computed_field, field_validator
from sqlalchemy import Column, Date, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import relationship
from src.constants import IBAN_PATTERN, Currency
from src.models._common import (
BaseModelDb,
ExportBaseModel,
IngestdbBase,
Sign,
_hash,
date2str,
)
class B4wExport(ExportBaseModel):
id: int = Field(alias="Id1", description="internal B4w ID")
account_iban: str = Field(alias="OwnrAcctIBAN", pattern=IBAN_PATTERN)
account_currency: Currency = Field(alias="OwnrAcctCcy")
date_valid: date = Field(alias="ValDt")
date_book: date = Field(alias="BookgDt")
amount: Decimal = Field(alias="Amt", gt=0, decimal_places=2)
amount_currency: Currency = Field(alias="AmtCcy")
sign: Sign = Field(alias="CdtDbtInd")
reference: str = Field(alias="RmtInf")
other_name: str = Field(alias="RmtdNm")
other_iban: str | None = Field(alias="RmtdAcctIBAN", pattern=IBAN_PATTERN)
category: Category | None = Field(
alias="Category",
examples=[
"Freizeit",
"Beruf:Gehalt:super Firma",
"Lebenshaltung:Lebensmitteleinkäuft",
"",
],
)
notes: str = Field(alias="Notes")
def parameters_to_dict(self) -> dict[str, str]:
return {
"Id1": f"{self.id}",
"OwnrAcctIBAN": f"{self.account_iban!s}",
"OwnrAcctCcy": f"{self.account_currency!s}",
"ValDt": date2str(self.date_valid),
"BookgDt": date2str(self.date_book),
"Amt": f"{self.amount!s}",
"AmtCcy": f"{self.amount_currency!s}",
"CdtDbtInd": f"{self.sign!s}",
"RmtInf": f"{self.reference}",
"RmtdNm": f"{self.other_name}",
"RmtdAcctIBAN": f"{self.other_iban or ''}",
"Category": f"{self.category.to_str() if self.category else ''}",
"Notes": f"{self.notes}",
}
@computed_field
@property
def hash_id(self) -> str:
return _hash(self.id)
@property
def amount_signed(self) -> Decimal:
return self.amount * self.sign.to_sign()
@field_validator("other_iban", mode="before")
@classmethod
def other_iban_blank_is_none(cls, iban_in: str | None) -> str | None:
if iban_in is None or (isinstance(iban_in, str) and not iban_in.strip()):
return None
return iban_in
@field_validator("category", mode="before")
@classmethod
def category_str_is_parsable(
cls, category_in: str | Category | None
) -> Category | None:
if category_in is None or category_in == "":
return None
if isinstance(category_in, Category):
return category_in
category = Category.from_str(category_in)
if category is None or category.to_str() != category_in:
raise ValueError(f"Category string cannot be parsed: {category_in!r}")
return category
class Category(ExportBaseModel):
name: str = Field(pattern=r"[^:\r\n]+")
parent: Category | None = None
@computed_field
@property
def hash_id(self) -> str:
return _hash(self.name, self.parent)
@classmethod
def from_str(cls, s: str) -> Category | None:
if not s:
return
components = s.split(":")
name = components[-1]
rest = ":".join(components[:-1])
return cls(name=name, parent=cls.from_str(rest))
def to_str(self) -> str | None:
if not self.parent:
return self.name
else:
return f"{self.parent.to_str()}:{self.name}"
def __str__(self):
return self.to_str()
def __repr__(self):
return f'Category.from_str("{self.__str__()}")'
class CategoryDb(BaseModelDb):
__tablename__ = "category"
__table_args__ = (
UniqueConstraint("name", "parent_id", name="uq_category_name_parent"),
)
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
parent_id = Column(Integer, ForeignKey("category.id"), nullable=True)
# Relationships
parent = relationship(
"CategoryDb", remote_side="CategoryDb.id", back_populates="children"
)
children = relationship("CategoryDb", back_populates="parent")
UniqueConstraint("name", "parent_id", name="uq_category_name_parent")
class B4wDb(IngestdbBase):
__tablename__ = "import_b4w"
id_b4w = Column(Integer, nullable=False, unique=True)
date_book = Column(Date, nullable=True)
other_name = Column(String, nullable=True)
other_iban = Column(String, nullable=True)
category_id = Column(Integer, ForeignKey("category.id"), nullable=True)
notes = Column(String, nullable=True)
# relationship
category = relationship("CategoryDb")
+159
View File
@@ -0,0 +1,159 @@
from datetime import date
from decimal import Decimal
from pydantic import ConfigDict, Field, computed_field, field_validator
from sqlalchemy import Column, Date, String
from src.models._common import (
ExportBaseModel,
IngestdbBase,
_hash,
_parse_date_german,
_parse_decimal_german,
date2str,
decimal2str,
)
class ComdirectBase(ExportBaseModel):
model_config = ConfigDict(populate_by_name=True)
date_val: date | None = Field(
alias="Wertstellung (Valuta)", examples=["07.09.2026"]
)
amount_eur: Decimal = Field(alias="Umsatz in EUR", examples=["32,43"])
@field_validator("date_val", mode="before")
@classmethod
def parse_date_val(cls, date_str: str | None) -> date | None:
if date_str is None:
return
return _parse_date_german(date_str)
@field_validator("amount_eur", mode="before")
@classmethod
def parse_amount(cls, decimal_str: str) -> Decimal:
return _parse_decimal_german(decimal_str)
@classmethod
def _date2str(cls, date_value: date | None) -> str:
return date2str(date_value, default="--", date_format="%d.%m.%Y")
def parameters_to_dict(self, by_name=True) -> dict[str, str]:
return {
"date_val" if by_name else "Wertstellung (Valuta)": self._date2str(
self.date_val
),
"amount_eur" if by_name else "Umsatz in EUR": decimal2str(self.amount_eur),
}
def get_orig_headers(self, sep: str = ";", bracket: str = '"'):
return sep.join(
f"{bracket}{txt}{bracket}" for txt in self.parameters_to_dict(by_name=False)
)
def get_orig_row(self, sep: str = ";", bracket: str = '"'):
return sep.join(
f"{bracket}{txt}{bracket}"
for txt in self.parameters_to_dict(by_name=False).values()
)
class ComdirectGiro(ComdirectBase):
# # Fields from parents:
# date_val
# amount_eur
date_book: date | None = Field(alias="Buchungstag", examples=["07.09.2026"])
type: str = Field(
alias="Vorgang", examples=["Lastschrift / Belastung", "Kartenumsatz"]
)
text: str = Field(alias="Buchungstext")
@computed_field
@property
def hash_id(self) -> str:
return _hash(self.date_val, self.date_book, self.text)
@field_validator("date_book", mode="before")
@classmethod
def parse_date_book(cls, date_str: str) -> date | None:
return _parse_date_german(date_str)
def parameters_to_dict(self, by_name: bool = True) -> dict[str, str]:
parameters = dict(**super().parameters_to_dict(by_name=by_name))
parameters.update(
{
"date_book" if by_name else "Buchungstag": self._date2str(
self.date_book
),
"type" if by_name else "Vorgang": self.type,
"text" if by_name else "Buchungstext": self.text,
}
)
return parameters
class ComdirectCreditCard(ComdirectGiro):
# # Fields from parents:
date_book: date | None = Field(alias="Buchungstag", examples=["07.09.2026"])
date_val: date | None = Field(alias="Umsatztag", default=None)
type: str = Field(
alias="Vorgang",
examples=["Lastschrift / Belastung", "Kartenumsatz"],
default="",
)
reference: str = Field(alias="Referenz", 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]:
parameters = dict(**super().parameters_to_dict(by_name=by_name))
parameters.update(
{
"Umsatztag": self._date2str(self.date_val),
"Referenz": self.reference,
}
)
return parameters
class ComdirectDepot(ComdirectBase):
# # Fields from parents:
# amount_eur
date_val: date | None = Field(alias="Geschäftstag")
num: Decimal = Field(alias="Stück / Nom.")
name: str = Field(alias="Bezeichnung")
wkn: str = Field(alias="WKN")
currency: str = Field(alias="Währung")
spot_price: Decimal = Field(alias="Ausführungskurs")
@field_validator("num", "spot_price", mode="before")
@classmethod
def parse_decimals(cls, decimal_in: str) -> Decimal:
return _parse_decimal_german(decimal_in)
def parameters_to_dict(self, by_name: bool = True) -> dict[str, str]:
parameters = dict(**super().parameters_to_dict(by_name=by_name))
parameters.update(
{
"Geschäftstag": self._date2str(self.date_val),
"Stück / Nom.": decimal2str(self.num),
"Bezeichnung": self.name,
"WKN": self.wkn,
"Währung": self.currency,
"Ausführungskurs": decimal2str(self.spot_price),
}
)
return parameters
class ComdirectBaseDb(IngestdbBase):
__abstract__ = True
class ComdirectGiroDb(ComdirectBaseDb):
__tablename__ = "comdirect_giro"
type = Column(String, nullable=True)
date_book = Column(Date, nullable=True)
+26
View File
@@ -0,0 +1,26 @@
from pathlib import Path
from pydantic import BaseModel, Field
from src.constants import IBAN_PATTERN, Currency
from src.models.b4w import B4wExport
from src.models.comdirect import ComdirectCreditCard, ComdirectDepot, ComdirectGiro
class Account(BaseModel):
name: str
model: type[B4wExport | ComdirectGiro | ComdirectCreditCard | ComdirectDepot]
iban: str | None = Field(pattern=IBAN_PATTERN, default=None)
currency: Currency | None = None
class Source(BaseModel):
name: str
source_path: Path
accounts: list[Account]
def get_account_names(self) -> list[str]:
return [a.name for a in self.accounts]
def get_account(self, name: str) -> Account | None:
return next((a for a in self.accounts if a.name == name), None)
-54
View File
@@ -1,54 +0,0 @@
from sqlalchemy import (
Column,
Date,
ForeignKey,
Integer,
Numeric,
String,
UniqueConstraint,
)
from sqlalchemy.orm import DeclarativeBase, relationship
class BaseModelDb(DeclarativeBase):
pass
class CategoryDb(BaseModelDb):
__tablename__ = "category"
__table_args__ = (
UniqueConstraint("name", "parent_id", name="uq_category_name_parent"),
)
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
parent_id = Column(Integer, ForeignKey("category.id"), nullable=True)
# Relationships
parent = relationship(
"CategoryDb", remote_side="CategoryDb.id", back_populates="children"
)
children = relationship("CategoryDb", back_populates="parent")
UniqueConstraint("name", "parent_id", name="uq_category_name_parent")
class B4wDb(BaseModelDb):
__tablename__ = "import_b4w"
id = Column(Integer, primary_key=True, autoincrement=True)
id_b4w = Column(Integer, nullable=False, unique=True)
account_iban = Column(String, nullable=False)
account_currency = Column(String(3), nullable=False)
date_valid = Column(Date, nullable=False)
date_book = Column(Date, nullable=True)
amount = Column(Numeric(12, 2), nullable=False)
amount_currency = Column(String(3), nullable=False)
reference = Column(String, nullable=True)
other_name = Column(String, nullable=True)
other_iban = Column(String, nullable=True)
category_id = Column(Integer, ForeignKey("category.id"), nullable=True)
notes = Column(String, nullable=True)
# relationship
category = relationship("CategoryDb")
-61
View File
@@ -1,61 +0,0 @@
import csv
import re
from enum import Enum, auto
from pathlib import Path
from src.models import B4wExport
def reader_raw(path: Path):
with path.open() as fid:
return list(csv.DictReader(fid))
def reader_b4w(path: Path):
raw = reader_raw(path)
return list(map(B4wExport.model_validate, raw))
def reader_comdirect(path: Path):
PATTERN_ACCOUNT_NAME = r'^"Umsätze ([^"]+)";"Zeitraum: .*$'
PATTERN_HEADER = r'"([^"]+)"'
PATTERN_DATA = r'"([^"]+)"'
class EXPECT_LINE(Enum):
ACCOUNT_NAME = auto()
BLANK = auto()
HEADER = auto()
DATA = auto()
with path.open(encoding="latin1") as fid:
lines = fid.readlines()
expect_next = EXPECT_LINE.ACCOUNT_NAME
account_name = None
result = {}
for line in lines:
if expect_next == EXPECT_LINE.ACCOUNT_NAME:
account_name = re.findall(PATTERN_ACCOUNT_NAME, line)
if account_name:
account_name = account_name[0]
result[account_name] = {}
expect_next = EXPECT_LINE.BLANK
elif expect_next == EXPECT_LINE.BLANK:
if line == "\n":
expect_next = EXPECT_LINE.HEADER
elif expect_next == EXPECT_LINE.HEADER:
header = re.findall(PATTERN_HEADER, line)
if header:
result[account_name]["data"] = []
expect_next = EXPECT_LINE.DATA
elif expect_next == EXPECT_LINE.DATA:
data = re.findall(PATTERN_DATA, line)
if data:
result[account_name]["data"].append(
{column: value for column, value in zip(header, data)}
)
expect_next = EXPECT_LINE.ACCOUNT_NAME
else:
expect_next = EXPECT_LINE.ACCOUNT_NAME
return result
+93 -2
View File
@@ -1,9 +1,100 @@
from polyfactory.factories.pydantic_factory import ModelFactory
from decimal import Decimal
from typing import Any
from src.models import B4wExport
from faker import Faker
from polyfactory.factories.pydantic_factory import ModelFactory
from polyfactory.fields import Use
from src.models.b4w import B4wExport
from src.models.comdirect import ComdirectCreditCard, ComdirectDepot, ComdirectGiro
DEFAULT_CATEGORY = "root_category:parent_category:leaf_category"
def _faker_or_none(faker: Faker, non_default: Any, default: Any = None, p_value=0.5):
if faker.pyfloat(min_value=0.0, max_value=1.0) < p_value:
return default
return non_default
def _amount() -> Decimal:
return B4wExportFactory.__faker__.pydecimal(
left_digits=6,
right_digits=2,
positive=True,
min_value=Decimal("0.01"),
)
def _amount_eur() -> str:
"""Generate German-formatted decimal string (e.g., '32,43')."""
val = Faker().pydecimal(
right_digits=2, min_value=Decimal("-4000.00"), max_value=Decimal("4000.00")
)
# Format as German string with comma decimal separator
return f"{val:.2f}".replace(".", ",").strip()
def _german_date() -> str:
return Faker().date("%d.%m.%Y")
def _sentence() -> str:
return B4wExportFactory.__faker__.sentence(
nb_words=8,
variable_nb_words=False,
)
def _names() -> str:
return B4wExportFactory.__faker__.company()
def _iban() -> str:
return B4wExportFactory.__faker__.iban()
def _iban_or_none() -> str:
return _faker_or_none(Faker(), Faker().iban(), default="")
class B4wExportFactory(ModelFactory[B4wExport]):
__faker__ = Faker("de_DE")
__use_examples__ = True
Amt = Use(_amount)
RmtInf = Use(_sentence)
Notes = Use(_sentence)
RmtdNm = Use(_names)
OwnrAcctIBAN = Use(_iban)
RmtdAcctIBAN = Use(_iban_or_none)
class ComdirectGiroFactory(ModelFactory[ComdirectGiro]):
__faker__ = Faker("de_DE")
__use_examples__ = True
text = Use(_sentence)
amount_eur = Use(_amount_eur)
date_val = Use(_german_date)
date_book = Use(_german_date)
class ComdirectCreditCardFactory(ModelFactory[ComdirectCreditCard]):
__faker__ = Faker("de_DE")
__use_examples__ = True
text = Use(_sentence)
amount_eur = Use(_amount_eur)
date_val = Use(_german_date)
date_book = Use(_german_date)
class ComdirectDepotFactory(ModelFactory[ComdirectDepot]):
__faker__ = Faker("de_DE")
__use_examples__ = True
amount_eur = Use(_amount_eur)
date_val = Use(_german_date)
num = Use(_amount_eur)
spot_price = Use(_amount_eur)
+51
View File
@@ -0,0 +1,51 @@
Id1,OwnrAcctIBAN,OwnrAcctCcy,ValDt,BookgDt,Amt,AmtCcy,CdtDbtInd,RmtInf,RmtdNm,RmtdAcctIBAN,Category,Notes
1036,DE07271846982018433140,GBP,2026-07-04,2023-10-12,608251.74,CHF,CRDT,Wohl Straße als gut Zeit laufen wenn Wissen.,Scholtz AG,DE05340442071223320995,Beruf:Gehalt:super Firma,Krank mich erschrecken Leute den nehmen fahren man.
4849,DE42187219246703133408,CHF,2023-09-10,2025-08-05,725899.19,USD,CRDT,Berg im Bruder fröhlich Herr wahr wer leben.,Oestrovsky,DE58802771240736400847,Freizeit,Maus wohnen vergessen Vater plötzlich Schule Meer fallen.
1914,DE70452496991202055362,USD,2025-04-15,2025-12-24,984194.63,GBP,CRDT,Schreiben sollen voll ab Wort kochen schlimm wohl.,Beckmann,DE59790637683034706744,Freizeit,Anfangen fünf weg Woche Boden schreiben hier hängen.
6930,DE67943298499847570990,USD,2025-11-09,2021-09-08,156898.87,USD,DBIT,Verlieren sein dich Papa fragen ließ denn packen.,Heinz,DE54265758309837023543,Beruf:Gehalt:super Firma,Halbe so Leute Bild Schuh an früh traurig.
1699,DE84149253738055473900,EUR,2026-08-01,2021-07-20,818063.51,EUR,CRDT,Rad grün schlecht Pferd beide noch erschrecken Uhr.,Hofmann Steinberg Stiftung & Co. KGaA,DE21220975806758335585,,Neun man kam sechs blau Sache langsam Blume.
7046,DE82274490061461449020,EUR,2023-01-23,2024-05-14,669696.87,USD,DBIT,Vogel hinein Tante brauchen Weihnachten nicht klein braun.,Hübel GmbH,DE39283738756876306318,Freizeit,Ja Loch Winter rufen laufen Woche gelb hinter.
366,DE25389044843010198038,USD,2024-02-09,2023-03-08,220167.30,USD,DBIT,Gefährlich nein Mann nie schlecht Arbeit treffen damit.,Scheuermann,DE77031936189338919289,Lebenshaltung:Lebensmitteleinkäuft,Sofort Blume ging warten gewinnen kaufen Wissen später.
6922,DE73307102808802733774,USD,2025-03-20,2023-01-30,129827.89,EUR,DBIT,Rennen jetzt damit Brief See dumm stellen legen.,Reuter auch Schlauchin GmbH & Co. KG,DE22372707001113794894,Lebenshaltung:Lebensmitteleinkäuft,Hart ins sagen fest eigentlich wo schlimm ganz.
9114,DE16057368814344996724,USD,2021-03-08,2020-10-16,725225.13,EUR,DBIT,Kam waschen Wohnung Sache sprechen kaufen gern spielen.,Hentschel Wilms KG,DE41311487016568247161,Beruf:Gehalt:super Firma,Haare Winter bauen Mädchen gesund Teller hoch deshalb.
6739,DE05970178849203811396,GBP,2020-05-13,2020-08-18,333023.84,GBP,CRDT,Bin schicken unser Tier will endlich dem gefährlich.,Heuser Wiek KG,DE75493228729510129693,Beruf:Gehalt:super Firma,Versuchen etwas fertig den schnell fast Bein Freude.
1278,DE96528552228817399257,CHF,2024-07-25,2024-11-16,385205.95,EUR,CRDT,Auge darin ohne Schnee Frage stark mein oft.,Scholl,DE70979826731102142893,Lebenshaltung:Lebensmitteleinkäuft,Spät Minutenmir Brief Essen geben heiß nah warm.
406,DE89506092918989440859,EUR,2024-11-13,2026-03-18,978962.38,CHF,DBIT,Kaufen turnen Weg dick ging vom rund Sommer.,Thies,DE36862593824516545526,Beruf:Gehalt:super Firma,Für Zeit Fenster Abend warten Geschichte dann stellen.
2026,DE14445771047668409127,GBP,2020-05-06,2024-02-20,967493.86,EUR,CRDT,Glück verlieren Wetter Bauer turnen baden Freude spät.,Mohaupt GmbH & Co. KG,DE52407611264263373889,,Winter nicht bis beide hängen Rad Meer lassen.
3041,DE58304085373743696607,USD,2024-03-10,2026-09-08,807803.15,EUR,CRDT,Auch schlimm groß bauen Welt das alt alle.,Hartmann AG,DE37816718253864239200,Lebenshaltung:Lebensmitteleinkäuft,Spiel erst nicht gelb Vogel über fahren Hilfe.
5405,DE05257576358009350425,GBP,2024-05-30,2022-05-29,611553.21,EUR,DBIT,Darin gut ihn reich mit mögen dauern Ferien.,Rosenow,DE29315257044202664260,Lebenshaltung:Lebensmitteleinkäuft,Tief Tag Polizei Zahl Sommer Abend neun vom.
6543,DE91371884570994329173,CHF,2025-07-16,2022-09-05,762232.56,USD,DBIT,Monate Glas waschen Brief fliegen gesund Essen kam.,Jäntsch Ullrich AG & Co. KG,DE33181542563621737977,,Versuchen waschen gestern Fahrrad Arbeit zwischen können ich.
5190,DE13303497125457728906,CHF,2020-06-26,2022-01-18,944585.95,EUR,DBIT,Das Sonne Klasse frei beide dort Vogel fiel.,Margraf,DE67573080640653441068,Beruf:Gehalt:super Firma,Reich fiel führen Tisch fertig gesund Stein Wagen.
6694,DE32400010286267483201,GBP,2024-02-15,2022-09-05,896628.93,EUR,CRDT,Genau Wiese danach auf Land Fuß Zahl braun.,Weinhold,DE82843035570517646373,Beruf:Gehalt:super Firma,Fehlen Licht hinein Frage möglich Hund sechs danach.
2153,DE70374437913789686109,USD,2024-02-23,2020-07-19,809680.27,USD,CRDT,Schon ließ Arzt Arbeit müde hoch Tag auch.,Roskoth,DE61342749791049237138,Beruf:Gehalt:super Firma,Zu weit Freund Brief noch durch seit sofort.
1462,DE07599923076063186293,USD,2022-03-05,2021-11-29,563454.79,CHF,CRDT,Bei stark über denn wo Lehrer gibt las.,Gehringer AG,DE71382372368571415704,Beruf:Gehalt:super Firma,Schwarz finden laut werden Zug Vater aber schreiben.
4268,DE75725070066084697801,USD,2023-12-21,2025-06-24,461694.56,GBP,DBIT,Schule Küche Erde letzte Schnee halten und sechs.,Scholtz Pärtzelt AG & Co. KG,DE63255914448215384283,Lebenshaltung:Lebensmitteleinkäuft,Zurück spät trinken Tür Stunde Boden Licht Ende.
3951,DE05688473582233220472,GBP,2022-08-15,2023-01-18,998250.23,GBP,DBIT,Weit draußen Feuer eigentlich wünschen Wagen fertig tragen.,Binner AG & Co. KGaA,DE73947638506756550295,Beruf:Gehalt:super Firma,Dumm verlieren nehmen erschrecken fressen zurück hängen weit.
1124,DE90298843179938014585,GBP,2023-11-14,2023-07-05,371813.63,USD,DBIT,Platz fragen wer weinen Geburtstag ganz einigen gar.,Boucsein Stiftung & Co. KG,DE05290733575870594578,Lebenshaltung:Lebensmitteleinkäuft,Weg andere wohnen steigen Hilfe schaffen mehr das.
1744,DE13238979080075655943,GBP,2022-12-14,2025-02-19,579578.97,EUR,DBIT,Immer fiel wieder überall bald legen Tisch wahr.,Heser Hahn GmbH,DE68405054791747194888,Lebenshaltung:Lebensmitteleinkäuft,Fünf See neu früh wird fehlen sonst wer.
5892,DE04011963778496207452,USD,2025-05-20,2023-05-10,648012.28,EUR,DBIT,Polizei weinen gut Kind immer Ding oder Licht.,Jacobi Jäckel,DE13372573108475954117,Lebenshaltung:Lebensmitteleinkäuft,Fragen immer acht Sache heute nämlich Platz warum.
362,DE92124261075456158918,EUR,2022-10-26,2025-08-31,299548.47,CHF,DBIT,Laufen ihr lassen Pferd ruhig müssen Hund unten.,Sölzer Meyer OHG mbH,DE93416735067530701655,Beruf:Gehalt:super Firma,Schwimmen voll neun beim sehr dich Ferien Nase.
9327,DE56968036606213916258,EUR,2022-09-12,2025-06-10,669422.17,EUR,CRDT,Reich kochen Ende früher denken Gott bauen Wagen.,Rogge Carsten AG & Co. KG,DE17055312400520689014,,Schwer verkaufen oder gefährlich sprechen Hunger rot ins.
680,DE41089047416450094816,GBP,2025-06-19,2020-11-14,514360.52,EUR,DBIT,Essen schlimm schnell sich vor drei Ding schwimmen.,Stroh Margraf GmbH,DE30951508691902962996,Freizeit,Drei gelb Meer mit rot Hand da führen.
4786,DE42895215744806933501,USD,2024-10-12,2021-06-23,421775.78,USD,DBIT,Rennen rund und acht werden wieder richtig Tür.,Pieper KG,DE50031826122338744320,Lebenshaltung:Lebensmitteleinkäuft,Monate Licht bis Himmel nächste von Stück ab.
5155,DE48350990582715356385,EUR,2024-01-16,2020-07-12,132026.65,CHF,DBIT,Freude setzen offen bauen offen aus Bruder wirklich.,Oestrovsky Mülichen Stiftung & Co. KG,DE31183689182369696496,,Hund legen Schiff einige Brief schlecht jeder voll.
9202,DE79223829316538123746,CHF,2024-10-23,2026-09-07,683650.17,GBP,CRDT,Platz gelb heiß uns Abend schreien bis ziehen.,Meyer OHG mbH,DE85492444746577878500,Lebenshaltung:Lebensmitteleinkäuft,Angst schwarz war Auge darauf Lehrerin gerade oft.
4370,DE96935887403881776524,CHF,2022-03-09,2025-06-12,238525.09,GBP,DBIT,Sitzen Katze vergessen schicken laufen spielen deshalb weiter.,Müller,DE64513317359137578745,,Gesicht fangen klein Glas ein ohne seit Gesicht.
6861,DE53091086333961111752,EUR,2022-01-18,2020-04-09,949477.16,GBP,DBIT,Gerade genau dürfen gelb klettern nächste zum Bauer.,Schleich,DE55073850335806125294,Beruf:Gehalt:super Firma,Mit weil rechnen Hund Wiese einmal ja gern.
1968,DE98454978214248264491,GBP,2024-01-14,2022-09-19,435993.30,CHF,CRDT,Apfel setzen Polizei einfach Minutenmir schnell zwischen vom.,Schulz GmbH,DE08932615790658012392,Beruf:Gehalt:super Firma,Schwer spielen Leben sind brauchen Finger dabei Essen.
934,DE72179461629364066237,GBP,2020-12-07,2023-12-31,936759.29,GBP,CRDT,Selbst aber treffen springen setzen hoch lesen zusammen.,Textor Schaaf AG & Co. KG,DE68736100578473881809,Lebenshaltung:Lebensmitteleinkäuft,Warm unser gehören Zug früher schlagen stehen langsam.
288,DE36079718826004443058,USD,2025-03-20,2024-07-16,410828.73,GBP,DBIT,Bauen Geschenk Hund beide Stück Sohn ab denken.,Drewes,DE37024088386878351977,Lebenshaltung:Lebensmitteleinkäuft,Dürfen jetzt danach Oma See alt Flasche sofort.
909,DE07992119720440262287,CHF,2024-02-20,2020-10-05,713491.26,GBP,CRDT,Ende krank Arzt Wohnung finden vom Blume Monat.,Kohl Ortmann AG,DE46570978124442579293,,Heute Haus nach ab heißen versuchen Schüler wahr.
6200,DE65667575255962659468,USD,2024-12-29,2023-07-30,859574.20,EUR,CRDT,Erklären schreiben Frau kann leise Papa Arzt fliegen.,Wulf Grein Groth AG & Co. KG,DE45122342340503421393,Freizeit,Pferd gut schenken spät wirklich Kind bauen lange.
5198,DE82737311374514326219,USD,2022-09-14,2025-12-23,899940.80,EUR,CRDT,Glauben wissen ab sehr zwischen Frau essen Minutenmir.,Schwital Mohaupt Stiftung & Co. KGaA,DE31111358336117661206,Lebenshaltung:Lebensmitteleinkäuft,Ihm rechnen drehen etwas jeder Gesicht zusammen vorbei.
2088,DE84805952945908475488,GBP,2020-06-23,2023-03-19,706456.94,CHF,CRDT,Gerade zeigen Stück ziehen dumm Minutenmir alt Brief.,Thies GbR,DE30848702698273936264,,Fröhlich kaufen im gefährlich gar Zeitung leben voll.
4094,DE96513254882011465283,USD,2021-03-12,2023-08-10,844148.26,GBP,DBIT,Fußball führen Tier nichts hoch Lehrerin brauchen grün.,Meister,DE39122919808000752227,Beruf:Gehalt:super Firma,Müde schauen rechnen Geschichte Apfel kann daran weinen.
407,DE34550143031579571279,GBP,2020-03-01,2021-10-24,802001.66,EUR,CRDT,Scheinen wahr Luft merken Hilfe Sommer Weg sprechen.,Sager AG,DE95396414587780784446,Freizeit,Wasser jeder los werfen Loch Kopf Sommer stehen.
1882,DE93756550001006525563,GBP,2020-05-22,2020-04-02,255936.18,CHF,CRDT,Schüler Wetter unser Nase lesen erklären Herr damit.,Bohlander e.G.,DE33912765450255003824,Lebenshaltung:Lebensmitteleinkäuft,Vielleicht Papa Sonntag Glück dem krank Ende Lehrer.
674,DE34636975346401640379,GBP,2025-04-21,2023-07-13,107958.15,GBP,CRDT,Um rufen Schnee nämlich leise Tag gesund hin.,Ruppersberger Förster AG & Co. KG,DE82042132477947420315,Beruf:Gehalt:super Firma,Ich wo Oma Leute gegen früh Herz hoch.
4938,DE18280828921382275721,CHF,2024-09-06,2026-06-01,307489.25,USD,CRDT,Zehn los stark wissen vor dein an Schule.,Holsten Bien e.G.,DE54634488965756369503,Freizeit,Er merken Seite von früher schlimm weit gestern.
7493,DE75969937757392474988,EUR,2024-07-01,2025-05-15,769646.98,USD,CRDT,Wollen kalt alt uns acht heißen deshalb Licht.,Losekann Mentzel AG,DE06861731830241040674,Beruf:Gehalt:super Firma,Sehen los Herr dort ließ fährt gibt als.
7741,DE27930829328207938603,USD,2023-12-10,2021-12-25,332130.35,CHF,DBIT,Unser Essen Ferien ließ Lehrerin bringen trinken wichtig.,Beer Dehmel OHG mbH,DE56877468032283033000,Lebenshaltung:Lebensmitteleinkäuft,Weil Dorf verlieren deshalb wirklich Himmel dein schreien.
8664,DE17107663775980137420,USD,2020-12-02,2026-04-02,695808.54,EUR,CRDT,Da aber frei schlafen sicher Ding schreiben endlich.,Steckel,DE34731945010152636545,Beruf:Gehalt:super Firma,Plötzlich beißen bringen Boden auch verstecken lieb vorbei.
7929,DE32942674407704323077,CHF,2020-09-06,2026-09-09,165324.34,EUR,CRDT,Sehen ob Bruder drei Kind dumm Hilfe auf.,Spieß Fröhlich KGaA,DE68174009519788157086,Freizeit,Stunde Hilfe ganz nie las kam oft voll.
8900,DE03689805605701261449,USD,2021-03-03,2020-11-12,186334.15,USD,CRDT,Brief gern der Eis Freude lange schlagen Flasche.,Radisch Keudel GbR,DE49885385858776558412,Lebenshaltung:Lebensmitteleinkäuft,Dafür singen ein sonst uns Mensch ins Ding.
1 Id1 OwnrAcctIBAN OwnrAcctCcy ValDt BookgDt Amt AmtCcy CdtDbtInd RmtInf RmtdNm RmtdAcctIBAN Category Notes
2 1036 DE07271846982018433140 GBP 2026-07-04 2023-10-12 608251.74 CHF CRDT Wohl Straße als gut Zeit laufen wenn Wissen. Scholtz AG DE05340442071223320995 Beruf:Gehalt:super Firma Krank mich erschrecken Leute den nehmen fahren man.
3 4849 DE42187219246703133408 CHF 2023-09-10 2025-08-05 725899.19 USD CRDT Berg im Bruder fröhlich Herr wahr wer leben. Oestrovsky DE58802771240736400847 Freizeit Maus wohnen vergessen Vater plötzlich Schule Meer fallen.
4 1914 DE70452496991202055362 USD 2025-04-15 2025-12-24 984194.63 GBP CRDT Schreiben sollen voll ab Wort kochen schlimm wohl. Beckmann DE59790637683034706744 Freizeit Anfangen fünf weg Woche Boden schreiben hier hängen.
5 6930 DE67943298499847570990 USD 2025-11-09 2021-09-08 156898.87 USD DBIT Verlieren sein dich Papa fragen ließ denn packen. Heinz DE54265758309837023543 Beruf:Gehalt:super Firma Halbe so Leute Bild Schuh an früh traurig.
6 1699 DE84149253738055473900 EUR 2026-08-01 2021-07-20 818063.51 EUR CRDT Rad grün schlecht Pferd beide noch erschrecken Uhr. Hofmann Steinberg Stiftung & Co. KGaA DE21220975806758335585 Neun man kam sechs blau Sache langsam Blume.
7 7046 DE82274490061461449020 EUR 2023-01-23 2024-05-14 669696.87 USD DBIT Vogel hinein Tante brauchen Weihnachten nicht klein braun. Hübel GmbH DE39283738756876306318 Freizeit Ja Loch Winter rufen laufen Woche gelb hinter.
8 366 DE25389044843010198038 USD 2024-02-09 2023-03-08 220167.30 USD DBIT Gefährlich nein Mann nie schlecht Arbeit treffen damit. Scheuermann DE77031936189338919289 Lebenshaltung:Lebensmitteleinkäuft Sofort Blume ging warten gewinnen kaufen Wissen später.
9 6922 DE73307102808802733774 USD 2025-03-20 2023-01-30 129827.89 EUR DBIT Rennen jetzt damit Brief See dumm stellen legen. Reuter auch Schlauchin GmbH & Co. KG DE22372707001113794894 Lebenshaltung:Lebensmitteleinkäuft Hart ins sagen fest eigentlich wo schlimm ganz.
10 9114 DE16057368814344996724 USD 2021-03-08 2020-10-16 725225.13 EUR DBIT Kam waschen Wohnung Sache sprechen kaufen gern spielen. Hentschel Wilms KG DE41311487016568247161 Beruf:Gehalt:super Firma Haare Winter bauen Mädchen gesund Teller hoch deshalb.
11 6739 DE05970178849203811396 GBP 2020-05-13 2020-08-18 333023.84 GBP CRDT Bin schicken unser Tier will endlich dem gefährlich. Heuser Wiek KG DE75493228729510129693 Beruf:Gehalt:super Firma Versuchen etwas fertig den schnell fast Bein Freude.
12 1278 DE96528552228817399257 CHF 2024-07-25 2024-11-16 385205.95 EUR CRDT Auge darin ohne Schnee Frage stark mein oft. Scholl DE70979826731102142893 Lebenshaltung:Lebensmitteleinkäuft Spät Minutenmir Brief Essen geben heiß nah warm.
13 406 DE89506092918989440859 EUR 2024-11-13 2026-03-18 978962.38 CHF DBIT Kaufen turnen Weg dick ging vom rund Sommer. Thies DE36862593824516545526 Beruf:Gehalt:super Firma Für Zeit Fenster Abend warten Geschichte dann stellen.
14 2026 DE14445771047668409127 GBP 2020-05-06 2024-02-20 967493.86 EUR CRDT Glück verlieren Wetter Bauer turnen baden Freude spät. Mohaupt GmbH & Co. KG DE52407611264263373889 Winter nicht bis beide hängen Rad Meer lassen.
15 3041 DE58304085373743696607 USD 2024-03-10 2026-09-08 807803.15 EUR CRDT Auch schlimm groß bauen Welt das alt alle. Hartmann AG DE37816718253864239200 Lebenshaltung:Lebensmitteleinkäuft Spiel erst nicht gelb Vogel über fahren Hilfe.
16 5405 DE05257576358009350425 GBP 2024-05-30 2022-05-29 611553.21 EUR DBIT Darin gut ihn reich mit mögen dauern Ferien. Rosenow DE29315257044202664260 Lebenshaltung:Lebensmitteleinkäuft Tief Tag Polizei Zahl Sommer Abend neun vom.
17 6543 DE91371884570994329173 CHF 2025-07-16 2022-09-05 762232.56 USD DBIT Monate Glas waschen Brief fliegen gesund Essen kam. Jäntsch Ullrich AG & Co. KG DE33181542563621737977 Versuchen waschen gestern Fahrrad Arbeit zwischen können ich.
18 5190 DE13303497125457728906 CHF 2020-06-26 2022-01-18 944585.95 EUR DBIT Das Sonne Klasse frei beide dort Vogel fiel. Margraf DE67573080640653441068 Beruf:Gehalt:super Firma Reich fiel führen Tisch fertig gesund Stein Wagen.
19 6694 DE32400010286267483201 GBP 2024-02-15 2022-09-05 896628.93 EUR CRDT Genau Wiese danach auf Land Fuß Zahl braun. Weinhold DE82843035570517646373 Beruf:Gehalt:super Firma Fehlen Licht hinein Frage möglich Hund sechs danach.
20 2153 DE70374437913789686109 USD 2024-02-23 2020-07-19 809680.27 USD CRDT Schon ließ Arzt Arbeit müde hoch Tag auch. Roskoth DE61342749791049237138 Beruf:Gehalt:super Firma Zu weit Freund Brief noch durch seit sofort.
21 1462 DE07599923076063186293 USD 2022-03-05 2021-11-29 563454.79 CHF CRDT Bei stark über denn wo Lehrer gibt las. Gehringer AG DE71382372368571415704 Beruf:Gehalt:super Firma Schwarz finden laut werden Zug Vater aber schreiben.
22 4268 DE75725070066084697801 USD 2023-12-21 2025-06-24 461694.56 GBP DBIT Schule Küche Erde letzte Schnee halten und sechs. Scholtz Pärtzelt AG & Co. KG DE63255914448215384283 Lebenshaltung:Lebensmitteleinkäuft Zurück spät trinken Tür Stunde Boden Licht Ende.
23 3951 DE05688473582233220472 GBP 2022-08-15 2023-01-18 998250.23 GBP DBIT Weit draußen Feuer eigentlich wünschen Wagen fertig tragen. Binner AG & Co. KGaA DE73947638506756550295 Beruf:Gehalt:super Firma Dumm verlieren nehmen erschrecken fressen zurück hängen weit.
24 1124 DE90298843179938014585 GBP 2023-11-14 2023-07-05 371813.63 USD DBIT Platz fragen wer weinen Geburtstag ganz einigen gar. Boucsein Stiftung & Co. KG DE05290733575870594578 Lebenshaltung:Lebensmitteleinkäuft Weg andere wohnen steigen Hilfe schaffen mehr das.
25 1744 DE13238979080075655943 GBP 2022-12-14 2025-02-19 579578.97 EUR DBIT Immer fiel wieder überall bald legen Tisch wahr. Heser Hahn GmbH DE68405054791747194888 Lebenshaltung:Lebensmitteleinkäuft Fünf See neu früh wird fehlen sonst wer.
26 5892 DE04011963778496207452 USD 2025-05-20 2023-05-10 648012.28 EUR DBIT Polizei weinen gut Kind immer Ding oder Licht. Jacobi Jäckel DE13372573108475954117 Lebenshaltung:Lebensmitteleinkäuft Fragen immer acht Sache heute nämlich Platz warum.
27 362 DE92124261075456158918 EUR 2022-10-26 2025-08-31 299548.47 CHF DBIT Laufen ihr lassen Pferd ruhig müssen Hund unten. Sölzer Meyer OHG mbH DE93416735067530701655 Beruf:Gehalt:super Firma Schwimmen voll neun beim sehr dich Ferien Nase.
28 9327 DE56968036606213916258 EUR 2022-09-12 2025-06-10 669422.17 EUR CRDT Reich kochen Ende früher denken Gott bauen Wagen. Rogge Carsten AG & Co. KG DE17055312400520689014 Schwer verkaufen oder gefährlich sprechen Hunger rot ins.
29 680 DE41089047416450094816 GBP 2025-06-19 2020-11-14 514360.52 EUR DBIT Essen schlimm schnell sich vor drei Ding schwimmen. Stroh Margraf GmbH DE30951508691902962996 Freizeit Drei gelb Meer mit rot Hand da führen.
30 4786 DE42895215744806933501 USD 2024-10-12 2021-06-23 421775.78 USD DBIT Rennen rund und acht werden wieder richtig Tür. Pieper KG DE50031826122338744320 Lebenshaltung:Lebensmitteleinkäuft Monate Licht bis Himmel nächste von Stück ab.
31 5155 DE48350990582715356385 EUR 2024-01-16 2020-07-12 132026.65 CHF DBIT Freude setzen offen bauen offen aus Bruder wirklich. Oestrovsky Mülichen Stiftung & Co. KG DE31183689182369696496 Hund legen Schiff einige Brief schlecht jeder voll.
32 9202 DE79223829316538123746 CHF 2024-10-23 2026-09-07 683650.17 GBP CRDT Platz gelb heiß uns Abend schreien bis ziehen. Meyer OHG mbH DE85492444746577878500 Lebenshaltung:Lebensmitteleinkäuft Angst schwarz war Auge darauf Lehrerin gerade oft.
33 4370 DE96935887403881776524 CHF 2022-03-09 2025-06-12 238525.09 GBP DBIT Sitzen Katze vergessen schicken laufen spielen deshalb weiter. Müller DE64513317359137578745 Gesicht fangen klein Glas ein ohne seit Gesicht.
34 6861 DE53091086333961111752 EUR 2022-01-18 2020-04-09 949477.16 GBP DBIT Gerade genau dürfen gelb klettern nächste zum Bauer. Schleich DE55073850335806125294 Beruf:Gehalt:super Firma Mit weil rechnen Hund Wiese einmal ja gern.
35 1968 DE98454978214248264491 GBP 2024-01-14 2022-09-19 435993.30 CHF CRDT Apfel setzen Polizei einfach Minutenmir schnell zwischen vom. Schulz GmbH DE08932615790658012392 Beruf:Gehalt:super Firma Schwer spielen Leben sind brauchen Finger dabei Essen.
36 934 DE72179461629364066237 GBP 2020-12-07 2023-12-31 936759.29 GBP CRDT Selbst aber treffen springen setzen hoch lesen zusammen. Textor Schaaf AG & Co. KG DE68736100578473881809 Lebenshaltung:Lebensmitteleinkäuft Warm unser gehören Zug früher schlagen stehen langsam.
37 288 DE36079718826004443058 USD 2025-03-20 2024-07-16 410828.73 GBP DBIT Bauen Geschenk Hund beide Stück Sohn ab denken. Drewes DE37024088386878351977 Lebenshaltung:Lebensmitteleinkäuft Dürfen jetzt danach Oma See alt Flasche sofort.
38 909 DE07992119720440262287 CHF 2024-02-20 2020-10-05 713491.26 GBP CRDT Ende krank Arzt Wohnung finden vom Blume Monat. Kohl Ortmann AG DE46570978124442579293 Heute Haus nach ab heißen versuchen Schüler wahr.
39 6200 DE65667575255962659468 USD 2024-12-29 2023-07-30 859574.20 EUR CRDT Erklären schreiben Frau kann leise Papa Arzt fliegen. Wulf Grein Groth AG & Co. KG DE45122342340503421393 Freizeit Pferd gut schenken spät wirklich Kind bauen lange.
40 5198 DE82737311374514326219 USD 2022-09-14 2025-12-23 899940.80 EUR CRDT Glauben wissen ab sehr zwischen Frau essen Minutenmir. Schwital Mohaupt Stiftung & Co. KGaA DE31111358336117661206 Lebenshaltung:Lebensmitteleinkäuft Ihm rechnen drehen etwas jeder Gesicht zusammen vorbei.
41 2088 DE84805952945908475488 GBP 2020-06-23 2023-03-19 706456.94 CHF CRDT Gerade zeigen Stück ziehen dumm Minutenmir alt Brief. Thies GbR DE30848702698273936264 Fröhlich kaufen im gefährlich gar Zeitung leben voll.
42 4094 DE96513254882011465283 USD 2021-03-12 2023-08-10 844148.26 GBP DBIT Fußball führen Tier nichts hoch Lehrerin brauchen grün. Meister DE39122919808000752227 Beruf:Gehalt:super Firma Müde schauen rechnen Geschichte Apfel kann daran weinen.
43 407 DE34550143031579571279 GBP 2020-03-01 2021-10-24 802001.66 EUR CRDT Scheinen wahr Luft merken Hilfe Sommer Weg sprechen. Sager AG DE95396414587780784446 Freizeit Wasser jeder los werfen Loch Kopf Sommer stehen.
44 1882 DE93756550001006525563 GBP 2020-05-22 2020-04-02 255936.18 CHF CRDT Schüler Wetter unser Nase lesen erklären Herr damit. Bohlander e.G. DE33912765450255003824 Lebenshaltung:Lebensmitteleinkäuft Vielleicht Papa Sonntag Glück dem krank Ende Lehrer.
45 674 DE34636975346401640379 GBP 2025-04-21 2023-07-13 107958.15 GBP CRDT Um rufen Schnee nämlich leise Tag gesund hin. Ruppersberger Förster AG & Co. KG DE82042132477947420315 Beruf:Gehalt:super Firma Ich wo Oma Leute gegen früh Herz hoch.
46 4938 DE18280828921382275721 CHF 2024-09-06 2026-06-01 307489.25 USD CRDT Zehn los stark wissen vor dein an Schule. Holsten Bien e.G. DE54634488965756369503 Freizeit Er merken Seite von früher schlimm weit gestern.
47 7493 DE75969937757392474988 EUR 2024-07-01 2025-05-15 769646.98 USD CRDT Wollen kalt alt uns acht heißen deshalb Licht. Losekann Mentzel AG DE06861731830241040674 Beruf:Gehalt:super Firma Sehen los Herr dort ließ fährt gibt als.
48 7741 DE27930829328207938603 USD 2023-12-10 2021-12-25 332130.35 CHF DBIT Unser Essen Ferien ließ Lehrerin bringen trinken wichtig. Beer Dehmel OHG mbH DE56877468032283033000 Lebenshaltung:Lebensmitteleinkäuft Weil Dorf verlieren deshalb wirklich Himmel dein schreien.
49 8664 DE17107663775980137420 USD 2020-12-02 2026-04-02 695808.54 EUR CRDT Da aber frei schlafen sicher Ding schreiben endlich. Steckel DE34731945010152636545 Beruf:Gehalt:super Firma Plötzlich beißen bringen Boden auch verstecken lieb vorbei.
50 7929 DE32942674407704323077 CHF 2020-09-06 2026-09-09 165324.34 EUR CRDT Sehen ob Bruder drei Kind dumm Hilfe auf. Spieß Fröhlich KGaA DE68174009519788157086 Freizeit Stunde Hilfe ganz nie las kam oft voll.
51 8900 DE03689805605701261449 USD 2021-03-03 2020-11-12 186334.15 USD CRDT Brief gern der Eis Freude lange schlagen Flasche. Radisch Keudel GbR DE49885385858776558412 Lebenshaltung:Lebensmitteleinkäuft Dafür singen ein sonst uns Mensch ins Ding.
@@ -0,0 +1,115 @@
;
"Umsätze Girokonto";"Zeitraum: 180 Tage";
"Neuer Kontostand";"2.363,30 EUR";
"Wertstellung (Valuta)";"Umsatz in EUR";"Buchungstag";"Vorgang";"Buchungstext"
"12.01.2023";"-2.494,62";"28.09.2013";"Kartenumsatz";"Jeder Feuer fünf Hand Kind Jahr plötzlich leben."
"03.11.2014";"3.840,37";"12.02.2023";"Kartenumsatz";"Nehmen Name schnell Tier schreien gefährlich Arzt Monate."
"07.09.2008";"-1.895,07";"05.07.1999";"Kartenumsatz";"Frau stehen bleiben Minute Hund ein Fisch gern."
"23.06.1971";"1.996,11";"27.05.2015";"Kartenumsatz";"Glück wie doch tief gefährlich rennen schlafen Milch."
"28.05.1998";"3.183,63";"23.08.1993";"Lastschrift / Belastung";"Vom braun Seite fiel arbeiten bin kommen wichtig."
"20.08.1972";"707,94";"22.10.2001";"Kartenumsatz";"Auch letzte böse geben kurz nämlich es nass."
"21.09.2024";"-3.019,63";"25.12.1989";"Lastschrift / Belastung";"Boden böse denken oder sie denken später Kind."
"09.06.2016";"-2.908,98";"30.01.1978";"Kartenumsatz";"Stadt sofort nein Lehrerin fünf reiten gehen Haare."
"09.03.1994";"-2.972,60";"21.11.2009";"Lastschrift / Belastung";"Laut Weihnachten wenig dauern Schuh schwer alle nächste."
"30.11.2017";"1.994,76";"29.09.2011";"Lastschrift / Belastung";"Früher ob darauf krank wo Leute nach für."
"18.08.2013";"2.925,64";"16.02.2011";"Lastschrift / Belastung";"Weg wirklich Freude reich da erst man sind."
"19.10.1973";"-.837,10";"04.09.1987";"Lastschrift / Belastung";"Los mögen Stunde Weihnachten nass wo drei ohne."
"15.09.1975";"3.691,37";"19.01.2010";"Kartenumsatz";"Treffen glauben Spiel gab will vom waschen antworten."
"10.07.2011";"3.650,99";"16.02.2012";"Lastschrift / Belastung";"Katze seit freuen freuen den Eis die traurig."
"07.05.2022";"722,12";"12.01.1989";"Lastschrift / Belastung";"Tun an als finden wie klein aber nun."
"06.02.2004";"1.499,05";"20.09.1986";"Lastschrift / Belastung";"Oder natürlich Essen zur rennen Platz spielen Monat."
"26.10.2009";"-3.454,94";"21.06.2024";"Kartenumsatz";"Es Erde Hand bauen Winter gerade böse vom."
"03.07.2013";"2.746,42";"23.01.1988";"Kartenumsatz";"Natürlich schon Tier Opa Fußball Stadt brauchen kochen."
"25.06.2014";"1.487,38";"26.11.1979";"Lastschrift / Belastung";"Dunkel her möglich essen Pferd Stadt ziehen seit."
"16.05.2009";"1.866,68";"21.12.1994";"Lastschrift / Belastung";"Genau wer glauben erklären nennen schicken Hilfe stellen."
"26.12.1998";"-1.925,12";"06.02.1992";"Lastschrift / Belastung";"Katze gegen wo stellen uns sprechen fertig nah."
"24.01.1996";"-1.089,91";"30.03.1992";"Lastschrift / Belastung";"Mich los Essen Lehrerin hinter heißen danach weit."
"21.07.1973";"-3.206,61";"27.08.1984";"Kartenumsatz";"Dein Schuh Garten glauben Vater Opa die Gott."
"22.11.1981";"1.112,51";"25.06.1974";"Lastschrift / Belastung";"Krank fast Papa verlieren Kopf Monate endlich ruhig."
"01.11.1973";"-2.077,99";"05.08.2017";"Lastschrift / Belastung";"Ein liegen See jung wie Name Sonntag ihm."
"26.08.1972";"-4.000,00";"25.01.2025";"Lastschrift / Belastung";"Darauf in dauern schön las jeder Freude Auto."
"21.06.1989";"-3.639,78";"04.08.1982";"Kartenumsatz";"Wissen merken nie Essen Rad sitzen wo letzte."
"02.12.1982";"-3.371,26";"19.07.2005";"Lastschrift / Belastung";"Setzen so dabei wenn es Hunger Meer Hase."
"16.05.1987";"-1.715,06";"15.10.1984";"Lastschrift / Belastung";"Feuer Wald vor wo auf wohl fressen Haare."
"05.04.2024";"322,78";"11.07.1988";"Lastschrift / Belastung";"Her Pferd zusammen arbeiten schenken gar Lehrer hinter."
"25.12.1984";"397,36";"15.03.1978";"Kartenumsatz";"Essen blau deshalb laufen für am sieht weinen."
"19.07.1970";"-2.245,70";"22.05.1991";"Kartenumsatz";"Vorbei wird braun bis Tier als erschrecken Ende."
"21.11.2019";"732,25";"19.06.1993";"Lastschrift / Belastung";"Acht Minutenmir man böse natürlich rund immer heißen."
"28.10.2003";"2.397,64";"21.07.2026";"Kartenumsatz";"Arzt kommen haben lassen fünf wer gar kochen."
"16.10.1973";"2.271,59";"27.02.1987";"Lastschrift / Belastung";"Blume Auge weiter böse fallen Essen Erde kam."
"12.01.2005";"1.472,05";"02.12.2006";"Kartenumsatz";"Lehrerin verkaufen Wald ziehen hin lesen Hilfe klettern."
"02.05.2015";"2.284,43";"08.08.1991";"Lastschrift / Belastung";"Zu Freude Geld will Glück die seit vielleicht."
"18.09.2012";"-1.940,73";"16.04.2016";"Lastschrift / Belastung";"So ein Mensch schenken am wir wollen merken."
"13.03.2010";"1.899,59";"10.02.1997";"Kartenumsatz";"Das auch glauben Tür gewinnen schreien fangen Flasche."
"01.03.1983";"3.566,27";"27.03.2015";"Kartenumsatz";"Bekommen warum ja wohnen gefährlich fährt langsam Monat."
"14.01.2011";"-2.962,06";"07.03.2010";"Lastschrift / Belastung";"Gesund Minutenmir etwas fallen Nacht schaffen nach ihr."
"30.05.2000";"-1.915,23";"29.08.1979";"Kartenumsatz";"Nennen leicht Name Schuh Bild reiten laut letzte."
"11.10.1970";"-3.359,95";"31.05.2025";"Lastschrift / Belastung";"Den singen Wald Geschenk Wald durch Stück Oma."
"10.09.2012";"3.953,53";"15.03.1993";"Kartenumsatz";"Um Opa fragen müssen endlich Bein leben zum."
"21.11.2012";"-1.197,06";"17.04.1999";"Kartenumsatz";"Ist also wo Eis zum Stück kommen Hunger."
"12.07.1996";"2.381,22";"23.04.2010";"Lastschrift / Belastung";"Dich Auge können Stück seit fliegen trinken können."
"13.03.1988";"1.748,03";"01.06.1996";"Lastschrift / Belastung";"Berg Leben steigen Licht nehmen vom Hilfe Kind."
"29.12.1979";"1.727,59";"26.07.1998";"Lastschrift / Belastung";"Nämlich laufen Stelle nächste noch schwimmen Wagen grün."
"09.02.1973";"-.393,09";"28.12.1979";"Lastschrift / Belastung";"Reich Onkel schön danach Flasche klein am wahr."
"12.11.1982";"727,08";"26.04.2001";"Kartenumsatz";"Lustig dem zehn mich fressen Straße Auto langsam."
"Alter Kontostand";"1.632,48 EUR";
"Umsätze Visa-Karte (Kreditkarte)";"Zeitraum: 180 Tage";
"Neuer Kontostand";"0,00 EUR";
"Wertstellung (Valuta)";"Umsatz in EUR";"Buchungstag";"Vorgang";"Buchungstext";"Umsatztag";"Referenz"
"10.08.2009";"846,45";"19.02.1980";"Kartenumsatz";"Zehn stehen zusammen liegen weil neu sollen Monat.";"10.08.2009";"awdexfhWsPiVoDLETNyk"
"16.07.2025";"-3.343,34";"19.07.2000";"Kartenumsatz";"Ihm Wagen Apfel Sommer sechs Rad Finger Wetter.";"16.07.2025";"KDPhAroRFDegPTQBwXZF"
"21.09.2011";"-3.094,59";"24.02.1998";"Kartenumsatz";"Bauen dick Opa wollen gegen See Tür erklären.";"21.09.2011";"TYCRvkfvRnKAZZHyjMJf"
"22.11.2019";"2.501,72";"18.09.2023";"Kartenumsatz";"Wiese Tür Beispiel als richtig oft hören Glück.";"22.11.2019";"MRXprlArMtGEOYyrivvb"
"25.06.1972";"-.797,60";"20.12.2000";"Kartenumsatz";"Minute alt sollen arbeiten Wissen ihn viel hören.";"25.06.1972";"WLNlXtEPMVPWNQinqVoX"
"23.06.1997";"3.688,65";"24.07.1983";"Kartenumsatz";"Glas sieben Freund bleiben Geld als laufen gelb.";"23.06.1997";"kMtKBlMZfVzaQpFJVrdf"
"07.08.1970";"236,47";"26.10.2025";"Lastschrift / Belastung";"Können sehr los Klasse gern Glas dabei baden.";"07.08.1970";"XcTWEhERXSrKhTpwxKwq"
"05.03.1995";"-3.699,34";"28.04.2022";"Lastschrift / Belastung";"Neu traurig damit gar plötzlich Küche kam erzählen.";"05.03.1995";"DCSPqPoExMJVIXDJEmba"
"13.05.2014";"-2.301,46";"02.02.1993";"Kartenumsatz";"Wiese Blume von verlieren trinken Lehrerin reiten dick.";"13.05.2014";"FyjQDvaDRthPmTPacdcN"
"12.07.2006";"-1.002,95";"16.06.1990";"Kartenumsatz";"Kam bin frei setzen Lehrer er Schnee ziehen.";"12.07.2006";"CxGyzurWIlEVpidlaHgz"
"03.01.1989";"-1.538,17";"17.11.2025";"Kartenumsatz";"Merken Geschichte nein sprechen holen kein Stein müde.";"03.01.1989";"LUBzVEGaOaXicGKGRXLK"
"27.10.1995";"-1.588,75";"09.08.1998";"Kartenumsatz";"Überall genau spielen es dort sechs ich es.";"27.10.1995";"IlEguSkSvqMwjWjgrPJJ"
"21.05.1976";"771,43";"05.02.2023";"Kartenumsatz";"Schüler früh Tür Dorf traurig er Ende der.";"21.05.1976";"OkireOnJlianvjBgiKLP"
"10.11.1991";"-3.682,38";"11.12.1976";"Kartenumsatz";"Mädchen der Tag lachen Spaß singen nehmen fangen.";"10.11.1991";"EtrQVskkdZQgaxCnaNuX"
"27.05.2005";"-2.049,77";"18.05.1992";"Lastschrift / Belastung";"Stehen Stadt kein Stein einige wir dürfen Monate.";"27.05.2005";"yayAivkfmnKnKjaEeqHx"
"28.09.2009";"2.950,65";"07.10.2017";"Kartenumsatz";"Einfach warm Spaß er wird sonst Seite jung.";"28.09.2009";"xTmXkYMLxHlUWVgzDurB"
"26.09.1978";"691,87";"09.04.2015";"Kartenumsatz";"Böse zusammen an jung Land spät Brot so.";"26.09.1978";"OZCVRrNQxLzDaTJaYgir"
"07.01.1986";"1.899,21";"16.06.1975";"Kartenumsatz";"Wird offen ruhig Teller Arbeit sofort bleiben Weihnachten.";"07.01.1986";"YexccmpQrHfXPEaNqjYN"
"22.11.1970";"-1.088,74";"22.06.1979";"Lastschrift / Belastung";"Kopf Arzt bei jeder krank Haus Haare laut.";"22.11.1970";"uiRZmISGnUnmICslCQKR"
"06.10.2011";"2.627,51";"06.09.2018";"Lastschrift / Belastung";"Berg Auge auch die Milch packen Junge Bett.";"06.10.2011";"HjQTAhQreZPzYhSlyoDp"
"Alter Kontostand";"0,00 EUR";
"Umsätze Tagesgeld PLUS-Konto";"Zeitraum: 180 Tage";
"Neuer Kontostand";"1.673,90 EUR";
"Wertstellung (Valuta)";"Umsatz in EUR";"Buchungstag";"Vorgang";"Buchungstext"
"02.09.2000";"-2.652,98";"13.12.1985";"Lastschrift / Belastung";"Uhr wohl führen See setzen mal krank hoch."
"27.02.1976";"-2.302,68";"11.05.1997";"Kartenumsatz";"Ohne gut Stein wirklich gar Winter Welt nämlich."
"28.05.1986";"-3.343,51";"24.04.2026";"Kartenumsatz";"Dich wenig Oma oft denn Ferien hinter halten."
"18.08.1980";"3.519,25";"08.06.1985";"Lastschrift / Belastung";"Erklären tragen See ging versuchen gern heiß vielleicht."
"04.10.1981";"3.268,02";"25.07.2014";"Kartenumsatz";"Nein Junge Tier beide noch Wasser hinein gefährlich."
"09.01.1991";"-3.052,80";"13.06.2023";"Kartenumsatz";"Fisch Schwester viel sicher bis dürfen es Opa."
"05.07.2011";"-.633,49";"27.04.1994";"Kartenumsatz";"Frei fährt hat mich gleich gibt er Mutter."
"22.02.2015";"-1.146,44";"16.04.2010";"Lastschrift / Belastung";"Tag stellen in nichts Winter unser Eltern traurig."
"17.03.1989";"350,28";"24.03.2000";"Kartenumsatz";"Ding fröhlich fangen Herz allein tief weiter ihn."
"26.10.1994";"3.490,67";"10.05.1988";"Kartenumsatz";"Gibt Zeit Zug doch hören Frau Zimmer Onkel."
"Alter Kontostand";"6.686,20 EUR";
"Umsätze Depot";"Zeitraum: 180 Tage";
"Wertstellung (Valuta)";"Umsatz in EUR";"Geschäftstag";"Stück / Nom.";"Bezeichnung";"WKN";"Währung";"Ausführungskurs"
"28.10.1974";"1.685,37";"28.10.1974";"3.782,02";"zVQTjfeGndFUgHRYoOJV";"UIKWcmkLpKvlEaxsJNAY";"IVIQpMQspcGkoCQUMKKQ";"-1.169,05"
"23.03.1994";"1.559,84";"23.03.1994";"-.507,97";"rWidmLRRjtCtJbGCxxiJ";"clkDytzVOZfNqNqNqwpq";"vinALLmPCGosxxYbxQOT";"-.170,74"
"21.07.1997";"1.885,70";"21.07.1997";"3.199,90";"zbFJlhZJhFyVckyTkbWF";"NCLFEpfFrQAsoRLpEdNB";"BdqBcCsARBsTONKUEMUG";"2.167,34"
"04.11.2007";"-2.336,08";"04.11.2007";"3.374,10";"UmhrGDbbJhTbtBVnMEWU";"kfrvJSzEonSnYSvSwhzR";"WsyqHHvMGewNQWnQVOab";"-44,87"
"10.05.1995";"2.777,21";"10.05.1995";"-.466,66";"QBcTBCTJDuIWJKthZKKN";"DOBQNhJNlHjRdTYQfihc";"tTdPJSTffQjGknRiARpp";"1.108,37"
"02.08.2021";"95,64";"02.08.2021";"-2.473,15";"sURiquSzaJuCrwWdnuBf";"VltMSrMBshpZqfZltkJz";"mDBoFnnxoaMnkJcJwXtE";"1.989,84"
"08.01.2006";"723,60";"08.01.2006";"-3.788,97";"lENmCDzBnAoOlUWoIEOh";"vsTfHGBstXTwHpVQQIMN";"woeYzPNzwxhbsVuBQSzW";"723,80"
"24.08.1972";"-2.752,39";"24.08.1972";"121,81";"QmboZenDJYKYJZHrwBzg";"TOOOVZqQAxZLvZBqhttK";"iCRZdDaRZbJzKjDwweZz";"1.508,94"
"17.10.2004";"-3.983,82";"17.10.2004";"-2.862,12";"tBoMSqznKQiDGPEaZgtK";"KCxxVLPJcUdCHWRHiLuG";"MuwYMYDVjiZdGPPchFan";"2.170,06"
"13.01.2003";"1.172,75";"13.01.2003";"3.559,62";"DZUeNsJNwFiobKvjqbFE";"lKBJTcOkTowTcbtEDnHC";"DDMOrfHrxGjnjDQplaXe";"515,78"
+2 -2
View File
@@ -1,9 +1,9 @@
from sqlalchemy import select
from sqlalchemy.orm import Session
from src.helper_db import upsert_category
from src.ingest.b4w import upsert_category
from src.models import Category
from src.models_db import CategoryDb
from src.models.b4w import CategoryDb
def test_upsert_category_returns_orm_object(db_session: Session):
+45
View File
@@ -0,0 +1,45 @@
import re
from datetime import date
from decimal import Decimal
from pathlib import Path
from sqlalchemy import select
from sqlalchemy.orm import Session
from src.constants import IBAN_PATTERN, Currency
from src.ingest.b4w import ingest_b4w
from src.models.b4w import B4wDb
b4w_db_column_names = [col.key for col in B4wDb.__mapper__.columns]
# TODO: Unit tests for other funtions in src/ingest/b4w.py
# TODO: Similar file for Comdirect (more work due to several tables)
def test_ingest_b4w_e2e(db_session: Session):
path = Path("tests/test_data/b4w_export_dummy_01.csv")
ingest_b4w(path, db_session)
result = db_session.execute(select(B4wDb)).scalars().all()
assert isinstance(result, list)
assert all(isinstance(row.id_b4w, int) for row in result)
assert all(isinstance(row.date_book, date) for row in result)
assert all(isinstance(row.other_name, str) for row in result)
assert all(isinstance(row.other_iban, str) for row in result)
assert all(re.findall(IBAN_PATTERN, str(row.other_iban)) for row in result)
assert all(
(isinstance(row.category_id, int) or row.category_id is None) for row in result
)
assert all(isinstance(row.notes, str) for row in result)
assert all(isinstance(row.id, int) for row in result)
assert all(isinstance(row.hash_id, str) for row in result)
assert all(isinstance(row.account_iban, str) for row in result)
assert all(re.findall(IBAN_PATTERN, str(row.account_iban)) for row in result)
assert all(isinstance(row.account_currency, str) for row in result)
assert all(isinstance(row.date_valid, date) for row in result)
assert all(isinstance(row.amount, Decimal) for row in result)
assert all(isinstance(row.amount_currency, str) for row in result)
assert all(row.amount_currency in list(Currency) for row in result)
assert all(isinstance(row.text, str) for row in result)
print(b4w_db_column_names)
+35
View File
@@ -0,0 +1,35 @@
from pathlib import Path
import pytest
from faker import Faker
from sqlalchemy.orm import Session
from src.constants import Currency
from src.ingest.comdirect import (
_reader_raw_comdirect,
ingest_comdirect,
reader_comdirect,
)
TEST_DATA_PATH = Path("tests/test_data/comdirect_export_dummy_01.txt")
def test_reader_comdirect_raw():
result = _reader_raw_comdirect(TEST_DATA_PATH)
assert result
def test_reader_comdirect():
result = reader_comdirect(TEST_DATA_PATH)
assert result
assert isinstance(result, dict)
@pytest.mark.skip
def test_ingest_comdirect(db_session: Session):
ingest_comdirect(
session=db_session,
path=TEST_DATA_PATH,
account_iban=Faker().iban(),
account_currency=Currency.EUR,
)
+63
View File
@@ -1,3 +1,5 @@
import re
import pytest
from pydantic import ValidationError
@@ -29,3 +31,64 @@ def test_b4w_export_account_iban_to_long():
def test_b4w_export_account_iban_to_short():
with pytest.raises(ValidationError):
_ = B4wExportFactory.build(OwnrAcctIBAN="DE123")
def test_b4w_export_other_iban_empty_is_none():
data = B4wExportFactory.build(RmtdAcctIBAN="")
assert data.other_iban is None
def test_b4w_export_other_iban_blank_is_none():
data = B4wExportFactory.build(RmtdAcctIBAN=" ")
assert data.other_iban is None
def test_b4w_export_other_iban_invalid():
with pytest.raises(ValidationError):
_ = B4wExportFactory.build(RmtdAcctIBAN="not an IBAN")
def test_b4w_export_other_iban_repr_round_trip_empty():
data = B4wExportFactory.build(RmtdAcctIBAN="")
data2 = eval(str(data))
assert data == data2
def test_b4w_export_parameters_to_dict():
data = B4wExportFactory.build()
params = data.parameters_to_dict()
expected = sorted(field.alias for field in B4wExport.model_fields.values())
assert sorted(params) == expected
assert "OwnrAcctCcy" in params
assert all(isinstance(value, str) for value in params.values())
def test_b4w_export_parameters_to_str():
data = B4wExportFactory.build()
params = data.parameters_to_str()
components = [s.strip() for s in re.split(r",\s*(?=\w+=)", params)]
param_names = [component.split("=")[0].strip() for component in components]
param_values = [component.split("=")[1].strip() for component in components]
assert "OwnrAcctCcy" in param_names
assert all(s.startswith("'") and s.endswith("'") for s in param_values)
def test_b4w_export_repr_round_trip():
data_batch = B4wExportFactory.batch(size=10)
assert isinstance(data_batch, list)
for data in data_batch:
data_str = str(data)
data2 = eval(data_str)
assert data == data2
def test_b4w_export_amount_two_decimal_places():
data = B4wExportFactory.build()
assert data.amount > 0
assert data.amount.as_tuple().exponent >= -2
def test_b4w_export_text_fields_non_empty():
data = B4wExportFactory.build()
assert data.reference
assert data.notes
+24
View File
@@ -0,0 +1,24 @@
from src.models.comdirect import ComdirectGiro
from tests.factories import ComdirectGiroFactory
def test_comdirect_giro_basic_and_stringification():
data = ComdirectGiroFactory.build()
assert data
assert isinstance(data, ComdirectGiro)
orig_headers = data.get_orig_headers()
assert orig_headers
assert isinstance(orig_headers, str)
header_list = orig_headers.split(";")
assert len(header_list) > 0
assert set(header_list) == {
'"Wertstellung (Valuta)"',
'"Umsatz in EUR"',
'"Buchungstag"',
'"Vorgang"',
'"Buchungstext"',
}
orig_row = data.get_orig_row()
assert orig_row
assert isinstance(orig_row, str)
assert len(orig_row.split(";")) > 0
+20 -4
View File
@@ -1,10 +1,26 @@
from pathlib import Path
from src.reader import reader_comdirect
import pytest
from src.ingest.comdirect import COMDIRECT_ACCOUNT_PARSERS, reader_comdirect
from src.models import ComdirectCreditCard, ComdirectDepot, ComdirectGiro
TEST_DATA_PATH = ".local/data/comdirect/umsaetze_9773063005_20260906-2043.csv"
def test_reader():
test_path = Path(".local/data/comdirect/umsaetze_9773063005_20260906-2043.csv")
assert test_path.exists()
@pytest.mark.skipif(
not Path(TEST_DATA_PATH).exists(),
reason=f"Testdaten fehlen: {TEST_DATA_PATH}",
)
def test_reader_comdirect():
test_path = Path(TEST_DATA_PATH)
data = reader_comdirect(test_path)
assert data
assert len(data) == len(COMDIRECT_ACCOUNT_PARSERS)
assert set(data) == set(COMDIRECT_ACCOUNT_PARSERS)
assert all(isinstance(row, ComdirectGiro) for row in data["Girokonto"])
assert all(
isinstance(row, ComdirectCreditCard) for row in data["Visa-Karte (Kreditkarte)"]
)
assert all(isinstance(row, ComdirectGiro) for row in data["Tagesgeld PLUS-Konto"])
assert all(isinstance(row, ComdirectDepot) for row in data["Depot"])
Generated
+44
View File
@@ -74,6 +74,19 @@ 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 = "debugpy"
version = "1.8.21"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" },
{ url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" },
{ url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" },
{ url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" },
{ url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" },
]
[[package]]
name = "dotenv"
version = "0.9.9"
@@ -114,7 +127,10 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "black" },
{ name = "debugpy" },
{ name = "faker" },
{ name = "polyfactory" },
{ name = "ruff" },
]
[package.metadata]
@@ -131,7 +147,10 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "black", specifier = ">=26.5.1" },
{ name = "debugpy", specifier = ">=1.8.21" },
{ name = "faker", specifier = ">=40.38.0" },
{ name = "polyfactory", specifier = ">=3.3.0" },
{ name = "ruff", specifier = ">=0.16.7" },
]
[[package]]
@@ -391,6 +410,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
]
[[package]]
name = "ruff"
version = "0.16.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" },
{ url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" },
{ url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" },
{ url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" },
{ url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" },
{ url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" },
{ url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" },
{ url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" },
{ url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" },
{ url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" },
{ url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" },
{ url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" },
{ url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" },
{ url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" },
{ url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" },
]
[[package]]
name = "sqlalchemy"
version = "2.0.52"