Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add address endpoint support #68

Merged
merged 5 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions parcllabs/parcllabs_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
50 changes: 50 additions & 0 deletions parcllabs/services/properties/property_address.py
Original file line number Diff line number Diff line change
@@ -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.
"""

required_params = ["address", "city", "state_abbreviation", "zip_code"]
for address in addresses:
param = Validators.validate_field_exists(address, required_params)

param = Validators.validate_input_str_param(
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.get("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
22 changes: 22 additions & 0 deletions parcllabs/services/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,25 @@ 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.
"""
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, 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.keys()]
if missing_fields:
raise ValueError(
f"Missing required fields: {', '.join(missing_fields)}. Provided request: {data}"
)
61 changes: 61 additions & 0 deletions tests/test_property_address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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