Skip to content

Commit 50c105b

Browse files
vdavezclaude
andcommitted
Add IT Dashboard investments endpoints
Adds list_itdashboard_investments and get_itdashboard_investment for the /api/itdashboard/ resource, including the ITDashboardInvestment model, ShapeConfig defaults, schema registration, and conformance entries. Filter coverage mirrors the API's tier-gated filterset (search; agency_code/type_of_investment/updated_time range; agency_name/cio_rating/ cio_rating_max/performance_risk) with explicit named params and proper bool/date serialization. Tests: - 4 unit tests covering default shape, filter forwarding, get-by-uii, and funding(*) / cio_evaluation(*) expansions - 14 integration tests with recorded cassettes covering shape variants, pagination, every filter, and dict + list-of-dict expansions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 19c8a07 commit 50c105b

22 files changed

Lines changed: 2822 additions & 0 deletions

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
- IT Dashboard investments: `list_itdashboard_investments`, `get_itdashboard_investment` (`/api/itdashboard/`) with shaping and filter params (`search`, `agency_code`, `agency_name`, `type_of_investment`, `updated_time_after`, `updated_time_before`, `cio_rating`, `cio_rating_max`, `performance_risk`). Tier-gated by the API: free tier gets `search`, pro adds structured filters, business+ adds CIO/performance analytics. New `ITDashboardInvestment` model and `ShapeConfig.ITDASHBOARD_INVESTMENTS_MINIMAL` / `ITDASHBOARD_INVESTMENTS_COMPREHENSIVE` defaults.
12+
1013
## [0.4.4] - 2026-03-25
1114

1215
### Added

scripts/check_filter_shape_conformance.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"agencies": "list_agencies",
5151
"naics": "list_naics",
5252
"gsa_elibrary_contracts": "list_gsa_elibrary_contracts",
53+
"itdashboard": "list_itdashboard_investments",
5354
# Resources not yet implemented in SDK
5455
"offices": None,
5556
}
@@ -66,6 +67,7 @@ def get_shape_config_entries() -> list[tuple[str, str, type[Any]]]:
6667
Forecast,
6768
Grant,
6869
GsaElibraryContract,
70+
ITDashboardInvestment,
6971
Notice,
7072
Opportunity,
7173
Organization,
@@ -98,6 +100,16 @@ def get_shape_config_entries() -> list[tuple[str, str, type[Any]]]:
98100
ShapeConfig.GSA_ELIBRARY_CONTRACTS_MINIMAL,
99101
GsaElibraryContract,
100102
),
103+
(
104+
"ITDASHBOARD_INVESTMENTS_MINIMAL",
105+
ShapeConfig.ITDASHBOARD_INVESTMENTS_MINIMAL,
106+
ITDashboardInvestment,
107+
),
108+
(
109+
"ITDASHBOARD_INVESTMENTS_COMPREHENSIVE",
110+
ShapeConfig.ITDASHBOARD_INVESTMENTS_COMPREHENSIVE,
111+
ITDashboardInvestment,
112+
),
101113
]
102114
for name, shape_str, model_cls in configs:
103115
entries.append((name, shape_str, model_cls))

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
GsaElibraryContract,
13+
ITDashboardInvestment,
1314
PaginatedResponse,
1415
RateLimitInfo,
1516
SearchFilters,
@@ -38,6 +39,7 @@
3839
"TangoRateLimitError",
3940
"RateLimitInfo",
4041
"GsaElibraryContract",
42+
"ITDashboardInvestment",
4143
"PaginatedResponse",
4244
"SearchFilters",
4345
"ShapeConfig",

tango/client.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Forecast,
2727
Grant,
2828
GsaElibraryContract,
29+
ITDashboardInvestment,
2930
Location,
3031
Notice,
3132
Opportunity,
@@ -1336,6 +1337,112 @@ def get_gsa_elibrary_contract(
13361337
data, shape, GsaElibraryContract, flat, flat_lists, joiner=joiner
13371338
)
13381339

