Skip to content

Commit 229f49c

Browse files
fix(shapes): correct Subaward field list to match server
The previous SUBAWARD_SCHEMA declared `id` and `amount` (rejected by the server with `unknown_field`) and was missing every real field on the resource. Replace it with a schema derived from `awards.serializers.subawards.SubawardSerializer` plus the runtime `available_fields` payload: `key` / `award_key` / `piid` / `usaspending_permalink`, the denormalized `prime_awardee_*` / `recipient_*` lookup columns, and expandable objects for `awarding_office`, `funding_office`, `prime_recipient`, `subaward_recipient`, `place_of_performance`, `subaward_details`, `fsrs_details`, and `highly_compensated_officers`. New nested schemas `SubawardDetails`, `FsrsDetails`, `SubawardPlaceOfPerformance`, and `HighlyCompensatedOfficer` back the expansions. Also update the `Subaward` dataclass in `tango/models.py` to match. Conformance and unit tests pass.
1 parent 7b233c5 commit 229f49c

3 files changed

Lines changed: 161 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4747
- `tango webhooks endpoints create` CLI now accepts and requires `--name` (passed through to `create_webhook_endpoint(name=...)`). Previously the option was absent, meaning the CLI could never set a custom endpoint name and every call would 400 server-side (the server enforces `unique(user, name)`).
4848
- `WebhookAlert.query_type` and `WebhookAlert.filters` tightened from `Optional` to non-optional (`str` and `dict[str, Any]` respectively). Legacy nullable rows were purged by the tango#2275 migration; the server model and serializer guarantee non-null values for all current data. `WebhookAlert.status` narrowed from `str` to `Literal["active", "paused"]` — the server serializer produces exactly those two values.
4949
- **Shape validator agrees with server on `naics(...)` / `psc(...)` expansions.** The client-side `ShapeParser.validate()` previously rejected the canonical `shape=naics(code,description)` form (which the server has always accepted) and also rejected the alias `shape=naics_code(code,description)`. The parser now mirrors the server's `_EXPAND_ALIASES` (introduced in Tango PR makegov/tango#2259) and rewrites `naics_code(...)` / `psc_code(...)` to their canonical `naics(...)` / `psc(...)` form at parse time. Bare scalar leaves (`shape=naics_code` / `shape=psc_code`) are left untouched and still return the raw column value, matching the server. Schemas for `Contract`, `Forecast`, `Opportunity`, `Notice`, and `Vehicle` gained explicit `naics` / `psc` expand entries backed by the existing `CodeDescription` nested model. Fixes makegov/tango#2266.
50+
- **`Subaward` schema matches the server's `SubawardSerializer`.** The previous `SUBAWARD_SCHEMA` declared two fields the server has never exposed (`id`, `amount`) and was missing every real field on the resource — including `piid`, `key`, `awarding_office` / `funding_office` / `place_of_performance` / `subaward_details` / `fsrs_details` / `highly_compensated_officers` / `usaspending_permalink`, and the denormalized `prime_awardee_*` / `recipient_*` lookup columns. Shape strings that referenced any real field (e.g. `shape="piid"`) would fail client-side validation with `unknown_field`, and conversely the SDK happily passed `shape="id"` / `shape="amount"` through to the server, where they were rejected. `SUBAWARD_SCHEMA` is now derived directly from `awards.serializers.subawards.SubawardSerializer` and the resource's runtime `available_fields`. The `Subaward` dataclass in `tango/models.py` was updated to match. New nested schemas `SubawardDetails`, `FsrsDetails`, `SubawardPlaceOfPerformance`, and `HighlyCompensatedOfficer` are registered so the corresponding shape expansions validate end-to-end.
5051

5152
## [0.6.0] - 2026-05-07
5253

tango/models.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -345,13 +345,40 @@ class OTIDV:
345345

346346
@dataclass
347347
class Subaward:
348-
"""Schema definition for Subaward (not used for instances)"""
348+
"""Schema definition for Subaward (not used for instances)
349+
350+
Mirrors the server's ``SubawardSerializer``. Fields are:
351+
352+
- ``key`` / ``award_key`` / ``piid`` — identifiers (the prime award PIID is
353+
denormalized onto the subaward row).
354+
- ``prime_awardee_*`` and ``recipient_*`` — denormalized lookup fields the
355+
API exposes alongside ``prime_recipient`` / ``subaward_recipient``
356+
expansions for filter parity.
357+
- ``usaspending_permalink`` — direct USAspending URL for the subaward.
358+
359+
Expandable objects (request via ``shape="..."``):
360+
``awarding_office``, ``funding_office`` — AwardOffice payload
361+
``prime_recipient``, ``subaward_recipient`` — RecipientProfile
362+
``place_of_performance`` — city/state/zip/country_code
363+
``subaward_details`` — action_date/amount/fiscal_year/number/type/description
364+
``fsrs_details`` — FSRS submission provenance
365+
``highly_compensated_officers`` — list of {name, amount}
366+
"""
349367

