Skip to content

Commit 5e23b8b

Browse files
committed
feat(contract-appeals): add the contract appeals resource
`list_contract_appeals` and `get_contract_appeal` cover `/api/contract_appeals/`, the Contract Disputes Act decisions of the civilian and defense boards of contract appeals, with all ten filters as named parameters and a 21-field `ContractAppeal` model whose fields are optional so a withheld `decision_text` stays absent rather than null. The vendored contract moves to 4.26.0 so the conformance gate can see the new method, which also picks up two SLED fields the SDK had been rejecting client-side. The shape schema is hand-curated because the overlay generator guesses four of these fields wrong, and the generated overlay wins at lookup time.
1 parent 7054e84 commit 5e23b8b

12 files changed

Lines changed: 716 additions & 2 deletions

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- **Contract Disputes Act appeals** (Tango API 4.26.0). Two methods over the new `/api/contract_appeals/` resource: `list_contract_appeals()` and `get_contract_appeal()`, plus a `ContractAppeal` schema and the `CONTRACT_APPEALS_MINIMAL` / `CONTRACT_APPEALS_COMPREHENSIVE` defaults. These are decisions of the Civilian Board of Contract Appeals (CBCA) and the Armed Services Board of Contract Appeals (ASBCA) — appeals of a contracting officer's final decision under an existing contract, which is a different dispute from a bid protest and shares no identifiers with `list_protests()`. All ten of the API's filters are explicit named parameters.
12+
13+
Three behaviors are documented on the methods and the model, because each is otherwise a support question. **`decision_text` needs an Enterprise plan, and below that tier the key is absent rather than null** — read it with `.get()`. **Searching that text is free at every plan**: `search=` matches inside the decision body and returns no fragment of it, so buying the body does not change what you can find. And **`docket` accepts a docket as it is cited** — a leading `CBCA` / `ASBCA` and `No.` / `Nos.` are ignored, while commas stay part of a docket (`7092-C(6682, 6765, 6767)`) rather than separating two.
14+
15+
Neither default shape names `decision_text`: the body runs to roughly 100K characters, so putting it in a default would make every fetch ask for something most callers cannot read. `ordering="rank"` requires a non-empty `search`, matching the API.
16+
- **`alerts.contract_appeal.match` is an alertable event type.** `create_webhook_alert(query_type="contract_appeal", ...)` works. That method's `query_type` docstring now names the full current set and points at `list_webhook_event_types()` as the authority, since a list written into a docstring can only go stale.
17+
18+
### Changed
19+
- Re-vendored `contracts/filter_shape_contract.json` (Tango API 4.26.0, schema_version 2, 49 resources) and regenerated `tango/shapes/generated_overlay.py` from it. Besides contract appeals this picks up two SLED fields the older contract could not see — `delisted_at` on a solicitation and `meta(jurisdiction_declared)` — which the API returns and the SDK schema was rejecting client-side. Both conformance gates report zero errors and zero new shape-coverage drift, and no baseline entries were added.
20+
- The `ContractAppeal` shape schema is hand-curated in `tango/shapes/explicit_schemas.py` rather than left to the generated overlay. The generator falls back to name heuristics for a resource the live-API type probe has not sampled yet, and those heuristics get four of this resource's fields wrong: `docket_numbers` is a list, `listed` and `decision_date_repaired` are booleans, and `listing_year` is an integer.
21+
22+
### Documentation
23+
- New **Contract Appeals** section in `docs/API_REFERENCE.md`, leading with what separates an appeal from a protest, plus the two new `ShapeConfig` constants in the shapes table.
24+
- Added `alerts.contract_appeal.match` to the event-type listing in `docs/WEBHOOKS.md`.
25+
1026
## [1.6.0] - 2026-09-10
1127

1228
### Added