1340+
# ============================================================================
1341+
# IT Dashboard Investments
1342+
# ============================================================================
1343+
1344+
def list_itdashboard_investments(
1345+
self,
1346+
page: int = 1,
1347+
limit: int = 25,
1348+
shape: str | None = None,
1349+
flat: bool = False,
1350+
flat_lists: bool = False,
1351+
joiner: str = ".",
1352+
search: str | None = None,
1353+
agency_code: int | None = None,
1354+
agency_name: str | None = None,
1355+
type_of_investment: str | None = None,
1356+
updated_time_after: str | date | datetime | None = None,
1357+
updated_time_before: str | date | datetime | None = None,
1358+
cio_rating: int | None = None,
1359+
cio_rating_max: int | None = None,
1360+
performance_risk: bool | None = None,
1361+
) -> PaginatedResponse:
1362+
"""List federal IT investments from the IT Dashboard (`/api/itdashboard/`).
1363+
1364+
Filters are tier-gated by the API:
1365+
1366+
- **Free**: ``search`` (full-text across UII, title, description, agency, bureau)
1367+
- **Pro**: ``agency_code``, ``type_of_investment``,
1368+
``updated_time_after`` / ``updated_time_before``
1369+
- **Business+**: ``agency_name`` (text), ``cio_rating``,
1370+
``cio_rating_max``, ``performance_risk``
1371+
1372+
Hitting a gated filter on a lower tier returns a 403 with upgrade info.
1373+
1374+
CIO ratings: 1=High Risk, 2=Moderately High, 3=Medium, 4=Moderately Low, 5=Low.
1375+
``performance_risk=True`` returns investments with at least one NOT MET metric.
1376+
"""
1377+
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1378+
if shape is None:
1379+
shape = ShapeConfig.ITDASHBOARD_INVESTMENTS_MINIMAL
1380+
if shape:
1381+
params["shape"] = shape
1382+
if flat:
1383+
params["flat"] = "true"
1384+
if joiner:
1385+
params["joiner"] = joiner
1386+
if flat_lists:
1387+
params["flat_lists"] = "true"
1388+
for k, val in (
1389+
("search", search),
1390+
("agency_code", agency_code),
1391+
("agency_name", agency_name),
1392+
("type_of_investment", type_of_investment),
1393+
("updated_time_after", updated_time_after),
1394+
("updated_time_before", updated_time_before),
1395+
("cio_rating", cio_rating),
1396+
("cio_rating_max", cio_rating_max),
1397+
("performance_risk", performance_risk),
1398+
):
1399+
if val is None:
1400+
continue
1401+
if isinstance(val, bool):
1402+
params[k] = "true" if val else "false"
1403+
elif isinstance(val, (date, datetime)):
1404+
params[k] = val.isoformat()
1405+
else:
1406+
params[k] = val
1407+
data = self._get("/api/itdashboard/", params)
1408+
results = [
1409+
self._parse_response_with_shape(
1410+
obj, shape, ITDashboardInvestment, flat, flat_lists, joiner=joiner
1411+
)
1412+
for obj in data.get("results", [])
1413+
]
1414+
return PaginatedResponse(
1415+
count=data.get("count", 0),
1416+
next=data.get("next"),
1417+
previous=data.get("previous"),
1418+
results=results,
1419+
)
1420+
1421+
def get_itdashboard_investment(
1422+
self,
1423+
uii: str,
1424+
shape: str | None = None,
1425+
flat: bool = False,
1426+
flat_lists: bool = False,
1427+
joiner: str = ".",
1428+
) -> Any:
1429+
"""Get a single IT Dashboard investment by UII (`/api/itdashboard/{uii}/`)."""
1430+
params: dict[str, Any] = {}
1431+
if shape is None:
1432+
shape = ShapeConfig.ITDASHBOARD_INVESTMENTS_COMPREHENSIVE
1433+
if shape:
1434+
params["shape"] = shape
1435+
if flat:
1436+
params["flat"] = "true"
1437+
if joiner:
1438+
params["joiner"] = joiner
1439+
if flat_lists:
1440+
params["flat_lists"] = "true"
1441+
data = self._get(f"/api/itdashboard/{uii}/", params)
1442+
return self._parse_response_with_shape(
1443+
data, shape, ITDashboardInvestment, flat, flat_lists, joiner=joiner
1444+
)
1445+
13391446
# ============================================================================
13401447
# Vehicles (Awards)
13411448
# ============================================================================

tango/models.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,45 @@ class GsaElibraryContract:
367367
sins: list[str] | None = None
368368

369369

370+
@dataclass
371+
class ITDashboardInvestment:
372+
"""Schema definition for IT Dashboard Investment (not used for instances)
373+
374+
Federal IT investment from itdashboard.gov, exposed at /api/itdashboard/.
375+
Identified by ``uii`` (Unique Investment Identifier).
376+
377+
Tier-gated shape expansions:
378+
Free base fields only
379+
Pro+ ``funding`` and ``details`` expansions
380+
Business+ nested sub-tables (``cio_evaluation``, ``contracts``,
381+
``projects``, ``cost_pools_towers``, ``funding_sources``,
382+
``performance_metrics``, ``performance_actual``,
383+
``operational_analysis``) and ``business_case_html``
384+
"""
385+
386+
uii: str
387+
agency_code: int | None = None
388+
agency_name: str | None = None
389+
bureau_code: int | None = None
390+
bureau_name: str | None = None
391+
investment_title: str | None = None
392+
type_of_investment: str | None = None
393+
part_of_it_portfolio: str | None = None
394+
updated_time: datetime | None = None
395+
url: str | None = None
396+
business_case_html: str | None = None
397+
funding: dict[str, Any] | None = None
398+
details: dict[str, Any] | None = None
399+
cio_evaluation: list[dict[str, Any]] | None = None
400+
contracts: list[dict[str, Any]] | None = None
401+
projects: list[dict[str, Any]] | None = None
402+
cost_pools_towers: list[dict[str, Any]] | None = None
403+
funding_sources: list[dict[str, Any]] | None = None
404+
performance_metrics: list[dict[str, Any]] | None = None
405+
performance_actual: list[dict[str, Any]] | None = None
406+
operational_analysis: list[dict[str, Any]] | None = None
407+
408+
370409
@dataclass
371410
class Vehicle:
372411
"""Schema definition for Vehicle (not used for instances)"""
@@ -687,3 +726,18 @@ class ShapeConfig:
687726
GSA_ELIBRARY_CONTRACTS_MINIMAL: Final = (
688727
"uuid,contract_number,schedule,recipient(display_name,uei),idv(key,award_date)"
689728
)
729+
730+
# Default for list_itdashboard_investments()
731+
# Free-tier safe: matches the API's INVESTMENT_LIST_DEFAULT_SHAPE.
732+
ITDASHBOARD_INVESTMENTS_MINIMAL: Final = (
733+
"uii,agency_name,bureau_name,investment_title,"
734+
"type_of_investment,part_of_it_portfolio,updated_time,url"
735+
)
736+
737+
# Default for get_itdashboard_investment()
738+
# Free-tier safe: matches the API's INVESTMENT_RETRIEVE_DEFAULT_SHAPE.
739+
ITDASHBOARD_INVESTMENTS_COMPREHENSIVE: Final = (
740+
"uii,agency_code,agency_name,bureau_code,bureau_name,"
741+
"investment_title,type_of_investment,part_of_it_portfolio,"
742+
"updated_time,url"
743+
)

