Skip to content

Commit 8bbc3bc

Browse files
authored
Merge pull request #67 from RadCod3/feat/62-add-destinationid
Upgrade Firefly III API schema to v6.4.16 with validation
2 parents a0484bc + 4bf5ba1 commit 8bbc3bc

18 files changed

+1457
-722
lines changed
Lines changed: 172 additions & 171 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ dependencies = [
55
"pydantic>=2.11.7",
66
"pydantic-settings>=2.10.1",
77
]
8-
description = "Add your description here"
8+
description = "A simple MCP server for Firefly III"
99
name = "lampyrid"
1010
readme = "README.md"
1111
requires-python = ">=3.14"
1212
version = "0.3.0"
1313

1414
[project.scripts]
1515
lampyrid = "lampyrid.__main__:main"
16+
update-schema = "lampyrid.scripts.update_schema:main"
17+
lint = "lampyrid.scripts.format:main"
1618

1719
[dependency-groups]
1820
dev = [
@@ -65,6 +67,6 @@ markers = [
6567
]
6668

6769
[tool.datamodel-codegen]
68-
input = "firefly-iii-6.4.14-v1.yaml"
70+
input = "firefly-iii-6.4.16-v1.yaml"
6971
output = "src/lampyrid/models/firefly_models.py"
7072
output-model-type = "pydantic_v2.BaseModel"

src/lampyrid/models/firefly_models.py

Lines changed: 117 additions & 116 deletions
Large diffs are not rendered by default.

src/lampyrid/models/lampyrid_models.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import date, datetime, timezone
44
from typing import List, Literal, Optional
55

6-
from pydantic import BaseModel, Field, model_validator
6+
from pydantic import BaseModel, ConfigDict, Field, model_validator
77

88
from .firefly_models import (
99
AccountRead,
@@ -171,6 +171,8 @@ def to_transaction_split_store(self) -> TransactionSplitStore:
171171
class ListAccountRequest(BaseModel):
172172
"""Request model for listing accounts."""
173173

174+
model_config = ConfigDict(extra='forbid')
175+
174176
type: AccountTypeFilter = Field(
175177
...,
176178
description=(
@@ -183,6 +185,8 @@ class ListAccountRequest(BaseModel):
183185
class SearchAccountRequest(BaseModel):
184186
"""Request model for searching accounts."""
185187

188+
model_config = ConfigDict(extra='forbid')
189+
186190
query: str = Field(
187191
...,
188192
description='Text to search for in account names (supports partial matching)',
@@ -199,6 +203,8 @@ class SearchAccountRequest(BaseModel):
199203
class GetAccountRequest(BaseModel):
200204
"""Request model for getting a single account."""
201205

206+
model_config = ConfigDict(extra='forbid')
207+
202208
id: str = Field(
203209
..., description='Unique identifier of the account (from list_accounts or search_accounts)'
204210
)
@@ -207,6 +213,8 @@ class GetAccountRequest(BaseModel):
207213
class CreateWithdrawalRequest(BaseModel):
208214
"""Request model for creating a withdrawal transaction."""
209215

216+
model_config = ConfigDict(extra='forbid')
217+
210218
amount: float = Field(
211219
..., description='Amount to withdraw as positive number (e.g., 25.50 for $25.50 expense)'
212220
)
@@ -224,11 +232,20 @@ class CreateWithdrawalRequest(BaseModel):
224232
'Must be an asset account you own.'
225233
),
226234
)
235+
destination_id: Optional[str] = Field(
236+
default=None,
237+
description=(
238+
'ID of the expense account receiving the money (from list_accounts type=expense). '
239+
'Use destination_id OR destination_name, not both. '
240+
'If neither provided, defaults to Cash.'
241+
),
242+
)
227243
destination_name: Optional[str] = Field(
228244
default=None,
229245
description=(
230246
'Where the money went ("Groceries", "Gas Station", "ATM"). '
231-
'Creates expense account if new. Leave blank for cash withdrawals.'
247+
'Creates expense account if new. Use destination_id OR destination_name, not both. '
248+
'If neither provided, defaults to Cash.'
232249
),
233250
)
234251
budget_id: Optional[str] = Field(
@@ -239,10 +256,21 @@ class CreateWithdrawalRequest(BaseModel):
239256
description='Name of budget if ID is unknown. Will use ID if both provided.',
240257
)
241258

259+
@model_validator(mode='after')
260+
def validate_destination_mutual_exclusivity(self):
261+
"""Ensure destination_id and destination_name are not both provided."""
262+
if self.destination_id is not None and self.destination_name is not None:
263+
raise ValueError(
264+
'Cannot specify both destination_id and destination_name. Use one or the other.'
265+
)
266+
return self
267+
242268

243269
class CreateDepositRequest(BaseModel):
244270
"""Request model for creating a deposit transaction."""
245271

272+
model_config = ConfigDict(extra='forbid')
273+
246274
amount: float = Field(
247275
..., description='Amount received as positive number (e.g., 2500.00 for $2500 salary)'
248276
)
@@ -253,11 +281,20 @@ class CreateDepositRequest(BaseModel):
253281
default_factory=utc_now,
254282
description='When the income was received (defaults to current time if not specified)',
255283
)
284+
source_id: Optional[str] = Field(
285+
default=None,
286+
description=(
287+
'ID of the revenue account the money comes from (from list_accounts type=revenue). '
288+
'Use source_id OR source_name, not both. '
289+
'If neither provided, defaults to Cash.'
290+
),
291+
)
256292
source_name: Optional[str] = Field(
257293
default=None,
258294
description=(
259295
'Where the money came from ("Employer", "Client Name", "Gift"). '
260-
'Creates revenue account if new.'
296+
'Creates revenue account if new. Use source_id OR source_name, not both. '
297+
'If neither provided, defaults to Cash.'
261298
),
262299
)
263300
destination_id: str = Field(
@@ -268,10 +305,19 @@ class CreateDepositRequest(BaseModel):
268305
),
269306
)
270307

308+
@model_validator(mode='after')
309+
def validate_source_mutual_exclusivity(self):
310+
"""Ensure source_id and source_name are not both provided."""
311+
if self.source_id is not None and self.source_name is not None:
312+
raise ValueError('Cannot specify both source_id and source_name. Use one or the other.')
313+
return self
314+
271315

272316
class CreateTransferRequest(BaseModel):
273317
"""Request model for creating a transfer transaction."""
274318

319+
model_config = ConfigDict(extra='forbid')
320+
275321
amount: float = Field(
276322
..., description='Amount to move as positive number (e.g., 500.00 to move $500)'
277323
)
@@ -296,6 +342,8 @@ class CreateTransferRequest(BaseModel):
296342
class GetTransactionsRequest(BaseModel):
297343
"""Request model for retrieving transactions."""
298344

345+
model_config = ConfigDict(extra='forbid')
346+
299347
account_id: Optional[str] = Field(
300348
None,
301349
description=(
@@ -332,6 +380,8 @@ class GetTransactionsRequest(BaseModel):
332380
class SearchTransactionsRequest(BaseModel):
333381
"""Request model for searching transactions."""
334382

383+
model_config = ConfigDict(extra='forbid')
384+
335385
query: str | None = Field(
336386
None,
337387
description=(
@@ -453,12 +503,16 @@ def validate_search_criteria(self):
453503
class DeleteTransactionRequest(BaseModel):
454504
"""Request model for deleting a transaction."""
455505

506+
model_config = ConfigDict(extra='forbid')
507+
456508
id: str = Field(..., description='Unique identifier of the transaction to permanently remove')
457509

458510

459511
class GetTransactionRequest(BaseModel):
460512
"""Request model for getting a single transaction."""
461513

514+
model_config = ConfigDict(extra='forbid')
515+
462516
id: str = Field(..., description='Unique identifier of the transaction to get details for')
463517

464518

@@ -536,6 +590,8 @@ def from_transaction_array(
536590
class ListBudgetsRequest(BaseModel):
537591
"""Request for listing budgets."""
538592

593+
model_config = ConfigDict(extra='forbid')
594+
539595
active: Optional[bool] = Field(
540596
None,
541597
description='Show only active budgets (true), inactive (false), or all budgets '
@@ -546,6 +602,8 @@ class ListBudgetsRequest(BaseModel):
546602
class GetBudgetRequest(BaseModel):
547603
"""Request for getting a single budget by ID."""
548604

605+
model_config = ConfigDict(extra='forbid')
606+
549607
id: str = Field(..., description='Unique identifier of the budget to get details for')
550608

551609

@@ -572,6 +630,8 @@ class BudgetSpending(BaseModel):
572630
class GetBudgetSpendingRequest(BaseModel):
573631
"""Request for getting budget spending data."""
574632

633+
model_config = ConfigDict(extra='forbid')
634+
575635
budget_id: str = Field(
576636
..., description='Unique identifier of the budget to analyze spending for'
577637
)
@@ -601,6 +661,8 @@ class BudgetSummary(BaseModel):
601661
class GetBudgetSummaryRequest(BaseModel):
602662
"""Request for getting budget summary."""
603663

664+
model_config = ConfigDict(extra='forbid')
665+
604666
start_date: Optional[date] = Field(
605667
None, description='Start date for summary period (YYYY-MM-DD), inclusive'
606668
)
@@ -621,6 +683,8 @@ class AvailableBudget(BaseModel):
621683
class GetAvailableBudgetRequest(BaseModel):
622684
"""Request for getting available budget."""
623685

686+
model_config = ConfigDict(extra='forbid')
687+
624688
start_date: Optional[date] = Field(
625689
None,
626690
description='Start date for budget analysis (YYYY-MM-DD). Defaults to '
@@ -637,6 +701,8 @@ class GetAvailableBudgetRequest(BaseModel):
637701
class CreateBulkTransactionsRequest(BaseModel):
638702
"""Create multiple transactions in one operation."""
639703

704+
model_config = ConfigDict(extra='forbid')
705+
640706
transactions: List[Transaction] = Field(
641707
...,
642708
description=(
@@ -672,6 +738,8 @@ def validate_transactions(self):
672738
class UpdateTransactionRequest(BaseModel):
673739
"""Update an existing transaction."""
674740

741+
model_config = ConfigDict(extra='forbid')
742+
675743
transaction_id: str = Field(..., description='Unique identifier of the transaction to modify')
676744
amount: Optional[float] = Field(None, description='New transaction amount (positive number)')
677745
description: Optional[str] = Field(
@@ -697,6 +765,8 @@ class UpdateTransactionRequest(BaseModel):
697765
class BulkUpdateTransactionsRequest(BaseModel):
698766
"""Update multiple transactions in one operation."""
699767

768+
model_config = ConfigDict(extra='forbid')
769+
700770
updates: List[UpdateTransactionRequest] = Field(
701771
...,
702772
description='Array of transaction modifications to apply in a single operation',

src/lampyrid/scripts/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Scripts for maintenance and development tasks."""

src/lampyrid/scripts/format.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Script to run code formatting and linting."""
2+
3+
import subprocess
4+
import sys
5+
from pathlib import Path
6+
7+
8+
def main() -> int:
9+
"""Run code formatting and linting fix."""
10+
root_dir = Path(__file__).parent.parent.parent.parent.resolve()
11+
12+
print(f'Running formatting in {root_dir}...')
13+
print('Running ruff format...')
14+
try:
15+
subprocess.run(['ruff', 'format', '.'], cwd=root_dir, check=True)
16+
print('Running ruff check --fix...')
17+
subprocess.run(['ruff', 'check', '--fix', '.'], cwd=root_dir, check=True)
18+
except subprocess.CalledProcessError as e:
19+
print(f'Error during formatting: {e}')
20+
return 1
21+
except FileNotFoundError:
22+
print("Error: 'ruff' not found. Ensure it is installed in your environment.")
23+
return 1
24+
25+
return 0
26+
27+
28+
if __name__ == '__main__':
29+
sys.exit(main())

0 commit comments

Comments
 (0)