162 lines
4.7 KiB
Python
162 lines
4.7 KiB
Python
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)
|