fix(DAT-121): make property_v2 limit a total cap and warn on silent truncation - #102
Merged
Merged
Conversation
…-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>
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>
Collaborator
Author
|
bugbot review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
…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>
zhibindai26
approved these changes
Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Fixes DAT-121. Surfaced by PAR-100 (Central Michigan University). Breaking changes deliberately carved out to DAT-122.
The bug
Passing any explicit
limittoproperty_v2.search.retrievesilently disabled auto-pagination:So
limit=1000meant "one page of 1,000, silently discard the other 107,295." No error, no warning, a populated DataFrame that looked complete. Omittinglimitwas 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_paginationbuilt two mock pages, then called_fetch_postwith 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 —The implementation had silently diverged from its own docs. This restores the documented intent rather than inventing new semantics.
Changes
limitis now a cap on total properties returned. Pagination is an internal detail of satisfying it. Page size is derived asmin(limit, 50_000).limitabove 50,000 paginates instead of failing. The schema'sle=50000bound rejected these in Pydantic before the pagination logic ran at all. Removing it does not weaken the server contract — every individual request still sendslimit <= 50000. (History check: the constant was 100,000 whileauto_paginatewas 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=Nonehas always returned unbounded data, so 50,000 was never a volume guardrail.)ParclLabsTruncationWarningwhenlimitwithholds matching data, reporting returned vs available counts. Once per session, so per-market loops stay usable.ParclLabsIncompleteResultWarning+metadata["incomplete_pages"], instead of beingprinted and skipped as if the result were complete. There was previously no retry logic anywhere in the SDK, and1.17.2added a 90s read timeout — so timeouts on 50,000-property pages are a live failure mode. NYC alone is 54 pages.auto_paginateflag into the request query string (every call was shipping?limit=…&auto_paginate=False).limitin paginated page URLs (?limit=50000&offset=50000&limit=50000)._get_metadatadeep-copies, so it no longer mutates the caller's raw first-page response through a shallow copy.Warnings use
warnings.warnrather thanprintso callers can filter or escalate them —printis exactly what failed this customer.Not changed
limit=NforN <= 50000issues 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_id2900187), Jan 2024,total_available108,295:limit=1000(the customer's setting)limit=60000limitomittedtotal_available)limit=60000previously returned HTTP 422. The integrity check passed silently in all three cases, confirming pagination stays lossless at 108,295.94 tests pass;
ruff checkandruff format --checkclean. Version bumped to1.18.0with 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
limitrejection, above-ceiling acceptance.Note for reviewers
While verifying,
ge=1on the schema turned out to already rejectlimit=0and negatives — so DAT-122's first item ("limit <= 0should 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.retrievenow treatslimitas 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"], andParclLabsTruncationWarning/ParclLabsIncompleteResultWarning(newparcllabs.warnings) replace silentprintbehavior. A post-assembly integrity check warns when distinctparcl_property_idcounts don’t match reportedreturned_count. The internalauto_paginateflag is no longer sent on requests;PropertyV2RetrieveParams.limitdrops the 50k schema cap;_get_metadatadeep-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.