Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
343 changes: 172 additions & 171 deletions firefly-iii-6.4.14-v1.yaml → firefly-iii-6.4.16-v1.yaml

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ dependencies = [
"pydantic>=2.11.7",
"pydantic-settings>=2.10.1",
]
description = "Add your description here"
description = "A simple MCP server for Firefly III"
name = "lampyrid"
readme = "README.md"
requires-python = ">=3.14"
version = "0.3.0"

[project.scripts]
lampyrid = "lampyrid.__main__:main"
update-schema = "lampyrid.scripts.update_schema:main"
lint = "lampyrid.scripts.format:main"

[dependency-groups]
dev = [
Expand Down Expand Up @@ -65,6 +67,6 @@ markers = [
]

[tool.datamodel-codegen]
input = "firefly-iii-6.4.14-v1.yaml"
input = "firefly-iii-6.4.16-v1.yaml"
output = "src/lampyrid/models/firefly_models.py"
output-model-type = "pydantic_v2.BaseModel"
233 changes: 117 additions & 116 deletions src/lampyrid/models/firefly_models.py

Large diffs are not rendered by default.

76 changes: 73 additions & 3 deletions src/lampyrid/models/lampyrid_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import date, datetime, timezone
from typing import List, Literal, Optional

from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator

from .firefly_models import (
AccountRead,
Expand Down Expand Up @@ -171,6 +171,8 @@ def to_transaction_split_store(self) -> TransactionSplitStore:
class ListAccountRequest(BaseModel):
"""Request model for listing accounts."""

model_config = ConfigDict(extra='forbid')

type: AccountTypeFilter = Field(
...,
description=(
Expand All @@ -183,6 +185,8 @@ class ListAccountRequest(BaseModel):
class SearchAccountRequest(BaseModel):
"""Request model for searching accounts."""

model_config = ConfigDict(extra='forbid')

query: str = Field(
...,
description='Text to search for in account names (supports partial matching)',
Expand All @@ -199,6 +203,8 @@ class SearchAccountRequest(BaseModel):
class GetAccountRequest(BaseModel):
"""Request model for getting a single account."""

model_config = ConfigDict(extra='forbid')

id: str = Field(
..., description='Unique identifier of the account (from list_accounts or search_accounts)'
)
Expand All @@ -207,6 +213,8 @@ class GetAccountRequest(BaseModel):
class CreateWithdrawalRequest(BaseModel):
"""Request model for creating a withdrawal transaction."""

model_config = ConfigDict(extra='forbid')

amount: float = Field(
..., description='Amount to withdraw as positive number (e.g., 25.50 for $25.50 expense)'
)
Expand All @@ -224,11 +232,20 @@ class CreateWithdrawalRequest(BaseModel):
'Must be an asset account you own.'
),
)
destination_id: Optional[str] = Field(
default=None,
description=(
'ID of the expense account receiving the money (from list_accounts type=expense). '
'Use destination_id OR destination_name, not both. '
'If neither provided, defaults to Cash.'
),
)
destination_name: Optional[str] = Field(
default=None,
description=(
'Where the money went ("Groceries", "Gas Station", "ATM"). '
'Creates expense account if new. Leave blank for cash withdrawals.'
'Creates expense account if new. Use destination_id OR destination_name, not both. '
'If neither provided, defaults to Cash.'
),
)
budget_id: Optional[str] = Field(
Expand All @@ -239,10 +256,21 @@ class CreateWithdrawalRequest(BaseModel):
description='Name of budget if ID is unknown. Will use ID if both provided.',
)

@model_validator(mode='after')
def validate_destination_mutual_exclusivity(self):
"""Ensure destination_id and destination_name are not both provided."""
if self.destination_id is not None and self.destination_name is not None:
raise ValueError(
'Cannot specify both destination_id and destination_name. Use one or the other.'
)
return self


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

model_config = ConfigDict(extra='forbid')

amount: float = Field(
..., description='Amount received as positive number (e.g., 2500.00 for $2500 salary)'
)
Expand All @@ -253,11 +281,20 @@ class CreateDepositRequest(BaseModel):
default_factory=utc_now,
description='When the income was received (defaults to current time if not specified)',
)
source_id: Optional[str] = Field(
default=None,
description=(
'ID of the revenue account the money comes from (from list_accounts type=revenue). '
'Use source_id OR source_name, not both. '
'If neither provided, defaults to Cash.'
),
)
source_name: Optional[str] = Field(
default=None,
description=(
'Where the money came from ("Employer", "Client Name", "Gift"). '
'Creates revenue account if new.'
'Creates revenue account if new. Use source_id OR source_name, not both. '
'If neither provided, defaults to Cash.'
),
)
destination_id: str = Field(
Expand All @@ -268,10 +305,19 @@ class CreateDepositRequest(BaseModel):
),
)

@model_validator(mode='after')
def validate_source_mutual_exclusivity(self):
"""Ensure source_id and source_name are not both provided."""
if self.source_id is not None and self.source_name is not None:
raise ValueError('Cannot specify both source_id and source_name. Use one or the other.')
return self


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

model_config = ConfigDict(extra='forbid')

amount: float = Field(
..., description='Amount to move as positive number (e.g., 500.00 to move $500)'
)
Expand All @@ -296,6 +342,8 @@ class CreateTransferRequest(BaseModel):
class GetTransactionsRequest(BaseModel):
"""Request model for retrieving transactions."""

model_config = ConfigDict(extra='forbid')

account_id: Optional[str] = Field(
None,
description=(
Expand Down Expand Up @@ -332,6 +380,8 @@ class GetTransactionsRequest(BaseModel):
class SearchTransactionsRequest(BaseModel):
"""Request model for searching transactions."""

model_config = ConfigDict(extra='forbid')

query: str | None = Field(
None,
description=(
Expand Down Expand Up @@ -453,12 +503,16 @@ def validate_search_criteria(self):
class DeleteTransactionRequest(BaseModel):
"""Request model for deleting a transaction."""

model_config = ConfigDict(extra='forbid')

id: str = Field(..., description='Unique identifier of the transaction to permanently remove')


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

model_config = ConfigDict(extra='forbid')

id: str = Field(..., description='Unique identifier of the transaction to get details for')


Expand Down Expand Up @@ -536,6 +590,8 @@ def from_transaction_array(
class ListBudgetsRequest(BaseModel):
"""Request for listing budgets."""

model_config = ConfigDict(extra='forbid')

active: Optional[bool] = Field(
None,
description='Show only active budgets (true), inactive (false), or all budgets '
Expand All @@ -546,6 +602,8 @@ class ListBudgetsRequest(BaseModel):
class GetBudgetRequest(BaseModel):
"""Request for getting a single budget by ID."""

model_config = ConfigDict(extra='forbid')

id: str = Field(..., description='Unique identifier of the budget to get details for')


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

model_config = ConfigDict(extra='forbid')

budget_id: str = Field(
..., description='Unique identifier of the budget to analyze spending for'
)
Expand Down Expand Up @@ -601,6 +661,8 @@ class BudgetSummary(BaseModel):
class GetBudgetSummaryRequest(BaseModel):
"""Request for getting budget summary."""

model_config = ConfigDict(extra='forbid')

start_date: Optional[date] = Field(
None, description='Start date for summary period (YYYY-MM-DD), inclusive'
)
Expand All @@ -621,6 +683,8 @@ class AvailableBudget(BaseModel):
class GetAvailableBudgetRequest(BaseModel):
"""Request for getting available budget."""

model_config = ConfigDict(extra='forbid')

start_date: Optional[date] = Field(
None,
description='Start date for budget analysis (YYYY-MM-DD). Defaults to '
Expand All @@ -637,6 +701,8 @@ class GetAvailableBudgetRequest(BaseModel):
class CreateBulkTransactionsRequest(BaseModel):
"""Create multiple transactions in one operation."""

model_config = ConfigDict(extra='forbid')

transactions: List[Transaction] = Field(
...,
description=(
Expand Down Expand Up @@ -672,6 +738,8 @@ def validate_transactions(self):
class UpdateTransactionRequest(BaseModel):
"""Update an existing transaction."""

model_config = ConfigDict(extra='forbid')

transaction_id: str = Field(..., description='Unique identifier of the transaction to modify')
amount: Optional[float] = Field(None, description='New transaction amount (positive number)')
description: Optional[str] = Field(
Expand All @@ -697,6 +765,8 @@ class UpdateTransactionRequest(BaseModel):
class BulkUpdateTransactionsRequest(BaseModel):
"""Update multiple transactions in one operation."""

model_config = ConfigDict(extra='forbid')

updates: List[UpdateTransactionRequest] = Field(
...,
description='Array of transaction modifications to apply in a single operation',
Expand Down
1 change: 1 addition & 0 deletions src/lampyrid/scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Scripts for maintenance and development tasks."""
29 changes: 29 additions & 0 deletions src/lampyrid/scripts/format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Script to run code formatting and linting."""

import subprocess
import sys
from pathlib import Path


def main() -> int:
"""Run code formatting and linting fix."""
root_dir = Path(__file__).parent.parent.parent.parent.resolve()

print(f'Running formatting in {root_dir}...')
print('Running ruff format...')
try:
subprocess.run(['ruff', 'format', '.'], cwd=root_dir, check=True)
print('Running ruff check --fix...')
subprocess.run(['ruff', 'check', '--fix', '.'], cwd=root_dir, check=True)
except subprocess.CalledProcessError as e:
print(f'Error during formatting: {e}')
return 1
except FileNotFoundError:
print("Error: 'ruff' not found. Ensure it is installed in your environment.")
return 1

return 0


if __name__ == '__main__':
sys.exit(main())
Loading