Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"][<filter name>]` 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
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ for warning in response.agency_warnings:
print(warning)
```

Each entry of `response.meta["resolved_filters"][<filter name>]` 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.

Expand Down
9 changes: 4 additions & 5 deletions tango/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading