Spaces:
Running on Zero
Running on Zero
Download finbot/models.py from spacedout-bits/Oracle: direct link, hf CLI and curl.
- Browser
- Download file 4.53 kB
-
https://huggingface.co/spaces/spacedout-bits/Oracle/resolve/main/finbot/models.py
- Command line
-
hf download hf://spaces/spacedout-bits/Oracle/finbot/models.py
-
curl -L -o models.py https://huggingface.co/spaces/spacedout-bits/Oracle/resolve/main/finbot/models.py
4.53 kB
| """Domain records. Plain dataclasses -- the storage layer maps them to SQLite.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import re | |
| import uuid | |
| from dataclasses import dataclass, field | |
| from datetime import date, datetime, timezone | |
| from typing import Optional | |
| from .money import format_money | |
| #: Where a record came from. Kept on every expense so a bad import can be | |
| #: found and reversed as a unit. | |
| SOURCES = ("text", "photo", "pdf", "csv", "xlsx", "manual") | |
| def _now() -> datetime: | |
| return datetime.now(timezone.utc) | |
| def _slug(text: str) -> str: | |
| """Loose normalisation for dedupe: case, punctuation and spacing-insensitive.""" | |
| return re.sub(r"[^a-z0-9]+", "", text.lower()) | |
| class Expense: | |
| user_id: int | |
| amount_minor: int | |
| currency: str | |
| description: str | |
| category: str | |
| occurred_on: date | |
| id: str = field(default_factory=lambda: uuid.uuid4().hex) | |
| merchant: str = "" | |
| source: str = "text" | |
| #: Groups rows that arrived together, so /undo can drop a whole import. | |
| batch_id: str = "" | |
| note: str = "" | |
| #: 1.0 for deterministic parses, lower for model-extracted rows. | |
| confidence: float = 1.0 | |
| created_at: datetime = field(default_factory=_now) | |
| def dedupe_key(self) -> str: | |
| """Stable identity for 'the same transaction'. | |
| Deliberately excludes id, source and confidence so that the same row | |
| re-imported from a re-downloaded statement collapses onto the original. | |
| """ | |
| basis = "|".join( | |
| [ | |
| str(self.user_id), | |
| str(self.amount_minor), | |
| self.currency.upper(), | |
| self.occurred_on.isoformat(), | |
| _slug(self.merchant or self.description), | |
| ] | |
| ) | |
| return hashlib.sha256(basis.encode("utf-8")).hexdigest()[:32] | |
| def display(self) -> str: | |
| amount = format_money(self.amount_minor, self.currency) | |
| label = self.merchant or self.description or self.category | |
| return f"{amount} 路 {label} 路 {self.category} 路 {self.occurred_on.isoformat()}" | |
| class Budget: | |
| user_id: int | |
| category: str | |
| amount_minor: int | |
| currency: str | |
| period: str = "monthly" | |
| created_at: datetime = field(default_factory=_now) | |
| class ExpenseDraft: | |
| """A candidate row awaiting confirmation. | |
| Statement and receipt imports land here first. Nothing model-extracted is | |
| written to the ledger without the user seeing the total and saying yes. | |
| """ | |
| amount_minor: int | |
| currency: str | |
| description: str | |
| occurred_on: date | |
| category: str = "other" | |
| merchant: str = "" | |
| confidence: float = 0.5 | |
| raw: str = "" | |
| def to_expense( | |
| self, user_id: int, source: str, batch_id: str, note: str = "" | |
| ) -> Expense: | |
| return Expense( | |
| user_id=user_id, | |
| amount_minor=self.amount_minor, | |
| currency=self.currency, | |
| description=self.description, | |
| category=self.category, | |
| occurred_on=self.occurred_on, | |
| merchant=self.merchant, | |
| source=source, | |
| batch_id=batch_id, | |
| note=note, | |
| confidence=self.confidence, | |
| ) | |
| class PendingImport: | |
| """Drafts held between 'here is your statement' and '/confirm'.""" | |
| user_id: int | |
| drafts: list[ExpenseDraft] | |
| source: str | |
| batch_id: str = field(default_factory=lambda: uuid.uuid4().hex) | |
| created_at: datetime = field(default_factory=_now) | |
| #: Rows dropped because they already exist in the ledger. | |
| duplicates: int = 0 | |
| #: Rows the extractor emitted but could not be made sense of. | |
| skipped: int = 0 | |
| label: str = "" | |
| def total_by_currency(self) -> dict[str, int]: | |
| totals: dict[str, int] = {} | |
| for d in self.drafts: | |
| totals[d.currency] = totals.get(d.currency, 0) + d.amount_minor | |
| return totals | |
| class Summary: | |
| """Aggregated spend for a period, ready to render.""" | |
| label: str | |
| start: date | |
| end: date | |
| currency: str | |
| total_minor: int | |
| count: int | |
| by_category: list[tuple[str, int]] | |
| previous_total_minor: Optional[int] = None | |
| def delta_pct(self) -> Optional[float]: | |
| if self.previous_total_minor is None or self.previous_total_minor == 0: | |
| return None | |
| return ( | |
| (self.total_minor - self.previous_total_minor) | |
| / self.previous_total_minor | |
| * 100.0 | |
| ) | |