Files
simkeep/server/models.py
T
2026-10-06 15:49:27 +08:00

162 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from typing import Literal
from zoneinfo import ZoneInfo,ZoneInfoNotFoundError
from pydantic import BaseModel,ConfigDict,Field,StrictInt,field_validator,model_validator
from .domain import parse_date
from .money import CURRENCIES,normalize_amount
class Model(BaseModel):
model_config=ConfigDict(extra='forbid',str_strip_whitespace=True)
class Money(Model):
currency:str=Field(min_length=3,max_length=3)
amount:str=Field(min_length=1,max_length=30)
@model_validator(mode='after')
def valid_money(self):
self.currency=self.currency.upper()
self.amount=normalize_amount(self.amount,self.currency)
return self
class Login(Model):
model_config=ConfigDict(extra='forbid',str_strip_whitespace=False)
email:str=Field(min_length=3,max_length=120)
password:str=Field(min_length=1,max_length=128)
@field_validator('email')
@classmethod
def email_format(cls,value):
value=value.strip()
if not re.fullmatch(r'[^\s@]+@[^\s@]+\.[^\s@]+',value):
raise ValueError('请输入有效邮箱')
return value.lower()
class Register(Login):
name:str=Field(min_length=1,max_length=45)
password:str=Field(min_length=10,max_length=128)
@field_validator('name')
@classmethod
def valid_name(cls,value):
value=value.strip()
if not value:
raise ValueError('请填写名字')
return value
class RuleInput(Model):
action:Literal['sms','call','purchase','reset','topup','custom']
interval:StrictInt=Field(ge=1,le=3650)
unit:Literal['days','months']
anchor:Literal['completion','scheduled']
dueDate:str
instructions:str=Field(default='',max_length=1000)
version:StrictInt|None=Field(default=None,ge=1)
cost:Money|None=None
@field_validator('dueDate')
@classmethod
def valid_date(cls,value):
parse_date(value)
return value
class CardInput(Model):
name:str=Field(min_length=1,max_length=45)
provider:str=Field(min_length=1,max_length=45)
type:Literal['SIM','eSIM']
country:Literal['GB','US','HK','JP','CN','DE','SG','OTHER']
phone:str=Field(default='',max_length=35)
activatedAt:str
notes:str=Field(default='',max_length=1000)
plan:str=Field(default='',max_length=60)
rules:list[RuleInput]=Field(default_factory=list,max_length=20)
version:StrictInt|None=Field(default=None,ge=1)
balances:list[Money]|None=Field(default=None,max_length=len(CURRENCIES))
@field_validator('balances')
@classmethod
def unique_currencies(cls,value):
if value is not None and len({item.currency for item in value})!=len(value):
raise ValueError('同一张卡的每个币种只能记录一条余额')
return value
@field_validator('activatedAt')
@classmethod
def valid_date(cls,value):
parse_date(value)
return value
class ArchiveInput(Model):
archived:bool
version:StrictInt=Field(ge=1)
class Completion(Model):
completedAt:str
note:str=Field(default='',max_length=1000)
requestId:str=Field(min_length=8,max_length=100)
version:StrictInt=Field(ge=1)
cost:Money|None=None
balanceAction:Literal['none','deduct','credit']='none'
cardVersion:StrictInt|None=Field(default=None,ge=1)
class PlatformInput(Model):
name:str=Field(min_length=1,max_length=45)
account:str=Field(default='',max_length=120)
purpose:str=Field(default='',max_length=80)
version:StrictInt=Field(ge=1)
class SettingsInput(Model):
timezone:str
time:str
offsets:list[StrictInt]=Field(max_length=12)
overdue:bool
telegram:bool
email:bool
emailAddress:str=Field(default='',max_length=120)
@field_validator('timezone')
@classmethod
def valid_zone(cls,value):
try:
ZoneInfo(value)
except (ZoneInfoNotFoundError,ValueError):
raise ValueError('请选择有效时区') from None
return value
@field_validator('time')
@classmethod
def valid_time(cls,value):
if not re.fullmatch(r'(?:[01]\d|2[0-3]):[0-5]\d',value):
raise ValueError('请输入有效提醒时间')
return value
@field_validator('offsets')
@classmethod
def valid_offsets(cls,value):
if any(v < 1 or v > 365 for v in value):
raise ValueError('提前提醒天数须为 1–365')
return sorted(set(value),reverse=True)
@field_validator('emailAddress')
@classmethod
def valid_email(cls,value):
if value:
return Login.email_format(value)
return value
class EmailCode(Model):
code:str=Field(min_length=6,max_length=6)
class TelegramCode(Model):
code:str=Field(min_length=20,max_length=100)