Skip to content

Commit 828dd36

Browse files
vdavezclaude
andcommitted
v0.6.0: vehicles lakehouse-cutover surface
Bring the vehicles client up to date with the upstream tango "lakehouse cutover" (May 2026). Adds new top-level fields, a `metrics(*)` shape expansion, the `/api/vehicles/{uuid}/orders/` endpoint, an `ordering` query param, and DeprecationWarning emission for fields the API now sends a `Deprecation: true` header for. Cassettes re-recorded against current production. Defaults verified to match production's shape contract (organization is a leaf dict, /orders/ rejects `uuid`, organization payload includes `organization_id`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 35f8d77 commit 828dd36

20 files changed

Lines changed: 1083 additions & 164 deletions

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
- Vehicles: new top-level fields `program_acronym`, `idv_count`, `total_obligated`, `is_synthetic_solicitation`, `latest_award_date`, `description`, `opportunity_id`, and a live nested `organization` object (office hierarchy) — reflecting the upstream tango lakehouse cutover (May 2026).
12+
- Vehicles: new `metrics(*)` shape expansion bundling 12 lakehouse metrics: `avg_offers_received`, `award_concentration_hhi`, `order_concentration_hhi`, `competed_rate`, `using_agency_count`, `avg_order_value`, `max_order_value`, `top_recipient_share`, `recent_obligations_24mo`, `recent_orders_24mo`, `days_since_last_order`, `obligation_to_ceiling_ratio`. Backed by a new `VehicleMetrics` schema.
13+
- `list_vehicle_orders(uuid, ...)` for the new `/api/vehicles/{uuid}/orders/` endpoint, returning task orders under the vehicle's IDVs with two-phase pagination.
14+
- `ordering` parameter on `list_vehicles` (whitelist: `vehicle_obligations`, `latest_award_date`; prefix `-` for descending) and on `list_vehicle_orders` (whitelist: `award_date`, `obligated`, `total_contract_value`).
15+
- `ShapeConfig.VEHICLE_ORDERS_MINIMAL` default for the new orders endpoint.
16+
- `Vehicle` and `VehicleMetrics` are now exported from the top-level `tango` package.
17+
18+
### Changed
19+
- `ShapeConfig.VEHICLES_MINIMAL` and `VEHICLES_COMPREHENSIVE` now include the new top-level fields and the `organization` expansion. `VEHICLES_COMPREHENSIVE` defaults to `metrics(*)` and no longer pulls the deprecated `competition_details(*)` blob.
20+
21+
### Deprecated
22+
- Vehicles shape fields `agency_details`, `competition_details`, and the `opportunity` expansion. The upstream API now sends a `Deprecation: true` header for these and recomputes them at request time. Explicit use in `shape=...` emits a Python `DeprecationWarning`. Sunset timeline TBD upstream.
23+
1024
## [0.5.0] - 2026-04-08
1125

1226
### Added

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,14 @@ otidvs = client.list_otidvs(limit=25)
169169
### Vehicles
170170

171171
```python
172-
vehicles = client.list_vehicles(search="GSA schedule", shape=ShapeConfig.VEHICLES_MINIMAL)
172+
vehicles = client.list_vehicles(
173+
search="GSA schedule",
174+
ordering="-vehicle_obligations",
175+
shape=ShapeConfig.VEHICLES_MINIMAL,
176+
)
173177
vehicle = client.get_vehicle("UUID", shape=ShapeConfig.VEHICLES_COMPREHENSIVE)
174178
awardees = client.list_vehicle_awardees("UUID")
179+
orders = client.list_vehicle_orders("UUID", ordering="-obligated")
175180
```
176181

177182
### Entities (Vendors/Recipients)

docs/API_REFERENCE.md

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,13 +503,14 @@ Vehicles provide a solicitation-centric way to discover groups of related IDVs a
503503

504504
### list_vehicles()
505505

506-
List vehicles with optional vehicle-level full-text search.
506+
List vehicles with optional vehicle-level full-text search and ordering.
507507

508508
```python
509509
vehicles = client.list_vehicles(
510510
page=1,
511511
limit=25,
512512
search="GSA schedule",
513+
ordering="-vehicle_obligations",
513514
shape=ShapeConfig.VEHICLES_MINIMAL,
514515
flat=False,
515516
flat_lists=False,
@@ -520,6 +521,7 @@ vehicles = client.list_vehicles(
520521
- `page` (int): Page number (default: 1)
521522
- `limit` (int): Results per page (default: 25, max: 100)
522523
- `search` (str, optional): Vehicle-level search term
524+
- `ordering` (str, optional): Server-side sort. Allowed: `vehicle_obligations`, `latest_award_date`. Prefix with `-` for descending.
523525
- `shape` (str, optional): Shape string (defaults to `ShapeConfig.VEHICLES_MINIMAL`)
524526
- `flat` (bool): Flatten nested objects in shaped response
525527
- `flat_lists` (bool): Flatten arrays using indexed keys
@@ -552,6 +554,67 @@ awardees = client.list_vehicle_awardees(
552554
)
553555
```
554556