‎contracts/filter_shape_contract.json‎

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"meta": {
3-
"api_version": "4.25.1",
3+
"api_version": "4.26.0",
44
"description": "Canonical API filter/shape contract. Downstream consumers (SDK, MCP) should validate their conformance against this manifest.",
55
"generated_from": "scripts/filter_shape_conformance.py",
66
"schema_version": 2
@@ -2471,6 +2471,156 @@
24712471
"show_shapes"
24722472
]
24732473
},
2474+
"contract_appeals": {
2475+
"basename": "contractappeal",
2476+
"docs_file": null,
2477+
"docs_params": [],
2478+
"prefix": "contract_appeals",
2479+
"resource_key": "contract_appeals",
2480+
"runtime": {
2481+
"filter_params": [
2482+
"appellant",
2483+
"board",
2484+
"decision_date_after",
2485+
"decision_date_before",
2486+
"decision_type",
2487+
"docket",
2488+
"document_id",
2489+
"judge",
2490+
"listed",
2491+
"search"
2492+
],
2493+
"filter_params_detail": {
2494+
"appellant": {
2495+
"filter_class": "BaseSmartFilter",
2496+
"type": "string"
2497+
},
2498+
"board": {
2499+
"filter_class": "BaseSmartFilter",
2500+
"type": "string"
2501+
},
2502+
"decision_date_after": {
2503+
"filter_class": "DateFilter",
2504+
"lookup": "gte",
2505+
"type": "date"
2506+
},
2507+
"decision_date_before": {
2508+
"filter_class": "DateFilter",
2509+
"lookup": "lte",
2510+
"type": "date"
2511+
},
2512+
"decision_type": {
2513+
"filter_class": "BaseSmartFilter",
2514+
"type": "string"
2515+
},
2516+
"docket": {
2517+
"filter_class": "DocketFilter",
2518+
"type": "string"
2519+
},
2520+
"document_id": {
2521+
"filter_class": "CharFilter",
2522+
"type": "string"
2523+
},
2524+
"judge": {
2525+
"filter_class": "BaseSmartFilter",
2526+
"type": "string"
2527+
},
2528+
"listed": {
2529+
"filter_class": "BooleanFilter",
2530+
"type": "boolean"
2531+
},
2532+
"search": {
2533+
"filter_class": "ContractAppealSearchFilter",
2534+
"type": "string"
2535+
}
2536+
},
2537+
"ordering_aliases": [],
2538+
"ordering_fields": [
2539+
"appellant",
2540+
"decision_date",
2541+
"first_listed_at",
2542+
"rank"
2543+
],
2544+
"pagination": {
2545+
"class": "StandardResultsSetPagination",
2546+
"max_page_size": 100
2547+
},
2548+
"shape": {
2549+
"expands": {},
2550+
"fields": [
2551+
"appellant",
2552+
"board",
2553+
"decision_date",
2554+
"decision_date_raw",
2555+
"decision_date_repaired",
2556+
"decision_text",
2557+
"decision_type",
2558+
"decision_type_raw",
2559+
"docket_numbers",
2560+
"docket_raw",
2561+
"docket_source",
2562+
"document_id",
2563+
"first_listed_at",
2564+
"judge",
2565+
"listed",
2566+
"listing_url",
2567+
"listing_year",
2568+
"text_char_count",
2569+
"text_status",
2570+
"url",
2571+
"uuid"
2572+
]
2573+
},
2574+
"shape_flat_paths": [
2575+
"appellant",
2576+
"board",
2577+
"decision_date",
2578+
"decision_date_raw",
2579+
"decision_date_repaired",
2580+
"decision_text",
2581+
"decision_type",
2582+
"decision_type_raw",
2583+
"docket_numbers",
2584+
"docket_raw",
2585+
"docket_source",
2586+
"document_id",
2587+
"first_listed_at",
2588+
"judge",
2589+
"listed",
2590+
"listing_url",
2591+
"listing_year",
2592+
"text_char_count",
2593+
"text_status",
2594+
"url",
2595+
"uuid"
2596+
],
2597+
"shape_gated_by_level": {
2598+
"business": [
2599+
"decision_text"
2600+
],
2601+
"free": [
2602+
"decision_text"
2603+
],
2604+
"pro": [
2605+
"decision_text"
2606+
]
2607+
},
2608+
"shape_supported": true,
2609+
"shape_tier_required": "enterprise_a",
2610+
"viewset": "appeals.views.ContractAppealViewSet"
2611+
},
2612+
"swagger_has_key": true,
2613+
"swagger_params": [
2614+
"flat",
2615+
"flat_lists",
2616+
"joiner",
2617+
"limit",
2618+
"ordering",
2619+
"page",
2620+
"shape",
2621+
"show_shapes"
2622+
]
2623+
},
24742624
"contracts": {
24752625
"basename": "contract",
24762626
"docs_file": null,
@@ -13885,6 +14035,7 @@
1388514035
"expands": {},
1388614036
"fields": [
1388714037
"attachment_count",
14038+
"jurisdiction_declared",
1388814039
"last_change_source_declared",
1388914040
"last_revision_kind",
1389014041
"revision_count"
@@ -13921,6 +14072,7 @@
1392114072
"bid_opening_date",
1392214073
"bid_opening_raw",
1392314074
"category_codes",
14075+
"delisted_at",
1392414076
"description",
1392514077
"first_seen_at",
1392614078
"has_documents",
@@ -13969,6 +14121,7 @@
1396914121
"contact.email",
1397014122
"contact.name",
1397114123
"contact.phone",
14124+
"delisted_at",
1397214125
"description",
1397314126
"first_seen_at",
1397414127
"has_documents",
@@ -13977,6 +14130,7 @@
1397714130
"last_seen_at",
1397814131
"meta",
1397914132
"meta.attachment_count",
14133+
"meta.jurisdiction_declared",
1398014134
"meta.last_change_source_declared",
1398114135
"meta.last_revision_kind",
1398214136
"meta.revision_count",
@@ -16577,6 +16731,7 @@
1657716731
"assistance_listings",
1657816732
"budget/accounts",
1657916733
"business_types",
16734+
"contract_appeals",
1658016735
"contracts",
1658116736
"contracts_subawards",
1658216737
"departments",

‎docs/API_REFERENCE.md‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Complete reference for all Tango Python SDK methods and functionality.
2222
- [Grants](#grants)
2323
- [GSA eLibrary Contracts](#gsa-elibrary-contracts)
2424
- [Protests](#protests)
25+
- [Contract Appeals](#contract-appeals)
2526
- [Budget](#budget)
2627
- [Business Types](#business-types)
2728
- [NAICS](#naics)
@@ -1214,6 +1215,85 @@ protest = client.get_protest(
12141215

12151216
---
12161217

1218+
## Contract Appeals
1219+
1220+
Contract Disputes Act appeal decisions from the Civilian Board of Contract Appeals (CBCA) and the Armed Services Board of Contract Appeals (ASBCA).
1221+
1222+
**These are not bid protests.** A protest challenges an award or a solicitation before performance; an appeal here challenges a contracting officer's final decision under an existing contract — a claim, a termination, a differing-site-conditions dispute. Protests live at [Protests](#protests) and share no identifiers with this resource.
1223+
1224+
One row is one decision as the board's own listing publishes it, so several fields describe the listing rather than the dispute. `listed` goes false once the board's newest listing stops carrying the decision; the row is kept, never dropped.
1225+
1226+
### list_contract_appeals()
1227+
1228+
List appeal decisions with filtering and shaping.
1229+
1230+
```python
1231+
appeals = client.list_contract_appeals(
1232+
page=1,
1233+
limit=25,
1234+
shape=ShapeConfig.CONTRACT_APPEALS_MINIMAL,
1235+
# Filter parameters (all optional)
1236+
board=None,
1237+
docket=None,
1238+
appellant=None,
1239+
judge=None,
1240+
decision_type=None,
1241+
decision_date_after=None,
1242+
decision_date_before=None,
1243+
listed=None,
1244+
document_id=None,
1245+
search=None,
1246+
ordering=None,
1247+
)
1248+
```
1249+
1250+
**Filter Parameters:**
1251+
- `board` - `"cbca"` or `"asbca"`; OR both with `"cbca|asbca"`
1252+
- `docket` - Exact docket match, e.g. `"3288-R"`, `"59116"` or `"7092-C(6682, 6765, 6767)"`. A leading `CBCA` / `ASBCA` and `No.` / `Nos.` are ignored, so a docket can be pasted as it is cited. Commas are part of a docket, never a separator; OR several with `|`
1253+
- `appellant` - Case-insensitive substring match on the appellant's name (min 2 characters)
1254+
- `judge` - Case-insensitive exact match on the judge as the listing names them
1255+
- `decision_type` - CBCA only: `"Decision"`, `"Dismissal"`, `"Order"`, `"Full Board Order"`, or the listing's own text. ASBCA rows carry no type
1256+
- `decision_date_after` / `decision_date_before` - Decision date range (`YYYY-MM-DD`)
1257+
- `listed` - Whether the board's newest listing still carries the decision
1258+
- `document_id` - The board's own document id
1259+
- `search` - Ranked full-text search over the appellant and the full decision text (min 2 characters); wrap in double quotes for a phrase
1260+
- `ordering` - `decision_date` (the default, as `-decision_date`), `appellant`, `first_listed_at` or `rank`. `rank` requires a non-empty `search`
1261+
1262+
**Returns:** [PaginatedResponse](#paginatedresponse) with appeal-decision dictionaries
1263+
1264+
**Example:**
1265+
```python
1266+
appeals = client.list_contract_appeals(
1267+
board="asbca",
1268+
search="differing site conditions",
1269+
decision_date_after="2025-01-01",
1270+
ordering="rank",
1271+
limit=25,
1272+
)
1273+
1274+
for appeal in appeals.results:
1275+
dockets = ", ".join(appeal["docket_numbers"])
1276+
print(f"{appeal['board'].upper()} {dockets}: {appeal['appellant']} ({appeal['decision_date']})")
1277+
```
1278+
1279+
### get_contract_appeal()
1280+
1281+
Get a single decision by `uuid`.
1282+
1283+
```python
1284+
appeal = client.get_contract_appeal(
1285+
"DECISION_UUID",
1286+
shape=ShapeConfig.CONTRACT_APPEALS_COMPREHENSIVE,
1287+
)
1288+
```
1289+
1290+
**Notes:**
1291+
- `docket_numbers` is a list — the dockets with the board prefix stripped (`3288-R`, `59116`), and a consolidated appeal carries several. `docket_raw` keeps the listing's own text, and `docket_source` says where the dockets came from.
1292+
- **Reading `decision_text` requires an Enterprise plan, and below that tier the key is absent rather than null** — read it with `.get()`, not `[...]`. Neither default shape names it: the body runs to roughly 100K characters, so asking for it on every row is rarely what you want.
1293+
- **Searching that text is free at every plan.** `search=` matches inside the decision body and returns no fragment of it, so buying the body does not change what you can find.
1294+
1295+
---
1296+
12171297
## State & Local (SLED)
12181298

12191299
State, local and education procurement — solicitations that never appear on SAM.gov because they were never federal. **Beta**: coverage is partial and grows one jurisdiction at a time.
@@ -2206,6 +2286,8 @@ entity = client.get_entity("UEI_KEY", shape=ShapeConfig.ENTITIES_COMPREHENSIVE)
22062286
| `SUBAWARDS_MINIMAL` | `list_subawards` | award_key, prime_recipient(uei,display_name), subaward_recipient(uei,display_name) |
22072287
| `GSA_ELIBRARY_CONTRACTS_MINIMAL` | `list_gsa_elibrary_contracts` | uuid, contract_number, schedule, recipient(display_name,uei), idv(key,award_date) |
22082288
| `PROTESTS_MINIMAL` | `list_protests` | case_id, case_number, title, source_system, outcome, filed_date |
2289+
| `CONTRACT_APPEALS_MINIMAL` | `list_contract_appeals` | uuid, board, docket_numbers, decision_date, appellant, judge, decision_type, url |
2290+
| `CONTRACT_APPEALS_COMPREHENSIVE` | `get_contract_appeal` | The list fields plus docket_raw, docket_source, decision_date_repaired, decision_type_raw, listing_year, first_listed_at, listed, text_status, text_char_count (omits `decision_text`, which needs an Enterprise plan) |
22092291
| `BUDGET_ACCOUNTS_MINIMAL` | `list_budget_accounts`, `get_budget_account` | id, federal_account_symbol, fiscal_year, agency_code/name, bureau_name, account_title, bea_category, on_off_budget, subfunction_code, lifecycle (requested/enacted/apportioned/obligated/outlayed/unobligated), contract & assistance rollups, key ratios, next-year growth |
22102292
| `VEHICLE_ORDERS_MINIMAL` | `list_vehicle_orders` | key, piid, award_date, recipient(display_name,uei), total_contract_value, obligated |
22112293
| `ITDASHBOARD_INVESTMENTS_MINIMAL` | `list_itdashboard_investments` | Minimal IT Dashboard investment fields |

‎docs/WEBHOOKS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ tango webhooks list-event-types
8888
# alerts.dibbs_rfq.match DLA DIBBS RFQ matched a saved alert
8989
# alerts.dibbs_rfp.match DLA DIBBS RFP matched a saved alert
9090
# alerts.sled_opportunity.match State/local/education solicitation matched a saved alert
91+
# alerts.contract_appeal.match CBCA/ASBCA appeal decision matched a saved alert
9192
```
9293

9394
This list is served by the API, so it is always current — the SDK does not hardcode

‎scripts/check_filter_shape_conformance.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"sled/opportunities": "list_sled_opportunities",
7878
"sled/forecasts": "list_sled_forecasts",
7979
"protests": "list_protests",
80+
"contract_appeals": "list_contract_appeals",
8081
"offices": "list_offices",
8182
"psc": "list_psc",
8283
"mas_sins": "list_mas_sins",

‎scripts/check_shape_coverage.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"budget/accounts": "BudgetAccount",
6969
"budget_accounts": "BudgetAccount",
7070
"protests": "Protest",
71+
"contract_appeals": "ContractAppeal",
7172
"offices": "Office",
7273
"assistance_listings": "AssistanceListing",
7374
"business_types": "BusinessType",

‎tango/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
)
1111
from .models import (
1212
BudgetAccount,
13+
ContractAppeal,
1314
DibbsAward,
1415
DibbsRfp,
1516
DibbsRfq,
@@ -65,6 +66,7 @@
6566
"ResolveCandidate",
6667
"ResolveResult",
6768
"BudgetAccount",
69+
"ContractAppeal",
6870
"DibbsAward",
6971
"DibbsRfp",
7072
"DibbsRfq",

0 commit comments

Comments
 (0)