Files
finance-dashboard/app/api/schemas.py
T

134 lines
3.6 KiB
Python

"""Transport schemas only; business validation lives in the shared services."""
from decimal import Decimal
from typing import Annotated, Literal
from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, StrictBool, model_validator
from models import cents, valid_date
AssetType = Literal['stock', 'etf', 'bond', 'crypto', 'interest', 'other']
Category = Literal['dividend', 'interest', 'distribution', 'other']
Identifier = Annotated[int, Field(strict=True, ge=1, le=9223372036854775807)]
MoneyString = Annotated[str, Field(pattern=r'^-?\d+\.\d{2}$', examples=['0.04'])]
def parse_amount(value):
if not isinstance(value, (str, Decimal)):
raise ValueError('Betrag als Dezimalstring senden, zum Beispiel "0.04".')
return Decimal(cents(value)) / 100
Amount = Annotated[Decimal, BeforeValidator(parse_amount, json_schema_input_type=str)]
Day = Annotated[str, BeforeValidator(valid_date)]
class RequestModel(BaseModel):
model_config = ConfigDict(extra='forbid')
class PatchModel(RequestModel):
@model_validator(mode='before')
@classmethod
def reject_required_nulls(cls, data):
if isinstance(data, dict):
for name, value in data.items():
if value is None and name not in {'note', 'ticker'}:
raise ValueError('Nur Notiz und Ticker dürfen null sein.')
return data
class AssetCreate(RequestModel):
name: str = Field(min_length=1, max_length=150)
ticker: str | None = Field(default=None, max_length=30)
asset_type: AssetType
active: StrictBool = True
class AssetPatch(PatchModel):
name: str | None = Field(default=None, min_length=1, max_length=150)
ticker: str | None = Field(default=None, max_length=30)
asset_type: AssetType | None = None
active: StrictBool | None = None
class AssetResponse(BaseModel):
id: int
name: str
ticker: str | None
asset_type: AssetType
active: bool
created_at: str
class IncomeCreate(RequestModel):
date: Day
asset_id: Identifier
category: Category
amount: Amount
note: str | None = Field(default=None, max_length=2000)
expected: StrictBool = False
received: StrictBool = True
class IncomePatch(PatchModel):
date: Day | None = None
asset_id: Identifier | None = None
category: Category | None = None
amount: Amount | None = None
note: str | None = Field(default=None, max_length=2000)
expected: StrictBool | None = None
received: StrictBool | None = None
class IncomeResponse(BaseModel):
id: int
date: str
asset: str = Field(description='Normalisierter Positionsname')
asset_id: int
category: Category
amount: MoneyString
note: str | None
expected: bool
received: bool
created_at: str
updated_at: str
class SummaryResponse(BaseModel):
current_month: MoneyString
current_month_previous_year: MoneyString
current_month_yoy_percent: MoneyString | None
current_year: MoneyString
previous_year: MoneyString
current_year_yoy_percent: MoneyString | None
all_time: MoneyString
current_year_payment_count: int
class MonthResponse(BaseModel):
month: int
amount: MoneyString
class MonthlyResponse(BaseModel):
year: int
months: list[MonthResponse]
total: MoneyString
class AssetShareResponse(BaseModel):
asset_id: int
asset: str
amount: MoneyString
percentage: MoneyString | None
class CategoryShareResponse(BaseModel):
category: Category
amount: MoneyString
percentage: MoneyString | None
class MetaResponse(BaseModel):
name: str = 'Finance Dashboard'
api_version: str = 'v1'
database: Literal['ok'] = 'ok'