Skip to content

Commit 5468bef

Browse files
vdavezclaude
andcommitted
feat(psc): add has_awards; retire the last CONTRACT_OMITTED_PARAMS entry
Re-vendors the contract now that makegov/tango#2948 merged, which taught the generator to publish params a viewset reads straight from query_params. Two consequences, both anticipated: 1. The self-cleaning warning added in this PR fired on its own carve-out — "mas_sins: CONTRACT_OMITTED_PARAMS entries no longer needed (the contract now publishes them): search". Removed it. The list is now empty, which is the goal state; the mechanism stays for the next case. 2. A new real gap surfaced: psc `has_awards` filters (2,526 codes -> 2,243) but list_psc did not expose it. It was invisible until #2948 published it. Added, sending False explicitly rather than dropping it, since the API distinguishes the two. Note that #2948 also fixed a truthiness bug where has_awards=false filtered identically to true. Production is still on 4.18.5 and staging is at 4.18.5e, so until that deploys, has_awards=False returns awards-only results. Verified live: 2,526 unfiltered, 2,243 for both True and False today. Gates: conformance exit 0 with no errors, shape coverage 0 gaps. 366 unit tests pass, 124 integration. mypy strict clean. The one failure, test_production_smoke::test_search_filters, is a live-API 504 reproduced on unmodified main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a9cb0da commit 5468bef

5 files changed

