Skip to content

fix(DAT-121): make property_v2 limit a total cap and warn on silent truncation - #102

Merged
zshamroukh merged 5 commits into
mainfrom
zach/dat-121-limit-total-cap-truncation-warning
Aug 5, 2026
Merged

fix(DAT-121): make property_v2 limit a total cap and warn on silent truncation#102
zshamroukh merged 5 commits into
mainfrom
zach/dat-121-limit-total-cap-truncation-warning

Conversation

@zshamroukh

@zshamroukh zshamroukh commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes DAT-121. Surfaced by PAR-100 (Central Michigan University). Breaking changes deliberately carved out to DAT-122.

The bug

Passing any explicit limit to property_v2.search.retrieve silently disabled auto-pagination:

if limit == 0 or limit is None:
    return max_limit, True       # omitted => fetch everything
return limit, False              # ANY explicit limit => one page, rest discarded

So limit=1000 meant "one page of 1,000, silently discard the other 107,295." No error, no warning, a populated DataFrame that looked complete. Omitting limit was the only way to get complete data — the opposite of what a caller would guess.

An academic customer built a 50-CBSA / 6-year research panel this way and received ~0.4% of the data. He only noticed because the row counts felt low.

Why this is a redesign rather than another patched condition

This was the third attempt at this logic — CHANGELOG v1.15.2 "fix limit logic bug", v1.16.1 "Fix bug in auto pagination logic" — and the multi-page path had no test coverage at all. test_fetch_post_pagination built two mock pages, then called _fetch_post with pagination disabled and asserted a single request, so the second page was never consumed. Two further tests asserted the buggy behaviour as correct. All three are rewritten here.

Worth noting: limit-as-a-total-cap is what the README has documented all along —

Use the limit parameter to specify the number of matched properties to return. If limit is not provided, all matched properties will be returned.

The implementation had silently diverged from its own docs. This restores the documented intent rather than inventing new semantics.

Changes

  • limit is now a cap on total properties returned. Pagination is an internal detail of satisfying it. Page size is derived as min(limit, 50_000).
  • limit above 50,000 paginates instead of failing. The schema's le=50000 bound rejected these in Pydantic before the pagination logic ran at all. Removing it does not weaken the server contract — every individual request still sends limit <= 50000. (History check: the constant was 100,000 while auto_paginate was public, and was lowered to 50,000 in the same commit that removed it — it tracked the per-request ceiling, never a product cap. limit=None has always returned unbounded data, so 50,000 was never a volume guardrail.)
  • ParclLabsTruncationWarning when limit withholds matching data, reporting returned vs available counts. Once per session, so per-market loops stay usable.
  • Failed pages are retried (3 attempts, exponential backoff) and then reported via ParclLabsIncompleteResultWarning + metadata["incomplete_pages"], instead of being printed and skipped as if the result were complete. There was previously no retry logic anywhere in the SDK, and 1.17.2 added a 90s read timeout — so timeouts on 50,000-property pages are a live failure mode. NYC alone is 54 pages.
  • Pagination integrity check warns if assembled pages do not yield the expected number of distinct properties. Offset pagination is only safe while the server applies a stable sort; this surfaces a regression here rather than in a customer's dataset.
  • Stop leaking the internal auto_paginate flag into the request query string (every call was shipping ?limit=…&auto_paginate=False).
  • Stop duplicating limit in paginated page URLs (?limit=50000&offset=50000&limit=50000).
  • _get_metadata deep-copies, so it no longer mutates the caller's raw first-page response through a shallow copy.

Warnings use warnings.warn rather than print so callers can filter or escalate them — print is exactly what failed this customer.

Not changed

limit=N for N <= 50000 issues an identical HTTP request and returns an identical result. The behavioural fix for PAR-100 is that partial results now announce themselves.

Verification

Live against prod — NYC (parcl_id 2900187), Jan 2024, total_available 108,295:

