Files
deals24togo_be/app/schemas/listing.py
belviskhoremk c4d836a0f9 Initial commit
2026-03-06 22:57:58 +00:00

103 lines
3.2 KiB
Python

"""Listing request / response schemas."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field, field_validator
import math
class ListingBase(BaseModel):
title: str = Field(..., min_length=1, max_length=255)
description: str = Field(..., min_length=1, max_length=5000)
price: float = Field(..., gt=0)
category_id: str
location: str = Field(..., min_length=1, max_length=255)
images: list[str] = Field(default_factory=list, max_length=20)
listing_type: str = Field(default="sale", pattern="^(sale|rent)$")
condition: Optional[str] = Field(None, pattern="^(new|used|refurbished)$")
negotiable: bool = False
@field_validator("price")
@classmethod
def price_must_be_finite(cls, v: float) -> float:
if not math.isfinite(v):
raise ValueError("price must be a finite number")
return v
@field_validator("images")
@classmethod
def images_must_be_http(cls, v: list) -> list:
for url in v:
if not (url.startswith("http://") or url.startswith("https://")):
raise ValueError(f"Image URL must start with http:// or https://: {url!r}")
return v
class ListingCreate(ListingBase):
pass
class ListingUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = Field(None, min_length=1, max_length=5000)
price: Optional[float] = Field(None, ge=0)
category_id: Optional[str] = None
location: Optional[str] = Field(None, min_length=1, max_length=255)
images: Optional[list[str]] = Field(None, max_length=20)
listing_type: Optional[str] = Field(None, pattern="^(sale|rent)$")
condition: Optional[str] = Field(None, pattern="^(new|used|refurbished)$")
negotiable: Optional[bool] = None
class ListingStatusUpdate(BaseModel):
status: str = Field(..., pattern="^(approved|rejected)$")
rejection_reason: Optional[str] = Field(None, max_length=1000)
class ListingResponse(BaseModel):
id: str
title: str
description: str
price: float
images: list[str]
status: str
agency_id: str
category_id: str
location: str
listing_type: str
condition: Optional[str] = None
negotiable: bool
views_count: int = 0
rejection_reason: Optional[str] = None
created_at: datetime
updated_at: Optional[datetime] = None
# Joined fields (optional)
agency_name: Optional[str] = None
category_name: Optional[str] = None
model_config = {"from_attributes": True}
class ListingListResponse(BaseModel):
listings: list[ListingResponse]
total: int
page: int
page_size: int
class ListingSearchParams(BaseModel):
search: Optional[str] = None
category: Optional[str] = None
min_price: Optional[float] = None
max_price: Optional[float] = None
location: Optional[str] = None
listing_type: Optional[str] = None
condition: Optional[str] = None
sort_by: str = Field(default="newest", pattern="^(newest|oldest|price_asc|price_desc|popular)$")
status: Optional[str] = None
page: int = Field(default=1, ge=1)
page_size: int = Field(default=20, ge=1, le=100)