from __future__ import annotations from enum import Enum from typing import Optional, Any from datetime import datetime from pydantic import BaseModel, computed_field, HttpUrl, Field class ReturnType(Enum): pydantic = "pydantic" pandas = "pandas" raw = "raw" class SiteName(Enum): ZILLOW = "zillow" REDFIN = "redfin" REALTOR = "realtor.com" @classmethod def get_by_value(cls, value): for item in cls: if item.value == value: return item raise ValueError(f"{value} not found in {cls}") class SearchPropertyType(Enum): SINGLE_FAMILY = "single_family" APARTMENT = "apartment" CONDOS = "condos" CONDO_TOWNHOME_ROWHOME_COOP = "condo_townhome_rowhome_coop" CONDO_TOWNHOME = "condo_townhome" TOWNHOMES = "townhomes" DUPLEX_TRIPLEX = "duplex_triplex" FARM = "farm" LAND = "land" MULTI_FAMILY = "multi_family" MOBILE = "mobile" class ListingType(Enum): FOR_SALE = "FOR_SALE" FOR_RENT = "FOR_RENT" PENDING = "PENDING" SOLD = "SOLD" class PropertyType(Enum): APARTMENT = "APARTMENT" BUILDING = "BUILDING" COMMERCIAL = "COMMERCIAL" GOVERNMENT = "GOVERNMENT" INDUSTRIAL = "INDUSTRIAL" CONDO_TOWNHOME = "CONDO_TOWNHOME" CONDO_TOWNHOME_ROWHOME_COOP = "CONDO_TOWNHOME_ROWHOME_COOP" CONDO = "CONDO" CONDOP = "CONDOP" CONDOS = "CONDOS" COOP = "COOP" DUPLEX_TRIPLEX = "DUPLEX_TRIPLEX" FARM = "FARM" INVESTMENT = "INVESTMENT" LAND = "LAND" MOBILE = "MOBILE" MULTI_FAMILY = "MULTI_FAMILY" RENTAL = "RENTAL" SINGLE_FAMILY = "SINGLE_FAMILY" TOWNHOMES = "TOWNHOMES" OTHER = "OTHER" class Address(BaseModel): full_line: str | None = None street: str | None = None unit: str | None = None city: str | None = Field(None, description="The name of the city") state: str | None = Field(None, description="The name of the state") zip: str | None = Field(None, description="zip code") # Additional address fields from GraphQL street_direction: str | None = None street_number: str | None = None street_name: str | None = None street_suffix: str | None = None @computed_field @property def formatted_address(self) -> str | None: """Computed property that combines full_line, city, state, and zip into a formatted address.""" parts = [] if self.full_line: parts.append(self.full_line) city_state_zip = [] if self.city: city_state_zip.append(self.city) if self.state: city_state_zip.append(self.state) if self.zip: city_state_zip.append(self.zip) if city_state_zip: parts.append(", ".join(city_state_zip)) return ", ".join(parts) if parts else None class Description(BaseModel): primary_photo: HttpUrl | None = None alt_photos: list[HttpUrl] | None = None style: PropertyType | None = None beds: int | None = Field(None, description="Total number of bedrooms") baths_full: int | None = Field(None, description="Total number of full bathrooms (4 parts: Sink, Shower, Bathtub and Toilet)") baths_half: int | None = Field(None, description="Total number of 1/2 bathrooms (2 parts: Usually Sink and Toilet)") sqft: int | None = Field(None, description="Square footage of the Home") lot_sqft: int | None = Field(None, description="Lot square footage") sold_price: int | None = Field(None, description="Sold price of home") year_built: int | None = Field(None, description="The year the building/home was built") garage: float | None = Field(None, description="Number of garage spaces") stories: int | None = Field(None, description="Number of stories in the building") text: str | None = None # Additional description fields name: str | None = None type: str | None = None class AgentPhone(BaseModel): number: str | None = None type: str | None = None primary: bool | None = None ext: str | None = None class Entity(BaseModel): name: str | None = None # Make name optional since it can be None uuid: str | None = None class Agent(Entity): mls_set: str | None = None nrds_id: str | None = None phones: list[dict] | AgentPhone | None = None email: str | None = None href: str | None = None state_license: str | None = Field(None, description="Advertiser agent state license number") class Office(Entity): mls_set: str | None = None email: str | None = None href: str | None = None phones: list[dict] | AgentPhone | None = None class Broker(Entity): pass class Builder(Entity): pass class Advertisers(BaseModel): agent: Agent | None = None broker: Broker | None = None builder: Builder | None = None office: Office | None = None class Property(BaseModel): property_url: HttpUrl property_id: str = Field(..., description="Unique Home identifier also known as property id") #: allows_cats: bool #: allows_dogs: bool listing_id: str | None = None permalink: str | None = None mls: str | None = None mls_id: str | None = None status: str | None = Field(None, description="Listing status: for_sale, for_rent, sold, off_market, active (New Home Subdivisions), other (if none of the above conditions were met)") address: Address | None = None list_price: int | None = Field(None, description="The current price of the Home") list_price_min: int | None = None list_price_max: int | None = None list_date: datetime | None = Field(None, description="The time this Home entered Move system") pending_date: datetime | None = Field(None, description="The date listing went into pending state") last_sold_date: datetime | None = Field(None, description="Last time the Home was sold") prc_sqft: int | None = None new_construction: bool | None = Field(None, description="Search for new construction homes") hoa_fee: int | None = Field(None, description="Search for homes where HOA fee is known and falls within specified range") days_on_mls: int | None = Field(None, description="An integer value determined by the MLS to calculate days on market") description: Description | None = None tags: list[str] | None = None details: list[HomeDetails] | None = None latitude: float | None = None longitude: float | None = None neighborhoods: Optional[str] = None county: Optional[str] = Field(None, description="County associated with home") fips_code: Optional[str] = Field(None, description="The FIPS (Federal Information Processing Standard) code for the county") nearby_schools: list[str] | None = None assessed_value: int | None = None estimated_value: int | None = None tax: int | None = None tax_history: list[TaxHistory] | None = None advertisers: Advertisers | None = None # Additional fields from GraphQL that aren't currently parsed mls_status: str | None = None last_sold_price: int | None = None # Structured data from GraphQL open_houses: list[OpenHouse] | None = None pet_policy: PetPolicy | None = None units: list[Unit] | None = None monthly_fees: HomeMonthlyFee | None = Field(None, description="Monthly fees. Currently only some rental data will have them.") one_time_fees: list[HomeOneTimeFee] | None = Field(None, description="One time fees. Currently only some rental data will have them.") parking: HomeParkingDetails | None = Field(None, description="Parking information. Currently only some rental data will have it.") terms: list[PropertyDetails] | None = None popularity: Popularity | None = None tax_record: TaxRecord | None = None parcel_info: dict | None = None # Keep as dict for flexibility current_estimates: list[PropertyEstimate] | None = None estimates: HomeEstimates | None = None photos: list[dict] | None = None # Keep as dict for photo structure flags: HomeFlags | None = Field(None, description="Home flags for Listing/Property") # Specialized models for GraphQL types class HomeMonthlyFee(BaseModel): description: str | None = None display_amount: str | None = None class HomeOneTimeFee(BaseModel): description: str | None = None display_amount: str | None = None class HomeParkingDetails(BaseModel): unassigned_space_rent: int | None = None assigned_spaces_available: int | None = None description: str | None = Field(None, description="Parking information. Currently only some rental data will have it.") assigned_space_rent: int | None = None class PetPolicy(BaseModel): cats: bool | None = Field(None, description="Search for homes which allow cats") dogs: bool | None = Field(None, description="Search for homes which allow dogs") dogs_small: bool | None = Field(None, description="Search for homes with allow small dogs") dogs_large: bool | None = Field(None, description="Search for homes which allow large dogs") class OpenHouse(BaseModel): start_date: datetime | None = None end_date: datetime | None = None description: str | None = None time_zone: str | None = None dst: bool | None = None href: HttpUrl | None = None methods: list[str] | None = None class HomeFlags(BaseModel): is_pending: bool | None = None is_contingent: bool | None = None is_new_construction: bool | None = None is_coming_soon: bool | None = None is_new_listing: bool | None = None is_price_reduced: bool | None = None is_foreclosure: bool | None = None class PopularityPeriod(BaseModel): clicks_total: int | None = None views_total: int | None = None dwell_time_mean: float | None = None dwell_time_median: float | None = None leads_total: int | None = None shares_total: int | None = None saves_total: int | None = None last_n_days: int | None = None class Popularity(BaseModel): periods: list[PopularityPeriod] | None = None class Assessment(BaseModel): building: int | None = None land: int | None = None total: int | None = None class TaxHistory(BaseModel): assessment: Assessment | None = None market: Assessment | None = Field(None, description="Market values as provided by the county or local taxing/assessment authority") appraisal: Assessment | None = Field(None, description="Appraised value given by taxing authority") value: Assessment | None = Field(None, description="Value closest to current market value used for assessment by county or local taxing authorities") tax: int | None = None year: int | None = None assessed_year: int | None = Field(None, description="Assessment year for which taxes were billed") class TaxRecord(BaseModel): cl_id: str | None = None public_record_id: str | None = None last_update_date: datetime | None = None apn: str | None = None tax_parcel_id: str | None = None class EstimateSource(BaseModel): type: str | None = Field(None, description="Type of the avm vendor, list of values: corelogic, collateral, quantarium") name: str | None = Field(None, description="Name of the avm vendor") class PropertyEstimate(BaseModel): estimate: int | None = Field(None, description="Estimated value of a property") estimate_high: int | None = Field(None, description="Estimated high value of a property") estimate_low: int | None = Field(None, description="Estimated low value of a property") date: datetime | None = Field(None, description="Date of estimation") is_best_home_value: bool | None = None source: EstimateSource | None = Field(None, description="Source of the latest estimate value") class HomeEstimates(BaseModel): current_values: list[PropertyEstimate] | None = Field(None, description="Current valuation and best value for home from multiple AVM vendors") class PropertyDetails(BaseModel): category: str | None = None text: list[str] | None = None parent_category: str | None = None class HomeDetails(BaseModel): category: str | None = None text: list[str] | None = None parent_category: str | None = None class UnitDescription(BaseModel): baths_consolidated: str | None = None baths: float | None = None # Changed to float to handle values like 2.5 beds: int | None = None sqft: int | None = None class UnitAvailability(BaseModel): date: datetime | None = None class Unit(BaseModel): availability: UnitAvailability | None = None description: UnitDescription | None = None photos: list[dict] | None = None # Keep as dict for photo structure list_price: int | None = None