Call Properties Warning
limit=1000 (the customer's setting) 1,000 "Returned 1,000 of 108,295…"
limit=60000 60,000 over 2 pages truncation
limit omitted 108,295 (= total_available) none

limit=60000 previously returned HTTP 422. The integrity check passed silently in all three cases, confirming pagination stays lossless at 108,295.

94 tests pass; ruff check and ruff format --check clean. Version bumped to 1.18.0 with CHANGELOG.

Test coverage added

Multi-page walk, stop-at-cap, final-page trimming, truncation warning fires once per session, no warning when complete, retry recovery, incomplete-pages warning on every affected call, metadata surfacing, no-mutation guard, integrity mismatch, non-positive limit rejection, above-ceiling acceptance.

Note for reviewers

While verifying, ge=1 on the schema turned out to already reject limit=0 and negatives — so DAT-122's first item ("limit <= 0 should raise") was based on a misreading of mine and is closed there. DAT-122 is now down to one genuinely breaking change (parcl_property_ids + limit).

🤖 Generated with Claude Code


Note

High Risk
Changes core property search pagination and limit semantics (previously silent data loss); behavior shifts for large limits and partial failures, though ≤50k limits keep the same HTTP shape when complete.

Overview
property_v2.search.retrieve now treats limit as a total cap on properties returned, with internal pagination to honor it (including values above the API’s 50k per-request ceiling). Explicit limits no longer disable pagination, fixing silent one-page truncation when callers expected a sample cap, not a single page.

Pagination is reworked: per-page fetches use retries with backoff, failed offsets surface in metadata["incomplete_pages"], and ParclLabsTruncationWarning / ParclLabsIncompleteResultWarning (new parcllabs.warnings) replace silent print behavior. A post-assembly integrity check warns when distinct parcl_property_id counts don’t match reported returned_count. The internal auto_paginate flag is no longer sent on requests; PropertyV2RetrieveParams.limit drops the 50k schema cap; _get_metadata deep-copies so callers’ raw responses aren’t mutated.

Docs and CHANGELOG describe credits-per-property vs event-level DataFrames; version 1.18.0; tests cover multi-page fetch, caps, warnings, retries, and metadata.

Reviewed by Cursor Bugbot for commit e01624c. Configure here.

zshamroukh and others added 2 commits August 4, 2026 16:16
…-121)

Passing any explicit `limit` to property_v2.search.retrieve silently disabled
auto-pagination, so `limit=1000` returned one page of 1,000 properties and
discarded every remaining match with no error and no warning. A customer built a
50-CBSA research panel this way and received ~0.4% of the data in a DataFrame
that looked complete.

`limit` is now a cap on the total number of properties returned, with pagination
handled internally to satisfy it. Calls with `limit <= 50000` are unaffected --
same request, same results -- so the behavioural fix here is that partial results
now announce themselves.

- `limit` above 50,000 paginates instead of failing the request (the schema's
  `le` bound rejected these before reaching the pagination logic at all)
- ParclLabsTruncationWarning when `limit` withholds matching data, reporting
  returned vs available counts; once per session so per-market loops stay usable
- failed pages are retried with exponential backoff, then reported via
  ParclLabsIncompleteResultWarning and metadata["incomplete_pages"], instead of
  being printed and skipped as if the result were complete
- pagination integrity check warns if assembled pages do not yield the expected
  number of distinct properties
- stop leaking the internal `auto_paginate` flag into the request query string
- stop duplicating `limit` in paginated page URLs
- `_get_metadata` deep-copies, so it no longer mutates the caller's raw response

The multi-page path previously had no test coverage: `test_fetch_post_pagination`
built two mock pages but called `_fetch_post` with pagination disabled and
asserted a single request, so the second page was never consumed. Two further
tests asserted the buggy behaviour as correct. All three are rewritten.

