Skip to content

feat: add rules management with 5 new MCP tools#75

Merged
RadCod3 merged 5 commits into
RadCod3:mainfrom
edmundhighcock:feat/add-rules-management
Jun 3, 2026
Merged

feat: add rules management with 5 new MCP tools#75
RadCod3 merged 5 commits into
RadCod3:mainfrom
edmundhighcock:feat/add-rules-management

Conversation

@edmundhighcock

Copy link
Copy Markdown
Contributor

Hi! Thanks for a great tool. I really needed rules functionality so I've added it to my fork. Hoping you might be happy to add it to the main branch.

Note: I've also added a one-click install to add this to home assistant, which many people use to host firefly iii. (See https://github.com/edmundhighcock/hassio-repository/tree/main/fireflyiii-mcp).

Claude summary

Add comprehensive rules functionality to manage Firefly III rules:

  • search_rules: Client-side filtering with regex pattern support
  • get_rule: Retrieve single rule details
  • update_rule: Modify rule configuration (triggers/actions)
  • test_rule: Preview rule matches without changes (read-only)
  • execute_rule: Apply changes with explicit confirmation requirement

Implementation includes:

  • RuleService in services layer with client-side search/filtering
  • 11 new request/response models for type safety
  • 5 new HTTP client methods for Firefly III rule APIs
  • Comprehensive unit tests (19 test cases)
  • Safety-first design: test before execute, confirmation required

This brings the total tool count from 22 to 27.

@coderabbitai

coderabbitai Bot commented Feb 1, 2026

Copy link
Copy Markdown
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Full rule management: search, retrieve, update, preview (test), and execute rules via new MCP endpoints and server.
    • Safer execution flow: requires explicit confirmation, validates date ranges, and returns clear success/failure results.
    • Advanced filtering: pagination, active/title/type filters, and regex-based pattern matching.
  • Tests

    • Added comprehensive unit and integration tests covering search, pagination, update, test (preview), and execute flows.

Walkthrough

Adds Firefly III rule-management: new FireflyClient rule endpoints, Pydantic request/response models, a RuleService with search/update/test/execute flows, an MCP "rules" server wiring, and corresponding unit and integration tests.

Changes

Cohort / File(s) Summary
Client API Extensions
src/lampyrid/clients/firefly.py
Added async rule endpoints: get_rules(page), get_rule(rule_id), update_rule(rule_id, rule_update), test_rule(...), trigger_rule(...). Uses new RuleArray/RuleSingle/RuleUpdate models and existing error/validation patterns.
Model Definitions
src/lampyrid/models/lampyrid_models.py
Introduced rule-related enums/types imports and new Pydantic models: RuleTriggerSimple, RuleActionSimple, Rule (+ from_rule_read), request models (SearchRulesRequest, GetRuleRequest, UpdateRuleRequest, TestRuleRequest, ExecuteRuleRequest) with validators, and result models (RuleTestResult, RuleExecuteResult).
Service Layer
src/lampyrid/services/rules.py
New RuleService implementing search_rules, get_rule, update_rule, test_rule, execute_rule. Implements pagination aggregation, client-side filtering (including regex), payload conversion/validation, and safety confirm guard for execution.
MCP Tooling & Composition
src/lampyrid/tools/rules.py, src/lampyrid/tools/__init__.py
Added create_rules_server(client) exposing MCP tools (search_rules, get_rule, update_rule, test_rule, execute_rule) and mounted the rules server in compose_all_servers().
Tests — Unit & Integration
tests/unit/test_rules_service.py, tests/integration/test_rules.py
New unit tests for RuleService covering filtering, pagination, validation, mapping, update flows, and execution guard; integration tests exercising MCP rule tools end-to-end with helper create/delete and cleanup fixture.
Pytest Config
pyproject.toml
Added pytest marker string for rule management tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client as MCP Client
    participant Tools as Rules Server
    participant Service as RuleService
    participant Firefly as FireflyClient

    Client->>Tools: call test_rule(rule_id, start, end, accounts)
    Tools->>Service: test_rule(TestRuleRequest)
    Service->>Firefly: get_rule(rule_id)
    Firefly-->>Service: RuleSingle (metadata)
    Service->>Firefly: test_rule(rule_id, start, end, accounts)
    Firefly-->>Service: TransactionArray (matches)
    Service->>Service: convert transactions -> Transaction models
    Service-->>Tools: RuleTestResult (count, matches)
    Tools-->>Client: return RuleTestResult