350-
id: str | None = None
368+
key: str | None = None
351369
award_key: str | None = None
352-
prime_uei: str | None = None
353-
sub_uei: str | None = None
354-
amount: Decimal | None = None
370+
piid: str | None = None
371+
usaspending_permalink: str | None = None
372+
prime_awardee_name: str | None = None
373+
prime_awardee_uei: str | None = None
374+
recipient_business_types: list[str] | None = None
375+
recipient_dba_name: str | None = None
376+
recipient_duns: str | None = None
377+
recipient_name: str | None = None
378+
recipient_parent_duns: str | None = None
379+
recipient_parent_name: str | None = None
380+
recipient_parent_uei: str | None = None
381+
recipient_uei: str | None = None
355382

356383

357384
@dataclass

tango/shapes/explicit_schemas.py

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,17 +1250,139 @@
12501250
}
12511251

12521252
# Subaward (prime/sub awards)
1253-
SUBAWARD_SCHEMA: dict[str, FieldSchema] = {
1253+
#
1254+
# Mirrors awards.serializers.subawards.SubawardSerializer on the server. The
1255+
# top-level field list is the canonical `Meta.fields` plus the denormalized
1256+
# lookup fields the API exposes for filter parity (prime_awardee_*,
1257+
# recipient_*, usaspending_permalink). Expandable objects are modeled with
1258+
# `nested_model` so callers can request, e.g. `awarding_office(office_code)`.
1259+
1260+
# `subaward_details` payload (action_date, amount, fiscal_year, ...).
1261+
SUBAWARD_DETAILS_SCHEMA: dict[str, FieldSchema] = {
1262+
"action_date": FieldSchema(name="action_date", type=date, is_optional=True, is_list=False),
1263+
"amount": FieldSchema(name="amount", type=Decimal, is_optional=True, is_list=False),
1264+
"description": FieldSchema(name="description", type=str, is_optional=True, is_list=False),
1265+
"fiscal_year": FieldSchema(name="fiscal_year", type=int, is_optional=True, is_list=False),
1266+
"number": FieldSchema(name="number", type=str, is_optional=True, is_list=False),
1267+
"type": FieldSchema(name="type", type=str, is_optional=True, is_list=False),
1268+
}
1269+
1270+
# `fsrs_details` payload — provenance for the underlying FSRS submission.
1271+
FSRS_DETAILS_SCHEMA: dict[str, FieldSchema] = {
12541272
"id": FieldSchema(name="id", type=str, is_optional=True, is_list=False),
1255-
"award_key": FieldSchema(name="award_key", type=str, is_optional=True, is_list=False),
1273+
"last_modified_date": FieldSchema(
1274+
name="last_modified_date", type=date, is_optional=True, is_list=False
1275+
),
1276+
"month": FieldSchema(name="month", type=int, is_optional=True, is_list=False),
1277+
"year": FieldSchema(name="year", type=int, is_optional=True, is_list=False),
1278+
}
1279+
1280+
# Subaward-specific place_of_performance — flat 4-key payload (city/state/zip/
1281+
# country_code), distinct from the richer PLACE_OF_PERFORMANCE_SCHEMA used by
1282+
# contracts/IDVs/vehicles.
1283+
SUBAWARD_PLACE_OF_PERFORMANCE_SCHEMA: dict[str, FieldSchema] = {
1284+
"city": FieldSchema(name="city", type=str, is_optional=True, is_list=False),
1285+
"country_code": FieldSchema(name="country_code", type=str, is_optional=True, is_list=False),
1286+
"state": FieldSchema(name="state", type=str, is_optional=True, is_list=False),
1287+
"zip": FieldSchema(name="zip", type=str, is_optional=True, is_list=False),
1288+
}
1289+
1290+
# `highly_compensated_officers` element shape (list-of-dict expansion).
1291+
HIGHLY_COMPENSATED_OFFICER_SCHEMA: dict[str, FieldSchema] = {
12561292
"amount": FieldSchema(name="amount", type=Decimal, is_optional=True, is_list=False),
1293+
"name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
1294+
}
1295+
1296+
SUBAWARD_SCHEMA: dict[str, FieldSchema] = {
1297+
# Core identifiers
1298+
"key": FieldSchema(name="key", type=str, is_optional=True, is_list=False),
1299+
"award_key": FieldSchema(name="award_key", type=str, is_optional=True, is_list=False),
1300+
"piid": FieldSchema(name="piid", type=str, is_optional=True, is_list=False),
1301+
"usaspending_permalink": FieldSchema(
1302+
name="usaspending_permalink", type=str, is_optional=True, is_list=False
1303+
),
1304+
# Denormalized prime-awardee lookup fields (mirrored from prime_awardee_uei)
1305+
"prime_awardee_name": FieldSchema(
1306+
name="prime_awardee_name", type=str, is_optional=True, is_list=False
1307+
),
1308+
"prime_awardee_uei": FieldSchema(
1309+
name="prime_awardee_uei", type=str, is_optional=True, is_list=False
1310+
),
1311+
# Denormalized subaward-recipient lookup fields (mirrored from recipient_uei)
1312+
"recipient_business_types": FieldSchema(
1313+
name="recipient_business_types", type=str, is_optional=True, is_list=True
1314+
),
1315+
"recipient_dba_name": FieldSchema(
1316+
name="recipient_dba_name", type=str, is_optional=True, is_list=False
1317+
),
1318+
"recipient_duns": FieldSchema(
1319+
name="recipient_duns", type=str, is_optional=True, is_list=False
1320+
),
1321+
"recipient_name": FieldSchema(
1322+
name="recipient_name", type=str, is_optional=True, is_list=False
1323+
),
1324+
"recipient_parent_duns": FieldSchema(
1325+
name="recipient_parent_duns", type=str, is_optional=True, is_list=False
1326+
),
1327+
"recipient_parent_name": FieldSchema(
1328+
name="recipient_parent_name", type=str, is_optional=True, is_list=False
1329+
),
1330+
"recipient_parent_uei": FieldSchema(
1331+
name="recipient_parent_uei", type=str, is_optional=True, is_list=False
1332+
),
1333+
"recipient_uei": FieldSchema(
1334+
name="recipient_uei", type=str, is_optional=True, is_list=False
1335+
),
1336+
# Expandable nested objects
1337+
"awarding_office": FieldSchema(
1338+
name="awarding_office",
1339+
type=dict,
1340+
is_optional=True,
1341+
is_list=False,
1342+
nested_model="AwardOffice",
1343+
),
1344+
"funding_office": FieldSchema(
1345+
name="funding_office",
1346+
type=dict,
1347+
is_optional=True,
1348+
is_list=False,
1349+
nested_model="AwardOffice",
1350+
),
1351+
"fsrs_details": FieldSchema(
1352+
name="fsrs_details",
1353+
type=dict,
1354+
is_optional=True,
1355+
is_list=False,
1356+
nested_model="FsrsDetails",
1357+
),
1358+
"highly_compensated_officers": FieldSchema(
1359+
name="highly_compensated_officers",
1360+
type=list,
1361+
is_optional=True,
1362+
is_list=True,
1363+
nested_model="HighlyCompensatedOfficer",
1364+
),
1365+
"place_of_performance": FieldSchema(
1366+
name="place_of_performance",
1367+
type=dict,
1368+
is_optional=True,
1369+
is_list=False,
1370+
nested_model="SubawardPlaceOfPerformance",
1371+
),
12571372
"prime_recipient": FieldSchema(
12581373
name="prime_recipient",
12591374
type=dict,
12601375
is_optional=True,
12611376
is_list=False,
12621377
nested_model="RecipientProfile",
12631378
),
1379+
"subaward_details": FieldSchema(
1380+
name="subaward_details",
1381+
type=dict,
1382+
is_optional=True,
1383+
is_list=False,
1384+
nested_model="SubawardDetails",
1385+
),
12641386
"subaward_recipient": FieldSchema(
12651387
name="subaward_recipient",
12661388
type=dict,
@@ -1403,6 +1525,10 @@
14031525
"OTA": OTA_SCHEMA,
14041526
"OTIDV": OTIDV_SCHEMA,
14051527
"Subaward": SUBAWARD_SCHEMA,
1528+
"SubawardDetails": SUBAWARD_DETAILS_SCHEMA,
1529+
"FsrsDetails": FSRS_DETAILS_SCHEMA,
1530+
"SubawardPlaceOfPerformance": SUBAWARD_PLACE_OF_PERFORMANCE_SCHEMA,
1531+
"HighlyCompensatedOfficer": HIGHLY_COMPENSATED_OFFICER_SCHEMA,
14061532
# GSA eLibrary
14071533
"GsaElibraryContract": GSA_ELIBRARY_CONTRACT_SCHEMA,
14081534
"GsaElibraryIdvRef": GSA_ELIBRARY_IDV_REF_SCHEMA,

0 commit comments

Comments
 (0)