tango/shapes/explicit_schemas.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,67 @@
11321132
),
11331133
}
11341134

1135+
# IT Dashboard Investment
1136+
ITDASHBOARD_INVESTMENT_SCHEMA: dict[str, FieldSchema] = {
1137+
"uii": FieldSchema(name="uii", type=str, is_optional=False, is_list=False),
1138+
"agency_code": FieldSchema(
1139+
name="agency_code", type=int, is_optional=True, is_list=False
1140+
),
1141+
"agency_name": FieldSchema(
1142+
name="agency_name", type=str, is_optional=True, is_list=False
1143+
),
1144+
"bureau_code": FieldSchema(
1145+
name="bureau_code", type=int, is_optional=True, is_list=False
1146+
),
1147+
"bureau_name": FieldSchema(
1148+
name="bureau_name", type=str, is_optional=True, is_list=False
1149+
),
1150+
"investment_title": FieldSchema(
1151+
name="investment_title", type=str, is_optional=True, is_list=False
1152+
),
1153+
"type_of_investment": FieldSchema(
1154+
name="type_of_investment", type=str, is_optional=True, is_list=False
1155+
),
1156+
"part_of_it_portfolio": FieldSchema(
1157+
name="part_of_it_portfolio", type=str, is_optional=True, is_list=False
1158+
),
1159+
"updated_time": FieldSchema(
1160+
name="updated_time", type=datetime, is_optional=True, is_list=False
1161+
),
1162+
"url": FieldSchema(name="url", type=str, is_optional=True, is_list=False),
1163+
"business_case_html": FieldSchema(
1164+
name="business_case_html", type=str, is_optional=True, is_list=False
1165+
),
1166+
# Expansions: dict (funding/details) and list-of-dict (nested sub-tables).
1167+
# Modeled as opaque dict/list since their inner shapes are dynamic.
1168+
"funding": FieldSchema(name="funding", type=dict, is_optional=True, is_list=False),
1169+
"details": FieldSchema(name="details", type=dict, is_optional=True, is_list=False),
1170+
"cio_evaluation": FieldSchema(
1171+
name="cio_evaluation", type=list, is_optional=True, is_list=True
1172+
),
1173+
"contracts": FieldSchema(
1174+
name="contracts", type=list, is_optional=True, is_list=True
1175+
),
1176+
"projects": FieldSchema(
1177+
name="projects", type=list, is_optional=True, is_list=True
1178+
),
1179+
"cost_pools_towers": FieldSchema(
1180+
name="cost_pools_towers", type=list, is_optional=True, is_list=True
1181+
),
1182+
"funding_sources": FieldSchema(
1183+
name="funding_sources", type=list, is_optional=True, is_list=True
1184+
),
1185+
"performance_metrics": FieldSchema(
1186+
name="performance_metrics", type=list, is_optional=True, is_list=True
1187+
),
1188+
"performance_actual": FieldSchema(
1189+
name="performance_actual", type=list, is_optional=True, is_list=True
1190+
),
1191+
"operational_analysis": FieldSchema(
1192+
name="operational_analysis", type=list, is_optional=True, is_list=True
1193+
),
1194+
}
1195+
11351196
# ============================================================================
11361197
# SCHEMA REGISTRY MAPPING
11371198
# ============================================================================
@@ -1176,6 +1237,8 @@
11761237
# GSA eLibrary
11771238
"GsaElibraryContract": GSA_ELIBRARY_CONTRACT_SCHEMA,
11781239
"GsaElibraryIdvRef": GSA_ELIBRARY_IDV_REF_SCHEMA,
1240+
# IT Dashboard
1241+
"ITDashboardInvestment": ITDASHBOARD_INVESTMENT_SCHEMA,
11791242
}
11801243

11811244

0 commit comments

Comments
 (0)