Loading
sequenceDiagram
    participant Client as MCP Client
    participant Tools as Rules Server
    participant Service as RuleService
    participant Firefly as FireflyClient

    Client->>Tools: call execute_rule(rule_id, start, end, confirm=true)
    Tools->>Service: execute_rule(ExecuteRuleRequest)
    Service->>Service: validate confirm == True
    Service->>Firefly: get_rule(rule_id)
    Firefly-->>Service: RuleSingle (metadata)
    Service->>Firefly: trigger_rule(rule_id, start, end, accounts)
    Firefly-->>Service: HTTP 204 -> boolean success
    Service-->>Tools: RuleExecuteResult (success, message)
    Tools-->>Client: return execution result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hop through rules with careful cheer,
I test, I match, I nudge things near.
Confirm me true before I leap,
I sort the transactions you keep.
A tiny rabbit, rules complete — hooray!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding rules management functionality with 5 new MCP tools.
Description check ✅ Passed The description is well-related to the changeset, providing context on the feature addition, implementation details, and mentioning the safety-first design approach.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/lampyrid/clients/firefly.py`:
- Around line 503-532: The rule-related APIs and their request models currently
use account_ids: Optional[list[str]] (e.g., test_rule and
ExecuteRuleRequest/TestRuleRequest) while insight methods use
Optional[list[int]]; unify them by changing all rule method signatures, query
param handling, and request models to use Optional[list[int]] (or pick int
consistently) so the shared query key 'accounts[]' always receives integers;
update test_rule, trigger_rule, TestRuleRequest, ExecuteRuleRequest and any
other rule-insight callers to accept/serialize list[int] and ensure
params['accounts[]'] passes ints (or converts incoming ints to strings only at
the HTTP layer if necessary).

In `@src/lampyrid/services/rules.py`:
- Around line 243-245: The current assignments use the faulty "or True"/"or
False" pattern which coerces falsy values like False to the defaults; update the
assignments to use explicit None-coalescing so an explicit False is preserved.
For example, replace the active and stop_processing assignments that reference
rule_attrs.active and rule_attrs.stop_processing with logic that checks "is
None" (e.g., set active = True if rule_attrs.active is None else
rule_attrs.active and set stop_processing = False if rule_attrs.stop_processing
is None else rule_attrs.stop_processing), leaving strict as rule_attrs.strict;
target the active/stop_processing assignments in the function/class where these
lines appear.
- Around line 135-137: The current assignment uses boolean "or" which forces
True for active and may mis-handle stop_processing; update the mappings where
you set active, strict, and stop_processing from rule_read.attributes to
explicitly check for None and only use the default when the attribute is None
(e.g., use a conditional that returns rule_read.attributes.active if it is not
None else True, and similarly for stop_processing defaulting to False), keeping
the field names (rule_read, attributes, active, strict, stop_processing) to
locate and modify the code.
- Around line 175-177: The assignment active=rule_attrs.active or True
incorrectly forces active to True even when rule_attrs.active is False; change
it to preserve explicit False by using a None-check such as active =
rule_attrs.active if rule_attrs.active is not None else True (or equivalent like
getattr(rule_attrs, "active", True) with an explicit None check) in the code
that constructs the rule (the lines referencing rule_attrs.active,
rule_attrs.strict, rule_attrs.stop_processing) so inactive rules remain
inactive.

In `@tests/unit/test_rules_service.py`:
- Around line 148-163: The test test_search_rules_by_trigger_type uses an
invalid trigger_type value 'amount_greater_than' when creating rule2 via
_make_rule_read; update the trigger_type to a valid RuleTriggerKeyword such as
'amount_more' so the mock rule matches the service's expected enums and the
SearchRulesRequest(trigger_type='description') test logic remains correct.
🧹 Nitpick comments (2)
src/lampyrid/models/lampyrid_models.py (1)

1190-1206: Consider adding a validator for trigger/action structure.

The triggers and actions fields use List[dict[str, Any]] which defers validation to the service layer. While this works, adding a basic validator to check for required keys (like type) at the model level would provide earlier feedback to users.

♻️ Optional: Add basic structure validation
+    `@model_validator`(mode='after')
+    def validate_trigger_action_structure(self):
+        """Validate that triggers/actions have required 'type' field."""
+        if self.triggers:
+            for i, t in enumerate(self.triggers):
+                if 'type' not in t:
+                    raise ValueError(f"triggers[{i}] missing required 'type' field")
+        if self.actions:
+            for i, a in enumerate(self.actions):
+                if 'type' not in a:
+                    raise ValueError(f"actions[{i}] missing required 'type' field")
+        return self
src/lampyrid/services/rules.py (1)

228-233: Move inline import to module level.

The RuleActionUpdate import is done inline within the function. While functional, this is inconsistent with RuleTriggerUpdate which is imported at the module level.

♻️ Proposed refactor

At line 13, add RuleActionUpdate to the existing import:

-from ..models.firefly_models import RuleTriggerUpdate, RuleUpdate
+from ..models.firefly_models import RuleActionUpdate, RuleTriggerUpdate, RuleUpdate

Then remove the inline import at line 229.

Comment thread src/lampyrid/clients/firefly.py
Comment thread src/lampyrid/services/rules.py Outdated
Comment thread src/lampyrid/services/rules.py Outdated
Comment thread src/lampyrid/services/rules.py Outdated
Comment thread tests/unit/test_rules_service.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/models/lampyrid_models.py`:
- Around line 1223-1244: Add a Pydantic validation to both TestRuleRequest and
ExecuteRuleRequest to ensure start_date <= end_date: implement a `@root_validator`
(or classmethod validator) in each model that checks the two fields and raises a
ValueError (or ValidationError) with a clear message when start_date is after
end_date so API returns a validation error instead of empty results; locate the
models by name (TestRuleRequest, ExecuteRuleRequest) in lampyrid_models.py and
add the validator logic to each class.
🧹 Nitpick comments (2)
src/lampyrid/models/lampyrid_models.py (2)

