""" Modelos Pydantic usados para validar entrada e formatar saída da API. """ import json from datetime import datetime from enum import Enum from typing import Optional from pydantic import BaseModel, Field, field_validator class TipoNegocio(str, Enum): venda = "Venda" locacao = "Locação" class TipoImovel(str, Enum): casa = "Casa" apartamento = "Apartamento" terreno = "Terreno" comercial = "Comercial" sala_comercial = "Sala Comercial" galpao = "Galpão" chacara_sitio = "Chácara/Sítio" cobertura = "Cobertura" kitnet = "Kitnet/Studio" class StatusAnuncio(str, Enum): disponivel = "Disponível" lancamento = "Lançamento" vendido = "Vendido" alugado = "Alugado" reservado = "Reservado" inativo = "Inativo" class AnuncioBase(BaseModel): tipo_negocio: TipoNegocio tipo_imovel: TipoImovel titulo: str = Field(..., min_length=3, max_length=150) descricao: str = Field("", max_length=3000) endereco: str = Field("", max_length=200) bairro: str = Field("", max_length=100) cidade: str = Field(..., min_length=2, max_length=100) estado: str = Field(..., min_length=2, max_length=2, description="Sigla da UF, ex: PR") cep: str = Field("", max_length=10) preco: float = Field(..., ge=0) condominio: Optional[float] = Field(0, ge=0) iptu: Optional[float] = Field(0, ge=0) area_util: Optional[float] = Field(0, ge=0) area_total: Optional[float] = Field(0, ge=0) quartos: Optional[int] = Field(0, ge=0) suites: Optional[int] = Field(0, ge=0) banheiros: Optional[int] = Field(0, ge=0) vagas_garagem: Optional[int] = Field(0, ge=0) status: StatusAnuncio = StatusAnuncio.disponivel destaque: Optional[bool] = False corretor_responsavel: str = Field("", max_length=100) telefone_contato: str = Field("", max_length=30) midias: Optional[str] = Field( "[]", description="JSON com a lista de URLs completas de imagens/vídeos, ex: [\"https://.../foto1.jpg\"]", ) @field_validator("estado") @classmethod def uf_maiuscula(cls, v: str) -> str: return v.upper() @field_validator("midias", mode="before") @classmethod def midias_json_valido(cls, v) -> str: """Aceita string JSON de lista de URLs, lista Python direta, ou vazio -> normaliza sempre para JSON string.""" if v in (None, ""): return "[]" if isinstance(v, list): return json.dumps([str(u) for u in v if u]) try: parsed = json.loads(v) except (json.JSONDecodeError, TypeError): raise ValueError("midias deve ser uma lista JSON válida de URLs, ex: [\"https://.../foto1.jpg\"]") if not isinstance(parsed, list): raise ValueError("midias deve ser uma lista JSON de URLs") return json.dumps([str(u) for u in parsed if u]) class AnuncioCreate(AnuncioBase): pass class AnuncioUpdate(BaseModel): """Todos os campos opcionais - usado no PATCH (atualização parcial).""" tipo_negocio: Optional[TipoNegocio] = None tipo_imovel: Optional[TipoImovel] = None titulo: Optional[str] = Field(None, min_length=3, max_length=150) descricao: Optional[str] = None endereco: Optional[str] = None bairro: Optional[str] = None cidade: Optional[str] = None estado: Optional[str] = Field(None, min_length=2, max_length=2) cep: Optional[str] = None preco: Optional[float] = Field(None, ge=0) condominio: Optional[float] = Field(None, ge=0) iptu: Optional[float] = Field(None, ge=0) area_util: Optional[float] = Field(None, ge=0) area_total: Optional[float] = Field(None, ge=0) quartos: Optional[int] = Field(None, ge=0) suites: Optional[int] = Field(None, ge=0) banheiros: Optional[int] = Field(None, ge=0) vagas_garagem: Optional[int] = Field(None, ge=0) status: Optional[StatusAnuncio] = None destaque: Optional[bool] = None corretor_responsavel: Optional[str] = None telefone_contato: Optional[str] = None midias: Optional[str] = None @field_validator("midias", mode="before") @classmethod def midias_json_valido(cls, v): if v in (None, ""): return v if isinstance(v, list): return json.dumps([str(u) for u in v if u]) try: parsed = json.loads(v) except (json.JSONDecodeError, TypeError): raise ValueError("midias deve ser uma lista JSON válida de URLs") if not isinstance(parsed, list): raise ValueError("midias deve ser uma lista JSON de URLs") return json.dumps([str(u) for u in parsed if u]) class AnuncioResponse(AnuncioBase): id: int data_cadastro: datetime data_atualizacao: datetime class OrdenacaoAnuncio(str, Enum): mais_recentes = "mais_recentes" mais_antigos = "mais_antigos" preco_asc = "preco_asc" preco_desc = "preco_desc" area_asc = "area_asc" area_desc = "area_desc" class FacetaItem(BaseModel): valor: str quantidade: int class FaixaValores(BaseModel): min: float max: float class OpcoesFiltroResponse(BaseModel): total_anuncios: int tipos_negocio: list[FacetaItem] tipos_imovel: list[FacetaItem] status: list[FacetaItem] cidades: list[FacetaItem] bairros: list[FacetaItem] faixa_preco: FaixaValores faixa_area: FaixaValores quartos_disponiveis: list[int] vagas_disponiveis: list[int] class FacetasBusca(BaseModel): tipos_imovel: list[FacetaItem] cidades: list[FacetaItem] bairros: list[FacetaItem] status: list[FacetaItem] faixa_preco: FaixaValores faixa_area: FaixaValores class BuscaExploratoriaResponse(BaseModel): total: int skip: int limit: int ordenar_por: OrdenacaoAnuncio resultados: list[AnuncioResponse] facetas: FacetasBusca class ResumoStats(BaseModel): total_anuncios: int total_venda: int total_locacao: int total_disponiveis: int preco_medio_venda: float preco_medio_locacao: float por_tipo_imovel: dict por_cidade: dict