Skip to content

Commit ea5a82c

Browse files
authored
Merge pull request #89 from ParclLabs/update-v2-params
Update v2 params
2 parents b9c167e + a654929 commit ea5a82c

File tree

13 files changed

+1141
-358
lines changed

13 files changed

+1141
-358
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### v1.14.4
2+
- Internal code improvements.
3+
14
### v1.14.3
25
- Update `property_v2.search` to simplify pagination logic..
36

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ name = "pypi"
77
requests = ">=2.25"
88
pandas = ">=1.2"
99
numpy = "*"
10+
pydantic = ">=2.0.0"
1011

1112
[dev-packages]
1213
parcllabs = {path = ".", editable = true}

Pipfile.lock

Lines changed: 528 additions & 244 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

parcllabs/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
from typing import Optional
2-
31
from parcllabs.__version__ import VERSION as __version__ # noqa: F401, N811
42

53
# Constants
64
DEFAULT_API_BASE = "https://api.parcllabs.com"
75

8-
api_key: Optional[str] = None # noqa: UP007
6+
api_key: str | None = None
97
api_base = DEFAULT_API_BASE
108

119
from parcllabs.parcllabs_client import ParclLabsClient # noqa: E402, F401

parcllabs/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "1.14.3"
1+
VERSION = "1.14.4"

parcllabs/schemas/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Schemas package for ParclLabs SDK

