Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### v1.16.1
- Fix bug in auto pagination logic

### v1.16.0
- Added the `entity_seller_name` parameter to the `property_v2.search` endpoint

Expand Down
2 changes: 1 addition & 1 deletion parcllabs/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "1.16.0"
VERSION = "1.16.1"
108 changes: 51 additions & 57 deletions parcllabs/services/properties/property_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,48 +32,46 @@ def _fetch_post(self, params: dict[str, Any], data: dict[str, Any]) -> list[dict
result = response.json()

all_data = [result]

if params["auto_paginate"] is False:
return all_data

# If we need to paginate, use concurrent requests
pagination = result.get("pagination")
metadata = result.get("metadata")
if pagination.get("has_more"):
print("More pages to fetch, paginating additional pages...")

if pagination:
limit = pagination.get("limit")
returned_count = metadata.get("results", {}).get("returned_count", 0)
# if we got fewer or equal results than requested, don't paginate
if returned_count <= limit:
return all_data

# If we need to paginate, use concurrent requests
if pagination.get("has_more"):
print("More pages to fetch, paginating additional pages...")
offset = pagination.get("offset")
total_available = metadata.get("results", {}).get("total_available", 0)

# Calculate how many more pages we need
remaining_pages = (total_available - limit) // limit
if (total_available - limit) % limit > 0:
remaining_pages += 1

# Generate all the URLs we need to fetch
urls = []
current_offset = offset + limit
for _ in range(remaining_pages):
urls.append(f"{self.full_post_url}?limit={limit}&offset={current_offset}")
current_offset += limit

# Use ThreadPoolExecutor to make concurrent requests
with ThreadPoolExecutor(max_workers=self.client.num_workers) as executor:
future_to_url = {
executor.submit(self._post, url=url, data=data, params=params): url
for url in urls
}

for future in as_completed(future_to_url):
try:
response = future.result()
page_result = response.json()
all_data.append(page_result)
except Exception as exc:
print(f"Request failed: {exc}")
offset = pagination.get("offset")
metadata = result.get("metadata")
total_available = metadata.get("results", {}).get("total_available", 0)

# Calculate how many more pages we need
remaining_pages = (total_available - limit) // limit
if (total_available - limit) % limit > 0:
remaining_pages += 1

# Generate all the URLs we need to fetch
urls = []
current_offset = offset + limit
for _ in range(remaining_pages):
urls.append(f"{self.full_post_url}?limit={limit}&offset={current_offset}")
current_offset += limit

# Use ThreadPoolExecutor to make concurrent requests
with ThreadPoolExecutor(max_workers=self.client.num_workers) as executor:
future_to_url = {
executor.submit(self._post, url=url, data=data, params=params): url
for url in urls
}

for future in as_completed(future_to_url):
try:
response = future.result()
page_result = response.json()
all_data.append(page_result)
except Exception as exc:
print(f"Request failed: {exc}")

return all_data

Expand Down Expand Up @@ -127,8 +125,6 @@ def _fetch_post_parcl_property_ids(
if idx < len(parcl_property_ids_chunks) - 1: # Don't delay after the last one
time.sleep(0.1)

# Helper functions to abstract raise statements

# Collect results as they complete
for future in as_completed(future_to_chunk):
chunk_num = future_to_chunk[future]
Expand Down Expand Up @@ -432,24 +428,20 @@ def _build_owner_filters(self, params: PropertyV2RetrieveParams) -> dict[str, An

return owner_filters

def _validate_limit(self, limit: int | None) -> int:
"""Validate limit parameter."""
def _set_limit_pagination(self, limit: int | None) -> tuple[int, bool]:
"""Validate and set limit and auto pagination."""
max_limit = RequestLimits.PROPERTY_V2_MAX.value

# If auto-paginate is enabled or no limit is provided, use maximum limit
if limit in (None, 0):
print(f"No limit provided. Setting limit to maximum value of {max_limit}.")
return max_limit
# If no limit is provided, use maximum limit and auto paginate
if limit == 0 or limit is None:
auto_paginate = True
print(f"""No limit provided. Using max limit of {max_limit}.
Auto pagination is {auto_paginate}""")
return max_limit, auto_paginate

# If limit exceeds maximum, cap it
if limit > max_limit:
Copy link
Contributor Author

@zhibindai26 zhibindai26 Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit input is managed by the request schema; it's not possible to pass in a value greater than max_limit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-08-14 at 2 35 28 PM

print(
f"Supplied limit value is too large for requested endpoint."
f"Setting limit to maximum value of {max_limit}."
)
return max_limit

return limit
auto_paginate = False
print(f"Limit is set at {limit}. Auto pagiation is {auto_paginate}")
return limit, auto_paginate

def _build_param_categories(
self, params: PropertyV2RetrieveParams
Expand Down Expand Up @@ -615,7 +607,9 @@ def retrieve(
request_params["limit"] = PARCL_PROPERTY_IDS_LIMIT
results = self._fetch_post_parcl_property_ids(params=request_params, data=data)
else:
request_params["limit"] = self._validate_limit(input_params.limit)
request_params["limit"], request_params["auto_paginate"] = self._set_limit_pagination(
input_params.limit
)
results = self._fetch_post(params=request_params, data=data)

# Get metadata from results
Expand Down
15 changes: 7 additions & 8 deletions tests/test_property_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,20 +208,19 @@ def test_schema_with_none_values() -> None:


def test_validate_limit(property_v2_service: PropertyV2Service) -> None:
assert property_v2_service._validate_limit(limit=None) == RequestLimits.PROPERTY_V2_MAX.value
assert property_v2_service._validate_limit(limit=None) == RequestLimits.PROPERTY_V2_MAX.value
assert property_v2_service._validate_limit(limit=100) == 100
assert (
property_v2_service._validate_limit(limit=1000000000) == RequestLimits.PROPERTY_V2_MAX.value
assert property_v2_service._set_limit_pagination(limit=None) == (
RequestLimits.PROPERTY_V2_MAX.value,
True,
)
assert property_v2_service._set_limit_pagination(limit=100) == (100, False)


@patch.object(PropertyV2Service, "_post")
def test_fetch_post_single_page(
mock_post: Mock, property_v2_service: PropertyV2Service, mock_response: Mock
) -> None:
mock_post.return_value = mock_response
result = property_v2_service._fetch_post(params={}, data={})
result = property_v2_service._fetch_post(params={"auto_paginate": False}, data={})

assert len(result) == 1
assert result[0] == mock_response.json()
Expand Down Expand Up @@ -251,7 +250,7 @@ def test_fetch_post_pagination(mock_post: Mock, property_v2_service: PropertyV2S
# Set up the mock to return different responses
mock_post.side_effect = [first_response, second_response]

result = property_v2_service._fetch_post(params={"limit": 1}, data={})
result = property_v2_service._fetch_post(params={"limit": 1, "auto_paginate": False}, data={})

assert len(result) == 1
assert result[0]["data"][0]["parcl_id"] == 123
Expand Down Expand Up @@ -311,7 +310,7 @@ def test_retrieve(

# check that the correct data was passed to _fetch_post
call_args = mock_fetch_post.call_args[1]
assert call_args["params"] == {"limit": 10}
assert call_args["params"] == {"limit": 10, "auto_paginate": False}

data = call_args["data"]
assert data["parcl_ids"] == [123]
Expand Down
Loading