From fc9c0ef05f09d1720c9abee73e5d3c049c414667 Mon Sep 17 00:00:00 2001 From: zhibindai26 <11509096+zhibindai26@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:45:21 -0500 Subject: [PATCH 1/3] Fix mutable default argument bug in service retrieve methods The `params: Mapping[str, Any] | None = {}` default was a single dict shared across all calls. Once mutated (e.g. by passing portfolio_size or property_type), the stale value persisted in subsequent calls that omitted params. Changed to `dict[str, Any] | None = None` with explicit `if params is None: params = {}` in the function body. Fixed in: PortfolioSizeService, PropertyTypeService, PropertyEventsService. Co-Authored-By: Claude Opus 4.6 (1M context) --- parcllabs/services/metrics/portfolio_size_service.py | 5 +++-- parcllabs/services/metrics/property_type_service.py | 5 +++-- parcllabs/services/properties/property_events_service.py | 3 +-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/parcllabs/services/metrics/portfolio_size_service.py b/parcllabs/services/metrics/portfolio_size_service.py index 9585e18..26b7fe4 100644 --- a/parcllabs/services/metrics/portfolio_size_service.py +++ b/parcllabs/services/metrics/portfolio_size_service.py @@ -1,4 +1,3 @@ -from collections.abc import Mapping from typing import Any import pandas as pd @@ -14,12 +13,14 @@ def retrieve( end_date: str | None = None, portfolio_size: str | None = None, limit: int | None = None, - params: Mapping[str, Any] | None = {}, + params: dict[str, Any] | None = None, auto_paginate: bool = False, ) -> pd.DataFrame: """ Retrieve portfolio size metrics for given parameters. """ + if params is None: + params = {} if portfolio_size: params["portfolio_size"] = portfolio_size.upper() diff --git a/parcllabs/services/metrics/property_type_service.py b/parcllabs/services/metrics/property_type_service.py index b04d81e..78d5961 100644 --- a/parcllabs/services/metrics/property_type_service.py +++ b/parcllabs/services/metrics/property_type_service.py @@ -1,4 +1,3 @@ -from collections.abc import Mapping from typing import Any import pandas as pd @@ -14,12 +13,14 @@ def retrieve( end_date: str | None = None, property_type: str | None = None, limit: int | None = None, - params: Mapping[str, Any] | None = {}, + params: dict[str, Any] | None = None, auto_paginate: bool = False, ) -> pd.DataFrame: """ Retrieve property type metrics for given parameters. """ + if params is None: + params = {} if property_type: params["property_type"] = property_type.upper() diff --git a/parcllabs/services/properties/property_events_service.py b/parcllabs/services/properties/property_events_service.py index 2d01cf6..a66f62c 100644 --- a/parcllabs/services/properties/property_events_service.py +++ b/parcllabs/services/properties/property_events_service.py @@ -1,5 +1,4 @@ from collections import deque -from collections.abc import Mapping from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Any @@ -67,7 +66,7 @@ def retrieve( entity_owner_name: str | None = None, record_updated_date_start: str | None = None, record_updated_date_end: str | None = None, - params: Mapping[str, Any] | None = {}, + params: dict[str, Any] | None = None, ) -> pd.DataFrame: """ Retrieve property events for given parameters. From c1039c567209bddfa95f8909c1b129128fc3a88f Mon Sep 17 00:00:00 2001 From: zhibindai26 <11509096+zhibindai26@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:03:44 -0500 Subject: [PATCH 2/3] claude init --- CLAUDE.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3c77cc2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,78 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Parcl Labs Python SDK — official Python client for the Parcl Labs real estate data API. Wraps REST endpoints into service classes that return pandas DataFrames. Supports 70,000+ US housing markets. + +## Commands + +```bash +make lint # ruff check --fix + ruff format +make lint-check # ruff check + ruff format --check (used in CI) +make test # python3 -m pytest -v +make test-readme # extract and run code examples from README +``` + +Run a single test: +```bash +python3 -m pytest tests/test_parcl_labs_service.py -v +python3 -m pytest tests/test_parcl_labs_service.py::test_function_name -v +``` + +## Architecture + +### Client → ServiceGroup → Service + +`ParclLabsClient` is the entry point. It organizes endpoints into `ServiceGroup` instances, each containing multiple service objects registered via `add_service()`. Users access services as chained attributes: `client.market_metrics.housing_event_prices.retrieve(...)`. + +### Service Hierarchy + +All services inherit from `ParclLabsService` (in `parcllabs/services/parcllabs_service.py`), which handles HTTP requests, pagination, error handling, and DataFrame conversion. + +Two intermediate subclasses add a single parameter each: +- `PropertyTypeService` — adds `property_type` param (SINGLE_FAMILY, CONDO, TOWNHOUSE, etc.) +- `PortfolioSizeService` — adds `portfolio_size` param (PORTFOLIO_2_TO_9, PORTFOLIO_10_TO_99, etc.) + +Specialized services for property-level operations: +- `PropertySearch`, `PropertyAddressSearch` — lookup by parcl_id or address +- `PropertyEventsService` — event history (sales, listings, rentals) +- `PropertyV2Service` — advanced search with chunking, concurrent pagination via ThreadPoolExecutor, and Pydantic validation +- `SearchMarkets` — market discovery with location_type, region, state filters + +### Request Flow + +1. `service.retrieve(parcl_ids, start_date, end_date, ...)` validates inputs and chunks parcl_ids into batches of 1000 +2. `_fetch()` routes to POST (if `post_url` configured) or GET +3. `_process_and_paginate_response()` follows `links.next` for auto-pagination +4. `_as_pd_dataframe()` normalizes JSON via `pd.json_normalize()`, reorders columns (parcl_id/date first), casts date types + +### Key Limits + +- GET: max 1000 per request (`RequestLimits.DEFAULT_SMALL`) +- POST: max 10000 per request (`RequestLimits.DEFAULT_LARGE`) +- PropertyV2: max 50000 (`RequestLimits.PROPERTY_V2_MAX`) +- parcl_ids chunked in batches of 1000 in `retrieve()` + +## Code Conventions + +- **Linting**: ruff with line-length=100, target py311. See `ruff.toml` for selected rules. +- **Type hints**: required on all functions (ruff ANN rules enforced). Use `str | None` union syntax. +- **Enums**: defined in `parcllabs/enums.py`. User input is uppercased before matching. +- **Validation**: `parcllabs/services/validators.py` — date format (YYYY-MM-DD), zip codes (5 digits), boolean params converted to string "true"/"false". +- **Error types**: `NotFoundError` (404), `DataValidationError` (422), both inherit `ParclLabsError`. Defined in `parcllabs/exceptions.py`. +- **Version**: single source of truth in `parcllabs/__version__.py`. + +## Testing Patterns + +- Tests use `unittest.mock` to patch service internals (`_fetch_get`, `_fetch_post`) +- Shared fixtures in `tests/conftest.py`: `client_mock`, `service`, `api_key`, `client` +- One test file per service (e.g., `tests/test_market_metrics_service.py`) +- CI runs on Python 3.11, ubuntu-latest. Requires `PARCL_LABS_API_KEY` secret for integration tests. + +## Adding a New Service + +1. Create service class inheriting from `ParclLabsService` (or `PropertyTypeService`/`PortfolioSizeService` if it needs those params) +2. Register it in the appropriate `_create_*_services()` method in `parcllabs/parcllabs_client.py` with url, post_url, and service_class +3. Add tests in `tests/test_*.py` From 7a7d273ce32cc0988f29982af72759a914ccf3ed Mon Sep 17 00:00:00 2001 From: zhibindai26 <11509096+zhibindai26@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:10:38 -0500 Subject: [PATCH 3/3] use configdict --- CLAUDE.md | 78 ------------------------------------ parcllabs/schemas/schemas.py | 13 +++--- 2 files changed, 6 insertions(+), 85 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 3c77cc2..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,78 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Parcl Labs Python SDK — official Python client for the Parcl Labs real estate data API. Wraps REST endpoints into service classes that return pandas DataFrames. Supports 70,000+ US housing markets. - -## Commands - -```bash -make lint # ruff check --fix + ruff format -make lint-check # ruff check + ruff format --check (used in CI) -make test # python3 -m pytest -v -make test-readme # extract and run code examples from README -``` - -Run a single test: -```bash -python3 -m pytest tests/test_parcl_labs_service.py -v -python3 -m pytest tests/test_parcl_labs_service.py::test_function_name -v -``` - -## Architecture - -### Client → ServiceGroup → Service - -`ParclLabsClient` is the entry point. It organizes endpoints into `ServiceGroup` instances, each containing multiple service objects registered via `add_service()`. Users access services as chained attributes: `client.market_metrics.housing_event_prices.retrieve(...)`. - -### Service Hierarchy - -All services inherit from `ParclLabsService` (in `parcllabs/services/parcllabs_service.py`), which handles HTTP requests, pagination, error handling, and DataFrame conversion. - -Two intermediate subclasses add a single parameter each: -- `PropertyTypeService` — adds `property_type` param (SINGLE_FAMILY, CONDO, TOWNHOUSE, etc.) -- `PortfolioSizeService` — adds `portfolio_size` param (PORTFOLIO_2_TO_9, PORTFOLIO_10_TO_99, etc.) - -Specialized services for property-level operations: -- `PropertySearch`, `PropertyAddressSearch` — lookup by parcl_id or address -- `PropertyEventsService` — event history (sales, listings, rentals) -- `PropertyV2Service` — advanced search with chunking, concurrent pagination via ThreadPoolExecutor, and Pydantic validation -- `SearchMarkets` — market discovery with location_type, region, state filters - -### Request Flow - -1. `service.retrieve(parcl_ids, start_date, end_date, ...)` validates inputs and chunks parcl_ids into batches of 1000 -2. `_fetch()` routes to POST (if `post_url` configured) or GET -3. `_process_and_paginate_response()` follows `links.next` for auto-pagination -4. `_as_pd_dataframe()` normalizes JSON via `pd.json_normalize()`, reorders columns (parcl_id/date first), casts date types - -### Key Limits - -- GET: max 1000 per request (`RequestLimits.DEFAULT_SMALL`) -- POST: max 10000 per request (`RequestLimits.DEFAULT_LARGE`) -- PropertyV2: max 50000 (`RequestLimits.PROPERTY_V2_MAX`) -- parcl_ids chunked in batches of 1000 in `retrieve()` - -## Code Conventions - -- **Linting**: ruff with line-length=100, target py311. See `ruff.toml` for selected rules. -- **Type hints**: required on all functions (ruff ANN rules enforced). Use `str | None` union syntax. -- **Enums**: defined in `parcllabs/enums.py`. User input is uppercased before matching. -- **Validation**: `parcllabs/services/validators.py` — date format (YYYY-MM-DD), zip codes (5 digits), boolean params converted to string "true"/"false". -- **Error types**: `NotFoundError` (404), `DataValidationError` (422), both inherit `ParclLabsError`. Defined in `parcllabs/exceptions.py`. -- **Version**: single source of truth in `parcllabs/__version__.py`. - -## Testing Patterns - -- Tests use `unittest.mock` to patch service internals (`_fetch_get`, `_fetch_post`) -- Shared fixtures in `tests/conftest.py`: `client_mock`, `service`, `api_key`, `client` -- One test file per service (e.g., `tests/test_market_metrics_service.py`) -- CI runs on Python 3.11, ubuntu-latest. Requires `PARCL_LABS_API_KEY` secret for integration tests. - -## Adding a New Service - -1. Create service class inheriting from `ParclLabsService` (or `PropertyTypeService`/`PortfolioSizeService` if it needs those params) -2. Register it in the appropriate `_create_*_services()` method in `parcllabs/parcllabs_client.py` with url, post_url, and service_class -3. Add tests in `tests/test_*.py` diff --git a/parcllabs/schemas/schemas.py b/parcllabs/schemas/schemas.py index ec4899d..b90d1d5 100644 --- a/parcllabs/schemas/schemas.py +++ b/parcllabs/schemas/schemas.py @@ -5,7 +5,7 @@ from datetime import datetime from typing import Any -from pydantic import BaseModel, Field, ValidationInfo, field_validator +from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator from parcllabs.enums import PropertyTypes, RequestLimits @@ -318,12 +318,11 @@ def validate_record_updated_date_range(cls, v: str | None, info: ValidationInfo) ) return v - class Config: - """Pydantic configuration.""" - - extra = "forbid" # Reject any extra fields - validate_assignment = True # Validate on assignment - str_strip_whitespace = True # Strip whitespace from strings + model_config = ConfigDict( + extra="forbid", + validate_assignment=True, + str_strip_whitespace=True, + ) class PropertyV2RetrieveParamCategories(BaseModel):