import re from decimal import Decimal from typing import Literal from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, field_validator class StrictModel(BaseModel): model_config = ConfigDict(extra='forbid', allow_inf_nan=False) class Customer(StrictModel): cnpj: str zap: str mail: str = Field(max_length=254) @field_validator('cnpj') @classmethod def cnpj_valid(cls, value): digits = re.sub(r'\D', '', value) if len(digits) != 14 or len(set(digits)) == 1 or not digits.isascii(): raise ValueError('Invalid CNPJ') def check(base, weights): rem = sum(int(n) * w for n,w in zip(base, weights)) % 11 return 0 if rem < 2 else 11-rem if check(digits[:12], [5,4,3,2,9,8,7,6,5,4,3,2]) != int(digits[12]) or check(digits[:13], [6,5,4,3,2,9,8,7,6,5,4,3,2]) != int(digits[13]): raise ValueError('Invalid CNPJ') return digits @field_validator('zap') @classmethod def phone_valid(cls, value): digits = re.sub(r'\D', '', value) if len(digits) not in (10,11) or not digits.isascii(): raise ValueError('Invalid phone') return digits @field_validator('mail') @classmethod def email_valid(cls, value): if not re.fullmatch(r'[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}', value.strip()): raise ValueError('Invalid email') return value.strip() class Freight(StrictModel): service: Literal['pickup','mock-standard'] = 'pickup' postal_code: str = Field(default='', max_length=8) class UploadStart(StrictModel): name: str = Field(min_length=1, max_length=200, pattern=r'^[^/\\\x00-\x1f]+$') size: int = Field(gt=0, strict=True) @field_validator('name') @classmethod def artwork_extension(cls, value): # Union of existing Site product formats; this is NOT malware/pre-flight validation. if not re.search(r'\.(png|jpe?g|webp|tiff?|pdf|psd|psb|ai|cdr)$', value, re.I): raise ValueError('Unsupported artwork file extension') return value class Item(StrictModel): mode: Literal['file','avulsa','uvfile','uv'] metres: Decimal = Field(gt=0, le=12000) grade: int = Field(ge=0, le=100, strict=True) uploads: list[UUID] = Field(min_length=1, max_length=20) class QuoteRequest(StrictModel): request_key: UUID customer: Customer items: list[Item] = Field(min_length=1, max_length=30) freight: Freight class Review(StrictModel): items: list[Item] = Field(min_length=1, max_length=30) class Pay(StrictModel): quote_id: UUID class Move(StrictModel): state: Literal['rec','tra','fil','imp','cor','fin'] version: int = Field(ge=0) reason: str = Field(default='', max_length=1000) class Register(StrictModel): customer: Customer password: str = Field(min_length=12, max_length=128) class Login(StrictModel): email: str = Field(min_length=3, max_length=254) password: str = Field(min_length=1, max_length=128) class OperatorLogin(StrictModel): username: str = Field(min_length=1, max_length=100) password: str = Field(min_length=1, max_length=128) class FileReference(StrictModel): upload_id: UUID item_index: int = Field(ge=0, strict=True) class ArtworkSubmission(StrictModel): version: int = Field(ge=0, strict=True) files: list[FileReference] = Field(min_length=1, max_length=60) note: str = Field(min_length=1, max_length=1000)