diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f085ef..bf5eb1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`commercial_item_acquisition_procedures` filter on `list_contracts()` and `list_idvs()`** (Tango API 5.10.0). Filters by the FPDS commercial item acquisition procedures code: `A` (commercial products/services), `B` (products or services pursuant to FAR 12.102(f)), `C` (services pursuant to FAR 12.102(g)), `D` (commercial procedures not used) or `E` (DoD Section 803 Commercial Solutions Opening). Codes are case-insensitive and pipe-separated for OR, e.g. `"A|E"`; an unknown or multi-character code matches nothing. The API does not offer it on OTAs or OTIDVs, so `list_otas()` and `list_otidvs()` do not take it. The field itself was already returned in responses. + +### Changed + +- Re-vendored `contracts/filter_shape_contract.json` (Tango API 5.10.0). + ## [1.16.0] - 2026-10-10 ### Added diff --git a/contracts/filter_shape_contract.json b/contracts/filter_shape_contract.json index 97e47dc..ca2935f 100644 --- a/contracts/filter_shape_contract.json +++ b/contracts/filter_shape_contract.json @@ -1,6 +1,6 @@ { "meta": { - "api_version": "5.9.1", + "api_version": "5.10.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 @@ -95,6 +95,7 @@ "award_date_lte", "award_type", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -150,6 +151,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", @@ -837,6 +842,7 @@ "award_date_lte", "award_type", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -892,6 +898,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", @@ -2701,6 +2711,7 @@ "award_date_lte", "award_type", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -2756,6 +2767,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", @@ -3434,6 +3449,7 @@ "award_date_lte", "award_type", "awarding_agency", + "commercial_item_acquisition_procedures", "cursor", "expiring_gte", "expiring_lte", @@ -4936,6 +4952,7 @@ "award_date_lte", "award_type", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -4991,6 +5008,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", @@ -5677,6 +5698,7 @@ "award_date_gte", "award_date_lte", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -5716,6 +5738,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", @@ -8629,6 +8655,7 @@ "award_date_gte", "award_date_lte", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -8668,6 +8695,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", @@ -9220,6 +9251,7 @@ "award_date_gte", "award_date_lte", "awarding_agency", + "commercial_item_acquisition_procedures", "cursor", "expiring_gte", "expiring_lte", @@ -9260,6 +9292,7 @@ "award_date_lte", "award_type", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -9315,6 +9348,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", @@ -10001,6 +10038,7 @@ "award_date_gte", "award_date_lte", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -10040,6 +10078,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", @@ -16717,6 +16759,7 @@ "award_date_lte", "award_type", "awarding_agency", + "commercial_item_acquisition_procedures", "expiring_gte", "expiring_lte", "fiscal_year", @@ -16772,6 +16815,10 @@ "filter_class": "PerformantAgencyFilter", "type": "string" }, + "commercial_item_acquisition_procedures": { + "filter_class": "BaseSmartFilter", + "type": "string" + }, "expiring_gte": { "filter_class": "DateFilter", "lookup": "gte", diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index cadec76..f4bcdc5 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -244,6 +244,7 @@ contracts = client.list_contracts( naics_code=None, # Mapped to 'naics' API param psc_code=None, # Mapped to 'psc' API param set_aside_type=None, # Mapped to 'set_aside' API param + commercial_item_acquisition_procedures=None, # FPDS code A-E, e.g. "A|E" # Type filters fiscal_year=None, fiscal_year_gte=None, @@ -290,6 +291,7 @@ contracts = client.list_contracts( - `naics_code` - NAICS industry code (mapped to 'naics' API param) - `psc_code` - Product/Service code (mapped to 'psc' API param) - `set_aside_type` - Set-aside type (mapped to 'set_aside' API param) +- `commercial_item_acquisition_procedures` - FPDS commercial item acquisition procedures code, case-insensitive: `A` (commercial products/services), `B` (products or services pursuant to FAR 12.102(f)), `C` (services pursuant to FAR 12.102(g)), `D` (commercial procedures not used) or `E` (DoD Section 803 Commercial Solutions Opening). Multi-value OR via `|`, e.g. `"A|E"`. An unknown or multi-character code matches nothing. **Type Filters:** - `fiscal_year` - Federal fiscal year (exact match) @@ -420,7 +422,7 @@ otas = client.list_otas( **Notes:** - Uses **keyset pagination** (`cursor` + `limit`) rather than page numbers. -- Filter parameters mirror those on `list_contracts`. +- Filter parameters mirror those on `list_contracts`, except `commercial_item_acquisition_procedures`, which the API does not offer on OTAs or OTIDVs. **Returns:** [PaginatedResponse](#paginatedresponse) with OTA dictionaries @@ -638,12 +640,14 @@ idvs = client.list_idvs( cursor=None, shape=ShapeConfig.IDVS_MINIMAL, awarding_agency="4700", + commercial_item_acquisition_procedures="E", # DoD Section 803 CSO ) ``` Notes: - This endpoint uses **keyset pagination** (`cursor` + `limit`) rather than page numbers. +- `commercial_item_acquisition_procedures` takes the same FPDS codes `A`–`E` as on `list_contracts()`, with `|` for OR. ### get_idv() diff --git a/tango/client.py b/tango/client.py index d205b52..9734ba1 100644 --- a/tango/client.py +++ b/tango/client.py @@ -632,6 +632,7 @@ def list_contracts( award_date_lte: str | None = None, award_type: str | None = None, awarding_agency: str | None = None, + commercial_item_acquisition_procedures: str | None = None, expiring_gte: str | None = None, expiring_lte: str | None = None, fiscal_year: int | None = None, @@ -674,6 +675,7 @@ def list_contracts( award_date_lte: Award date <= (YYYY-MM-DD) award_type: Award type code awarding_agency: Awarding agency code (e.g., "4700" for GSA) + commercial_item_acquisition_procedures: FPDS commercial item acquisition procedures code, case-insensitive: "A" (commercial products/services), "B" (products or services pursuant to FAR 12.102(f)), "C" (services pursuant to FAR 12.102(g)), "D" (commercial procedures not used) or "E" (DoD Section 803 Commercial Solutions Opening). Pipe-separate codes for OR, e.g. "A|E"; an unknown code matches nothing. expiring_gte: Expiring on or after date expiring_lte: Expiring on or before date fiscal_year: Fiscal year (exact match) @@ -751,6 +753,7 @@ def list_contracts( ("award_date_lte", award_date_lte), ("award_type", award_type), ("awarding_agency", awarding_agency), + ("commercial_item_acquisition_procedures", commercial_item_acquisition_procedures), ("expiring_gte", expiring_gte), ("expiring_lte", expiring_lte), ("fiscal_year", fiscal_year), @@ -915,6 +918,7 @@ def list_idvs( award_date_gte: str | None = None, award_date_lte: str | None = None, awarding_agency: str | None = None, + commercial_item_acquisition_procedures: str | None = None, expiring_gte: str | None = None, expiring_lte: str | None = None, fiscal_year: int | None = None, @@ -941,6 +945,8 @@ def list_idvs( This mirrors `/api/idvs/` and supports the same filter parameters as the API, plus shaping via `shape`. + + `commercial_item_acquisition_procedures` takes an FPDS commercial item acquisition procedures code `A`–`E` (case-insensitive; `E` is DoD Section 803 Commercial Solutions Opening), pipe-separated for OR, e.g. `"A|E"`; an unknown code matches nothing. """ params: dict[str, Any] = {"limit": min(limit, 100)} if cursor: @@ -962,6 +968,7 @@ def list_idvs( ("award_date_gte", award_date_gte), ("award_date_lte", award_date_lte), ("awarding_agency", awarding_agency), + ("commercial_item_acquisition_procedures", commercial_item_acquisition_procedures), ("expiring_gte", expiring_gte), ("expiring_lte", expiring_lte), ("fiscal_year", fiscal_year), diff --git a/tests/test_client.py b/tests/test_client.py index 607b3a2..beb1856 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2247,6 +2247,33 @@ def test_list_entities_omits_unset_socioeconomic(self, mock_request): assert "socioeconomic" not in mock_request.call_args[1]["params"] +class TestCommercialItemAcquisitionProceduresFilter: + @pytest.mark.parametrize("method", ["list_contracts", "list_idvs"]) + @patch("tango.client.httpx.Client.request") + def test_forwards_pipe_separated_codes_verbatim(self, mock_request, method): + _stub_empty_page(mock_request) + getattr(TangoClient(api_key="test-key"), method)( + commercial_item_acquisition_procedures="a|E" + ) + assert ( + mock_request.call_args[1]["params"]["commercial_item_acquisition_procedures"] == "a|E" + ) + + @pytest.mark.parametrize("method", ["list_contracts", "list_idvs"]) + @patch("tango.client.httpx.Client.request") + def test_omits_unset_filter(self, mock_request, method): + _stub_empty_page(mock_request) + getattr(TangoClient(api_key="test-key"), method)() + assert "commercial_item_acquisition_procedures" not in mock_request.call_args[1]["params"] + + @pytest.mark.parametrize("method", ["list_otas", "list_otidvs"]) + def test_not_accepted_on_other_transaction_lists(self, method): + with pytest.raises(TypeError, match="commercial_item_acquisition_procedures"): + getattr(TangoClient(api_key="test-key"), method)( + commercial_item_acquisition_procedures="E" + ) + + class TestAgencyFilterDiagnostics: """`meta` from the API's agency-filter diagnostics.