Lines changed: 74 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- `list_protests()` now accepts `naics_code`, filtering protests by the solicitation's NAICS code. The API has always supported it (verified live: 15,027 protests unfiltered, 12 for `541519`, 2 for `336411`), but `protests` was absent from the conformance checker's resource-to-method map, so the gap was never reported.
12+
- `list_psc()` now accepts `has_awards`, restricting results to codes with contract award history (2,526 PSC codes unfiltered, 2,243 with awards). `False` is sent explicitly rather than dropped, since the API distinguishes the two. This param has always worked but was invisible to the contract until [makegov/tango#2948](https://github.com/makegov/tango/pull/2948) added the `view_handled_params` declaration for params a viewset reads straight from `query_params`. Note that the same PR fixed a truthiness bug where `has_awards=false` filtered identically to `true`; until it reaches production, passing `has_awards=False` still returns awards-only results.
1213

1314
### Fixed
1415
- **The reverse shape-coverage gate was blind to seven resources, and the SDK's schemas had silently drifted behind four of them.** The gate skipped any resource whose contract shape tree was falsy (`if not shape: continue`). Tango published `"shape": null` for `entities`, `opportunities`, `notices`, `protests`, `itdashboard`, `events`, and `news` — five of them because its contract generator crashed on tier-aware viewsets ([makegov/tango#2944](https://github.com/makegov/tango/pull/2944)) — so the gate reported full coverage while checking nothing for them. Re-vendoring the fixed contract surfaced **69 real gaps** (36 fields, 20 expands, 13 flattened expands) across entities, notices, protests, and itdashboard, all now closed by a regenerated overlay.
@@ -20,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2021
- **`refresh_contract.py` + `probe_shape_types.py` re-run against the corrected contract.** `contracts/observed_shape_types.json` gained live-sampled types for the five newly-visible resources (entities alone contributes 136 observed paths), so the regenerated overlay resolves their types from real API responses rather than name heuristics.
2122

2223
### Changed
23-
- `CONTRACT_OMITTED_PARAMS` now warns when an entry is no longer needed, mirroring how baseline entries are burned down. Added `mas_sins: search`the API applies it (330 results unfiltered, 0 for a nonsense term, 32 for `office`) but the contract records `filter_params: []`, so the checker would otherwise flag a working parameter as a silent no-op and invite its removal. Dropped `budget/accounts: search`, which makegov/tango#2944's `SearchFilter` extraction now publishes.
24+
- `CONTRACT_OMITTED_PARAMS` now warns when an entry is no longer needed, mirroring how baseline entries are burned down, and the list is now **empty** — which is the goal state. It briefly held `mas_sins: search`, added because the API applies it (330 results unfiltered, 0 for a nonsense term, 32 for `office`) while the contract recorded `filter_params: []`, so the checker would otherwise have flagged a working parameter as a silent no-op and invited its removal. Both that entry and `budget/accounts: search` were retired by the new warning once [makegov/tango#2944](https://github.com/makegov/tango/pull/2944) and [#2948](https://github.com/makegov/tango/pull/2948) taught the contract generator to publish them. The carve-out mechanism stays for the next such case.
2425

2526
## [1.3.0] - 2026-07-19
2627

contracts/filter_shape_contract.json

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4613,8 +4613,15 @@
46134613
"prefix": "mas_sins",
46144614
"resource_key": "mas_sins",
46154615
"runtime": {
4616-
"filter_params": [],
4617-
"filter_params_detail": {},
4616+
"filter_params": [
4617+
"search"
4618+
],
4619+
"filter_params_detail": {
4620+
"search": {
4621+
"filter_class": "ViewHandledParam",
4622+
"type": "string"
4623+
}
4624+
},
46184625
"ordering_aliases": [],
46194626
"ordering_fields": [],
46204627
"pagination": {
@@ -6882,8 +6889,15 @@
68826889
"prefix": "psc",
68836890
"resource_key": "psc",
68846891
"runtime": {
6885-
"filter_params": [],
6886-
"filter_params_detail": {},
6892+
"filter_params": [
6893+
"has_awards"
6894+
],
6895+
"filter_params_detail": {
6896+
"has_awards": {
6897+
"filter_class": "ViewHandledParam",
6898+
"type": "boolean"
6899+
}
6900+
},
68876901
"ordering_aliases": [],
68886902
"ordering_fields": [],
68896903
"pagination": {

scripts/check_filter_shape_conformance.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,11 @@
9898
# removed once Tango's generator surfaces it — `check_conformance` warns when an
9999
# entry is no longer needed, so this list cannot quietly rot.
100100
#
101-
# mas_sins `search`: verified 2026-07-20 — 330 results unfiltered vs 0 for a
102-
# nonsense term, 32 for "office", 3 for "cloud". The endpoint clearly applies
103-
# it, and the contract records `filter_params: []` for the resource.
104-
#
105-
# (budget/accounts `search` lived here until 2026-07-20; makegov/tango#2944's
106-
# SearchFilter extraction now publishes it, so the carve-out was removed.)
107-
CONTRACT_OMITTED_PARAMS: dict[str, frozenset[str]] = {
108-
"mas_sins": frozenset({"search"}),
109-
}
101+
# Currently empty, and that is the goal state. Two entries have come and gone:
102+
# budget/accounts `search` (published by makegov/tango#2944's SearchFilter
103+
# extraction) and mas_sins `search` (published by makegov/tango#2948's
104+
# `view_handled_params` declaration). Both were retired by the warning above.
105+
CONTRACT_OMITTED_PARAMS: dict[str, frozenset[str]] = {}
110106

111107
# SDK-level conveniences that never correspond to API filter params.
112108
# `sort`/`order` compose into the API's `ordering`; `filters` is the legacy

tango/client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4448,15 +4448,22 @@ def list_psc(
44484448
shape: str | None = None,
44494449
flat: bool = False,
44504450
flat_lists: bool = False,
4451+
has_awards: bool | None = None,
44514452
) -> PaginatedResponse[dict[str, Any]]:
4452-
"""List Product Service Codes (`/api/psc/`)."""
4453+
"""List Product Service Codes (`/api/psc/`).
4454+
4455+
Args:
4456+
has_awards: When True, return only codes with contract award history.
4457+
"""
44534458
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
44544459
if shape:
44554460
params["shape"] = shape
44564461
if flat:
44574462
params["flat"] = "true"
44584463
if flat_lists:
44594464
params["flat_lists"] = "true"
4465+
if has_awards is not None:
4466+
params["has_awards"] = "true" if has_awards else "false"
44604467
data = self._get("/api/psc/", params)
44614468
return PaginatedResponse(
44624469
count=int(data.get("count", 0)),

tests/test_client.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,3 +1949,44 @@ def test_list_protests_omits_unset_naics_code(self, mock_request):
19491949
client.list_protests()
19501950

19511951
assert "naics_code" not in mock_request.call_args[1]["params"]
1952+
1953+
1954+
def _stub_empty_page(mock_request):
1955+
"""Wire a mocked httpx response for an empty paginated result."""
1956+
mock_response = Mock()
1957+
mock_response.is_success = True
1958+
mock_response.status_code = 200
1959+
mock_response.json.return_value = {
1960+
"count": 0,
1961+
"next": None,
1962+
"previous": None,
1963+
"results": [],
1964+
}
1965+
mock_response.content = b'{"count": 0}'
1966+
mock_request.return_value = mock_response
1967+
1968+
1969+
class TestPscFilters:
1970+
"""`has_awards` reached the SDK only after makegov/tango#2948 published it.
1971+
1972+
The param filtered for real but was absent from the contract, so the conformance check had nothing to compare against and the gap went unreported.
1973+
"""
1974+
1975+
@patch("tango.client.httpx.Client.request")
1976+
def test_list_psc_forwards_has_awards_true(self, mock_request):
1977+
_stub_empty_page(mock_request)
1978+
TangoClient(api_key="test-key").list_psc(has_awards=True)
1979+
assert mock_request.call_args[1]["params"]["has_awards"] == "true"
1980+
1981+
@patch("tango.client.httpx.Client.request")
1982+
def test_list_psc_forwards_has_awards_false(self, mock_request):
1983+
"""False must be sent, not dropped — the API distinguishes them."""
1984+
_stub_empty_page(mock_request)
1985+
TangoClient(api_key="test-key").list_psc(has_awards=False)
1986+
assert mock_request.call_args[1]["params"]["has_awards"] == "false"
1987+
1988+
@patch("tango.client.httpx.Client.request")
1989+
def test_list_psc_omits_unset_has_awards(self, mock_request):
1990+
_stub_empty_page(mock_request)
1991+
TangoClient(api_key="test-key").list_psc()
1992+
assert "has_awards" not in mock_request.call_args[1]["params"]

0 commit comments

Comments
 (0)