From 1983636df9525c53f11e67698ff822e8387bcffb Mon Sep 17 00:00:00 2001 From: zhibindai26 <11509096+zhibindai26@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:37:10 -0500 Subject: [PATCH 1/5] add address endpoint support --- parcllabs/parcllabs_client.py | 14 +++++ .../services/properties/property_address.py | 50 ++++++++++++++++ parcllabs/services/validators.py | 13 +++++ tests/test_property_address.py | 58 +++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 parcllabs/services/properties/property_address.py create mode 100644 tests/test_property_address.py diff --git a/parcllabs/parcllabs_client.py b/parcllabs/parcllabs_client.py index 1b3e66b..93370a3 100644 --- a/parcllabs/parcllabs_client.py +++ b/parcllabs/parcllabs_client.py @@ -7,6 +7,7 @@ from parcllabs.services.metrics.portfolio_size_service import PortfolioSizeService from parcllabs.services.properties.property_events_service import PropertyEventsService from parcllabs.services.properties.property_search import PropertySearch +from parcllabs.services.properties.property_address import PropertyAddressSearch class ServiceGroup: @@ -66,6 +67,7 @@ def _initialize_services(self): self.portfolio_metrics = self._create_portfolio_metrics_services() self.search = self._create_search_services() self.property = self._create_property_services() + self.property_address = self._create_property_address_services() def _create_service_group(self): return ServiceGroup(self) @@ -273,3 +275,15 @@ def _create_property_services(self): } self._add_services_to_group(group, services) return group + + def _create_property_address_services(self): + group = self._create_service_group() + services = { + "search": { + "url": "/v1/property/search_address", + "post_url": "/v1/property/search_address", + "service_class": PropertyAddressSearch, + }, + } + self._add_services_to_group(group, services) + return group diff --git a/parcllabs/services/properties/property_address.py b/parcllabs/services/properties/property_address.py new file mode 100644 index 0000000..f34575a --- /dev/null +++ b/parcllabs/services/properties/property_address.py @@ -0,0 +1,50 @@ +from typing import List, Dict + +import pandas as pd + +from parcllabs.common import VALID_US_STATE_ABBREV +from parcllabs.services.validators import Validators +from parcllabs.services.parcllabs_service import ParclLabsService + + +class PropertyAddressSearch(ParclLabsService): + """ + Retrieve parcl_property_ids based on provided addresses. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def retrieve( + self, + addresses: List[Dict], + ): + """ + Retrieve parcl_property_ids based on provided addresses. + + Args: + + addresses (list): A list of dictionaries containing address information. + + Returns: + + DataFrame: A DataFrame containing the parcl_property_id and address information. + """ + + for address in addresses: + param = Validators.validate_input_str_param( + param=address["state_abbreviation"], + param_name="state_abbreviation", + valid_values=VALID_US_STATE_ABBREV, + params_dict={}, + ) + + param = Validators.validate_us_zip_code( + zip_code=address["zip_code"] + ) + + response = self._post(url=self.full_url, data=addresses) + resp_data = response.json() + results = pd.DataFrame(resp_data) + self.client.estimated_session_credit_usage += results.shape[0] + return results diff --git a/parcllabs/services/validators.py b/parcllabs/services/validators.py index 3f7664f..e10cedb 100644 --- a/parcllabs/services/validators.py +++ b/parcllabs/services/validators.py @@ -85,3 +85,16 @@ def validate_parcl_ids(parcl_ids): if isinstance(parcl_ids, int): parcl_ids = [parcl_ids] return parcl_ids + + + @staticmethod + def validate_us_zip_code(zip_code: str) -> str: + """ + Validates the US zip code string and returns it in the expected format. + Raises ValueError if the zip code is invalid or not in the expected format. + """ + if zip_code: + zip_code = zip_code.strip() + if not zip_code.isdigit() or len(zip_code) != 5: + raise ValueError(f"Zip code {zip_code} is not a valid 5-digit US zip code.") + return zip_code diff --git a/tests/test_property_address.py b/tests/test_property_address.py new file mode 100644 index 0000000..a3daa2d --- /dev/null +++ b/tests/test_property_address.py @@ -0,0 +1,58 @@ +import json +import pytest +import pandas as pd +from unittest.mock import MagicMock, patch +from parcllabs.services.properties.property_address import PropertyAddressSearch + + +SAMPLE_ADDRESSES = [ + { + "address": "5967 coplin street", + "city": "detroit", + "state_abbreviation": "mi", + "zip_code": "90001", + "source_id": "123", + }, + { + "address": "7239 rea croft dr", + "city": "charlotte", + "state_abbreviation": "NC", + "zip_code": "28226", + "source_id": "456", + }, +] + + +SAMPLE_RESPONSE = """[ + { + "parcl_property_id": 130959387, + "source_id": "123" + }, + { + "parcl_property_id": 60057527, + "source_id": "456" + } +]""" + +@pytest.fixture +def property_events_service(): + client_mock = MagicMock() + client_mock.api_url = "https://api.parcllabs.com" + client_mock.api_key = "test_api_key" + client_mock.num_workers = 1 + service = PropertyAddressSearch(client=client_mock, url="/v1/property/search_address") + return service + + +@patch("parcllabs.services.properties.property_address.PropertyAddressSearch._post") +def test_retrieve_success(mock_post, property_events_service): + mock_response = MagicMock() + mock_response.json.return_value = json.loads(SAMPLE_RESPONSE) + mock_post.return_value = mock_response + + result = property_events_service.retrieve(addresses=SAMPLE_ADDRESSES) + + assert isinstance(result, pd.DataFrame) + assert len(result) == 2 + assert "parcl_property_id" in result.columns + assert "source_id" in result.columns From a2fd757cee7a902d9150c1c480c19f8033b28686 Mon Sep 17 00:00:00 2001 From: zhibindai26 <11509096+zhibindai26@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:59:11 -0500 Subject: [PATCH 2/5] validate fields exist --- .../services/properties/property_address.py | 11 ++++++----- parcllabs/services/validators.py | 16 +++++++++++----- tests/test_property_address.py | 5 ++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/parcllabs/services/properties/property_address.py b/parcllabs/services/properties/property_address.py index f34575a..1b0cc15 100644 --- a/parcllabs/services/properties/property_address.py +++ b/parcllabs/services/properties/property_address.py @@ -32,16 +32,17 @@ def retrieve( """ for address in addresses: + param = Validators.validate_field_exists(address, "address") + param = Validators.validate_field_exists(address, "city") + param = Validators.validate_field_exists(address, "state_abbreviation") + param = Validators.validate_field_exists(address, "zip_code") param = Validators.validate_input_str_param( - param=address["state_abbreviation"], + param=address.get("state_abbreviation"), param_name="state_abbreviation", valid_values=VALID_US_STATE_ABBREV, params_dict={}, ) - - param = Validators.validate_us_zip_code( - zip_code=address["zip_code"] - ) + param = Validators.validate_us_zip_code(zip_code=address.get("zip_code")) response = self._post(url=self.full_url, data=addresses) resp_data = response.json() diff --git a/parcllabs/services/validators.py b/parcllabs/services/validators.py index e10cedb..8a344db 100644 --- a/parcllabs/services/validators.py +++ b/parcllabs/services/validators.py @@ -86,15 +86,21 @@ def validate_parcl_ids(parcl_ids): parcl_ids = [parcl_ids] return parcl_ids - @staticmethod def validate_us_zip_code(zip_code: str) -> str: """ Validates the US zip code string and returns it in the expected format. Raises ValueError if the zip code is invalid or not in the expected format. """ - if zip_code: - zip_code = zip_code.strip() - if not zip_code.isdigit() or len(zip_code) != 5: - raise ValueError(f"Zip code {zip_code} is not a valid 5-digit US zip code.") + zip_code = zip_code.strip() + if not zip_code.isdigit() or len(zip_code) != 5: + raise ValueError(f"Zip code {zip_code} is not a valid 5-digit US zip code.") return zip_code + + @staticmethod + def validate_field_exists(data: dict, field_name: str): + if field_name not in data: + raise ValueError( + f"Field '{field_name}' is required. Provided request: {data}" + ) + return data[field_name] diff --git a/tests/test_property_address.py b/tests/test_property_address.py index a3daa2d..246c6d5 100644 --- a/tests/test_property_address.py +++ b/tests/test_property_address.py @@ -34,13 +34,16 @@ } ]""" + @pytest.fixture def property_events_service(): client_mock = MagicMock() client_mock.api_url = "https://api.parcllabs.com" client_mock.api_key = "test_api_key" client_mock.num_workers = 1 - service = PropertyAddressSearch(client=client_mock, url="/v1/property/search_address") + service = PropertyAddressSearch( + client=client_mock, url="/v1/property/search_address" + ) return service From a26b26790a7d51d6d57a565d622d29e181da14a1 Mon Sep 17 00:00:00 2001 From: zhibindai26 <11509096+zhibindai26@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:01:12 -0500 Subject: [PATCH 3/5] loop --- parcllabs/services/properties/property_address.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/parcllabs/services/properties/property_address.py b/parcllabs/services/properties/property_address.py index 1b0cc15..1e4a14b 100644 --- a/parcllabs/services/properties/property_address.py +++ b/parcllabs/services/properties/property_address.py @@ -31,11 +31,11 @@ def retrieve( DataFrame: A DataFrame containing the parcl_property_id and address information. """ + required_params = ["address", "city", "state_abbreviation", "zip_code"] for address in addresses: - param = Validators.validate_field_exists(address, "address") - param = Validators.validate_field_exists(address, "city") - param = Validators.validate_field_exists(address, "state_abbreviation") - param = Validators.validate_field_exists(address, "zip_code") + for param in required_params: + param = Validators.validate_field_exists(address, param) + param = Validators.validate_input_str_param( param=address.get("state_abbreviation"), param_name="state_abbreviation", From fad42b6f87f1bcacb83b14d77166359a09e6ee9d Mon Sep 17 00:00:00 2001 From: zhibindai26 <11509096+zhibindai26@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:08:25 -0500 Subject: [PATCH 4/5] refactor --- parcllabs/services/properties/property_address.py | 3 +-- parcllabs/services/validators.py | 11 +++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/parcllabs/services/properties/property_address.py b/parcllabs/services/properties/property_address.py index 1e4a14b..122b02b 100644 --- a/parcllabs/services/properties/property_address.py +++ b/parcllabs/services/properties/property_address.py @@ -33,8 +33,7 @@ def retrieve( required_params = ["address", "city", "state_abbreviation", "zip_code"] for address in addresses: - for param in required_params: - param = Validators.validate_field_exists(address, param) + param = Validators.validate_field_exists(address, required_params) param = Validators.validate_input_str_param( param=address.get("state_abbreviation"), diff --git a/parcllabs/services/validators.py b/parcllabs/services/validators.py index 8a344db..20c8b9f 100644 --- a/parcllabs/services/validators.py +++ b/parcllabs/services/validators.py @@ -98,9 +98,12 @@ def validate_us_zip_code(zip_code: str) -> str: return zip_code @staticmethod - def validate_field_exists(data: dict, field_name: str): - if field_name not in data: + def validate_field_exists(data: dict, fields: List[str]): + """ + Validates that the required fields exist in the provided dictionary. + """ + missing_fields = [field for field in fields if field not in data] + if missing_fields: raise ValueError( - f"Field '{field_name}' is required. Provided request: {data}" + f"Missing required fields: {', '.join(missing_fields)}. Provided data: {data}" ) - return data[field_name] From c4ed316d19802dcf1795a5067cead11096bc370b Mon Sep 17 00:00:00 2001 From: zhibindai26 <11509096+zhibindai26@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:11:09 -0500 Subject: [PATCH 5/5] cleanup; --- parcllabs/services/validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parcllabs/services/validators.py b/parcllabs/services/validators.py index 20c8b9f..20d9bf8 100644 --- a/parcllabs/services/validators.py +++ b/parcllabs/services/validators.py @@ -102,8 +102,8 @@ def validate_field_exists(data: dict, fields: List[str]): """ Validates that the required fields exist in the provided dictionary. """ - missing_fields = [field for field in fields if field not in data] + missing_fields = [field for field in fields if field not in data.keys()] if missing_fields: raise ValueError( - f"Missing required fields: {', '.join(missing_fields)}. Provided data: {data}" + f"Missing required fields: {', '.join(missing_fields)}. Provided request: {data}" )