Skip to content

Upgrade Firefly III API schema to v6.4.16 with validation#67

Merged
RadCod3 merged 8 commits intomainfrom
feat/62-add-destinationid
Jan 30, 2026
Merged

Upgrade Firefly III API schema to v6.4.16 with validation#67
RadCod3 merged 8 commits intomainfrom
feat/62-add-destinationid

Conversation

@RadCod3
Copy link
Owner

@RadCod3 RadCod3 commented Jan 29, 2026

This PR does the following,

- Add `destination_id` to `CreateWithdrawalRequest` to fix issue #62.
- Add `source_id` to `CreateDepositRequest` for symmetric API support.
- Configure all 18 request models with `extra='forbid'` to prevent
silent failure of misspelled or unknown fields.
- Implement mutual exclusivity validation for account ID and account
name pairs using Pydantic model validators.
- Update `TransactionService` to pass the new ID fields to the Firefly
III API.
- Fix a latent bug in `test_get_budget_summary` integration test
discovered by the new strict validation.
- Add comprehensive unit and integration tests for new fields and
validation logic.
- Implement update-schema to automate Firefly III API model regeneration
- Implement lint script for combined code formatting and auto-fixing
- Register scripts as package entry points in pyproject.toml
@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Create withdrawals/deposits by specifying destination/source ID or name (mutually exclusive).
  • Updates

    • API schema bumped to v6.4.16; example dates and API version examples moved to January 2026.
    • Request validation tightened to forbid unexpected extra fields.
    • Added "daily" option for liability/interest period.
  • Chores

    • Added maintenance scripts and CLI entries for schema updates and code formatting.
  • Tests

    • New tests for deposit/withdrawal ID handling and scripts; removed obsolete transaction service tests.
  • Other

    • Renamed notes filter keyword from notes_start to notes_starts.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Bumps Firefly III OpenAPI to v6.4.16 and regenerates models; tightens many request models to forbid extras and adds mutual-exclusivity validators; adds destination_id/source_id handling in transaction payloads; introduces maintenance scripts (update_schema, format); updates tests and fixtures to cover new fields and behaviors.

Changes

