diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7f3ad..f1b11de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`matched_by` on agency-filter diagnostics** (Tango API 5.9.0). Each entry of `PaginatedResponse.meta["resolved_filters"][]` that resolved now says how its token matched: `key` (an organization UUID), `code` (a 3-digit CGAC or 4-digit FPDS code), `name` (the organization's name, including a department's everyday name, a spelling variant or a rename), `alias` (an abbreviation or the organization's own alias) or `fuzzy` (a looser text match, worth checking against the resolved name). An entry that did not resolve has no `matched_by`. `meta` is passed through as the API sends it, so no code change was needed to receive the field; this release documents it and pins it with a test. `agency_warnings`, `unresolved_agency_tokens` and `resolved_agencies` are unchanged. + ## [1.14.0] - 2026-10-02 ### Added diff --git a/README.md b/README.md index 8a14fad..d8c0ebc 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,15 @@ for warning in response.agency_warnings: print(warning) ``` +Each entry of `response.meta["resolved_filters"][]` that resolved also says how its token matched, in `matched_by` (Tango API 5.9.0+): `key` (an organization UUID), `code` (a 3-digit CGAC or 4-digit FPDS code), `name` (the organization's name, including a department's everyday name, a spelling variant or a rename), `alias` (an abbreviation or the organization's own alias) or `fuzzy` (a looser text match). +A `fuzzy` match is the one worth checking against the resolved name: + +```python +for entry in response.meta["resolved_filters"].get("awarding_agency", []): + if entry.get("matched_by") == "fuzzy": + print(f"{entry['token']!r} loosely matched {entry['resolved']['name']}") +``` + If *every* token for a filter fails to resolve, the API returns `400` and the SDK raises `TangoValidationError` naming the offending value, rather than an empty page. diff --git a/tango/models.py b/tango/models.py index f573999..91f12bc 100644 --- a/tango/models.py +++ b/tango/models.py @@ -1454,11 +1454,10 @@ class PaginatedResponse[T]: results: List of result items (type depends on shape parameter) cursor: Cursor token for cursor-based pagination (None if not available) meta: Response-level metadata the API attached to this page, when present. - Currently carries agency-filter diagnostics: ``resolved_filters`` maps - each agency filter to the organizations its ``|``-separated tokens - resolved to (or ``None``), and ``warnings`` lists human-readable notes - about tokens that were dropped or matched loosely. See - :meth:`agency_warnings` and :meth:`unresolved_agency_tokens`. + Currently carries agency-filter diagnostics: ``resolved_filters`` maps each agency filter to the organizations its ``|``-separated tokens resolved to (or ``None``), and ``warnings`` lists human-readable notes about tokens that were dropped or matched loosely. + Each ``resolved_filters`` entry that resolved also carries ``matched_by`` (Tango API 5.9.0+), saying how the token matched: ``"key"`` (an organization UUID), ``"code"`` (a 3-digit CGAC or 4-digit FPDS code), ``"name"`` (the organization's name, including a department's everyday name, a spelling variant or a rename), ``"alias"`` (an abbreviation or the organization's own alias) or ``"fuzzy"`` (a looser text match, worth checking against the resolved ``name``). + An entry that did not resolve has no ``matched_by``. + See :meth:`agency_warnings`, :meth:`unresolved_agency_tokens` and :meth:`resolved_agencies`. page_metadata: Always ``None`` — the API has never emitted a ``page_metadata`` key. Retained so existing attribute access keeps working; use ``meta`` instead. diff --git a/tests/test_client.py b/tests/test_client.py index b11ab49..607b3a2 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2292,6 +2292,22 @@ def test_meta_is_carried_through_to_the_response(self, mock_request): assert response.meta == meta + @patch("tango.client.httpx.Client.request") + def test_matched_by_reaches_the_caller_and_leaves_the_accessors_unchanged(self, mock_request): + entries = [ + {"token": "HUD", "matched_by": "alias", "resolved": self.HUD}, + {"token": "Housing and Urban Developmnt", "matched_by": "fuzzy", "resolved": self.HUD}, + {"token": "HUDD", "resolved": None}, + ] + client = self._mock(mock_request, {"resolved_filters": {"awarding_agency": entries}}) + + response = client.list_contracts(awarding_agency="HUD|Housing and Urban Developmnt|HUDD") + + returned = response.meta["resolved_filters"]["awarding_agency"] + assert [entry.get("matched_by") for entry in returned] == ["alias", "fuzzy", None] + assert response.unresolved_agency_tokens == {"awarding_agency": ["HUDD"]} + assert response.resolved_agencies == {"awarding_agency": [self.HUD, self.HUD]} + @patch("tango.client.httpx.Client.request") def test_dropped_tokens_are_reported_per_filter(self, mock_request): client = self._mock(