Verified against prod (NYC parcl_id 2900187, Jan 2024, 108,295 available):
  limit=1000  -> 1,000 properties + truncation warning
  limit=60000 -> 60,000 properties over 2 pages (previously HTTP 422)
  limit omitted -> 108,295 properties, unchanged

Breaking changes deferred to DAT-122.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (DAT-121)

The README's limit guidance predated the pagination fix: it framed limit as a
sampling tool without noting that values above the API's 50,000 per-request
maximum are now paginated, that credits bill per property rather than per event,
or that len(df) is not bounded by limit because the frame is event-level.

Also documents the total_available vs returned_count assertion so callers have a
programmatic truncation check, and the metadata["incomplete_pages"] contract.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread parcllabs/services/properties/property_v2.py Outdated
zshamroukh and others added 2 commits August 4, 2026 16:35
CI's test-readme step extracts every ```python block from the README and runs
it against the live API. The example I added set limit=1000 and then asserted
returned_count == total_available, which is false by construction -- it failed
CI on exactly the truncation it was demonstrating.

Rewritten as a reporting check rather than an assertion, which is also better
guidance: a reader copying it does not get a crash. Prose notes that you can
make it fatal if your pipeline wants that.

Also scoped the warnings-filter example inside warnings.catch_warnings(). As
written it set filterwarnings("error", ParclLabsIncompleteResultWarning)
globally, and since all README blocks are concatenated into one script that
would have made any later example raise on a partial page or integrity
mismatch.

Dropped the example limit from 1000 to 5 -- it still truncates against 4M
matching properties, so it demonstrates the same thing for 5 credits per CI run
instead of 1,000.

Verified with `make test-readme` locally: exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When pagination lost pages, _fetch_post emitted the incomplete-result warning
with the real retrieved count and then still called warn_truncation with
`target`. Three problems:

- the truncation message claimed the full cap was returned when fewer properties
  had actually been fetched
- it directly contradicted the incomplete warning issued a line earlier
- truncation fires once per session, so it burned that budget on the misleading
  message and would have suppressed a legitimate truncation notice later in the
  same run

Failed pages now return early and warn only about incompleteness, which is the
stronger and non-contradictory signal. `total_available` is passed into that
warning so capping information is not lost by skipping the truncation notice.

Also made the truncation message report what was actually returned rather than
what was requested, in both the early-return and paginated paths, and extracted
the page-sum into _total_returned.

Two regression tests: failed pages must not emit a truncation warning and must
leave the once-per-session flag intact; the truncation message must state the
returned count, not the requested one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zshamroukh

Copy link
Copy Markdown
Collaborator Author

bugbot review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e01624c. Configure here.

Comment thread parcllabs/services/properties/property_v2.py
…Bugbot)

Regression I introduced in e01624c. Fixing the truncation message to report the
returned count rather than the requested one, I also changed the gate from
`target < total_available` to `retrieved < total_available` -- one edit too many.

That made the early-return path warn on ANY shortfall, including an uncapped
request whose response ended pagination early (has_more=False while more
properties matched). It told the caller their result was short "because `limit`
capped the result" when no limit had been passed: unactionable advice, and it
consumed the once-per-session budget that a later legitimate truncation needs.

The gate and the count answer different questions. Gate on `target` (did a cap
withhold data?), report `retrieved` (what did we actually return?). The
multi-page path already did this correctly, so the two paths were inconsistent.

Verified against the three relevant states:
  no cap, server ends early  -> silent (was: warned, blaming limit)
  cap below available        -> warns, honest count
  no cap, consistent         -> silent

Deliberately not warning at all in the first case: it requires an inconsistent
server response, total_available remains in metadata, and misreporting it as
truncation is worse than staying quiet. Routing it to
ParclLabsIncompleteResultWarning instead is a possible follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zshamroukh
zshamroukh merged commit 218000a into main Aug 5, 2026
1 check passed
@zshamroukh
zshamroukh deleted the zach/dat-121-limit-total-cap-truncation-warning branch August 5, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants