Spaces:
Running on Zero
Running on Zero
Download tests/test_analytics.py from spacedout-bits/Oracle: direct link, hf CLI and curl.
- Browser
- Download file 7.31 kB
-
https://huggingface.co/spaces/spacedout-bits/Oracle/resolve/main/tests/test_analytics.py
- Command line
-
hf download hf://spaces/spacedout-bits/Oracle/tests/test_analytics.py
-
curl -L -o test_analytics.py https://huggingface.co/spaces/spacedout-bits/Oracle/resolve/main/tests/test_analytics.py
7.31 kB
| from datetime import date | |
| import pytest | |
| from finbot.analytics import ( | |
| by_category, | |
| budget_status, | |
| detect_recurring, | |
| dominant_currency, | |
| month_bounds, | |
| period_bounds, | |
| previous_month_bounds, | |
| project_month_end, | |
| split_by_currency, | |
| summarise, | |
| top_merchants, | |
| total_minor, | |
| ) | |
| from finbot.models import Budget, Expense | |
| TODAY = date(2026, 8, 15) | |
| def make(amount, category="other", currency="INR", day=TODAY, merchant="", desc="x"): | |
| return Expense( | |
| user_id=1, | |
| amount_minor=amount, | |
| currency=currency, | |
| description=desc, | |
| category=category, | |
| occurred_on=day, | |
| merchant=merchant, | |
| ) | |
| class TestPeriods: | |
| def test_bounds(self, name, start, end): | |
| got_start, got_end, _label = period_bounds(name, TODAY) | |
| assert (got_start, got_end) == (start, end) | |
| def test_unknown_period_defaults_to_month(self): | |
| start, end, _ = period_bounds("nonsense", TODAY) | |
| assert (start, end) == (date(2026, 8, 1), TODAY) | |
| def test_month_bounds_handles_month_length(self): | |
| assert month_bounds(date(2026, 2, 10))[1] == date(2026, 2, 28) | |
| def test_previous_month_across_a_year_boundary(self): | |
| assert previous_month_bounds(date(2026, 1, 5)) == ( | |
| date(2025, 12, 1), | |
| date(2025, 12, 31), | |
| ) | |
| class TestAggregation: | |
| def test_total_and_by_category(self): | |
| expenses = [ | |
| make(10000, "food_dining"), | |
| make(5000, "food_dining"), | |
| make(20000, "rent"), | |
| ] | |
| assert total_minor(expenses) == 35000 | |
| assert by_category(expenses) == [("rent", 20000), ("food_dining", 15000)] | |
| def test_split_by_currency(self): | |
| expenses = [make(100, currency="INR"), make(200, currency="USD")] | |
| buckets = split_by_currency(expenses) | |
| assert set(buckets) == {"INR", "USD"} | |
| def test_dominant_currency_by_count(self): | |
| expenses = [make(100, currency="INR"), make(100, currency="INR"), make(999999, currency="USD")] | |
| assert dominant_currency(expenses, "EUR") == "INR" | |
| def test_dominant_currency_falls_back_when_empty(self): | |
| assert dominant_currency([], "SGD") == "SGD" | |
| def test_summarise_only_counts_the_named_currency(self): | |
| expenses = [make(10000, currency="INR"), make(50000, currency="USD")] | |
| summary = summarise(expenses, "INR", "Aug", date(2026, 8, 1), TODAY) | |
| assert summary.total_minor == 10000 | |
| assert summary.count == 1 | |
| def test_delta_pct(self): | |
| current = [make(15000)] | |
| previous = [make(10000)] | |
| summary = summarise(current, "INR", "Aug", date(2026, 8, 1), TODAY, previous) | |
| assert summary.delta_pct == pytest.approx(50.0) | |
| def test_delta_pct_is_none_without_a_baseline(self): | |
| summary = summarise([make(100)], "INR", "Aug", date(2026, 8, 1), TODAY, []) | |
| assert summary.delta_pct is None | |
| def test_top_merchants(self): | |
| expenses = [ | |
| make(100, merchant="Swiggy"), | |
| make(300, merchant="Swiggy"), | |
| make(200, merchant="Uber"), | |
| ] | |
| assert top_merchants(expenses)[0] == ("Swiggy", 400) | |
| class TestProjection: | |
| def test_projects_to_month_end(self): | |
| # 15 days elapsed of a 31-day August, ₹150 spent -> ~₹310. | |
| expenses = [make(15000, day=date(2026, 8, 5))] | |
| projected = project_month_end(expenses, TODAY) | |
| assert projected == pytest.approx(31000, rel=0.01) | |
| class TestRecurring: | |
| def _monthly(self, merchant, amount, months): | |
| return [ | |
| make(amount, day=date(2026, m, 5), merchant=merchant, desc=merchant) | |
| for m in months | |
| ] | |
| def test_detects_a_monthly_subscription(self): | |
| expenses = self._monthly("Netflix", 64900, [5, 6, 7]) | |
| found = detect_recurring(expenses) | |
| assert len(found) == 1 | |
| assert found[0].months == 3 | |
| assert found[0].typical_minor == 64900 | |
| def test_ignores_charges_in_too_few_months(self): | |
| expenses = self._monthly("Netflix", 64900, [6, 7]) | |
| assert detect_recurring(expenses) == [] | |
| def test_ignores_repeat_spending_with_erratic_amounts(self): | |
| # Same shop every month, wildly different amounts -- not a subscription. | |
| expenses = [ | |
| make(10000, day=date(2026, 5, 5), merchant="Bigbasket", desc="Bigbasket"), | |
| make(90000, day=date(2026, 6, 5), merchant="Bigbasket", desc="Bigbasket"), | |
| make(250000, day=date(2026, 7, 5), merchant="Bigbasket", desc="Bigbasket"), | |
| ] | |
| assert detect_recurring(expenses) == [] | |
| def test_three_charges_in_one_month_is_not_recurring(self): | |
| expenses = [ | |
| make(64900, day=date(2026, 7, d), merchant="Netflix", desc="Netflix") | |
| for d in (1, 10, 20) | |
| ] | |
| assert detect_recurring(expenses) == [] | |
| def test_normalises_noisy_merchant_strings(self): | |
| expenses = [ | |
| make(64900, day=date(2026, 5, 5), desc="NETFLIX.COM 447192"), | |
| make(64900, day=date(2026, 6, 5), desc="NETFLIX.COM 883021"), | |
| make(64900, day=date(2026, 7, 5), desc="Netflix"), | |
| ] | |
| found = detect_recurring(expenses) | |
| assert len(found) == 1 | |
| assert found[0].months == 3 | |
| def test_annualised(self): | |
| found = detect_recurring(self._monthly("Netflix", 50000, [5, 6, 7])) | |
| assert found[0].annualised_minor == 600000 | |
| def test_separates_currencies(self): | |
| expenses = self._monthly("Netflix", 64900, [5, 6, 7]) + [ | |
| make(1000, day=date(2026, m, 5), merchant="Netflix", desc="Netflix", currency="USD") | |
| for m in (5, 6, 7) | |
| ] | |
| found = detect_recurring(expenses) | |
| assert {f.currency for f in found} == {"INR", "USD"} | |
| class TestBudgets: | |
| def test_status_calculation(self): | |
| budgets = [Budget(user_id=1, category="food_dining", amount_minor=100000, currency="INR")] | |
| expenses = [make(60000, "food_dining")] | |
| status = budget_status(budgets, expenses)[0] | |
| assert status.spent_minor == 60000 | |
| assert status.pct == pytest.approx(60.0) | |
| assert status.remaining_minor == 40000 | |
| assert not status.over | |
| def test_over_budget(self): | |
| budgets = [Budget(user_id=1, category="rent", amount_minor=10000, currency="INR")] | |
| status = budget_status(budgets, [make(15000, "rent")])[0] | |
| assert status.over | |
| assert status.remaining_minor == -5000 | |
| def test_ignores_other_currencies(self): | |
| budgets = [Budget(user_id=1, category="rent", amount_minor=10000, currency="INR")] | |
| status = budget_status(budgets, [make(99999, "rent", currency="USD")])[0] | |
| assert status.spent_minor == 0 | |
| def test_no_budgets_yields_no_statuses(self): | |
| assert budget_status([], [make(100)]) == [] | |