From 553e1a4c83d2bb80377d619d1b579d4342dc67cb Mon Sep 17 00:00:00 2001 From: Mathias Scheider Date: Fri, 16 Oct 2026 22:30:36 +0200 Subject: [PATCH] Refactor domain_utils.py overlap helpers, add ONE_DAY constant, remove inflated comments, and standardize _add_row() calls --- .coverage | Bin 53248 -> 86016 bytes AGENTS.md | 26 +++++++ README.md | 180 ++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 17 +++++ src/domain_utils.py | 80 +++++--------------- 5 files changed, 240 insertions(+), 63 deletions(-) diff --git a/.coverage b/.coverage index 52b0b0a8ddb4e967cdef6e2d45eab57f67c1513e..d22ad8060f26e264dfa57eaf06e0ddc7e8945c37 100644 GIT binary patch literal 86016 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 delta 695 zcmX|-PiPZC6vlV5|FS!~^LEoF5C|k7L{TxNO4TTe#TsiOf`?ujf`WJvf}jK|Vm5+a zdT4M#4OEC;dJ{{~fLJVuU<(m@QZLCx6Dit?DXFGS*2xrmncsWgH#6VkSw+z*^7$ar zXG4_;8Yrzq4jMAS@9|w6FPTOV+wpi*I$;W2PPUig`|MK*kWL!3)mfW5E8FvN4T1e6 zF=#vSKIt@}ruV;D$|o#7fbYQw2pW((;0l+vVI1?s0*nppOYfDgvx9}QXz%y8b87M#zCSeioLyhk3B4m3}h?oq9 z!Jq+yP8hJxplA$k%pj`_lEMIP20>=v+4#s5Stkk%E}lVk33N(Dr$cf^?=3.3.0", "ruff>=0.16.7", ] + +[tool.pytest.ini_options] +addopts = "--cov=src --cov-report=term-missing" + +[tool.coverage.run] +branch = true +source = ["src"] + +[tool.coverage.report] +fail_under = 85 +skip_covered = true +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "^[a-zA-Z_]+\\s*=\\s*$" +] diff --git a/src/domain_utils.py b/src/domain_utils.py index 5b3096e..9d86750 100644 --- a/src/domain_utils.py +++ b/src/domain_utils.py @@ -6,6 +6,9 @@ from sqlalchemy.orm import Session from src.models.category import Category, CategoryBudget, CategoryBudgetDb, CategoryDb +ONE_DAY = timedelta(days=1) + + def find_category_db(session: Session, category: Category | None) -> CategoryDb | None: """Find a CategoryDb in the database given a Pydantic Category object. @@ -57,6 +60,7 @@ def set_category_budget(session: Session, category_budget: CategoryBudget): new_start = category_budget.valid_from new_end = category_budget.valid_to + new_amount = category_budget.amount_eur # Helper to add a budget row def _add_row(amount_eur: Decimal, valid_from: date | None, valid_to: date | None): @@ -70,13 +74,7 @@ def set_category_budget(session: Session, category_budget: CategoryBudget): # Helper to check if two date ranges overlap def _ranges_overlap(start1: date | None, end1: date | None, start2: date | None, end2: date | None) -> bool: - """Check if two date ranges overlap. - - Two bounded ranges overlap if start2 < end1 and start1 < end2. - An unbounded range (None, None) only overlaps with another fully unbounded range. - Partially unbounded ranges (e.g., valid_from=None but valid_to=date) - only overlap if the bounded ranges themselves would overlap. - """ + """Check if two date ranges overlap.""" # Both completely unbounded - always overlap if start1 is None and end1 is None and start2 is None and end2 is None: return True @@ -94,47 +92,19 @@ def set_category_budget(session: Session, category_budget: CategoryBudget): start2: date | None, end2: date | None) -> tuple[date | None, date | None]: """Return the overlapping sub-range of two ranges.""" - # Handle completely unbounded cases + # Handle completely unbounded cases first if start1 is None and end1 is None: - # First range is completely unbounded - overlap is second range - return (start2, end2) if (start2 is not None or end2 is not None) else (None, None) - + return (start2, end2) if any(x is not None for x in (start2, end2)) else (None, None) if start2 is None and end2 is None: - # Second range is completely unbounded - overlap is first range - return (start1, end1) if (start1 is not None or end1 is not None) else (None, None) + return (start1, end1) if any(x is not None for x in (start1, end1)) else (None, None) - # Initialize with defaults - o_start = None - o_end = None - - # Handle one-sided unbounded cases - if start1 is not None and end1 is not None: - # Range 1 is bounded, range 2 may be unbounded - if start2 is None: # New starts at beginning, so overlap starts at existing_start - o_start = start1 - else: - o_start = max(start1, start2) - - if end2 is None: # New ends at end of time, so overlap ends at existing_end - o_end = end1 - elif o_start is not None: - o_end = min(end1, end2) - - if start2 is not None and end2 is not None: - # Range 2 is bounded, range 1 may be unbounded - if start1 is None: # Existing starts at beginning - o_start = start2 - elif o_start is None: - o_start = max(start1, start2) - - if end1 is None: # Existing ends at end of time - o_end = end2 - elif o_start is not None: - o_end = min(end1, end2) + # Both ranges are bounded or partially unbounded + o_start = max((s for s in (start1, start2) if s is not None), default=None) + o_end = min((e for e in (end1, end2) if e is not None), default=None) # Check for actual overlap if o_start is None or o_start > o_end: - return (None, None) # No overlap + return (None, None) return (o_start, o_end) @@ -163,7 +133,7 @@ def set_category_budget(session: Session, category_budget: CategoryBudget): if overlap_start is None or overlap_end is None: # Full unbounded overlap - replace entirely with new budget - row = _add_row(category_budget.amount_eur, category_budget.valid_from, category_budget.valid_to) + row = _add_row(new_amount, new_start, new_end) session.add(row) return [row] @@ -172,28 +142,16 @@ def set_category_budget(session: Session, category_budget: CategoryBudget): # Part 1: Before the overlap (if any) if budget.valid_from is not None and budget.valid_from < overlap_start: - row = _add_row( - amount_eur=budget.amount_eur, - valid_from=budget.valid_from, - valid_to=overlap_start - timedelta(days=1), - ) + row = _add_row(budget.amount_eur, budget.valid_from, overlap_start - ONE_DAY) parts.append(row) # Part 2: After the overlap (if any) if budget.valid_to is not None and overlap_end < budget.valid_to: - row = _add_row( - amount_eur=budget.amount_eur, - valid_from=overlap_end + timedelta(days=1), - valid_to=budget.valid_to, - ) + row = _add_row(budget.amount_eur, overlap_end + ONE_DAY, budget.valid_to) parts.append(row) # Add the new budget - row = _add_row( - amount_eur=category_budget.amount_eur, - valid_from=category_budget.valid_from, - valid_to=category_budget.valid_to, - ) + row = _add_row(new_amount, new_start, new_end) parts.append(row) # Add all parts to session @@ -220,9 +178,5 @@ def set_category_budget(session: Session, category_budget: CategoryBudget): # Always add the new budget unless it was already added during overlap handling if not new_budget_added: - row = _add_row( - amount_eur=category_budget.amount_eur, - valid_from=category_budget.valid_from, - valid_to=category_budget.valid_to, - ) + row = _add_row(new_amount, new_start, new_end) session.add(row)