From 99200607bbf01a87babcc81325772e0a9106d306 Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Mon, 19 Oct 2026 12:02:39 -0500 Subject: [PATCH 1/2] feat(awards): obligations_by_fiscal_year and attachments(extraction_status) (API 5.11.0) Contracts, IDVs, OTAs and OTIDVs accept the `obligations_by_fiscal_year(fiscal_year, obligated)` shape expand, parsed as a list of `{fiscal_year: int, obligated: Decimal}`. It is always `None` on IDVs and OTIDVs, whose transactions report a running lifetime total. Opportunities and notices accept `attachments(extraction_status)`. Re-vendors the API contract at 5.11.0 and regenerates the shape overlay. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 ++ contracts/filter_shape_contract.json | 168 ++++++++++++++++++++++- docs/API_REFERENCE.md | 16 +++ tango/models.py | 11 ++ tango/shapes/explicit_schemas.py | 35 +++++ tango/shapes/generated_overlay.py | 18 +++ tests/test_obligations_by_fiscal_year.py | 88 ++++++++++++ 7 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 tests/test_obligations_by_fiscal_year.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d8e709c..be9bc8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`obligations_by_fiscal_year(fiscal_year, obligated)` shape expand on contracts, IDVs, OTAs and OTIDVs** (Tango API 5.11.0). Returns the award's obligations per federal fiscal year, each rounded to cents, with `fiscal_year` parsed as `int` and `obligated` as `Decimal`. It is always `None` on IDVs and OTIDVs, whose transactions report a running lifetime total. +- **`attachments(extraction_status)` shape field on opportunities and notices** (Tango API 5.11.0). Reports `pending`, `extracting`, `completed`, `failed` or `unsupported` for each attachment, so you can tell why `extracted_text` is missing. + +### Changed + +- Re-vendored `contracts/filter_shape_contract.json` (Tango API 5.11.0) and regenerated the shape overlay. + ## [1.17.0] - 2026-10-16 ### Added diff --git a/contracts/filter_shape_contract.json b/contracts/filter_shape_contract.json index ca2935f..fed5961 100644 --- a/contracts/filter_shape_contract.json +++ b/contracts/filter_shape_contract.json @@ -1,6 +1,6 @@ { "meta": { - "api_version": "5.10.0", + "api_version": "5.11.0", "description": "Canonical API filter/shape contract. Downstream consumers (SDK, MCP) should validate their conformance against this manifest.", "generated_from": "scripts/filter_shape_conformance.py", "schema_version": 2 @@ -434,6 +434,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -723,6 +730,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", @@ -1181,6 +1191,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -1470,6 +1487,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", @@ -3050,6 +3070,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -3339,6 +3366,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", @@ -5291,6 +5321,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -5580,6 +5617,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", @@ -5968,6 +6008,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -6215,6 +6262,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", @@ -6671,6 +6721,13 @@ "organization_id" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "parent_award": { "expands": {}, "fields": [ @@ -6788,6 +6845,9 @@ "non_governmental_dollars", "non_traditional_government_contractor_participation", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "parent_award", "parent_award.key", "parent_award.piid", @@ -7007,6 +7067,13 @@ "organization_id" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "period_of_performance": { "expands": {}, "fields": [ @@ -7114,6 +7181,9 @@ "non_governmental_dollars", "non_traditional_government_contractor_participation", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "period_of_performance", "period_of_performance.current_end_date", "period_of_performance.start_date", @@ -8925,6 +8995,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -9172,6 +9249,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", @@ -9631,6 +9711,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -9920,6 +10007,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", @@ -10308,6 +10398,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -10555,6 +10652,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", @@ -11451,6 +11551,7 @@ "doc_role", "doc_role_alt", "extracted_text", + "extraction_status", "file_size", "mime_type", "name", @@ -11581,6 +11682,7 @@ "attachments.doc_role", "attachments.doc_role_alt", "attachments.extracted_text", + "attachments.extraction_status", "attachments.file_size", "attachments.mime_type", "attachments.name", @@ -11910,6 +12012,7 @@ "doc_role", "doc_role_alt", "extracted_text", + "extraction_status", "file_size", "mime_type", "name", @@ -12084,6 +12187,7 @@ "attachments.doc_role", "attachments.doc_role_alt", "attachments.extracted_text", + "attachments.extraction_status", "attachments.file_size", "attachments.mime_type", "attachments.name", @@ -12674,6 +12778,13 @@ "organization_id" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "parent_award": { "expands": {}, "fields": [ @@ -12791,6 +12902,9 @@ "non_governmental_dollars", "non_traditional_government_contractor_participation", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "parent_award", "parent_award.key", "parent_award.piid", @@ -13018,6 +13132,13 @@ "organization_id" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "period_of_performance": { "expands": {}, "fields": [ @@ -13125,6 +13246,9 @@ "non_governmental_dollars", "non_traditional_government_contractor_participation", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "period_of_performance", "period_of_performance.current_end_date", "period_of_performance.start_date", @@ -13355,6 +13479,13 @@ "organization_id" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "parent_award": { "expands": {}, "fields": [ @@ -13472,6 +13603,9 @@ "non_governmental_dollars", "non_traditional_government_contractor_participation", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "parent_award", "parent_award.key", "parent_award.piid", @@ -15399,6 +15533,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -15577,6 +15718,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -15955,6 +16103,7 @@ "expands": {}, "fields": [ "attachment_id", + "extraction_status", "file_size", "mime_type", "name", @@ -16272,6 +16421,9 @@ "awardees.number_of_actions", "awardees.number_of_offers_source", "awardees.obligated", + "awardees.obligations_by_fiscal_year", + "awardees.obligations_by_fiscal_year.fiscal_year", + "awardees.obligations_by_fiscal_year.obligated", "awardees.officers", "awardees.officers.highly_compensated_officer_1_amount", "awardees.officers.highly_compensated_officer_1_name", @@ -16381,6 +16533,9 @@ "awardees.orders.number_of_actions", "awardees.orders.number_of_offers_source", "awardees.orders.obligated", + "awardees.orders.obligations_by_fiscal_year", + "awardees.orders.obligations_by_fiscal_year.fiscal_year", + "awardees.orders.obligations_by_fiscal_year.obligated", "awardees.orders.officers", "awardees.orders.officers.highly_compensated_officer_1_amount", "awardees.orders.officers.highly_compensated_officer_1_name", @@ -16584,6 +16739,7 @@ "opportunity.archive_date", "opportunity.attachments", "opportunity.attachments.attachment_id", + "opportunity.attachments.extraction_status", "opportunity.attachments.file_size", "opportunity.attachments.mime_type", "opportunity.attachments.name", @@ -17098,6 +17254,13 @@ "description" ] }, + "obligations_by_fiscal_year": { + "expands": {}, + "fields": [ + "fiscal_year", + "obligated" + ] + }, "officers": { "expands": {}, "fields": [ @@ -17387,6 +17550,9 @@ "number_of_actions", "number_of_offers_source", "obligated", + "obligations_by_fiscal_year", + "obligations_by_fiscal_year.fiscal_year", + "obligations_by_fiscal_year.obligated", "officers", "officers.highly_compensated_officer_1_amount", "officers.highly_compensated_officer_1_name", diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index f4bcdc5..e894002 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -381,6 +381,22 @@ contracts = client.list_contracts( - `psc` - Product/service code (nested) - `place_of_performance` - Location (nested) +**Obligations by fiscal year — `obligations_by_fiscal_year(fiscal_year, obligated)`** (Tango API 5.11.0): + +```python +contracts = client.list_contracts( + shape="key,piid,obligated,obligations_by_fiscal_year(fiscal_year,obligated)", + piid="SOME_PIID", +) +for year in contracts.results[0]["obligations_by_fiscal_year"] or []: + print(year["fiscal_year"], year["obligated"]) # int, Decimal +``` + +- Sums the award's transactions per federal fiscal year (October 1 starts the next fiscal year), oldest year first, each rounded to cents. A de-obligation nets against its own year, so a year can be zero or negative. +- Returned only when named: no default shape includes it. +- The same expand works on `list_otas()`, `list_idvs()`, `list_otidvs()` and the `get_*` methods. On IDVs and OTIDVs it is always `None`, because their transactions report a running lifetime total rather than each action's obligation. +- `None` on a contract or OTA with no dated transactions. + --- ## OTAs diff --git a/tango/models.py b/tango/models.py index c40548c..47ea496 100644 --- a/tango/models.py +++ b/tango/models.py @@ -281,6 +281,17 @@ class AwardTransaction: obligated: Decimal | None = None +@dataclass +class FiscalYearObligation: + """Schema definition for FiscalYearObligation (not used for instances). + + One entry of an award's ``obligations_by_fiscal_year``: the obligations dated in one federal fiscal year, rounded to cents. + """ + + fiscal_year: int + obligated: Decimal + + @dataclass class OrganizationOfficePayload: """Schema definition for OrganizationOfficePayload (not used for instances). diff --git a/tango/shapes/explicit_schemas.py b/tango/shapes/explicit_schemas.py index 2af94a9..56a79d7 100644 --- a/tango/shapes/explicit_schemas.py +++ b/tango/shapes/explicit_schemas.py @@ -197,6 +197,12 @@ } +FISCAL_YEAR_OBLIGATION_SCHEMA: dict[str, FieldSchema] = { + "fiscal_year": FieldSchema(name="fiscal_year", type=int, is_optional=False, is_list=False), + "obligated": FieldSchema(name="obligated", type=Decimal, is_optional=False, is_list=False), +} + + DEPARTMENT_SCHEMA: dict[str, FieldSchema] = { "abbreviation": FieldSchema(name="abbreviation", type=str, is_optional=True, is_list=False), "code": FieldSchema(name="code", type=int, is_optional=True, is_list=False), @@ -432,6 +438,13 @@ is_list=False, nested_model="Vehicle", ), + "obligations_by_fiscal_year": FieldSchema( + name="obligations_by_fiscal_year", + type=dict, + is_optional=True, + is_list=True, + nested_model="FiscalYearObligation", + ), } @@ -1290,6 +1303,13 @@ "idv_contracts_value": FieldSchema( name="idv_contracts_value", type=Decimal, is_optional=True, is_list=False ), + "obligations_by_fiscal_year": FieldSchema( + name="obligations_by_fiscal_year", + type=dict, + is_optional=True, + is_list=True, + nested_model="FiscalYearObligation", + ), } @@ -1439,6 +1459,13 @@ is_list=False, nested_model="RecipientProfile", ), + "obligations_by_fiscal_year": FieldSchema( + name="obligations_by_fiscal_year", + type=dict, + is_optional=True, + is_list=True, + nested_model="FiscalYearObligation", + ), } # OTIDV (Other Transaction IDV) - IDV-like @@ -1459,6 +1486,13 @@ is_list=False, nested_model="RecipientProfile", ), + "obligations_by_fiscal_year": FieldSchema( + name="obligations_by_fiscal_year", + type=dict, + is_optional=True, + is_list=True, + nested_model="FiscalYearObligation", + ), } # Subaward (prime/sub awards) @@ -1707,6 +1741,7 @@ "LegislativeMandates": LEGISLATIVE_MANDATES_SCHEMA, "SubawardsSummary": SUBAWARDS_SUMMARY_SCHEMA, "Transaction": TRANSACTION_SCHEMA, + "FiscalYearObligation": FISCAL_YEAR_OBLIGATION_SCHEMA, "Department": DEPARTMENT_SCHEMA, "Contact": CONTACT_SCHEMA, "RecipientProfile": RECIPIENT_PROFILE_SCHEMA, diff --git a/tango/shapes/generated_overlay.py b/tango/shapes/generated_overlay.py index 4bba193..4574944 100644 --- a/tango/shapes/generated_overlay.py +++ b/tango/shapes/generated_overlay.py @@ -85,6 +85,9 @@ "doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False), "doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False), "extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False), + "extraction_status": FieldSchema( + name="extraction_status", type=str, is_optional=True, is_list=False + ), "file_size": FieldSchema(name="file_size", type=int, is_optional=True, is_list=False), "mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False), "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False), @@ -99,6 +102,9 @@ "doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False), "doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False), "extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False), + "extraction_status": FieldSchema( + name="extraction_status", type=str, is_optional=True, is_list=False + ), "file_size": FieldSchema(name="file_size", type=int, is_optional=True, is_list=False), "mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False), "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False), @@ -135,6 +141,9 @@ ATTACHMENTS4_SCHEMA: dict[str, FieldSchema] = { "attachment_id": FieldSchema(name="attachment_id", type=str, is_optional=True, is_list=False), + "extraction_status": FieldSchema( + name="extraction_status", type=str, is_optional=True, is_list=False + ), "file_size": FieldSchema(name="file_size", type=str, is_optional=True, is_list=False), "mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False), "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False), @@ -149,6 +158,9 @@ "doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False), "doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False), "extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False), + "extraction_status": FieldSchema( + name="extraction_status", type=str, is_optional=True, is_list=False + ), "file_size": FieldSchema(name="file_size", type=str, is_optional=True, is_list=False), "mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False), "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False), @@ -758,6 +770,11 @@ "type": FieldSchema(name="type", type=str, is_optional=True, is_list=False), } +OBLIGATIONS_BY_FISCAL_YEAR_SCHEMA: dict[str, FieldSchema] = { + "fiscal_year": FieldSchema(name="fiscal_year", type=str, is_optional=True, is_list=False), + "obligated": FieldSchema(name="obligated", type=str, is_optional=True, is_list=False), +} + OFFICERS_SCHEMA: dict[str, FieldSchema] = { "highly_compensated_officer_1_amount": FieldSchema( name="highly_compensated_officer_1_amount", type=Decimal, is_optional=True, is_list=False @@ -1161,6 +1178,7 @@ "NoticeHistory": NOTICE_HISTORY_SCHEMA, "NoticeHistory2": NOTICE_HISTORY2_SCHEMA, "NoticeType": NOTICE_TYPE_SCHEMA, + "ObligationsByFiscalYear": OBLIGATIONS_BY_FISCAL_YEAR_SCHEMA, "Officers": OFFICERS_SCHEMA, "Opportunity": OPPORTUNITY_SCHEMA, "Opportunity2": OPPORTUNITY2_SCHEMA, diff --git a/tests/test_obligations_by_fiscal_year.py b/tests/test_obligations_by_fiscal_year.py new file mode 100644 index 0000000..be8cbf0 --- /dev/null +++ b/tests/test_obligations_by_fiscal_year.py @@ -0,0 +1,88 @@ +"""The `obligations_by_fiscal_year(...)` expand on contracts, IDVs, OTAs and OT IDVs.""" + +from decimal import Decimal +from unittest.mock import Mock, patch + +import pytest + +from tango import TangoClient +from tango.models import IDV, OTA, OTIDV, Contract, Notice, Opportunity +from tango.shapes.parser import ShapeParser + +SHAPE = "key,obligations_by_fiscal_year(fiscal_year,obligated)" + + +def _mock(mock_request, payload): + response = Mock() + response.is_success = True + response.json.return_value = payload + response.content = b"{}" + mock_request.return_value = response + + +def _page(rows): + return {"count": len(rows), "next": None, "previous": None, "results": rows} + + +class TestObligationsByFiscalYearShape: + @pytest.mark.parametrize("model", [Contract, IDV, OTA, OTIDV]) + @pytest.mark.parametrize( + "shape", + [ + SHAPE, + "key,obligations_by_fiscal_year(*)", + "key,obligations_by_fiscal_year", + ], + ) + def test_shape_validates(self, model, shape): + parser = ShapeParser(cache_enabled=False) + parser.validate(parser.parse(shape), model) + + @pytest.mark.parametrize("model", [Contract, OTA]) + def test_unknown_sub_field_is_rejected(self, model): + parser = ShapeParser(cache_enabled=False) + with pytest.raises(Exception, match="amount"): + parser.validate(parser.parse("key,obligations_by_fiscal_year(amount)"), model) + + +class TestObligationsByFiscalYearResponses: + @pytest.mark.parametrize("method", ["list_contracts", "list_otas"]) + @patch("tango.client.httpx.Client.request") + def test_list_parses_each_year(self, mock_request, method): + _mock( + mock_request, + _page( + [ + { + "key": "K1", + "obligations_by_fiscal_year": [ + {"fiscal_year": 2023, "obligated": 1000.0}, + {"fiscal_year": 2024, "obligated": -250.5}, + ], + } + ] + ), + ) + + (row,) = getattr(TangoClient(api_key="k"), method)(shape=SHAPE).results + + years = row["obligations_by_fiscal_year"] + assert [y["fiscal_year"] for y in years] == [2023, 2024] + assert [y["obligated"] for y in years] == [Decimal("1000.0"), Decimal("-250.5")] + assert all(isinstance(y["obligated"], Decimal) for y in years) + + @pytest.mark.parametrize("method", ["list_idvs", "list_otidvs"]) + @patch("tango.client.httpx.Client.request") + def test_cumulative_award_types_return_none(self, mock_request, method): + _mock(mock_request, _page([{"key": "K1", "obligations_by_fiscal_year": None}])) + + (row,) = getattr(TangoClient(api_key="k"), method)(shape=SHAPE).results + + assert row["obligations_by_fiscal_year"] is None + + +class TestAttachmentExtractionStatusShape: + @pytest.mark.parametrize("model", [Opportunity, Notice]) + def test_shape_validates(self, model): + parser = ShapeParser(cache_enabled=False) + parser.validate(parser.parse("title,attachments(name,extraction_status)"), model) From 6190c284aace114bbad33552d13682c61b0c3117 Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Mon, 19 Oct 2026 12:22:15 -0500 Subject: [PATCH 2/2] docs: recipient_uei and uei accept several UEIs joined by | (API 5.11.0) Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + docs/API_REFERENCE.md | 2 +- tango/client.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be9bc8d..cc13083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Re-vendored `contracts/filter_shape_contract.json` (Tango API 5.11.0) and regenerated the shape overlay. +- Documented that `recipient_uei` on `list_contracts()` (and `uei` on the IDV, OTA and OTIDV lists) accepts several UEIs joined by `|` (Tango API 5.11.0); the SDK already passed the value through unchanged. ## [1.17.0] - 2026-10-16 diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index e894002..2cb7d6f 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -285,7 +285,7 @@ contracts = client.list_contracts( - `awarding_agency` - Agency code (e.g., "4700" for GSA) - `funding_agency` - Funding agency code - `recipient_name` - Vendor/recipient name (mapped to 'recipient' API param) -- `recipient_uei` - Vendor UEI (mapped to 'uei' API param) +- `recipient_uei` - Vendor UEI (mapped to 'uei' API param). Several UEIs joined by `|` match awards to any of them, up to 500 (Tango API 5.11.0). **Classification:** - `naics_code` - NAICS industry code (mapped to 'naics' API param) diff --git a/tango/client.py b/tango/client.py index 9734ba1..2a362d4 100644 --- a/tango/client.py +++ b/tango/client.py @@ -695,7 +695,7 @@ def list_contracts( naics_code: NAICS code (mapped to 'naics' API param) psc_code: PSC code (mapped to 'psc' API param) recipient_name: Vendor/recipient name (mapped to 'recipient' API param) - recipient_uei: Vendor UEI (mapped to 'uei' API param) + recipient_uei: Vendor UEI (mapped to 'uei' API param); join several with `|` for OR, up to 500 set_aside_type: Set-aside type (mapped to 'set_aside' API param) sort: Field to sort by (combined with 'order' to produce 'ordering') order: Sort order ('asc' or 'desc', default 'asc')