Cohort / File(s) Summary
OpenAPI & Project Config
firefly-iii-6.4.16-v1.yaml, pyproject.toml
Updated OpenAPI metadata to v6.4.16, advanced example dates to Jan 2026, added daily enum, renamed notes_startnotes_starts, and pointed datamodel-codegen input to the new YAML; added script entry points.
Generated Firefly Models
src/lampyrid/models/firefly_models.py
Regenerated models: updated example timestamps and api version example; added daily to InterestPeriodPropertyEnum; renamed RuleTriggerKeyword member to notes_starts.
Lampyrid Request Models
src/lampyrid/models/lampyrid_models.py
Added model_config = ConfigDict(extra='forbid') across many request models; introduced optional identifier fields (destination_id, source_id) and after-validators enforcing mutual exclusivity with corresponding name fields.
Service Layer
src/lampyrid/services/transactions.py
Include destination_id on withdrawal payloads and source_id on deposit payloads when building TransactionSplitStore.
Maintenance Scripts
src/lampyrid/scripts/__init__.py, src/lampyrid/scripts/format.py, src/lampyrid/scripts/update_schema.py
New scripts package; format.py runs Ruff formatting/check; update_schema.py discovers latest stable Firefly version, downloads versioned OpenAPI YAML, updates pyproject.toml, regenerates Pydantic models via datamodel-codegen, runs formatting, and optionally removes old schema files.
Tests & Fixtures
tests/conftest.py, tests/fixtures/transactions.py, tests/integration/test_transactions.py, tests/integration/test_budgets.py, tests/unit/*
Added account-object fixtures; updated fixture helpers to accept destination_id/source_id; added integration tests for ID-based withdrawal/deposit; changed budget test params to start_date/end_date; added unit tests for scripts and model validation; removed older TransactionService unit tests.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant UpdateScript as update_schema.py
    participant DocsAPI as Firefly Docs API
    participant FS as Filesystem
    participant Codegen as datamodel-codegen
    participant Ruff as Ruff Formatter

    User->>UpdateScript: run update-schema
    UpdateScript->>DocsAPI: GET versions HTML
    DocsAPI-->>UpdateScript: versions HTML
    UpdateScript->>UpdateScript: parse & select latest stable
    UpdateScript->>DocsAPI: GET /openapi.yaml for selected version
    DocsAPI-->>UpdateScript: OpenAPI YAML bytes
    UpdateScript->>FS: write firefly-iii-<v>-v1.yaml
    UpdateScript->>FS: update pyproject.toml
    UpdateScript->>Codegen: invoke datamodel-codegen (generate models)
    Codegen-->>FS: write generated models
    UpdateScript->>Ruff: run formatting
    Ruff-->>FS: format files
    UpdateScript->>FS: remove old schema (optional)
    UpdateScript-->>User: exit status / report
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐇 I found a YAML, crisp and bright,

Models hopped forward into January light,
IDs now guide coins where names once led,
Scripts tidy code while I nibble my bread,
Tests clap their paws — the schema's fed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The PR description accurately reflects the changeset, mentioning issue #62 resolution, script additions for schema/model generation, and dependency upgrades.
Linked Issues check ✅ Passed The PR successfully addresses issue #62 by adding destination_id field support, mutual exclusivity validation, and passing destination_id through the transaction creation flow.
Out of Scope Changes check ✅ Passed All changes align with PR objectives: schema upgrade to v6.4.16, destination_id/source_id support, validation scripts, and supporting infrastructure. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title Check ✅ Passed Title check skipped as CodeRabbit has written the PR title.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/62-add-destinationid

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot changed the title @coderabbit Upgrade Firefly III API schema to v6.4.16 with validation Jan 29, 2026
@codecov-commenter
Copy link

codecov-commenter commented Jan 29, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 74.79675% with 62 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.53%. Comparing base (a0484bc) to head (4bf5ba1).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
src/lampyrid/scripts/update_schema.py 53.48% 60 Missing ⚠️
src/lampyrid/models/lampyrid_models.py 96.77% 1 Missing ⚠️
src/lampyrid/scripts/format.py 95.00% 1 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #67      +/-   ##
==========================================
- Coverage   99.81%   97.53%   -2.28%     
==========================================
  Files          15       17       +2     
  Lines        2661     2841     +180     
==========================================
+ Hits         2656     2771     +115     
- Misses          5       70      +65     
Flag Coverage Δ
integration 97.53% <74.79%> (-2.28%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/lampyrid/scripts/format.py`:
- Around line 7-21: main currently runs ruff with cwd='.' which is
caller-dependent; change main to resolve the repository/project root (e.g., via
pathlib on __file__ or git metadata) and pass that path as cwd to both
subprocess.run calls (the ones invoking ruff format and ruff check --fix) so
formatting is anchored to the repo root; ensure the resolved root is reused for
both calls and keep existing exception handling (CalledProcessError,
FileNotFoundError) intact.

In `@src/lampyrid/scripts/update_schema.py`:
- Around line 128-152: update_pyproject_toml currently silently no-ops if
neither the exact old_filename nor the regex matches; change it to return a
boolean success flag and surface failures: read the original content via
PYPROJECT_PATH.read_text(), compute the modified content (as currently done),
then compare original vs modified content and only call
PYPROJECT_PATH.write_text(content) if they differ; return True on change and
False otherwise so callers (the code that prints success) can check the boolean
and report an error when the update didn't occur. Ensure function signature
update_pyproject_toml(old_version: str | None, new_version: str) -> bool and
update any caller logic to handle a False result.

In `@tests/unit/test_models.py`:
- Around line 108-118: Update the docstrings in the test functions so they
reflect the actual assertions: change the docstring in
test_create_withdrawal_request_allows_neither_destination to state that
destination_id and destination_name can be omitted (not "cannot"), and make the
analogous docstring edit for the other test at the 177-185 range; ensure the
wording matches the test intent (e.g., "Test that destination_id and
destination_name may be omitted").
🧹 Nitpick comments (2)
tests/conftest.py (1)

281-290: Avoid fixed index access in fixtures; lookup by account name instead.

Index-based access makes these fixtures brittle if setup order changes or more accounts are added. Consider resolving by name to keep tests stable.

♻️ Proposed refactor
 def test_expense_account_obj() -> Account:
@@
-    if _cached_test_accounts is None or len(_cached_test_accounts) < 3:
+    if not _cached_test_accounts:
         raise RuntimeError('Test accounts not initialized. Check if _setup_test_data ran.')
-    return _cached_test_accounts[2]  # Index 2 is expense account
+    account = next(
+        (acct for acct in _cached_test_accounts if acct.name == 'Test Expense'),
+        None,
+    )
+    if account is None:
+        raise RuntimeError('Test Expense account not found in cached test accounts.')
+    return account
@@
 def test_revenue_account_obj() -> Account:
@@
-    if _cached_test_accounts is None or len(_cached_test_accounts) < 5:
+    if not _cached_test_accounts:
         raise RuntimeError('Test accounts not initialized. Check if _setup_test_data ran.')
-    return _cached_test_accounts[4]  # Index 4 is revenue account
+    account = next(
+        (acct for acct in _cached_test_accounts if acct.name == 'Test Revenue'),
+        None,
+    )
+    if account is None:
+        raise RuntimeError('Test Revenue account not found in cached test accounts.')
+    return account

Also applies to: 314-323

tests/fixtures/transactions.py (1)

22-54: Consider using keyword-only parameters to establish a safer API contract.

These new optional parameters appear to have no current call sites. While this means there's no immediate risk of positional-argument breakage, adding the * separator to make these parameters keyword-only would improve the fixture API design and prevent future misuse. This is particularly valuable for test fixtures that may be used in multiple test files over time.

♻️ Suggested refactor
 def make_create_withdrawal_request(
     amount: float,
     description: str,
     source_id: str,
+    *,
     destination_name: str | None = None,
     destination_id: str | None = None,
     budget_id: str | None = None,
     date: datetime | None = None,
 ) -> CreateWithdrawalRequest:
 def make_create_deposit_request(
     amount: float,
     description: str,
     destination_id: str,
+    *,
     source_name: str | None = None,
     source_id: str | None = None,
     date: datetime | None = None,
 ) -> CreateDepositRequest:

@RadCod3 RadCod3 force-pushed the feat/62-add-destinationid branch from ad224ae to 3d64134 Compare January 29, 2026 18:47
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@tests/conftest.py`:
- Around line 321-337: The fixture test_revenue_account_obj currently requires
an exact name match which can conflict with the case-insensitive substring
selection used in _setup_test_data; update test_revenue_account_obj to first try
an exact match on acct.name == 'Test Revenue' against _cached_test_accounts, and
if that yields None, fall back to a case-insensitive substring search (e.g.,
'Test Revenue' in acct.name.lower()) to find accounts like "Test Revenue 2",
then raise the RuntimeError only if both lookups fail.

In `@tests/unit/test_scripts.py`:
- Around line 3-10: Remove the unused imports sys, pytest, and httpx from the
top import block and reorder the remaining imports into standard groups (stdlib,
third-party, local) in sorted order; keep only Path from pathlib and MagicMock,
patch from unittest.mock, then the local imports from lampyrid.scripts (format,
update_schema) so the import block is clean and CI-friendly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/lampyrid/scripts/update_schema.py`:
- Around line 263-282: The script currently prints a warning when
regenerate_models() returns False but still exits with 0; update the end of the
regeneration block so that if regenerate_models() returns False the script
terminates with a non-zero status (e.g., call sys.exit(1) or raise SystemExit)
and include a clear message referencing MODELS_OUTPUT; also consider treating
formatting failures (the subprocess.run call that invokes 'uv run ruff format'
and the except block catching subprocess.CalledProcessError) as fatal in CI by
exiting non-zero there as well to avoid masking failures.
🧹 Nitpick comments (5)
tests/unit/test_format.py (1)

1-19: Duplicate test class exists in tests/unit/test_scripts.py.

This entire file duplicates the TestFormatScript class already present in tests/unit/test_scripts.py (lines 11-74), which contains more comprehensive tests including error handling scenarios. Consider removing this file to avoid test duplication and maintenance overhead.

#!/bin/bash
# Description: Verify the duplicate TestFormatScript classes in both files

echo "=== tests/unit/test_format.py ==="
rg -n "class TestFormatScript" tests/unit/test_format.py

echo ""
echo "=== tests/unit/test_scripts.py ==="
rg -n "class TestFormatScript" tests/unit/test_scripts.py

echo ""
echo "=== Test method counts ==="
echo "test_format.py methods:"
rg -c "def test_" tests/unit/test_format.py

echo "test_scripts.py TestFormatScript methods:"
ast-grep --pattern $'class TestFormatScript {
  $$$
  def test_$_($$$) {
    $$$
  }
  $$$
}'
tests/unit/test_scripts.py (2)

26-36: Fragile assertion assumes specific project directory name.

The assertion kwargs.get('cwd').name == 'LamPyrid' (line 31) assumes the project directory is always named "LamPyrid". This could break if the repository is cloned to a differently named directory.

Consider asserting that cwd is a valid Path and exists, or remove this specific name check.

♻️ Suggested fix
         args, kwargs = mock_run.call_args_list[0]
         assert args[0] == ['ruff', 'format', '.']
         assert kwargs.get('check') is True
         assert isinstance(kwargs.get('cwd'), Path)
-        assert kwargs.get('cwd').name == 'LamPyrid'
+        # Verify cwd is set to project root (contains pyproject.toml)
+        assert (kwargs.get('cwd') / 'pyproject.toml').exists() or True  # Mock doesn't check filesystem

Or simply remove the name check since the important assertion is that cwd is a Path:

-        assert kwargs.get('cwd').name == 'LamPyrid'

243-251: Test name doesn't match test behavior.

test_update_pyproject_toml_regex_fallback actually tests get_current_schema_version(), not update_pyproject_toml(). The test should either be renamed to reflect what it's testing or be updated to actually test the regex fallback behavior in update_pyproject_toml.

♻️ Suggested fix - rename to match behavior
     `@patch`('pathlib.Path.read_text')
-    `@patch`('pathlib.Path.write_text')
-    def test_update_pyproject_toml_regex_fallback(self, mock_write, mock_read):
-        """Test update_pyproject_toml when direct string replacement fails."""
+    def test_get_current_schema_version_parses_version(self, mock_read):
+        """Test get_current_schema_version extracts version from pyproject.toml."""
         mock_read.return_value = 'input = "firefly-iii-6.4.14-v1.yaml"'

         result = update_schema.get_current_schema_version()

         assert result == '6.4.14'
tests/unit/test_models.py (2)

54-91: Missing mutual exclusivity test for CreateWithdrawalRequest.

The TestCreateDepositRequest class includes a test for mutual exclusivity validation (test_create_deposit_request_mutual_exclusivity), but TestCreateWithdrawalRequest is missing an equivalent test. According to the model definition, CreateWithdrawalRequest also has a validator that raises an error when both destination_id and destination_name are provided.

🧪 Suggested test to add
def test_create_withdrawal_request_mutual_exclusivity(self):
    """Test that destination_id and destination_name cannot both be provided."""
    with pytest.raises(ValidationError) as exc_info:
        CreateWithdrawalRequest(
            amount=25.50,
            description='Test withdrawal',
            source_id='1',
            destination_id='5',
            destination_name='Groceries',
        )

    errors = exc_info.value.errors()
    assert len(errors) == 1
    assert 'Cannot specify both destination_id and destination_name' in str(errors[0]['msg'])

147-164: Consider adding test coverage for transaction type validation.

The CreateBulkTransactionsRequest model has a validator that only allows withdrawal, deposit, and transfer transaction types. Consider adding a test to verify that invalid transaction types (like reconciliation or opening_balance) raise a ValidationError.

🧪 Suggested test to add
def test_create_bulk_transactions_request_rejects_invalid_type(self):
    """Test that CreateBulkTransactionsRequest rejects invalid transaction types."""
    transaction = Transaction(
        type=TransactionTypeProperty.reconciliation,  # Invalid for bulk create
        amount=25.50,
        description='Test reconciliation',
        date=utc_now(),
        source_id='1',
        destination_id='2',
    )

    with pytest.raises(ValidationError) as exc_info:
        CreateBulkTransactionsRequest(transactions=[transaction])

    errors = exc_info.value.errors()
    assert 'Invalid transaction type' in str(errors[0]['msg'])

@RadCod3 RadCod3 merged commit 8bbc3bc into main Jan 30, 2026
5 checks passed
@RadCod3 RadCod3 deleted the feat/62-add-destinationid branch January 30, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

create_withdrawal endpoint ignores destination_id parameter, always defaults to Cash account

2 participants