tango-python 0.4.3__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tango/__init__.py +3 -1
- tango/client.py +122 -0
- tango/models.py +54 -0
- tango/shapes/explicit_schemas.py +63 -0
- {tango_python-0.4.3.dist-info → tango_python-0.5.0.dist-info}/METADATA +1 -1
- {tango_python-0.4.3.dist-info → tango_python-0.5.0.dist-info}/RECORD +8 -8
- {tango_python-0.4.3.dist-info → tango_python-0.5.0.dist-info}/WHEEL +0 -0
- {tango_python-0.4.3.dist-info → tango_python-0.5.0.dist-info}/licenses/LICENSE +0 -0
tango/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ from .exceptions import (
|
|
|
10
10
|
)
|
|
11
11
|
from .models import (
|
|
12
12
|
GsaElibraryContract,
|
|
13
|
+
ITDashboardInvestment,
|
|
13
14
|
PaginatedResponse,
|
|
14
15
|
RateLimitInfo,
|
|
15
16
|
SearchFilters,
|
|
@@ -28,7 +29,7 @@ from .shapes import (
|
|
|
28
29
|
TypeGenerator,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
|
-
__version__ = "0.
|
|
32
|
+
__version__ = "0.5.0"
|
|
32
33
|
__all__ = [
|
|
33
34
|
"TangoClient",
|
|
34
35
|
"TangoAPIError",
|
|
@@ -38,6 +39,7 @@ __all__ = [
|
|
|
38
39
|
"TangoRateLimitError",
|
|
39
40
|
"RateLimitInfo",
|
|
40
41
|
"GsaElibraryContract",
|
|
42
|
+
"ITDashboardInvestment",
|
|
41
43
|
"PaginatedResponse",
|
|
42
44
|
"SearchFilters",
|
|
43
45
|
"ShapeConfig",
|
tango/client.py
CHANGED
|
@@ -26,6 +26,7 @@ from tango.models import (
|
|
|
26
26
|
Forecast,
|
|
27
27
|
Grant,
|
|
28
28
|
GsaElibraryContract,
|
|
29
|
+
ITDashboardInvestment,
|
|
29
30
|
Location,
|
|
30
31
|
Notice,
|
|
31
32
|
Opportunity,
|
|
@@ -59,6 +60,8 @@ class TangoClient:
|
|
|
59
60
|
self,
|
|
60
61
|
api_key: str | None = None,
|
|
61
62
|
base_url: str = "https://tango.makegov.com",
|
|
63
|
+
user_agent: str | None = None,
|
|
64
|
+
extra_headers: dict[str, str] | None = None,
|
|
62
65
|
):
|
|
63
66
|
"""
|
|
64
67
|
Initialize the Tango API client
|
|
@@ -67,6 +70,8 @@ class TangoClient:
|
|
|
67
70
|
api_key: API key for authentication. If not provided, will attempt to load from
|
|
68
71
|
TANGO_API_KEY environment variable.
|
|
69
72
|
base_url: Base URL for the API
|
|
73
|
+
user_agent: Custom User-Agent header value.
|
|
74
|
+
extra_headers: Additional headers to include in every request.
|
|
70
75
|
"""
|
|
71
76
|
# Load API key from environment if not provided
|
|
72
77
|
self.api_key = api_key or os.getenv("TANGO_API_KEY")
|
|
@@ -76,9 +81,14 @@ class TangoClient:
|
|
|
76
81
|
headers = {}
|
|
77
82
|
if self.api_key:
|
|
78
83
|
headers["X-API-KEY"] = self.api_key
|
|
84
|
+
if user_agent:
|
|
85
|
+
headers["User-Agent"] = user_agent
|
|
86
|
+
if extra_headers:
|
|
87
|
+
headers.update(extra_headers)
|
|
79
88
|
|
|
80
89
|
self.client = httpx.Client(headers=headers, timeout=30.0)
|
|
81
90
|
self._last_rate_limit_info: RateLimitInfo | None = None
|
|
91
|
+
self._last_response_headers: httpx.Headers | None = None
|
|
82
92
|
|
|
83
93
|
# Use hardcoded sensible defaults
|
|
84
94
|
cache_size = 100
|
|
@@ -105,6 +115,11 @@ class TangoClient:
|
|
|
105
115
|
"""Rate limit info from the most recent API response."""
|
|
106
116
|
return self._last_rate_limit_info
|
|
107
117
|
|
|
118
|
+
@property
|
|
119
|
+
def last_response_headers(self) -> httpx.Headers | None:
|
|
120
|
+
"""Full HTTP headers from the most recent API response."""
|
|
121
|
+
return self._last_response_headers
|
|
122
|
+
|
|
108
123
|
@staticmethod
|
|
109
124
|
def _parse_rate_limit_headers(headers: httpx.Headers) -> RateLimitInfo:
|
|
110
125
|
"""Extract rate limit info from response headers."""
|
|
@@ -140,6 +155,7 @@ class TangoClient:
|
|
|
140
155
|
|
|
141
156
|
try:
|
|
142
157
|
response = self.client.request(method=method, url=url, params=params, json=json_data)
|
|
158
|
+
self._last_response_headers = response.headers
|
|
143
159
|
self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
|
|
144
160
|
|
|
145
161
|
if response.status_code == 401:
|
|
@@ -1321,6 +1337,112 @@ class TangoClient:
|
|
|
1321
1337
|
data, shape, GsaElibraryContract, flat, flat_lists, joiner=joiner
|
|
1322
1338
|
)
|
|
1323
1339
|
|
|
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
|
+
|
|
1324
1446
|
# ============================================================================
|
|
1325
1447
|
# Vehicles (Awards)
|
|
1326
1448
|
# ============================================================================
|
tango/models.py
CHANGED
|
@@ -367,6 +367,45 @@ class GsaElibraryContract:
|
|
|
367
367
|
sins: list[str] | None = None
|
|
368
368
|
|
|
369
369
|
|
|
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
|
+
|
|
370
409
|
@dataclass
|
|
371
410
|
class Vehicle:
|
|
372
411
|
"""Schema definition for Vehicle (not used for instances)"""
|
|
@@ -687,3 +726,18 @@ class ShapeConfig:
|
|
|
687
726
|
GSA_ELIBRARY_CONTRACTS_MINIMAL: Final = (
|
|
688
727
|
"uuid,contract_number,schedule,recipient(display_name,uei),idv(key,award_date)"
|
|
689
728
|
)
|
|
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
CHANGED
|
@@ -1132,6 +1132,67 @@ GSA_ELIBRARY_CONTRACT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1132
1132
|
),
|
|
1133
1133
|
}
|
|
1134
1134
|
|
|
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
|
+
|
|
1135
1196
|
# ============================================================================
|
|
1136
1197
|
# SCHEMA REGISTRY MAPPING
|
|
1137
1198
|
# ============================================================================
|
|
@@ -1176,6 +1237,8 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
|
|
|
1176
1237
|
# GSA eLibrary
|
|
1177
1238
|
"GsaElibraryContract": GSA_ELIBRARY_CONTRACT_SCHEMA,
|
|
1178
1239
|
"GsaElibraryIdvRef": GSA_ELIBRARY_IDV_REF_SCHEMA,
|
|
1240
|
+
# IT Dashboard
|
|
1241
|
+
"ITDashboardInvestment": ITDASHBOARD_INVESTMENT_SCHEMA,
|
|
1179
1242
|
}
|
|
1180
1243
|
|
|
1181
1244
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
tango/__init__.py,sha256=
|
|
2
|
-
tango/client.py,sha256=
|
|
1
|
+
tango/__init__.py,sha256=jhfw7xJNlVM9N2MjC42_i-mGFMgvKlKOZuC-4-d8z_I,1198
|
|
2
|
+
tango/client.py,sha256=Pr9lYMj5diZ2ecyrHL88y2B_WF41tYZ9Qm2S2xT3NPU,92545
|
|
3
3
|
tango/exceptions.py,sha256=aRvDm0dUCEtNDfRVYCX7SEDdd1WlIVVY6sN78Tzo-a0,3114
|
|
4
|
-
tango/models.py,sha256=
|
|
4
|
+
tango/models.py,sha256=fe8SDB1n8sG4DgVnCYnpkrWpY6Kz4CE475lh8MCCJfs,23207
|
|
5
5
|
tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
|
|
6
|
-
tango/shapes/explicit_schemas.py,sha256=
|
|
6
|
+
tango/shapes/explicit_schemas.py,sha256=_99ywtXRQoJ6KxesVTzWjOWMzxE1h9f2YGx19kW0FnA,57834
|
|
7
7
|
tango/shapes/factory.py,sha256=ytpMi5Uw72XZ8MimhuSsLDVXF3zO_Zt3_tAL6NF7LnU,34318
|
|
8
8
|
tango/shapes/generator.py,sha256=61V1T3lm8Ps_KSMJAezQJLQVFbNKt1jtoLyhiqNtFTs,23380
|
|
9
9
|
tango/shapes/models.py,sha256=h3pIhOqrrdlN953Y6r0oney5HFbKPOD-frRndRWimJ0,3018
|
|
10
10
|
tango/shapes/parser.py,sha256=k6OsI2w3GH6-IBbc-XTLgL1mWH7bMf7A_dA6pr1xKfw,24619
|
|
11
11
|
tango/shapes/schema.py,sha256=VRPOB1sBdjFyimNchrZKIpTHn83CyX4RfU9077aQtIU,14136
|
|
12
12
|
tango/shapes/types.py,sha256=27jrAE0VIdrKaLjR_FK71hfIIGX2Tg3ex7REEBV1TFE,1301
|
|
13
|
-
tango_python-0.
|
|
14
|
-
tango_python-0.
|
|
15
|
-
tango_python-0.
|
|
16
|
-
tango_python-0.
|
|
13
|
+
tango_python-0.5.0.dist-info/METADATA,sha256=Ht7PpwMjaP7R4yX-tWAvdvzsRIzlfFu5ZPwJybmH04s,17595
|
|
14
|
+
tango_python-0.5.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
+
tango_python-0.5.0.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
|
|
16
|
+
tango_python-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|