1191-1207: Consider using typed models for triggers and actions instead of dict[str, Any].

Using untyped dicts forgoes compile-time and validation-time safety. Invalid payloads (e.g. missing type key, wrong value types) will only surface as downstream API errors rather than clear Pydantic validation failures. A lightweight typed model (even a TypedDict or a small Pydantic model with type: str required and value: Optional[str]) would catch malformed input earlier and provide better error messages to MCP clients.

That said, the current approach does offer flexibility for passing API-specific fields that may not all be modeled yet, so this is a tradeoff.


1215-1221: Duplicated _parse_string_input validator across TestRuleRequest and ExecuteRuleRequest.

Both classes define identical mode='before' validators. If more request models with date fields are added, this pattern will spread further. Consider extracting a shared mixin or base class:

♻️ Example DRY refactor
+class _JsonStringParseMixin(BaseModel):
+    """Mixin for models that need to handle JSON-string input from MCP clients."""
+
+    `@model_validator`(mode='before')
+    `@classmethod`
+    def _parse_string_input(cls, data: Any) -> Any:
+        if isinstance(data, str):
+            return json.loads(data)
+        return data
+
+
-class TestRuleRequest(BaseModel):
+class TestRuleRequest(_JsonStringParseMixin):
     """Request model for testing a rule (preview matches)."""
 
     model_config = ConfigDict(extra='forbid')
 
-    `@model_validator`(mode='before')
-    `@classmethod`
-    def _parse_string_input(cls, data: Any) -> Any:
-        """Handle MCP clients that serialize the request as a JSON string."""
-        if isinstance(data, str):
-            return json.loads(data)
-        return data
-
     rule_id: str = Field(...)
     ...

-class ExecuteRuleRequest(BaseModel):
+class ExecuteRuleRequest(_JsonStringParseMixin):
     """Request model for executing a rule (apply changes)."""
 
     model_config = ConfigDict(extra='forbid')
 
-    `@model_validator`(mode='before')
-    `@classmethod`
-    def _parse_string_input(cls, data: Any) -> Any:
-        """Handle MCP clients that serialize the request as a JSON string."""
-        if isinstance(data, str):
-            return json.loads(data)
-        return data
-
     rule_id: str = Field(...)
     ...

Also applies to: 1252-1258