557+
### list_vehicle_orders()
558+
559+
List task orders under a vehicle's IDVs (`/api/vehicles/{uuid}/orders/`). Backed by a denormalized lakehouse table for fast pagination over large vehicles.
560+
561+
```python
562+
orders = client.list_vehicle_orders(
563+
uuid="00000000-0000-0000-0000-000000000001",
564+
limit=25,
565+
ordering="-obligated",
566+
shape=ShapeConfig.VEHICLE_ORDERS_MINIMAL,
567+
)
568+
```
569+
570+
**Parameters:**
571+
- `uuid` (str): Vehicle UUID
572+
- `page` (int): Page number (default: 1)
573+
- `limit` (int): Results per page (default: 25, max: 100)
574+
- `ordering` (str, optional): Server-side sort. Allowed: `award_date` (default), `obligated`, `total_contract_value`. Prefix with `-` for descending.
575+
- `shape` (str, optional): Shape string (defaults to `ShapeConfig.VEHICLE_ORDERS_MINIMAL`)
576+
- `flat`, `flat_lists`, `joiner`: as on other vehicles methods
577+
578+
**Returns:** [PaginatedResponse](#paginatedresponse) with order (Contract) dictionaries
579+
580+
### Vehicle response fields
581+
582+
The post-cutover (May 2026) vehicle response includes these top-level fields, all addressable via the `shape` parameter:
583+
584+
| Field | Type | Notes |
585+
| ----- | ---- | ----- |
586+
| `uuid` | str | Stable identifier. |
587+
| `solicitation_identifier` | str | Solicitation shared by underlying IDVs. |
588+
| `is_synthetic_solicitation` | bool | `True` for GWAC orphans recovered via `ACRO:` prefix. |
589+
| `agency_id` | str | From IDV award-key suffix. |
590+
| `program_acronym` | str \| None | New post-cutover field. |
591+
| `organization_id` | str \| None | Awarding organization. |
592+
| `organization` | dict \| None | Live awarding-org snapshot `{organization_id, office_code, office_name, agency_code, agency_name, department_code, department_name}`. Selected as a leaf field (`shape=...,organization`); not currently sub-selectable. |
593+
| `vehicle_type`, `who_can_use`, `type_of_idc`, `contract_type` | dict \| None | Returned as `{code, description}`. |
594+
| `description` | str \| None | Common text across IDV descriptions. |
595+
| `descriptions` | list[str] \| None | Distinct IDV descriptions. |
596+
| `idv_count`, `awardee_count`, `order_count` | int \| None | Denormalized rollups. |
597+
| `total_obligated`, `vehicle_obligations`, `vehicle_contracts_value` | Decimal \| None | Denormalized rollups. |
598+
| `award_date`, `latest_award_date`, `last_date_to_order` | date \| None | |
599+
| `solicitation_title`, `solicitation_description`, `solicitation_date`, `opportunity_id` | str / date / None | From SAM.gov via the linked Opportunity. |
600+
| `naics_code`, `psc_code`, `set_aside`, `fiscal_year` | int / str / None | |
601+
602+
### Vehicle shape expansions
603+
604+
- `awardees(...)` — underlying IDV awards. Supports nested `orders(...)`.
605+
- `metrics(*)` — bundled lakehouse metrics: `avg_offers_received`, `award_concentration_hhi`, `order_concentration_hhi`, `competed_rate`, `using_agency_count`, `avg_order_value`, `max_order_value`, `top_recipient_share`, `recent_obligations_24mo`, `recent_orders_24mo`, `days_since_last_order`, `obligation_to_ceiling_ratio`. Defaults included in `ShapeConfig.VEHICLES_COMPREHENSIVE`.
606+
- `organization` — live awarding-org snapshot (selected as a leaf field; not sub-selectable).
607+
608+
### Deprecated shape fields
609+
610+
The following fields and expansions are still served by the API (recomputed at request time from the underlying IDVs) but the API now returns a `Deprecation: true` response header for them. They will be removed in a future tango API release.
611+
612+
- `agency_details` (top-level field and `agency_details(*)` expansion)
613+
- `competition_details` (top-level field and `competition_details(*)` expansion)
614+
- `opportunity(*)` expansion (use the new top-level `solicitation_*` and `opportunity_id` fields instead)
615+
616+
If you pass any of these in `shape=...`, the SDK will emit a Python `DeprecationWarning`. The default shapes (`VEHICLES_MINIMAL`, `VEHICLES_COMPREHENSIVE`) no longer include them.
617+
555618
---
556619

557620
## IDVs

docs/SHAPES.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ idvs = client.list_idvs(shape=ShapeConfig.IDVS_MINIMAL)
5757
grants = client.list_grants(shape=ShapeConfig.GRANTS_MINIMAL)
5858
```
5959

60-
**Available constants:** Contracts (`CONTRACTS_MINIMAL`), Entities (`ENTITIES_MINIMAL`, `ENTITIES_COMPREHENSIVE`), Forecasts, Opportunities, Notices, Grants, IDVs, Vehicles, Organizations, OTAs, OTIDVs, Subawards. See [API Reference – ShapeConfig](API_REFERENCE.md#shapeconfig-predefined-shapes) for the full table and which method uses which constant.
60+
**Available constants:** Contracts (`CONTRACTS_MINIMAL`), Entities (`ENTITIES_MINIMAL`, `ENTITIES_COMPREHENSIVE`), Forecasts, Opportunities, Notices, Grants, IDVs, Vehicles (`VEHICLES_MINIMAL`, `VEHICLES_COMPREHENSIVE`, `VEHICLE_AWARDEES_MINIMAL`, `VEHICLE_ORDERS_MINIMAL`), Organizations, OTAs, OTIDVs, Subawards. See [API Reference – ShapeConfig](API_REFERENCE.md#shapeconfig-predefined-shapes) for the full table and which method uses which constant.
61+
62+
> **Vehicles `metrics(*)` expansion:** The post-cutover (May 2026) vehicles surface bundles 12 lakehouse metrics under a single `metrics(*)` expansion (e.g. `award_concentration_hhi`, `competed_rate`, `top_recipient_share`). It is included in `VEHICLES_COMPREHENSIVE` by default. The `agency_details`, `competition_details`, and `opportunity` shape entries are deprecated and emit `DeprecationWarning` if requested explicitly.
6163
6264
## Basic Shaping
6365

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "tango-python"
7-
version = "0.5.0"
7+
version = "0.6.0"
88
description = "Python SDK for the Tango API"
99
readme = "README.md"
1010
requires-python = ">=3.12"

tango/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
RateLimitInfo,
1616
SearchFilters,
1717
ShapeConfig,
18+
Vehicle,
19+
VehicleMetrics,
1820
WebhookEndpoint,
1921
WebhookEventType,
2022
WebhookEventTypesResponse,
@@ -29,7 +31,7 @@
2931
TypeGenerator,
3032
)
3133

32-
__version__ = "0.5.0"
34+
__version__ = "0.6.0"
3335
__all__ = [
3436
"TangoClient",
3537
"TangoAPIError",
@@ -43,6 +45,8 @@
4345
"PaginatedResponse",
4446
"SearchFilters",
4547
"ShapeConfig",
48+
"Vehicle",
49+
"VehicleMetrics",
4650
"WebhookEndpoint",
4751
"WebhookEventType",
4852
"WebhookEventTypesResponse",

tango/client.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tango API Client"""
22

33
import os
4+
import warnings
45
from datetime import date, datetime
56
from decimal import Decimal
67
from typing import Any
@@ -123,6 +124,7 @@ def last_response_headers(self) -> httpx.Headers | None:
123124
@staticmethod
124125
def _parse_rate_limit_headers(headers: httpx.Headers) -> RateLimitInfo:
125126
"""Extract rate limit info from response headers."""
127+
126128
def _int_or_none(val: str | None) -> int | None:
127129
if val is None:
128130
return None
@@ -1447,6 +1449,40 @@ def get_itdashboard_investment(
14471449
# Vehicles (Awards)
14481450
# ============================================================================
14491451

1452+
@staticmethod
1453+
def _warn_deprecated_vehicle_shape(shape: str | None) -> None:
1454+
# Upstream sends `Deprecation: true` for these fields/expansions; warn
1455+
# callers who request them explicitly so they have time to migrate
1456+
# before tango publishes a Sunset timeline.
1457+
from tango.shapes.explicit_schemas import DEPRECATED_VEHICLE_SHAPE_FIELDS
1458+
1459+
if not shape:
1460+
return
1461+
# Match top-level field tokens, ignoring nesting inside parentheses.
1462+
depth = 0
1463+
token = ""
1464+
tokens: list[str] = []
1465+
for ch in shape:
1466+
if ch == "(":
1467+
depth += 1
1468+
elif ch == ")":
1469+
depth = max(0, depth - 1)
1470+
elif ch == "," and depth == 0:
1471+
tokens.append(token.strip())
1472+
token = ""
1473+
continue
1474+
token += ch
1475+
tokens.append(token.strip())
1476+
used = {t.split("(", 1)[0] for t in tokens} & DEPRECATED_VEHICLE_SHAPE_FIELDS
1477+
if used:
1478+
warnings.warn(
1479+
f"Vehicle shape field(s) {sorted(used)!r} are deprecated upstream "
1480+
"and may be removed in a future tango API version. The API currently "
1481+
"returns a `Deprecation: true` header for these.",
1482+
DeprecationWarning,
1483+
stacklevel=3,
1484+
)
1485+
14501486
def list_vehicles(
14511487
self,
14521488
page: int = 1,
@@ -1456,12 +1492,20 @@ def list_vehicles(
14561492
flat_lists: bool = False,
14571493
joiner: str = ".",
14581494
search: str | None = None,
1495+
ordering: str | None = None,
14591496
) -> PaginatedResponse:
1460-
"""List Vehicles (solicitation-centric groupings of IDVs)."""
1497+
"""List Vehicles (solicitation-centric groupings of IDVs).
1498+
1499+
Args:
1500+
ordering: Server-side sort. Allowed: ``vehicle_obligations``,
1501+
``latest_award_date``. Prefix with ``-`` for descending.
1502+
"""
14611503
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
14621504

14631505
if shape is None:
14641506
shape = ShapeConfig.VEHICLES_MINIMAL
1507+
else:
1508+
self._warn_deprecated_vehicle_shape(shape)
14651509
if shape:
14661510
params["shape"] = shape
14671511
if flat:
@@ -1473,6 +1517,8 @@ def list_vehicles(
14731517

14741518
if search:
14751519
params["search"] = search
1520+
if ordering:
1521+
params["ordering"] = ordering
14761522

14771523
data = self._get("/api/vehicles/", params)
14781524

@@ -1504,6 +1550,8 @@ def get_vehicle(
15041550

15051551
if shape is None:
15061552
shape = ShapeConfig.VEHICLES_COMPREHENSIVE
1553+
else:
1554+
self._warn_deprecated_vehicle_shape(shape)
15071555
if shape:
15081556
params["shape"] = shape
15091557
if flat:
@@ -1560,6 +1608,54 @@ def list_vehicle_awardees(
15601608
results=results,
15611609
)
15621610

1611+
def list_vehicle_orders(
1612+
self,
1613+
uuid: str,
1614+
page: int = 1,
1615+
limit: int = 25,
1616+
shape: str | None = None,
1617+
flat: bool = False,
1618+
flat_lists: bool = False,
1619+
joiner: str = ".",
1620+
ordering: str | None = None,
1621+
) -> PaginatedResponse:
1622+
"""List task orders under a Vehicle's IDVs (``/api/vehicles/{uuid}/orders/``).
1623+
1624+
Args:
1625+
ordering: Server-side sort. Allowed: ``award_date`` (default),
1626+
``obligated``, ``total_contract_value``. Prefix with ``-`` for
1627+
descending.
1628+
"""
1629+
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1630+
1631+
if shape is None:
1632+
shape = ShapeConfig.VEHICLE_ORDERS_MINIMAL
1633+
if shape:
1634+
params["shape"] = shape
1635+
if flat:
1636+
params["flat"] = "true"
1637+
if joiner:
1638+
params["joiner"] = joiner
1639+
if flat_lists:
1640+
params["flat_lists"] = "true"
1641+
1642+
if ordering:
1643+
params["ordering"] = ordering
1644+
1645+
data = self._get(f"/api/vehicles/{uuid}/orders/", params)
1646+
1647+
results = [
1648+
self._parse_response_with_shape(order, shape, Contract, flat, flat_lists, joiner=joiner)
1649+
for order in data["results"]
1650+
]
1651+
1652+
return PaginatedResponse(
1653+
count=data["count"],
1654+
next=data.get("next"),
1655+
previous=data.get("previous"),
1656+
results=results,
1657+
)
1658+
15631659
# Business Types endpoints
15641660
def list_business_types(self, page: int = 1, limit: int = 25) -> PaginatedResponse:
15651661
"""List business types"""

0 commit comments

Comments
 (0)