Skip to content

Commit 0347797

Browse files
vdavezclaude
andcommitted
feat(models): surface the API's agency-filter diagnostics on PaginatedResponse
Agency values resolve fuzzily. A token can match nothing and be dropped, or match an organization the caller never intended and quietly scope the query to that subtree. Both look identical to "no such records exist" from the client side. The API reports both in a response-level `meta` block, but the SDK built `PaginatedResponse` key-by-key — `count`, `next`, `previous`, `results` — so `meta` was read by nobody and discarded. SDK users were the one group that could not see the diagnostics at all. `PaginatedResponse.meta` now carries it, populated at all 48 construction sites, with three accessors over the raw dict: - `unresolved_agency_tokens` — tokens that matched nothing, keyed by filter, for failing loudly in a pipeline. - `resolved_agencies` — the organization each token *did* match. This is the one that catches the wrong-organization case: nothing is dropped there, so an unresolved-token check cannot detect it. Comparing the resolved `name` is the only client-side signal. - `agency_warnings` — the API's human-readable notes. All three return empty rather than raising when `meta` is absent (most responses) or malformed, since `meta` is server-controlled and a shape change must not break a caller's loop. Also documents `page_metadata` as always `None`: the API has never emitted a `page_metadata` key, so the field has only ever read something that does not exist. Retained so existing attribute access keeps working. No change was needed for the 400 path — a fully-unresolvable agency filter already raises `TangoValidationError` via the existing `error`-key handler — but it is new behavior for `list_subawards()`, `list_opportunities()`, `list_notices()` and `list_vehicles()`, which previously returned an empty page. Covered by a test so the contract is pinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 732a38a commit 0347797

5 files changed

Lines changed: 312 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- **`relationships(type, source)` on entities.** Tango API 4.20.0 added two keys to each entry in the entity `relationships` expand: `type`, the stable relationship-type code (`prime_sub`, `parent_subsidiary`, `ultimate_parent`, `predecessor`), and `source`, where the tie came from (`sam`, `subawards`). Both are shape-selectable, so `shape="uei,relationships(type,source,uei)"` now resolves against the SDK's schema. Re-vendored the contract and regenerated the shape overlay; the coverage gate reports 0 gaps.
12+
- `PaginatedResponse.meta` now carries the response-level `meta` block the API returns, with three accessors over it: `unresolved_agency_tokens` (agency tokens that matched nothing, per filter), `resolved_agencies` (the organization each token *did* match), and `agency_warnings` (the API's human-readable notes). Agency values resolve fuzzily, so a token can match an organization the caller did not intend and quietly scope the query to that subtree — and a token that resolves to nothing was previously dropped with no signal at all. Both cases were indistinguishable from "no such records exist". The API now reports both; until now the SDK read the envelope key-by-key and dropped `meta` on the floor, so SDK users were the one group that could not see it. `resolved_agencies` is the accessor that matters for the wrong-organization case: nothing is dropped there, so an unresolved-token check cannot detect it. All accessors return empty rather than raising when `meta` is absent (most responses) or malformed.
13+
- A fully-unresolvable agency filter now raises `TangoValidationError` naming the offending value instead of returning an empty page. This needed no SDK change — the existing 400 handler already reads the API's `error` key — but it is new behavior for callers of `list_subawards()`, `list_opportunities()`, `list_notices()`, and `list_vehicles()`, which previously returned an empty result set. Contracts, IDVs, OTAs, and OTIDVs already behaved this way.
1214

1315
### Changed
1416
- **The `relation` value vocabulary changed upstream — match on `type` instead.** In Tango API 4.20.0 the `relation` label stopped collapsing to `affiliate` for subcontracting and corporate-succession ties and now names the partner's role: `subcontractor` / `prime`, `predecessor` / `successor`, and `descendant` (rather than `child`) on the far side of an ultimate-parent tie. `affiliate` survives only as a fallback for a type the API doesn't recognize. This affects the large majority of relationship entries.
1517

1618
No SDK code change is required — `relation` was and remains a `str`. But if you have application code branching on the string `affiliate`, switch it to `type`, which is stable and won't churn again. See the [entities data dictionary](https://docs.makegov.com/data-dictionary/entities/#relationships) for the full vocabulary table.
1719

20+
### Notes
21+
- `PaginatedResponse.page_metadata` is documented as always `None`: the API has never emitted a `page_metadata` key, so the field has only ever read a value that does not exist. It is retained so existing attribute access keeps working. Use `meta`.
22+
1823
## [1.4.0] - 2026-07-20
1924

2025
### Added

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,37 @@ contracts = client.list_contracts(
137137
- `expiring_gte`, `expiring_lte` - Contract expiration date range
138138

139139
**Party Filters:**
140-
- `awarding_agency`, `funding_agency` - Agency codes
140+
- `awarding_agency`, `funding_agency` - Agency codes, names, abbreviations, or organization UUIDs. Multi-value OR via `|`.
141141
- `recipient_name`, `recipient_uei` - Vendor/recipient filters
142142

143+
### Checking how agency filters resolved
144+
145+
Agency values are resolved fuzzily, so a token can match an organization you did not
146+
intend — which silently scopes the query to that organization's subtree. A short result
147+
set is then indistinguishable from "no such records exist". Responses expose what
148+
actually happened:
149+
150+
```python
151+
response = client.list_contracts(awarding_agency="HUD|HUDD")
152+
153+
# Tokens that matched nothing and were ignored.
154+
if response.unresolved_agency_tokens:
155+
raise SystemExit(f"dropped: {response.unresolved_agency_tokens}")
156+
# {'awarding_agency': ['HUDD']}
157+
158+
# What the tokens that DID match resolved to — the only way to catch a
159+
# plausible-but-wrong match, where nothing was dropped at all.
160+
for org in response.resolved_agencies.get("awarding_agency", []):
161+
print(org["name"], org["cgac"])
162+
# Department of Housing and Urban Development 086
163+
164+
for warning in response.agency_warnings:
165+
print(warning)
166+
```
167+
168+
If *every* token for a filter fails to resolve, the API returns `400` and the SDK raises
169+
`TangoValidationError` naming the offending value, rather than an empty page.
170+
143171
**Classification:**
144172
- `naics_code`, `psc_code` - Industry/product codes
145173
- `set_aside_type` - Set-aside type

0 commit comments

Comments
 (0)