Comment thread src/lampyrid/models/lampyrid_models.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/models/lampyrid_models.py`:
- Around line 1215-1221: The pre-validator _parse_string_input currently calls
json.loads(data) unguarded; wrap that call in a try/except that catches
json.JSONDecodeError and re-raises a plain ValueError (or TypeError) with a
clear message so Pydantic will convert it into a ValidationError; update this
logic in both classes that define _parse_string_input (TestRuleRequest and
ExecuteRuleRequest) so malformed JSON strings produce a validation-friendly
error instead of an uncaught JSONDecodeError.

In `@src/lampyrid/services/rules.py`:
- Around line 84-107: Extract the duplicated mapping logic into a single private
helper method (e.g., _to_rule) that accepts the Firefly response object (handle
both rule_single.data and rule_read.attributes cases) and returns a Rule; inside
the helper read rule_attrs = ...attributes, map
id/title/description/trigger/actions/triggers to Rule, replacing brittle "or
True"/"or False" with explicit None checks (e.g., active = rule_attrs.active if
rule_attrs.active is not None else True; prohibited = t.prohibited if
t.prohibited is not None else False; stop_processing likewise) and construct
nested RuleTriggerSimple and RuleActionSimple; then update create_rule,
search_rules, get_rule, and update_rule to call self._to_rule(...) (for
search_rules map each result through the helper).
- Around line 67-72: Change RuleStore.rule_group_id to be Optional[str] in the
Firefly model (update its type annotation and any serializers/deserializers) and
then modify the code that builds the RuleStore instance in the rules service so
it sets rule_group_id to '1' only when rule_group_title is not provided (i.e.,
rule_group_id = '1' if not req.rule_group_title else None). Update any
imports/uses of RuleStore and ensure RuleUpdate typing remains compatible.
🧹 Nitpick comments (7)
src/lampyrid/models/lampyrid_models.py (3)

1148-1163: SearchRulesRequest validator treats empty strings as "no criterion" — reasonable but worth noting.

The truthiness-based check (any([self.trigger_type, ...])) means passing trigger_type="" would not count as a criterion. This is acceptable behavior since an empty string isn't a meaningful filter, but it differs from the SearchTransactionsRequest validator (lines 476-503) which explicitly handles this with isinstance(field, str) and field.strip() != ''. The inconsistency is minor but could confuse future maintainers.


1191-1207: triggers and actions typed as List[dict[str, Any]] — validation deferred to service layer.

This is a deliberate loose-typing choice, but it means invalid trigger/action structures won't be caught until the service layer calls RuleTriggerStore(**t) / RuleActionStore(**a). For UpdateRuleRequest and CreateRuleRequest, consider using List[RuleTriggerStore] / List[RuleActionStore] (or their update variants) to shift validation left to the model boundary. This is a tradeoff with MCP tool flexibility, so flagging as optional.

Also applies to: 1340-1357


1324-1331: Use RuleTriggerType enum instead of str for the trigger field to improve type safety.

The trigger field accepts only 'store-journal', 'update-journal', or 'manual-activation', but is typed as str. Invalid values fail later at the service layer when RuleTriggerType(req.trigger) is called, producing less informative errors. Since RuleTriggerType already exists in firefly_models and other enum types from that module are already used in this file, constraining the field to the enum would catch invalid values at the model boundary and align with existing patterns.

♻️ Proposed fix
-    trigger: str = Field(
-        'store-journal',
+    trigger: RuleTriggerType = Field(
+        RuleTriggerType.store_journal,
         description=(
             'When the rule should fire. '
             '"store-journal" = on new transactions (most common), '
             '"update-journal" = on transaction updates.'
         ),
     )

Add RuleTriggerType to the imports from firefly_models at the top of the file.

src/lampyrid/services/rules.py (4)

295-301: Inline import of RuleActionUpdate is inconsistent with top-level imports.

RuleTriggerUpdate is imported at the top of the file (line 13), but RuleActionUpdate is imported inline here. Move it to the top-level import for consistency.

♻️ Proposed fix

At line 13:

-from ..models.firefly_models import RuleActionStore, RuleStore, RuleTriggerStore, RuleTriggerType, RuleTriggerUpdate, RuleUpdate
+from ..models.firefly_models import RuleActionStore, RuleActionUpdate, RuleStore, RuleTriggerStore, RuleTriggerType, RuleTriggerUpdate, RuleUpdate

At lines 295-301:

         if req.actions is not None:
             try:
-                from ..models.firefly_models import RuleActionUpdate
-
                 rule_update.actions = [RuleActionUpdate(**a) for a in req.actions]
             except ValidationError as e:
                 raise ValueError(f'Invalid action format: {e}')

141-154: Unbounded pagination loop has no safety limit.

If the API returns inconsistent pagination metadata (e.g., current_page never reaching total_pages due to concurrent rule creation), this loop runs indefinitely. Consider adding a max page limit.

🛡️ Proposed fix
         all_rules = []
         page = 1
-        while True:
+        max_pages = 100  # Safety limit
+        while page <= max_pages:
             rule_array = await self._client.get_rules(page)
             all_rules.extend(rule_array.data)
 
             # Check pagination safely (can be None)
             if (
                 not rule_array.meta.pagination
                 or rule_array.meta.pagination.current_page >= rule_array.meta.pagination.total_pages
             ):
                 break
             page += 1

332-361: test_rule makes two sequential API calls; the first (get_rule) is only for the title.

The extra round-trip to fetch the rule title is unnecessary latency. Consider whether the title is required in the result, or if it could be populated from the test response itself. If the title is essential, this is acceptable — just noting the tradeoff.


125-139: User-supplied regex compiled without bounds — low ReDoS risk but worth noting.

User-provided trigger_value_pattern and action_value_pattern are compiled directly. In an MCP context, the user is typically the LLM agent or a trusted client, so the risk is minimal. If this were ever exposed to untrusted input, consider adding a timeout or pattern complexity limit.

Comment thread src/lampyrid/models/lampyrid_models.py Outdated
Comment thread src/lampyrid/services/rules.py Outdated
Comment thread src/lampyrid/services/rules.py Outdated
@RadCod3

RadCod3 commented Feb 15, 2026

Copy link
Copy Markdown
Owner

Hi @edmundhighcock,

Apologies for the late reply and thank you for your contribution!
If you don't mind can we add some integration tests as well?

Thanks!

Add comprehensive rules functionality to manage Firefly III rules:
- search_rules: Client-side filtering with regex pattern support
- get_rule: Retrieve single rule details
- update_rule: Modify rule configuration (triggers/actions)
- test_rule: Preview rule matches without changes (read-only)
- execute_rule: Apply changes with explicit confirmation requirement

Implementation includes:
- RuleService in services layer with client-side search/filtering
- 11 new request/response models for type safety
- 5 new HTTP client methods for Firefly III rule APIs
- Comprehensive unit tests (19 test cases)
- Safety-first design: test before execute, confirmation required

This brings the total tool count from 22 to 27.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@edmundhighcock edmundhighcock force-pushed the feat/add-rules-management branch from ffd5f40 to 470f5e9 Compare March 29, 2026 07:39
@codecov-commenter

codecov-commenter commented Mar 29, 2026

Copy link
Copy Markdown

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

Codecov Report

❌ Patch coverage is 99.12281% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.75%. Comparing base (011d2f4) to head (9437c6e).

Files with missing lines Patch % Lines
src/lampyrid/clients/firefly.py 93.75% 2 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #75      +/-   ##
==========================================
+ Coverage   97.66%   97.75%   +0.09%     
==========================================
  Files          19       21       +2     
  Lines        3121     3348     +227     
==========================================
+ Hits         3048     3273     +225     
- Misses         73       75       +2     
Flag Coverage Δ
integration 97.75% <99.12%> (+0.09%) ⬆️

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.

edmundhighcock and others added 2 commits April 3, 2026 16:50
- Fix `active or True` logic error that made inactive rules appear active
- Replace all `or <default>` patterns with explicit None-coalescing
- Extract Rule.from_rule_read() factory method (DRY: 3x duplication → 1)
- Fix test helper using wrong enum (RuleTriggerType vs RuleTriggerKeyword)
- Use Pydantic's Field(deprecated=...) for the trigger field
- Move RuleActionUpdate to top-level import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/lampyrid/models/lampyrid_models.py (1)

1249-1269: ⚠️ Potential issue | 🟡 Minor

Reject inverted date ranges up front.

start_date > end_date is still accepted for both preview and execute requests, so a caller typo turns into an empty preview or a no-op execution instead of a validation error. Add an after validator on both models before these requests reach the service.

Also applies to: 1278-1306

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lampyrid/models/lampyrid_models.py` around lines 1249 - 1269, Add a
post-validation check to the request models that declare start_date and
end_date: implement a `@root_validator` (or an `@validator` on end_date that
accesses start_date) that raises a ValueError when start_date > end_date so
inverted date ranges are rejected; apply this to both the preview and execute
request models that define the start_date and end_date fields (validate the
start_date and end_date attributes and raise a clear error message like
"start_date must be on or before end_date").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lampyrid/models/lampyrid_models.py`:
- Around line 1243-1246: The TestRuleRequest Pydantic model is being collected
by pytest due to the "Test*" class pattern; add a class attribute __test__ =
False inside the TestRuleRequest class (e.g., directly under the docstring and
above model_config) so pytest will skip collecting it, leaving the class name
unchanged and keeping model_config = ConfigDict(extra='forbid') intact.
- Around line 1224-1240: The triggers and actions fields currently use
Optional[List[dict[str, Any]]] which allows malformed payloads and silent
dropping of unknown fields; change these to use the dedicated nested request
models (e.g., replace types with Optional[List[RuleTriggerUpdate]] and
Optional[List[RuleActionUpdate]]), ensure the RuleTriggerUpdate and
RuleActionUpdate classes mark the type field as required and set Config.extra =
'forbid' (or equivalent Pydantic config) so unknown fields are rejected, and
update any validators/usage in lampyrid_models.py to reflect the new types so
invalid payloads like {} or {"prohibited": true} fail validation immediately.

In `@src/lampyrid/services/rules.py`:
- Around line 106-121: The code currently excludes empty-string trigger/action
values by using truthy checks (e.g., list comprehensions in the trigger_values
and action_values where you use "if t.value" and "if a.value"), which prevents
regexes like ^$ or .* from matching intentionally-blank values; update the
filters to only exclude None (use explicit None checks such as "if t.value is
not None" and "if a.value is not None") so empty strings remain in
trigger_values and action_values for pattern matching while still filtering out
missing values in rule_attrs.triggers and rule_attrs.actions.

---

Duplicate comments:
In `@src/lampyrid/models/lampyrid_models.py`:
- Around line 1249-1269: Add a post-validation check to the request models that
declare start_date and end_date: implement a `@root_validator` (or an `@validator`
on end_date that accesses start_date) that raises a ValueError when start_date >
end_date so inverted date ranges are rejected; apply this to both the preview
and execute request models that define the start_date and end_date fields
(validate the start_date and end_date attributes and raise a clear error message
like "start_date must be on or before end_date").
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3be0f6a3-291a-41ea-9167-504801ffd06e

📥 Commits

Reviewing files that changed from the base of the PR and between ca02a03 and f6b07c5.

📒 Files selected for processing (6)
  • src/lampyrid/clients/firefly.py
  • src/lampyrid/models/lampyrid_models.py
  • src/lampyrid/services/rules.py
  • src/lampyrid/tools/__init__.py
  • src/lampyrid/tools/rules.py
  • tests/unit/test_rules_service.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/test_rules_service.py
  • src/lampyrid/clients/firefly.py

Comment thread src/lampyrid/models/lampyrid_models.py
Comment thread src/lampyrid/models/lampyrid_models.py
Comment thread src/lampyrid/services/rules.py
…r rules

- Fix test_search_rules_by_trigger_type: use valid enum value amount_more
- Fix invalid trigger/action dict tests: use invalid enum value instead
  of extra fields (generated models silently ignore extra fields)
- Fix empty-string filtering: use `is not None` instead of truthiness
  check so regex patterns can match empty trigger/action values
- Add start_date <= end_date validation to TestRuleRequest and
  ExecuteRuleRequest with model_validator
- Add 13 integration tests for rules (search, get, update, test, execute)
- Add 'rules' pytest marker to pyproject.toml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/integration/test_rules.py (1)

15-48: Test helpers access private _client member.

The helpers directly access firefly_client._client to make raw HTTP calls. While acceptable for integration tests that need to bypass the public API, consider adding a public method to FireflyClient for raw requests if this pattern expands, or document that these helpers are test-only utilities.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_rules.py` around lines 15 - 48, The test helpers
_create_rule_via_api and _delete_rule_via_api directly access the private
attribute firefly_client._client; add a public test-only method on FireflyClient
(e.g., request_raw or api_request) that wraps the internal HTTP client and
exposes post/delete and raise_for_status behavior, then update
_create_rule_via_api and _delete_rule_via_api to call FireflyClient.request_raw
(or the chosen method) instead of accessing _client; alternatively, if you
prefer not to change the class, document these helpers as test-only and justify
the private access in the test module's docstring.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lampyrid/services/rules.py`:
- Around line 79-84: The pagination loop can raise TypeError when
pagination.current_page or pagination.total_pages is None; update the loop
condition in the block using rule_array.meta.pagination to guard against None by
treating missing values as terminal (i.e., break if pagination is missing OR
current_page is None OR total_pages is None OR current_page >= total_pages).
Locate the check referencing rule_array.meta.pagination.current_page and
rule_array.meta.pagination.total_pages and add explicit None checks before
performing the >= comparison (ensure variable page increment remains unchanged).

