mirror of
https://github.com/Bunsly/HomeHarvest.git
synced 2026-03-04 19:44:29 -08:00
335 lines
11 KiB
Python
335 lines
11 KiB
Python
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
|
|
|
|
|
|
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[dict] | 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[dict] | 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: dict | None = None # Keep as dict for flexibility
|
|
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 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 PropertyEstimate(BaseModel):
|
|
estimate: int | None = None
|
|
estimate_high: int | None = None
|
|
estimate_low: int | None = None
|
|
date: datetime | None = None
|
|
is_best_home_value: bool | None = None
|
|
|
|
|
|
class PropertyDetails(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
|