102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
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)
|