---

Nitpick comments:
In `@tests/integration/test_rules.py`:
- Around line 15-48: The test helpers _create_rule_via_api and
_delete_rule_via_api directly access the private attribute
firefly_client._client; add a public test-only method on FireflyClient (e.g.,
request_raw or api_request) that wraps the internal HTTP client and exposes
post/delete and raise_for_status behavior, then update _create_rule_via_api and
_delete_rule_via_api to call FireflyClient.request_raw (or the chosen method)
instead of accessing _client; alternatively, if you prefer not to change the
class, document these helpers as test-only and justify the private access in the
test module's docstring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf238a46-16fa-4ba4-a259-564e75b607c7

📥 Commits

Reviewing files that changed from the base of the PR and between f6b07c5 and b3cb5c5.

📒 Files selected for processing (5)
  • pyproject.toml
  • src/lampyrid/models/lampyrid_models.py
  • src/lampyrid/services/rules.py
  • tests/integration/test_rules.py
  • tests/unit/test_rules_service.py
✅ Files skipped from review due to trivial changes (1)
  • pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/test_rules_service.py

Comment thread src/lampyrid/services/rules.py
The Firefly III API returns 500 when creating rules with only
rule_group_title. Fix by fetching/creating a rule group first and
using its ID. Also improve error reporting on creation failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/integration/test_rules.py`:
- Around line 441-472: The test currently creates a real rule via
_create_rule_via_api (rule_id) that matches transactions containing "Seed:" and
then executes it, leaving tags on persisted transactions; change the test to
avoid side effects by using a no-match trigger_value (e.g., set trigger_value to
a unique random string like "NO_MATCH_<uuid>" or "THIS_SHOULD_NOT_MATCH") when
calling _create_rule_via_api in the test (tests/integration/test_rules.py) so
the execute_rule call (mcp_client.call_tool 'execute_rule' with rule_id,
start_date, end_date, confirm) runs without mutating real transactions;
alternatively, if you prefer the other approach, create a throwaway transaction
before execute, run the rule against that transaction, then remove the
transaction and ensure rule_cleanup removes the rule—update references to
rule_id, rule_cleanup, and the execute_rule call accordingly.
- Around line 356-361: The test currently asserts that
test_result['matched_transaction_count'] is an int and then tautologically
checks it is >= 0; to ensure the preview actually matched the seeded "Seed:"
transactions change the second assertion to require a positive match
(test_result['matched_transaction_count'] > 0). Update the assertion that
references test_result['matched_transaction_count'] so the test will fail if no
transactions were matched, thereby guaranteeing the preview matched seeded data.
- Around line 28-39: The current lookup caches groups[0]['id'] which may pick an
arbitrary existing group; change the logic in the helper that sets
_test_rule_group_id to search the fetched groups list for an item whose 'title'
equals "Test Rules" and use that id if found, only issuing the POST to
firefly_client._client.post('/api/v1/rule-groups', json={'title': 'Test Rules',
'order': 1}) to create the group when no matching title exists; update any
references to _test_rule_group_id to use the found-or-created id.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f05f04a8-451f-4627-9f1d-8867db79b178

📥 Commits

Reviewing files that changed from the base of the PR and between b3cb5c5 and 9437c6e.

📒 Files selected for processing (1)
  • tests/integration/test_rules.py

Comment thread tests/integration/test_rules.py
Comment thread tests/integration/test_rules.py
Comment thread tests/integration/test_rules.py
@edmundhighcock

Copy link
Copy Markdown
Contributor Author

Hi @RadCod3, just a heads-up — integration tests have been added as requested. The latest push includes:

  • 12 integration tests covering search, get, update, test (preview), and execute rule flows
  • Fixes for the 3 failing unit tests
  • A few code quality improvements flagged by CodeRabbit (empty-value filtering bug, date range validation)

CI is all green. Let me know if there's anything else you'd like changed!

@RadCod3

RadCod3 commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Hey @edmundhighcock I marked the coderabbit stuff as resolved and will proceed to merge this.
Thanks alot for the contribution!!

@RadCod3 RadCod3 merged commit 297aa57 into RadCod3:main Jun 3, 2026
5 checks passed
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.

3 participants