From 51f19470116e707b025382d22cc4ffe3850ca2b7 Mon Sep 17 00:00:00 2001 From: Jesus Leal Trujillo Date: Fri, 3 Apr 2026 15:36:01 -0400 Subject: [PATCH 1/3] feat: add has_pool typed boolean filter to property search endpoints (DAT-15) Adds has_pool as a first-class parameter to both property.search.retrieve (v1) and property_v2.search.retrieve (v2), replacing the need to pass it via the params dict catch-all. Bumps version to 1.17.0. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 3 ++ parcllabs/__version__.py | 2 +- parcllabs/schemas/schemas.py | 7 +++ .../services/properties/property_search.py | 6 +++ parcllabs/services/properties/property_v2.py | 5 ++ tests/test_property.py | 48 +++++++++++++++++++ tests/test_property_v2.py | 20 ++++++++ 7 files changed, 90 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9c151..0b94f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### v1.17.0 +- Added `has_pool` parameter to `property.search.retrieve` and `property_v2.search.retrieve` to filter properties by pool availability. + ### v1.16.2 - Deprecate `volatility` endpoints. diff --git a/parcllabs/__version__.py b/parcllabs/__version__.py index 0d1c3b5..b32887e 100644 --- a/parcllabs/__version__.py +++ b/parcllabs/__version__.py @@ -1 +1 @@ -VERSION = "1.16.2" +VERSION = "1.17.0" diff --git a/parcllabs/schemas/schemas.py b/parcllabs/schemas/schemas.py index ec4899d..4716df8 100644 --- a/parcllabs/schemas/schemas.py +++ b/parcllabs/schemas/schemas.py @@ -127,6 +127,13 @@ class PropertyV2RetrieveParams(BaseModel): current_investor_owned_flag: bool | None = Field( default=None, description="Whether to filter by current investor owned flag" ) + + # Property features + has_pool: bool | None = Field( + default=None, + description="Whether to filter by pool. True returns properties with a pool. False returns properties where pool data is not available.", + ) + current_entity_owner_name: str | None = Field( default=None, description="Current entity owner name to filter by" ) diff --git a/parcllabs/services/properties/property_search.py b/parcllabs/services/properties/property_search.py index 2c73737..5722bcb 100644 --- a/parcllabs/services/properties/property_search.py +++ b/parcllabs/services/properties/property_search.py @@ -71,6 +71,7 @@ def _prepare_params( record_added_date_start: str | None = None, record_added_date_end: str | None = None, current_on_market_flag: bool | None = None, + has_pool: bool | None = None, ) -> dict: params = {} @@ -89,6 +90,7 @@ def _prepare_params( "current_owner_occupied_flag": current_owner_occupied_flag, "current_investor_owned_flag": current_investor_owned_flag, "current_on_market_flag": current_on_market_flag, + "has_pool": has_pool, } for param_name, param_value in bool_flags.items(): @@ -140,6 +142,7 @@ def retrieve( record_added_date_start: str | None = None, record_added_date_end: str | None = None, current_on_market_flag: bool | None = None, + has_pool: bool | None = None, ) -> pd.DataFrame: """ Retrieve parcl_property_id for geographic markets based on specified criteria. @@ -180,6 +183,8 @@ def retrieve( this date (YYYY-MM-DD). Defaults to None. current_on_market_flag (bool, optional): Filter properties currently on the market. Defaults to None. + has_pool (bool, optional): Filter properties by pool availability. + True returns properties with a pool. Defaults to None. Returns: pd.DataFrame: A DataFrame containing the parcl_property_id and other detail @@ -206,6 +211,7 @@ def retrieve( record_added_date_start=record_added_date_start, record_added_date_end=record_added_date_end, current_on_market_flag=current_on_market_flag, + has_pool=has_pool, ) output_data = deque() diff --git a/parcllabs/services/properties/property_v2.py b/parcllabs/services/properties/property_v2.py index 0aa720c..74877c6 100644 --- a/parcllabs/services/properties/property_v2.py +++ b/parcllabs/services/properties/property_v2.py @@ -336,6 +336,8 @@ def _build_boolean_filters(self, params: PropertyV2RetrieveParams) -> dict[str, filters["current_investor_owned_flag"] = self.simple_bool_validator( params.current_investor_owned_flag ) + if params.has_pool is not None: + filters["has_pool"] = self.simple_bool_validator(params.has_pool) return filters @@ -488,6 +490,7 @@ def retrieve( current_new_construction_flag: bool | None = None, current_owner_occupied_flag: bool | None = None, current_investor_owned_flag: bool | None = None, + has_pool: bool | None = None, current_entity_owner_name: str | None = None, include_events: bool | None = None, include_full_event_history: bool | None = None, @@ -532,6 +535,7 @@ def retrieve( current_new_construction_flag: Whether to filter by current_new_construction flag. current_owner_occupied_flag: Whether to filter by current_owner_occupied flag. current_investor_owned_flag: Whether to filter by current_investor_owned flag. + has_pool: Whether to filter by pool availability. current_entity_owner_name: Current entity owner name to filter by. include_events: Whether to include events in the response. include_full_event_history: Whether to include full event history in the response. @@ -577,6 +581,7 @@ def retrieve( current_new_construction_flag=current_new_construction_flag, current_owner_occupied_flag=current_owner_occupied_flag, current_investor_owned_flag=current_investor_owned_flag, + has_pool=has_pool, current_entity_owner_name=current_entity_owner_name, include_events=include_events, include_full_event_history=include_full_event_history, diff --git a/tests/test_property.py b/tests/test_property.py index 4e7e674..2f822e4 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -181,3 +181,51 @@ def test_retrieve_with_on_market_flag( # Check that .json() was called on the response mock twice assert mock_response.json.call_count == 2 + + +@patch("parcllabs.services.properties.property_search.PropertySearch._get") +def test_retrieve_with_has_pool( + mock_get: Mock, property_search_service: PropertySearch +) -> None: + """Test retrieve method with has_pool parameter.""" + mock_response = MagicMock() + mock_response.json.return_value = json.loads(sample_search_response) + mock_get.return_value = mock_response + + parcl_ids_to_test = [5503877] + property_type_to_test = "single_family" + + # Test with flag = True + property_search_service.retrieve( + parcl_ids=parcl_ids_to_test, + property_type=property_type_to_test, + has_pool=True, + ) + + # Test with flag = False + property_search_service.retrieve( + parcl_ids=parcl_ids_to_test, + property_type=property_type_to_test, + has_pool=False, + ) + + expected_params_on = { + "property_type": property_type_to_test.upper(), + "has_pool": "true", + "parcl_id": parcl_ids_to_test[0], + } + expected_params_off = { + "property_type": property_type_to_test.upper(), + "has_pool": "false", + "parcl_id": parcl_ids_to_test[0], + } + + assert len(mock_get.call_args_list) == 2 + + # Check first call (has_pool=True) + _, call_on_kwargs = mock_get.call_args_list[0] + assert call_on_kwargs["params"] == expected_params_on + + # Check second call (has_pool=False) + _, call_off_kwargs = mock_get.call_args_list[1] + assert call_off_kwargs["params"] == expected_params_off diff --git a/tests/test_property_v2.py b/tests/test_property_v2.py index b1e46bd..b06e7d4 100644 --- a/tests/test_property_v2.py +++ b/tests/test_property_v2.py @@ -85,6 +85,7 @@ def test_build_property_filters_from_schema(property_v2_service: PropertyV2Servi include_property_details=True, min_record_added_date="2023-01-01", max_record_added_date="2023-12-31", + has_pool=True, ) filters = property_v2_service._build_property_filters(params) @@ -102,6 +103,7 @@ def test_build_property_filters_from_schema(property_v2_service: PropertyV2Servi "include_property_details": "true", "min_record_added_date": "2023-01-01", "max_record_added_date": "2023-12-31", + "has_pool": "true", } @@ -374,3 +376,21 @@ def test_retrieve_with_schema_validation_errors( parcl_ids=[123], min_event_date="2023/01/01", ) + + +def test_build_boolean_filters_has_pool(property_v2_service: PropertyV2Service) -> None: + """Test has_pool boolean filter.""" + # True + params = PropertyV2RetrieveParams(has_pool=True) + filters = property_v2_service._build_boolean_filters(params) + assert filters["has_pool"] == "true" + + # False + params = PropertyV2RetrieveParams(has_pool=False) + filters = property_v2_service._build_boolean_filters(params) + assert filters["has_pool"] == "false" + + # None (omitted) + params = PropertyV2RetrieveParams() + filters = property_v2_service._build_boolean_filters(params) + assert "has_pool" not in filters From 73a18327b89ec125e369057fc696841b86571c6b Mon Sep 17 00:00:00 2001 From: Jesus Leal Trujillo Date: Fri, 3 Apr 2026 15:38:12 -0400 Subject: [PATCH 2/3] fix: wrap has_pool description to satisfy line length lint rule Co-Authored-By: Claude Opus 4.6 (1M context) --- parcllabs/schemas/schemas.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/parcllabs/schemas/schemas.py b/parcllabs/schemas/schemas.py index 4716df8..2a27f66 100644 --- a/parcllabs/schemas/schemas.py +++ b/parcllabs/schemas/schemas.py @@ -131,7 +131,10 @@ class PropertyV2RetrieveParams(BaseModel): # Property features has_pool: bool | None = Field( default=None, - description="Whether to filter by pool. True returns properties with a pool. False returns properties where pool data is not available.", + description=( + "Whether to filter by pool. True returns properties with a pool. " + "False returns properties where pool data is not available." + ), ) current_entity_owner_name: str | None = Field( From e61c3ea258813535dc2fc63498f8677e0989f9c9 Mon Sep 17 00:00:00 2001 From: Jesus Leal Trujillo Date: Fri, 3 Apr 2026 15:40:48 -0400 Subject: [PATCH 3/3] style: format test_property.py with ruff Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_property.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_property.py b/tests/test_property.py index 2f822e4..7e29141 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -184,9 +184,7 @@ def test_retrieve_with_on_market_flag( @patch("parcllabs.services.properties.property_search.PropertySearch._get") -def test_retrieve_with_has_pool( - mock_get: Mock, property_search_service: PropertySearch -) -> None: +def test_retrieve_with_has_pool(mock_get: Mock, property_search_service: PropertySearch) -> None: """Test retrieve method with has_pool parameter.""" mock_response = MagicMock() mock_response.json.return_value = json.loads(sample_search_response)