parcllabs/schemas/schemas.py

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
"""
2+
Pydantic schemas for PropertyV2Service input parameters.
3+
"""
4+
5+
from datetime import datetime
6+
from typing import Any
7+
8+
from pydantic import BaseModel, Field, ValidationInfo, field_validator
9+
10+
from parcllabs.enums import PropertyTypes, RequestLimits
11+
12+
13+
class GeoCoordinates(BaseModel):
14+
"""Schema for geographic coordinates with radius."""
15+
16+
latitude: float = Field(..., ge=-90, le=90, description="Latitude in decimal degrees")
17+
longitude: float = Field(..., ge=-180, le=180, description="Longitude in decimal degrees")
18+
radius: float = Field(..., gt=0, description="Radius in miles")
19+
20+
21+
class PropertyV2RetrieveParams(BaseModel):
22+
"""
23+
Input parameters schema for PropertyV2Service.retrieve() method.
24+
25+
This schema validates and manages all input parameters for property searches,
26+
including search criteria, property filters, event filters, and owner filters.
27+
"""
28+
29+
# Search criteria
30+
parcl_ids: list[int] | None = Field(default=None, description="List of parcl_ids to filter by")
31+
parcl_property_ids: list[int] | None = Field(
32+
default=None, description="List of parcl_property_ids to filter by"
33+
)
34+
geo_coordinates: GeoCoordinates | None = Field(
35+
default=None, description="Geographic coordinates with radius to filter by"
36+
)
37+
38+
# Property filters
39+
property_types: list[str] | None = Field(
40+
default=None, description="List of property types to filter by"
41+
)
42+
min_beds: int | None = Field(default=None, ge=0, description="Minimum number of bedrooms")
43+
max_beds: int | None = Field(default=None, ge=0, description="Maximum number of bedrooms")
44+
min_baths: float | None = Field(default=None, ge=0, description="Minimum number of bathrooms")
45+
max_baths: float | None = Field(default=None, ge=0, description="Maximum number of bathrooms")
46+
min_sqft: int | None = Field(default=None, ge=0, description="Minimum square footage")
47+
max_sqft: int | None = Field(default=None, ge=0, description="Maximum square footage")
48+
min_year_built: int | None = Field(
49+
default=None, ge=1800, le=2100, description="Minimum year built"
50+
)
51+
max_year_built: int | None = Field(
52+
default=None, ge=1800, le=2100, description="Maximum year built"
53+
)
54+
include_property_details: bool | None = Field(
55+
default=None, description="Whether to include property details"
56+
)
57+
min_record_added_date: str | None = Field(
58+
default=None, description="Minimum record added date (YYYY-MM-DD)"
59+
)
60+
max_record_added_date: str | None = Field(
61+
default=None, description="Maximum record added date (YYYY-MM-DD)"
62+
)
63+
64+
# Event filters
65+
event_names: list[str] | None = Field(
66+
default=None, description="List of event names to filter by"
67+
)
68+
min_event_date: str | None = Field(default=None, description="Minimum event date (YYYY-MM-DD)")
69+
max_event_date: str | None = Field(default=None, description="Maximum event date (YYYY-MM-DD)")
70+
min_price: int | None = Field(default=None, ge=0, description="Minimum price")
71+
max_price: int | None = Field(default=None, ge=0, description="Maximum price")
72+
is_new_construction: bool | None = Field(
73+
default=None, description="Whether to filter by new construction"
74+
)
75+
min_record_updated_date: str | None = Field(
76+
default=None, description="Minimum record updated date (YYYY-MM-DD)"
77+
)
78+
max_record_updated_date: str | None = Field(
79+
default=None, description="Maximum record updated date (YYYY-MM-DD)"
80+
)
81+
82+
# Owner filters
83+
is_current_owner: bool | None = Field(
84+
default=None, description="Whether to filter by current owner"
85+
)
86+
owner_name: list[str] | None = Field(
87+
default=None, description="List of owner names to filter by"
88+
)
89+
is_investor_owned: bool | None = Field(
90+
default=None, description="Whether to filter by investor owned"
91+
)
92+
is_owner_occupied: bool | None = Field(
93+
default=None, description="Whether to filter by owner occupied"
94+
)
95+
96+
# Market flags
97+
current_on_market_flag: bool | None = Field(
98+
default=None, description="Whether to filter by current on market flag"
99+
)
100+
current_on_market_rental_flag: bool | None = Field(
101+
default=None, description="Whether to filter by current on market rental flag"
102+
)
103+
104+
# Pagination
105+
limit: int | None = Field(
106+
default=None,
107+
ge=1,
108+
le=RequestLimits.PROPERTY_V2_MAX.value,
109+
description=f"Number of results to return (max: {RequestLimits.PROPERTY_V2_MAX.value})",
110+
)
111+
112+
# Additional parameters
113+
params: dict[str, Any] | None = Field(
114+
default_factory=dict, description="Additional parameters to pass to the request"
115+
)
116+
117+
@field_validator("property_types")
118+
@classmethod
119+
def validate_property_types(cls, v: list[str] | None) -> list[str] | None:
120+
"""Validate property types against allowed values."""
121+
if v is not None:
122+
valid_types = [pt.value for pt in PropertyTypes]
123+
for prop_type in v:
124+
if prop_type.upper() not in valid_types:
125+
raise ValueError(f"Invalid property type: {prop_type}")
126+
return [pt.upper() for pt in v]
127+
return v
128+
129+
@field_validator("event_names")
130+
@classmethod
131+
def validate_event_names(cls, v: list[str] | None) -> list[str] | None:
132+
"""Validate event names and convert to uppercase."""
133+
if v is not None:
134+
return [name.upper() for name in v]
135+
return v
136+
137+
@field_validator("owner_name")
138+
@classmethod
139+
def validate_owner_names(cls, v: list[str] | None) -> list[str] | None:
140+
"""Validate owner names and convert to uppercase."""
141+
if v is not None:
142+
return [name.upper() for name in v]
143+
return v
144+
145+
@field_validator(
146+
"min_record_added_date",
147+
"max_record_added_date",
148+
"min_event_date",
149+
"max_event_date",
150+
"min_record_updated_date",
151+
"max_record_updated_date",
152+
)
153+
@classmethod
154+
def validate_date_format(cls, v: str | None) -> str | None:
155+
"""Validate date format is YYYY-MM-DD."""
156+
if v is not None:
157+
try:
158+
datetime.fromisoformat(v)
159+
except ValueError as err:
160+
raise ValueError(f"Date must be in YYYY-MM-DD format, got: {v}") from err
161+
else:
162+
return v
163+
return v
164+
165+
@field_validator("min_beds", "max_beds")
166+
@classmethod
167+
def validate_bedroom_range(cls, v: int | None, info: ValidationInfo) -> int | None:
168+
"""Validate bedroom range consistency."""
169+
if v is not None and info.data:
170+
field_name = info.field_name
171+
if field_name == "max_beds" and info.data.get("min_beds"):
172+
if v < info.data["min_beds"]:
173+
raise ValueError("max_beds cannot be less than min_beds")
174+
elif field_name == "min_beds" and info.data.get("max_beds"):
175+
if v > info.data["max_beds"]:
176+
raise ValueError("min_beds cannot be greater than max_beds")
177+
return v
178+
179+
@field_validator("min_baths", "max_baths")
180+
@classmethod
181+
def validate_bathroom_range(cls, v: float | None, info: ValidationInfo) -> float | None:
182+
"""Validate bathroom range consistency."""
183+
if v is not None and info.data:
184+
field_name = info.field_name
185+
if field_name == "max_baths" and info.data.get("min_baths"):
186+
if v < info.data["min_baths"]:
187+
raise ValueError("max_baths cannot be less than min_baths")
188+
elif field_name == "min_baths" and info.data.get("max_baths"):
189+
if v > info.data["max_baths"]:
190+
raise ValueError("min_baths cannot be greater than max_baths")
191+
return v
192+
193+
@field_validator("min_sqft", "max_sqft")
194+
@classmethod
195+
def validate_sqft_range(cls, v: int | None, info: ValidationInfo) -> int | None:
196+
"""Validate square footage range consistency."""
197+
if v is not None and info.data:
198+
field_name = info.field_name
199+
if field_name == "max_sqft" and info.data.get("min_sqft"):
200+
if v < info.data["min_sqft"]:
201+
raise ValueError("max_sqft cannot be less than min_sqft")
202+
elif field_name == "min_sqft" and info.data.get("max_sqft"):
203+
if v > info.data["max_sqft"]:
204+
raise ValueError("min_sqft cannot be greater than max_sqft")
205+
return v
206+
207+
@field_validator("min_year_built", "max_year_built")
208+
@classmethod
209+
def validate_year_built_range(cls, v: int | None, info: ValidationInfo) -> int | None:
210+
"""Validate year built range consistency."""
211+
if v is not None and info.data:
212+
field_name = info.field_name
213+
if field_name == "max_year_built" and info.data.get("min_year_built"):
214+
if v < info.data["min_year_built"]:
215+
raise ValueError("max_year_built cannot be less than min_year_built")
216+
elif field_name == "min_year_built" and info.data.get("max_year_built"):
217+
if v > info.data["max_year_built"]:
218+
raise ValueError("min_year_built cannot be greater than max_year_built")
219+
return v
220+
221+
@field_validator("min_price", "max_price")
222+
@classmethod
223+
def validate_price_range(cls, v: int | None, info: ValidationInfo) -> int | None:
224+
"""Validate price range consistency."""
225+
if v is not None and info.data:
226+
field_name = info.field_name
227+
if field_name == "max_price" and info.data.get("min_price"):
228+
if v < info.data["min_price"]:
229+
raise ValueError("max_price cannot be less than min_price")
230+
elif field_name == "min_price" and info.data.get("max_price"):
231+
if v > info.data["max_price"]:
232+
raise ValueError("min_price cannot be greater than max_price")
233+
return v
234+
235+
@field_validator("min_record_added_date", "max_record_added_date")
236+
@classmethod
237+
def validate_record_added_date_range(cls, v: str | None, info: ValidationInfo) -> str | None:
238+
"""Validate record added date range consistency."""
239+
if v is not None and info.data:
240+
field_name = info.field_name
241+
if field_name == "max_record_added_date" and info.data.get("min_record_added_date"):
242+
if v < info.data["min_record_added_date"]:
243+
raise ValueError("max_record_added_date cannot be before min_record_added_date")
244+
elif field_name == "min_record_added_date" and info.data.get("max_record_added_date"):
245+
if v > info.data["max_record_added_date"]:
246+
raise ValueError("min_record_added_date cannot be after max_record_added_date")
247+
return v
248+
249+
@field_validator("min_event_date", "max_event_date")
250+
@classmethod
251+
def validate_event_date_range(cls, v: str | None, info: ValidationInfo) -> str | None:
252+
"""Validate event date range consistency."""
253+
if v is not None and info.data:
254+
field_name = info.field_name
255+
if field_name == "max_event_date" and info.data.get("min_event_date"):
256+
if v < info.data["min_event_date"]:
257+
raise ValueError("max_event_date cannot be before min_event_date")
258+
elif field_name == "min_event_date" and info.data.get("max_event_date"):
259+
if v > info.data["max_event_date"]:
260+
raise ValueError("min_event_date cannot be after max_event_date")
261+
return v
262+
263+
@field_validator("min_record_updated_date", "max_record_updated_date")
264+
@classmethod
265+
def validate_record_updated_date_range(cls, v: str | None, info: ValidationInfo) -> str | None:
266+
"""Validate record updated date range consistency."""
267+
if v is not None and info.data:
268+
field_name = info.field_name
269+
if field_name == "max_record_updated_date" and info.data.get("min_record_updated_date"):
270+
if v < info.data["min_record_updated_date"]:
271+
raise ValueError(
272+
"max_record_updated_date cannot be before min_record_updated_date"
273+
)
274+
elif field_name == "min_record_updated_date" and info.data.get(
275+
"max_record_updated_date"
276+
):
277+
if v > info.data["max_record_updated_date"]:
278+
raise ValueError(
279+
"min_record_updated_date cannot be after max_record_updated_date"
280+
)
281+
return v
282+
283+
class Config:
284+
"""Pydantic configuration."""
285+
286+
extra = "forbid" # Reject any extra fields
287+
validate_assignment = True # Validate on assignment
288+
str_strip_whitespace = True # Strip whitespace from strings
289+
290+
291+
class PropertyV2RetrieveParamCategories(BaseModel):
292+
"""High level categories for PropertyV2RetrieveParams."""
293+
294+
property_filters: dict[str, Any] = Field(default_factory=dict, description="Property filters")
295+
event_filters: dict[str, Any] = Field(default_factory=dict, description="Event filters")
296+
owner_filters: dict[str, Any] = Field(default_factory=dict, description="Owner filters")

0 commit comments

Comments
 (0)