Loading muffin/__main__.py +1 −1 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ output_rows = [ remaining := item.amount - spent, item.amount, ) for item in budget_plan.items.values() for item in budget_plan.items ] formatted_output_rows = [ Loading muffin/budget_plan/BudgetPlan.py +22 −1 Original line number Diff line number Diff line from dataclasses import dataclass from typing import Self from collections.abc import Iterable, Sequence from .BudgetaryItem import BudgetaryItem from .BudgetaryGroup import BudgetaryGroup @dataclass(frozen=True, eq=False) class BudgetPlan: items: dict[str, BudgetaryItem] _groups: dict[str, BudgetaryGroup] @classmethod def from_groups(cls, groups: Iterable[BudgetaryGroup]) -> Self: return cls({group.id: group for group in groups}) @property def groups(self) -> Sequence[BudgetaryGroup]: return tuple(self._groups.values()) def group(self, id: str) -> BudgetaryGroup: return self._groups @property def items(self) -> Sequence[BudgetaryItem]: return [item for group in self.groups for item in group.items] def item(self, id: str) -> BudgetaryItem: pass # TODO muffin/budget_plan/BudgetPlanLoader.py +61 −7 Original line number Diff line number Diff line Loading @@ -3,25 +3,79 @@ import subprocess from pathlib import Path from collections.abc import Sequence, Iterable from ..amount.AmountParser import parse_euro_amount from ..cash_flow.CashFlowDirection import CashFlowDirection from .BudgetPlan import BudgetPlan from .BudgetaryGroup import BudgetaryGroup from .BudgetaryItem import BudgetaryItem from ..amount.AmountParser import parse_euro_amount def _is_budgetary_item_row(row: Sequence[str]) -> bool: return len(row) >= 3 and row[0] and row[1] and row[2] def _parse_budgetary_item_row(row: Sequence[str]) -> BudgetaryItem: return BudgetaryItem(id=row[0], name=row[1], amount=parse_euro_amount(row[2])) def _is_combined_row(row: Sequence[str]) -> bool: return row[0] and all(not row[i] for i in range(1, len(row))) def _parse_combined_row(row: Sequence[str]) -> ...: lines = row[0].split("\n") return (lines[0][0], lines[0][1:]) # TODO def _load_csv_lines(csv_lines: Iterable[str]) -> BudgetPlan: reader = csv.reader(csv_lines) items = [ BudgetaryItem(id=row[0], name=row[1], amount=parse_euro_amount(row[2])) for row in reader if _is_budgetary_item_row(row) ] return BudgetPlan({item.id: item for item in items}) groups: list[BudgetaryGroup] = [] current_group_id: Optional[str] = None current_group_name: Optional[str] = None current_cash_flow_direction: Optional[CashFlowDirection] = None current_items: list[BudgetaryItem] = [] def add_group(): nonlocal current_group_id nonlocal current_group_name nonlocal current_items if current_group_id is None: return groups.append( BudgetaryGroup.from_items( id=current_group_id, name=current_group_name, cash_flow_direction=current_cash_flow_direction, items=current_items, ) ) current_group_id = None current_group_name = None current_items = [] for row in reader: if not row: continue elif row[0] == "Einnahmen": current_cash_flow_direction = CashFlowDirection.INCOMING elif row[0] == "Ausgaben": current_cash_flow_direction = CashFlowDirection.OUTGOING elif _is_budgetary_item_row(row): current_items.append(_parse_budgetary_item_row(row)) elif _is_combined_row(row): add_group() # Add the previous group, which is now completed current_group_id, current_group_name = _parse_combined_row(row) add_group() # Add the last group return BudgetPlan.from_groups(groups) def load_csv(csv_path: Path | str) -> BudgetPlan: Loading muffin/cash_flow/CashFlowDirection.py 0 → 100644 +6 −0 Original line number Diff line number Diff line from enum import Enum class CashFlowDirection(Enum): INCOMING = 1 OUTGOING = -1 Loading
muffin/__main__.py +1 −1 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ output_rows = [ remaining := item.amount - spent, item.amount, ) for item in budget_plan.items.values() for item in budget_plan.items ] formatted_output_rows = [ Loading
muffin/budget_plan/BudgetPlan.py +22 −1 Original line number Diff line number Diff line from dataclasses import dataclass from typing import Self from collections.abc import Iterable, Sequence from .BudgetaryItem import BudgetaryItem from .BudgetaryGroup import BudgetaryGroup @dataclass(frozen=True, eq=False) class BudgetPlan: items: dict[str, BudgetaryItem] _groups: dict[str, BudgetaryGroup] @classmethod def from_groups(cls, groups: Iterable[BudgetaryGroup]) -> Self: return cls({group.id: group for group in groups}) @property def groups(self) -> Sequence[BudgetaryGroup]: return tuple(self._groups.values()) def group(self, id: str) -> BudgetaryGroup: return self._groups @property def items(self) -> Sequence[BudgetaryItem]: return [item for group in self.groups for item in group.items] def item(self, id: str) -> BudgetaryItem: pass # TODO
muffin/budget_plan/BudgetPlanLoader.py +61 −7 Original line number Diff line number Diff line Loading @@ -3,25 +3,79 @@ import subprocess from pathlib import Path from collections.abc import Sequence, Iterable from ..amount.AmountParser import parse_euro_amount from ..cash_flow.CashFlowDirection import CashFlowDirection from .BudgetPlan import BudgetPlan from .BudgetaryGroup import BudgetaryGroup from .BudgetaryItem import BudgetaryItem from ..amount.AmountParser import parse_euro_amount def _is_budgetary_item_row(row: Sequence[str]) -> bool: return len(row) >= 3 and row[0] and row[1] and row[2] def _parse_budgetary_item_row(row: Sequence[str]) -> BudgetaryItem: return BudgetaryItem(id=row[0], name=row[1], amount=parse_euro_amount(row[2])) def _is_combined_row(row: Sequence[str]) -> bool: return row[0] and all(not row[i] for i in range(1, len(row))) def _parse_combined_row(row: Sequence[str]) -> ...: lines = row[0].split("\n") return (lines[0][0], lines[0][1:]) # TODO def _load_csv_lines(csv_lines: Iterable[str]) -> BudgetPlan: reader = csv.reader(csv_lines) items = [ BudgetaryItem(id=row[0], name=row[1], amount=parse_euro_amount(row[2])) for row in reader if _is_budgetary_item_row(row) ] return BudgetPlan({item.id: item for item in items}) groups: list[BudgetaryGroup] = [] current_group_id: Optional[str] = None current_group_name: Optional[str] = None current_cash_flow_direction: Optional[CashFlowDirection] = None current_items: list[BudgetaryItem] = [] def add_group(): nonlocal current_group_id nonlocal current_group_name nonlocal current_items if current_group_id is None: return groups.append( BudgetaryGroup.from_items( id=current_group_id, name=current_group_name, cash_flow_direction=current_cash_flow_direction, items=current_items, ) ) current_group_id = None current_group_name = None current_items = [] for row in reader: if not row: continue elif row[0] == "Einnahmen": current_cash_flow_direction = CashFlowDirection.INCOMING elif row[0] == "Ausgaben": current_cash_flow_direction = CashFlowDirection.OUTGOING elif _is_budgetary_item_row(row): current_items.append(_parse_budgetary_item_row(row)) elif _is_combined_row(row): add_group() # Add the previous group, which is now completed current_group_id, current_group_name = _parse_combined_row(row) add_group() # Add the last group return BudgetPlan.from_groups(groups) def load_csv(csv_path: Path | str) -> BudgetPlan: Loading
muffin/cash_flow/CashFlowDirection.py 0 → 100644 +6 −0 Original line number Diff line number Diff line from enum import Enum class CashFlowDirection(Enum): INCOMING = 1 OUTGOING = -1