tango-python 1.0.0__py3-none-any.whl → 1.1.1__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 +441 -12
- tango/models.py +149 -10
- tango/shapes/explicit_schemas.py +7 -9
- tango/webhooks/cli.py +2 -2
- {tango_python-1.0.0.dist-info → tango_python-1.1.1.dist-info}/METADATA +5 -4
- {tango_python-1.0.0.dist-info → tango_python-1.1.1.dist-info}/RECORD +10 -10
- {tango_python-1.0.0.dist-info → tango_python-1.1.1.dist-info}/WHEEL +0 -0
- {tango_python-1.0.0.dist-info → tango_python-1.1.1.dist-info}/entry_points.txt +0 -0
- {tango_python-1.0.0.dist-info → tango_python-1.1.1.dist-info}/licenses/LICENSE +0 -0
tango/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .exceptions import (
|
|
|
9
9
|
TangoValidationError,
|
|
10
10
|
)
|
|
11
11
|
from .models import (
|
|
12
|
+
BudgetAccount,
|
|
12
13
|
GsaElibraryContract,
|
|
13
14
|
ITDashboardInvestment,
|
|
14
15
|
PaginatedResponse,
|
|
@@ -43,7 +44,7 @@ from .webhooks import (
|
|
|
43
44
|
)
|
|
44
45
|
from .webhooks.receiver import Delivery, WebhookReceiver
|
|
45
46
|
|
|
46
|
-
__version__ = "1.
|
|
47
|
+
__version__ = "1.1.1"
|
|
47
48
|
__all__ = [
|
|
48
49
|
"TangoClient",
|
|
49
50
|
"TangoAPIError",
|
|
@@ -54,6 +55,7 @@ __all__ = [
|
|
|
54
55
|
"RateLimitInfo",
|
|
55
56
|
"ResolveCandidate",
|
|
56
57
|
"ResolveResult",
|
|
58
|
+
"BudgetAccount",
|
|
57
59
|
"GsaElibraryContract",
|
|
58
60
|
"ITDashboardInvestment",
|
|
59
61
|
"PaginatedResponse",
|
tango/client.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import warnings
|
|
5
5
|
from datetime import date, datetime
|
|
6
6
|
from decimal import Decimal
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any, Literal, cast
|
|
8
8
|
from urllib.parse import urljoin
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
@@ -21,6 +21,7 @@ from tango.models import (
|
|
|
21
21
|
OTA,
|
|
22
22
|
OTIDV,
|
|
23
23
|
Agency,
|
|
24
|
+
BudgetAccount,
|
|
24
25
|
BusinessType,
|
|
25
26
|
Contract,
|
|
26
27
|
Entity,
|
|
@@ -220,9 +221,7 @@ class TangoClient:
|
|
|
220
221
|
picking one — that ambiguity would hide caller bugs.
|
|
221
222
|
"""
|
|
222
223
|
if json_data is not None and json is not None:
|
|
223
|
-
raise TangoValidationError(
|
|
224
|
-
"_post: pass `json_data` or `json`, not both."
|
|
225
|
-
)
|
|
224
|
+
raise TangoValidationError("_post: pass `json_data` or `json`, not both.")
|
|
226
225
|
body = json_data if json_data is not None else json
|
|
227
226
|
if body is None:
|
|
228
227
|
body = {}
|
|
@@ -243,9 +242,7 @@ class TangoClient:
|
|
|
243
242
|
picking one — that ambiguity would hide caller bugs.
|
|
244
243
|
"""
|
|
245
244
|
if json_data is not None and json is not None:
|
|
246
|
-
raise TangoValidationError(
|
|
247
|
-
"_patch: pass `json_data` or `json`, not both."
|
|
248
|
-
)
|
|
245
|
+
raise TangoValidationError("_patch: pass `json_data` or `json`, not both.")
|
|
249
246
|
body = json_data if json_data is not None else json
|
|
250
247
|
if body is None:
|
|
251
248
|
body = {}
|
|
@@ -673,8 +670,9 @@ class TangoClient:
|
|
|
673
670
|
params: dict[str, Any] = {"limit": min(limit, 100)}
|
|
674
671
|
if cursor:
|
|
675
672
|
params["cursor"] = cursor
|
|
676
|
-
|
|
677
|
-
|
|
673
|
+
# /api/contracts/ is cursor-only (KeysetPagination). When no cursor is
|
|
674
|
+
# supplied, send neither page nor cursor — the API returns the first
|
|
675
|
+
# page by default. (Previously sent page=1, which the endpoint ignores.)
|
|
678
676
|
|
|
679
677
|
# Handle legacy filters parameter (backward compatibility)
|
|
680
678
|
filter_dict: dict[str, Any] = {}
|
|
@@ -772,6 +770,89 @@ class TangoClient:
|
|
|
772
770
|
cursor=data.get("cursor"),
|
|
773
771
|
)
|
|
774
772
|
|
|
773
|
+
def get_contract(
|
|
774
|
+
self,
|
|
775
|
+
key: str,
|
|
776
|
+
shape: str | None = None,
|
|
777
|
+
flat: bool = False,
|
|
778
|
+
flat_lists: bool = False,
|
|
779
|
+
joiner: str = ".",
|
|
780
|
+
) -> Any:
|
|
781
|
+
"""Get a single contract by key (`/api/contracts/{key}/`)."""
|
|
782
|
+
params: dict[str, Any] = {}
|
|
783
|
+
if shape is None:
|
|
784
|
+
shape = ShapeConfig.CONTRACTS_MINIMAL
|
|
785
|
+
if shape:
|
|
786
|
+
params["shape"] = shape
|
|
787
|
+
if flat:
|
|
788
|
+
params["flat"] = "true"
|
|
789
|
+
if joiner:
|
|
790
|
+
params["joiner"] = joiner
|
|
791
|
+
if flat_lists:
|
|
792
|
+
params["flat_lists"] = "true"
|
|
793
|
+
data = self._get(f"/api/contracts/{key}/", params)
|
|
794
|
+
return self._parse_response_with_shape(
|
|
795
|
+
data, shape, Contract, flat, flat_lists, joiner=joiner
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
def get_contract_subawards(
|
|
799
|
+
self,
|
|
800
|
+
key: str,
|
|
801
|
+
page: int = 1,
|
|
802
|
+
limit: int = 25,
|
|
803
|
+
shape: str | None = None,
|
|
804
|
+
flat: bool = False,
|
|
805
|
+
flat_lists: bool = False,
|
|
806
|
+
ordering: str | None = None,
|
|
807
|
+
) -> PaginatedResponse:
|
|
808
|
+
"""List subawards under a contract (`/api/contracts/{key}/subawards/`)."""
|
|
809
|
+
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
|
|
810
|
+
if shape is None:
|
|
811
|
+
shape = ShapeConfig.SUBAWARDS_MINIMAL
|
|
812
|
+
if shape:
|
|
813
|
+
params["shape"] = shape
|
|
814
|
+
if flat:
|
|
815
|
+
params["flat"] = "true"
|
|
816
|
+
if flat_lists:
|
|
817
|
+
params["flat_lists"] = "true"
|
|
818
|
+
if ordering:
|
|
819
|
+
params["ordering"] = ordering
|
|
820
|
+
data = self._get(f"/api/contracts/{key}/subawards/", params)
|
|
821
|
+
raw_results = data.get("results") or []
|
|
822
|
+
results = [
|
|
823
|
+
self._parse_response_with_shape(obj, shape, Subaward, flat, flat_lists)
|
|
824
|
+
for obj in raw_results
|
|
825
|
+
]
|
|
826
|
+
return PaginatedResponse(
|
|
827
|
+
count=int(data.get("count") or len(results)),
|
|
828
|
+
next=data.get("next"),
|
|
829
|
+
previous=data.get("previous"),
|
|
830
|
+
results=results,
|
|
831
|
+
cursor=data.get("cursor"),
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
def get_contract_transactions(
|
|
835
|
+
self,
|
|
836
|
+
key: str,
|
|
837
|
+
limit: int = 100,
|
|
838
|
+
cursor: str | None = None,
|
|
839
|
+
ordering: str | None = None,
|
|
840
|
+
) -> PaginatedResponse:
|
|
841
|
+
"""List transactions under a contract (`/api/contracts/{key}/transactions/`)."""
|
|
842
|
+
params: dict[str, Any] = {"limit": min(limit, 500)}
|
|
843
|
+
if cursor:
|
|
844
|
+
params["cursor"] = cursor
|
|
845
|
+
if ordering:
|
|
846
|
+
params["ordering"] = ordering
|
|
847
|
+
data = self._get(f"/api/contracts/{key}/transactions/", params)
|
|
848
|
+
return PaginatedResponse(
|
|
849
|
+
count=int(data.get("count") or len(data.get("results") or [])),
|
|
850
|
+
next=data.get("next"),
|
|
851
|
+
previous=data.get("previous"),
|
|
852
|
+
results=data.get("results") or [],
|
|
853
|
+
cursor=data.get("cursor"),
|
|
854
|
+
)
|
|
855
|
+
|
|
775
856
|
# ============================================================================
|
|
776
857
|
# IDVs (Awards)
|
|
777
858
|
# ============================================================================
|
|
@@ -1246,6 +1327,59 @@ class TangoClient:
|
|
|
1246
1327
|
data = self._get(f"/api/otidvs/{key}/", params)
|
|
1247
1328
|
return self._parse_response_with_shape(data, shape, OTIDV, flat, flat_lists, joiner=joiner)
|
|
1248
1329
|
|
|
1330
|
+
def list_otidv_awards(
|
|
1331
|
+
self,
|
|
1332
|
+
key: str,
|
|
1333
|
+
limit: int = 25,
|
|
1334
|
+
cursor: str | None = None,
|
|
1335
|
+
shape: str | None = None,
|
|
1336
|
+
flat: bool = False,
|
|
1337
|
+
flat_lists: bool = False,
|
|
1338
|
+
joiner: str = ".",
|
|
1339
|
+
ordering: str | None = None,
|
|
1340
|
+
) -> PaginatedResponse:
|
|
1341
|
+
"""List child awards under an OTIDV (`/api/otidvs/{key}/awards/`).
|
|
1342
|
+
|
|
1343
|
+
Args:
|
|
1344
|
+
key: The OTIDV key.
|
|
1345
|
+
limit: Results per page (max 100).
|
|
1346
|
+
cursor: Cursor token for keyset pagination.
|
|
1347
|
+
shape: Response shape string (defaults to minimal contract shape).
|
|
1348
|
+
flat: If True, flatten nested objects using dot notation.
|
|
1349
|
+
flat_lists: If True, flatten arrays using indexed keys.
|
|
1350
|
+
joiner: Separator used when flattening.
|
|
1351
|
+
ordering: Server-side sort (prefix with '-' for descending).
|
|
1352
|
+
"""
|
|
1353
|
+
params: dict[str, Any] = {"limit": min(limit, 100)}
|
|
1354
|
+
if cursor:
|
|
1355
|
+
params["cursor"] = cursor
|
|
1356
|
+
if shape is None:
|
|
1357
|
+
shape = ShapeConfig.CONTRACTS_MINIMAL
|
|
1358
|
+
if shape:
|
|
1359
|
+
params["shape"] = shape
|
|
1360
|
+
if flat:
|
|
1361
|
+
params["flat"] = "true"
|
|
1362
|
+
if joiner:
|
|
1363
|
+
params["joiner"] = joiner
|
|
1364
|
+
if flat_lists:
|
|
1365
|
+
params["flat_lists"] = "true"
|
|
1366
|
+
if ordering:
|
|
1367
|
+
params["ordering"] = ordering
|
|
1368
|
+
data = self._get(f"/api/otidvs/{key}/awards/", params)
|
|
1369
|
+
raw_results = data.get("results") or []
|
|
1370
|
+
results = [
|
|
1371
|
+
self._parse_response_with_shape(obj, shape, Contract, flat, flat_lists, joiner=joiner)
|
|
1372
|
+
for obj in raw_results
|
|
1373
|
+
]
|
|
1374
|
+
return PaginatedResponse(
|
|
1375
|
+
count=int(data.get("count") or len(results)),
|
|
1376
|
+
next=data.get("next"),
|
|
1377
|
+
previous=data.get("previous"),
|
|
1378
|
+
results=results,
|
|
1379
|
+
cursor=data.get("cursor"),
|
|
1380
|
+
page_metadata=data.get("page_metadata"),
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1249
1383
|
def list_subawards(
|
|
1250
1384
|
self,
|
|
1251
1385
|
page: int = 1,
|
|
@@ -1300,6 +1434,31 @@ class TangoClient:
|
|
|
1300
1434
|
results=results,
|
|
1301
1435
|
)
|
|
1302
1436
|
|
|
1437
|
+
def get_subaward(
|
|
1438
|
+
self,
|
|
1439
|
+
key: str,
|
|
1440
|
+
shape: str | None = None,
|
|
1441
|
+
flat: bool = False,
|
|
1442
|
+
flat_lists: bool = False,
|
|
1443
|
+
joiner: str = ".",
|
|
1444
|
+
) -> Any:
|
|
1445
|
+
"""Get a single subaward by key (`/api/subawards/{key}/`)."""
|
|
1446
|
+
params: dict[str, Any] = {}
|
|
1447
|
+
if shape is None:
|
|
1448
|
+
shape = ShapeConfig.SUBAWARDS_MINIMAL
|
|
1449
|
+
if shape:
|
|
1450
|
+
params["shape"] = shape
|
|
1451
|
+
if flat:
|
|
1452
|
+
params["flat"] = "true"
|
|
1453
|
+
if joiner:
|
|
1454
|
+
params["joiner"] = joiner
|
|
1455
|
+
if flat_lists:
|
|
1456
|
+
params["flat_lists"] = "true"
|
|
1457
|
+
data = self._get(f"/api/subawards/{key}/", params)
|
|
1458
|
+
return self._parse_response_with_shape(
|
|
1459
|
+
data, shape, Subaward, flat, flat_lists, joiner=joiner
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1303
1462
|
# ============================================================================
|
|
1304
1463
|
# GSA eLibrary Contracts
|
|
1305
1464
|
# ============================================================================
|
|
@@ -1922,6 +2081,12 @@ class TangoClient:
|
|
|
1922
2081
|
data = self._get(f"/api/entities/{key}/", params)
|
|
1923
2082
|
return self._parse_response_with_shape(data, shape, Entity, flat, flat_lists)
|
|
1924
2083
|
|
|
2084
|
+
def get_entity_budget_flows(self, uei: str) -> dict[str, Any]:
|
|
2085
|
+
"""Get budget flows for an entity (`/api/entities/{uei}/budget-flows/`)."""
|
|
2086
|
+
if not uei:
|
|
2087
|
+
raise TangoValidationError("UEI is required")
|
|
2088
|
+
return self._get(f"/api/entities/{uei}/budget-flows/")
|
|
2089
|
+
|
|
1925
2090
|
# Forecast endpoints
|
|
1926
2091
|
def list_forecasts(
|
|
1927
2092
|
self,
|
|
@@ -2014,6 +2179,31 @@ class TangoClient:
|
|
|
2014
2179
|
results=results,
|
|
2015
2180
|
)
|
|
2016
2181
|
|
|
2182
|
+
def get_forecast(
|
|
2183
|
+
self,
|
|
2184
|
+
id: str,
|
|
2185
|
+
shape: str | None = None,
|
|
2186
|
+
flat: bool = False,
|
|
2187
|
+
flat_lists: bool = False,
|
|
2188
|
+
joiner: str = ".",
|
|
2189
|
+
) -> Any:
|
|
2190
|
+
"""Get a single forecast by id (`/api/forecasts/{id}/`)."""
|
|
2191
|
+
params: dict[str, Any] = {}
|
|
2192
|
+
if shape is None:
|
|
2193
|
+
shape = ShapeConfig.FORECASTS_MINIMAL
|
|
2194
|
+
if shape:
|
|
2195
|
+
params["shape"] = shape
|
|
2196
|
+
if flat:
|
|
2197
|
+
params["flat"] = "true"
|
|
2198
|
+
if joiner:
|
|
2199
|
+
params["joiner"] = joiner
|
|
2200
|
+
if flat_lists:
|
|
2201
|
+
params["flat_lists"] = "true"
|
|
2202
|
+
data = self._get(f"/api/forecasts/{id}/", params)
|
|
2203
|
+
return self._parse_response_with_shape(
|
|
2204
|
+
data, shape, Forecast, flat, flat_lists, joiner=joiner
|
|
2205
|
+
)
|
|
2206
|
+
|
|
2017
2207
|
# Opportunity endpoints
|
|
2018
2208
|
def list_opportunities(
|
|
2019
2209
|
self,
|
|
@@ -2112,6 +2302,31 @@ class TangoClient:
|
|
|
2112
2302
|
results=results,
|
|
2113
2303
|
)
|
|
2114
2304
|
|
|
2305
|
+
def get_opportunity(
|
|
2306
|
+
self,
|
|
2307
|
+
opportunity_id: str,
|
|
2308
|
+
shape: str | None = None,
|
|
2309
|
+
flat: bool = False,
|
|
2310
|
+
flat_lists: bool = False,
|
|
2311
|
+
joiner: str = ".",
|
|
2312
|
+
) -> Any:
|
|
2313
|
+
"""Get a single opportunity by id (`/api/opportunities/{opportunity_id}/`)."""
|
|
2314
|
+
params: dict[str, Any] = {}
|
|
2315
|
+
if shape is None:
|
|
2316
|
+
shape = ShapeConfig.OPPORTUNITIES_MINIMAL
|
|
2317
|
+
if shape:
|
|
2318
|
+
params["shape"] = shape
|
|
2319
|
+
if flat:
|
|
2320
|
+
params["flat"] = "true"
|
|
2321
|
+
if joiner:
|
|
2322
|
+
params["joiner"] = joiner
|
|
2323
|
+
if flat_lists:
|
|
2324
|
+
params["flat_lists"] = "true"
|
|
2325
|
+
data = self._get(f"/api/opportunities/{opportunity_id}/", params)
|
|
2326
|
+
return self._parse_response_with_shape(
|
|
2327
|
+
data, shape, Opportunity, flat, flat_lists, joiner=joiner
|
|
2328
|
+
)
|
|
2329
|
+
|
|
2115
2330
|
# Notice endpoints
|
|
2116
2331
|
def list_notices(
|
|
2117
2332
|
self,
|
|
@@ -2201,6 +2416,29 @@ class TangoClient:
|
|
|
2201
2416
|
results=results,
|
|
2202
2417
|
)
|
|
2203
2418
|
|
|
2419
|
+
def get_notice(
|
|
2420
|
+
self,
|
|
2421
|
+
notice_id: str,
|
|
2422
|
+
shape: str | None = None,
|
|
2423
|
+
flat: bool = False,
|
|
2424
|
+
flat_lists: bool = False,
|
|
2425
|
+
joiner: str = ".",
|
|
2426
|
+
) -> Any:
|
|
2427
|
+
"""Get a single notice by id (`/api/notices/{notice_id}/`)."""
|
|
2428
|
+
params: dict[str, Any] = {}
|
|
2429
|
+
if shape is None:
|
|
2430
|
+
shape = ShapeConfig.NOTICES_MINIMAL
|
|
2431
|
+
if shape:
|
|
2432
|
+
params["shape"] = shape
|
|
2433
|
+
if flat:
|
|
2434
|
+
params["flat"] = "true"
|
|
2435
|
+
if joiner:
|
|
2436
|
+
params["joiner"] = joiner
|
|
2437
|
+
if flat_lists:
|
|
2438
|
+
params["flat_lists"] = "true"
|
|
2439
|
+
data = self._get(f"/api/notices/{notice_id}/", params)
|
|
2440
|
+
return self._parse_response_with_shape(data, shape, Notice, flat, flat_lists, joiner=joiner)
|
|
2441
|
+
|
|
2204
2442
|
# Protest endpoints
|
|
2205
2443
|
# See https://tango.makegov.com/docs/api-reference/protests.md
|
|
2206
2444
|
# Note: Protests API does not support ordering (returns 400 if provided).
|
|
@@ -2328,6 +2566,170 @@ class TangoClient:
|
|
|
2328
2566
|
data = self._get(f"/api/protests/{case_id}/", params)
|
|
2329
2567
|
return self._parse_response_with_shape(data, shape, Protest, flat, flat_lists)
|
|
2330
2568
|
|
|
2569
|
+
# ============================================================================
|
|
2570
|
+
# Budget (federal account x fiscal year rollups)
|
|
2571
|
+
# ============================================================================
|
|
2572
|
+
|
|
2573
|
+
def list_budget_accounts(
|
|
2574
|
+
self,
|
|
2575
|
+
page: int = 1,
|
|
2576
|
+
limit: int = 25,
|
|
2577
|
+
shape: str | None = None,
|
|
2578
|
+
flat: bool = False,
|
|
2579
|
+
flat_lists: bool = False,
|
|
2580
|
+
federal_account_symbol: str | None = None,
|
|
2581
|
+
fiscal_year: int | None = None,
|
|
2582
|
+
fiscal_year_gte: int | None = None,
|
|
2583
|
+
fiscal_year_lte: int | None = None,
|
|
2584
|
+
agency_code: str | None = None,
|
|
2585
|
+
bureau_name: str | None = None,
|
|
2586
|
+
account_title: str | None = None,
|
|
2587
|
+
bea_category: str | None = None,
|
|
2588
|
+
on_off_budget: str | None = None,
|
|
2589
|
+
subfunction_code: str | None = None,
|
|
2590
|
+
search: str | None = None,
|
|
2591
|
+
ordering: str | None = None,
|
|
2592
|
+
) -> PaginatedResponse:
|
|
2593
|
+
"""List budget accounts (`/api/budget/accounts/`).
|
|
2594
|
+
|
|
2595
|
+
One row per ``(federal_account_symbol, fiscal_year)`` covering the full
|
|
2596
|
+
budget lifecycle, pre-computed ratios + trends, the
|
|
2597
|
+
contract/assistance/unlinked breakdown, and request-vs-actual spend.
|
|
2598
|
+
|
|
2599
|
+
Args:
|
|
2600
|
+
page: Page number.
|
|
2601
|
+
limit: Results per page (max 100).
|
|
2602
|
+
shape: Response shape string (defaults to minimal shape).
|
|
2603
|
+
flat: If True, flatten nested objects using dot notation.
|
|
2604
|
+
flat_lists: If True, flatten arrays using indexed keys.
|
|
2605
|
+
federal_account_symbol: Exact federal account symbol.
|
|
2606
|
+
fiscal_year: Fiscal year (exact).
|
|
2607
|
+
fiscal_year_gte: Fiscal year >=.
|
|
2608
|
+
fiscal_year_lte: Fiscal year <=.
|
|
2609
|
+
agency_code: Agency code (exact).
|
|
2610
|
+
bureau_name: Bureau name (exact).
|
|
2611
|
+
account_title: Account title (icontains).
|
|
2612
|
+
bea_category: BEA category (exact).
|
|
2613
|
+
on_off_budget: On/off budget flag (exact).
|
|
2614
|
+
subfunction_code: Subfunction code (exact).
|
|
2615
|
+
search: Full-text search over account_title/agency_name/bureau_name.
|
|
2616
|
+
ordering: Sort field (prefix with '-' for descending).
|
|
2617
|
+
"""
|
|
2618
|
+
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
|
|
2619
|
+
if shape is None:
|
|
2620
|
+
shape = ShapeConfig.BUDGET_ACCOUNTS_MINIMAL
|
|
2621
|
+
if shape:
|
|
2622
|
+
params["shape"] = shape
|
|
2623
|
+
if flat:
|
|
2624
|
+
params["flat"] = "true"
|
|
2625
|
+
if flat_lists:
|
|
2626
|
+
params["flat_lists"] = "true"
|
|
2627
|
+
for key, val in (
|
|
2628
|
+
("federal_account_symbol", federal_account_symbol),
|
|
2629
|
+
("fiscal_year", fiscal_year),
|
|
2630
|
+
("fiscal_year__gte", fiscal_year_gte),
|
|
2631
|
+
("fiscal_year__lte", fiscal_year_lte),
|
|
2632
|
+
("agency_code", agency_code),
|
|
2633
|
+
("bureau_name", bureau_name),
|
|
2634
|
+
("account_title__icontains", account_title),
|
|
2635
|
+
("bea_category", bea_category),
|
|
2636
|
+
("on_off_budget", on_off_budget),
|
|
2637
|
+
("subfunction_code", subfunction_code),
|
|
2638
|
+
("search", search),
|
|
2639
|
+
("ordering", ordering),
|
|
2640
|
+
):
|
|
2641
|
+
if val is not None:
|
|
2642
|
+
params[key] = val
|
|
2643
|
+
data = self._get("/api/budget/accounts/", params)
|
|
2644
|
+
results = [
|
|
2645
|
+
self._parse_response_with_shape(obj, shape, BudgetAccount, flat, flat_lists)
|
|
2646
|
+
for obj in data.get("results", [])
|
|
2647
|
+
]
|
|
2648
|
+
return PaginatedResponse(
|
|
2649
|
+
count=data.get("count", 0),
|
|
2650
|
+
next=data.get("next"),
|
|
2651
|
+
previous=data.get("previous"),
|
|
2652
|
+
results=results,
|
|
2653
|
+
)
|
|
2654
|
+
|
|
2655
|
+
def get_budget_account(
|
|
2656
|
+
self,
|
|
2657
|
+
id: str | int,
|
|
2658
|
+
shape: str | None = None,
|
|
2659
|
+
flat: bool = False,
|
|
2660
|
+
flat_lists: bool = False,
|
|
2661
|
+
joiner: str = ".",
|
|
2662
|
+
) -> Any:
|
|
2663
|
+
"""Get a single budget account by id (`/api/budget/accounts/{id}/`)."""
|
|
2664
|
+
params: dict[str, Any] = {}
|
|
2665
|
+
if shape is None:
|
|
2666
|
+
shape = ShapeConfig.BUDGET_ACCOUNTS_MINIMAL
|
|
2667
|
+
if shape:
|
|
2668
|
+
params["shape"] = shape
|
|
2669
|
+
if flat:
|
|
2670
|
+
params["flat"] = "true"
|
|
2671
|
+
if joiner:
|
|
2672
|
+
params["joiner"] = joiner
|
|
2673
|
+
if flat_lists:
|
|
2674
|
+
params["flat_lists"] = "true"
|
|
2675
|
+
data = self._get(f"/api/budget/accounts/{id}/", params)
|
|
2676
|
+
return self._parse_response_with_shape(
|
|
2677
|
+
data, shape, BudgetAccount, flat, flat_lists, joiner=joiner
|
|
2678
|
+
)
|
|
2679
|
+
|
|
2680
|
+
def get_budget_account_quarters(
|
|
2681
|
+
self,
|
|
2682
|
+
id: str | int,
|
|
2683
|
+
tas: str | None = None,
|
|
2684
|
+
limit: int = 25,
|
|
2685
|
+
) -> PaginatedResponse:
|
|
2686
|
+
"""Get quarterly TAS-grain flow for a budget account.
|
|
2687
|
+
|
|
2688
|
+
(`/api/budget/accounts/{id}/quarters/`). FY21+ only.
|
|
2689
|
+
|
|
2690
|
+
Args:
|
|
2691
|
+
id: Budget account id.
|
|
2692
|
+
tas: Narrow to a single Treasury Account Symbol.
|
|
2693
|
+
limit: Results per page (max 100).
|
|
2694
|
+
"""
|
|
2695
|
+
params: dict[str, Any] = {"limit": min(limit, 100)}
|
|
2696
|
+
if tas:
|
|
2697
|
+
params["tas"] = tas
|
|
2698
|
+
data = self._get(f"/api/budget/accounts/{id}/quarters/", params)
|
|
2699
|
+
return PaginatedResponse(
|
|
2700
|
+
count=int(data.get("count") or len(data.get("results") or [])),
|
|
2701
|
+
next=data.get("next"),
|
|
2702
|
+
previous=data.get("previous"),
|
|
2703
|
+
results=data.get("results") or [],
|
|
2704
|
+
)
|
|
2705
|
+
|
|
2706
|
+
def get_budget_account_recipients(
|
|
2707
|
+
self,
|
|
2708
|
+
id: str | int,
|
|
2709
|
+
funding_organization_id: str | None = None,
|
|
2710
|
+
limit: int = 25,
|
|
2711
|
+
) -> PaginatedResponse:
|
|
2712
|
+
"""Get funding-office x recipient contract-flow detail for a budget account.
|
|
2713
|
+
|
|
2714
|
+
(`/api/budget/accounts/{id}/recipients/`).
|
|
2715
|
+
|
|
2716
|
+
Args:
|
|
2717
|
+
id: Budget account id.
|
|
2718
|
+
funding_organization_id: Narrow to a single funding office
|
|
2719
|
+
(Organization UUID).
|
|
2720
|
+
limit: Results per page (max 100).
|
|
2721
|
+
"""
|
|
2722
|
+
params: dict[str, Any] = {"limit": min(limit, 100)}
|
|
2723
|
+
if funding_organization_id:
|
|
2724
|
+
params["funding_organization_id"] = funding_organization_id
|
|
2725
|
+
data = self._get(f"/api/budget/accounts/{id}/recipients/", params)
|
|
2726
|
+
return PaginatedResponse(
|
|
2727
|
+
count=int(data.get("count") or len(data.get("results") or [])),
|
|
2728
|
+
next=data.get("next"),
|
|
2729
|
+
previous=data.get("previous"),
|
|
2730
|
+
results=data.get("results") or [],
|
|
2731
|
+
)
|
|
2732
|
+
|
|
2331
2733
|
# Grant endpoints
|
|
2332
2734
|
def list_grants(
|
|
2333
2735
|
self,
|
|
@@ -2341,6 +2743,7 @@ class TangoClient:
|
|
|
2341
2743
|
cfda_number: str | None = None,
|
|
2342
2744
|
funding_categories: str | None = None,
|
|
2343
2745
|
funding_instruments: str | None = None,
|
|
2746
|
+
grant_id: str | None = None,
|
|
2344
2747
|
opportunity_number: str | None = None,
|
|
2345
2748
|
ordering: str | None = None,
|
|
2346
2749
|
posted_date_after: str | None = None,
|
|
@@ -2364,6 +2767,8 @@ class TangoClient:
|
|
|
2364
2767
|
cfda_number: CFDA number filter
|
|
2365
2768
|
funding_categories: Funding categories filter
|
|
2366
2769
|
funding_instruments: Funding instruments filter
|
|
2770
|
+
grant_id: Filter by grant ID (matches the detail-endpoint
|
|
2771
|
+
identifier). Supports multi-value OR via '|' (e.g. "123|456").
|
|
2367
2772
|
opportunity_number: Opportunity number filter
|
|
2368
2773
|
ordering: Sort field (prefix with '-' for descending)
|
|
2369
2774
|
posted_date_after: Posted date after
|
|
@@ -2390,6 +2795,7 @@ class TangoClient:
|
|
|
2390
2795
|
("cfda_number", cfda_number),
|
|
2391
2796
|
("funding_categories", funding_categories),
|
|
2392
2797
|
("funding_instruments", funding_instruments),
|
|
2798
|
+
("grant_id", grant_id),
|
|
2393
2799
|
("opportunity_number", opportunity_number),
|
|
2394
2800
|
("ordering", ordering),
|
|
2395
2801
|
("posted_date_after", posted_date_after),
|
|
@@ -2417,6 +2823,29 @@ class TangoClient:
|
|
|
2417
2823
|
results=results,
|
|
2418
2824
|
)
|
|
2419
2825
|
|
|
2826
|
+
def get_grant(
|
|
2827
|
+
self,
|
|
2828
|
+
grant_id: str,
|
|
2829
|
+
shape: str | None = None,
|
|
2830
|
+
flat: bool = False,
|
|
2831
|
+
flat_lists: bool = False,
|
|
2832
|
+
joiner: str = ".",
|
|
2833
|
+
) -> Any:
|
|
2834
|
+
"""Get a single grant by its grant id (`/api/grants/{grant_id}/`)."""
|
|
2835
|
+
params: dict[str, Any] = {}
|
|
2836
|
+
if shape is None:
|
|
2837
|
+
shape = ShapeConfig.GRANTS_MINIMAL
|
|
2838
|
+
if shape:
|
|
2839
|
+
params["shape"] = shape
|
|
2840
|
+
if flat:
|
|
2841
|
+
params["flat"] = "true"
|
|
2842
|
+
if joiner:
|
|
2843
|
+
params["joiner"] = joiner
|
|
2844
|
+
if flat_lists:
|
|
2845
|
+
params["flat_lists"] = "true"
|
|
2846
|
+
data = self._get(f"/api/grants/{grant_id}/", params)
|
|
2847
|
+
return self._parse_response_with_shape(data, shape, Grant, flat, flat_lists, joiner=joiner)
|
|
2848
|
+
|
|
2420
2849
|
# ============================================================================
|
|
2421
2850
|
# Webhooks (v2)
|
|
2422
2851
|
# ============================================================================
|
|
@@ -2630,13 +3059,13 @@ class TangoClient:
|
|
|
2630
3059
|
return WebhookAlert(
|
|
2631
3060
|
alert_id=str(data.get("alert_id") or data.get("id") or ""),
|
|
2632
3061
|
name=str(data.get("name") or data.get("subscription_name") or ""),
|
|
2633
|
-
query_type=
|
|
2634
|
-
filters=data.get("filters") or data.get("filter_definition"),
|
|
3062
|
+
query_type=str(data.get("query_type") or ""),
|
|
3063
|
+
filters=data.get("filters") or data.get("filter_definition") or {},
|
|
2635
3064
|
frequency=str(data.get("frequency", "realtime")),
|
|
2636
3065
|
cron_expression=(
|
|
2637
3066
|
str(data["cron_expression"]) if data.get("cron_expression") is not None else None
|
|
2638
3067
|
),
|
|
2639
|
-
status=
|
|
3068
|
+
status=cast("Literal['active', 'paused']", data.get("status", "active")),
|
|
2640
3069
|
created_at=str(data.get("created_at", "")),
|
|
2641
3070
|
last_checked_at=(
|
|
2642
3071
|
str(data["last_checked_at"]) if data.get("last_checked_at") is not None else None
|
tango/models.py
CHANGED
|
@@ -281,21 +281,66 @@ class AwardTransaction:
|
|
|
281
281
|
obligated: Decimal | None = None
|
|
282
282
|
|
|
283
283
|
|
|
284
|
+
@dataclass
|
|
285
|
+
class OrganizationOfficePayload:
|
|
286
|
+
"""Schema definition for OrganizationOfficePayload (not used for instances).
|
|
287
|
+
|
|
288
|
+
Returned for a contract's ``awarding_office`` / ``funding_office``.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
organization_id: str | None = None
|
|
292
|
+
office_code: str | None = None
|
|
293
|
+
office_name: str | None = None
|
|
294
|
+
agency_code: str | None = None
|
|
295
|
+
agency_name: str | None = None
|
|
296
|
+
department_code: int | None = None
|
|
297
|
+
department_name: str | None = None
|
|
298
|
+
|
|
299
|
+
|
|
284
300
|
@dataclass
|
|
285
301
|
class Contract:
|
|
286
|
-
"""Schema definition for Contract (not used for instances)
|
|
302
|
+
"""Schema definition for Contract (not used for instances).
|
|
287
303
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
304
|
+
Mirrors the API ``ContractList`` schema (``ContractListSerializer``).
|
|
305
|
+
``/api/contracts/`` is shape-on-demand: which fields appear in a response
|
|
306
|
+
depends on the ``?shape=`` query param, so every field is optional.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
key: str | None = None
|
|
310
|
+
piid: str | None = None
|
|
293
311
|
award_date: date | None = None
|
|
294
312
|
fiscal_year: int | None = None
|
|
313
|
+
obligated: Decimal | None = None
|
|
314
|
+
base_and_exercised_options_value: Decimal | None = None
|
|
315
|
+
total_contract_value: Decimal | None = None
|
|
316
|
+
naics_code: int | None = None
|
|
317
|
+
psc_code: str | None = None
|
|
318
|
+
set_aside: str | None = None
|
|
319
|
+
solicitation_identifier: str | None = None
|
|
320
|
+
description: str | None = None
|
|
321
|
+
awarding_office: OrganizationOfficePayload | None = None
|
|
322
|
+
funding_office: OrganizationOfficePayload | None = None
|
|
295
323
|
recipient: RecipientProfile | None = None
|
|
324
|
+
parent_award: ParentAward | None = None
|
|
325
|
+
legislative_mandates: LegislativeMandates | None = None
|
|
326
|
+
place_of_performance: PlaceOfPerformance | None = None
|
|
327
|
+
subawards_summary: SubawardsSummary | None = None
|
|
328
|
+
|
|
329
|
+
# --- Deprecated fields (never returned by the API; removed in 2.0.0) ---
|
|
330
|
+
# Declared with None defaults so existing consumers reading these do not
|
|
331
|
+
# raise AttributeError; they always resolve to None.
|
|
332
|
+
id: str | None = None
|
|
333
|
+
""".. deprecated:: Never returned by the API. Removed in 2.0.0."""
|
|
334
|
+
award_id: str | None = None
|
|
335
|
+
""".. deprecated:: Never returned by the API. Removed in 2.0.0."""
|
|
336
|
+
recipient_name: str | None = None
|
|
337
|
+
""".. deprecated:: Use ``recipient.display_name``. Removed in 2.0.0."""
|
|
338
|
+
award_amount: Decimal | None = None
|
|
339
|
+
""".. deprecated:: Use ``obligated`` / ``total_contract_value``. Removed in 2.0.0."""
|
|
296
340
|
awarding_agency: Agency | None = None
|
|
341
|
+
""".. deprecated:: Use ``awarding_office``. Removed in 2.0.0."""
|
|
297
342
|
funding_agency: Agency | None = None
|
|
298
|
-
|
|
343
|
+
""".. deprecated:: Use ``funding_office``. Removed in 2.0.0."""
|
|
299
344
|
|
|
300
345
|
|
|
301
346
|
@dataclass
|
|
@@ -626,9 +671,7 @@ class WebhookSamplePayloadAllResponse(TypedDict):
|
|
|
626
671
|
note: str
|
|
627
672
|
|
|
628
673
|
|
|
629
|
-
WebhookSamplePayloadResponse =
|
|
630
|
-
WebhookSamplePayloadSingleResponse | WebhookSamplePayloadAllResponse
|
|
631
|
-
)
|
|
674
|
+
WebhookSamplePayloadResponse = WebhookSamplePayloadSingleResponse | WebhookSamplePayloadAllResponse
|
|
632
675
|
|
|
633
676
|
|
|
634
677
|
@dataclass
|
|
@@ -713,6 +756,90 @@ class ValidateResult:
|
|
|
713
756
|
errors: list[str] | None = None
|
|
714
757
|
|
|
715
758
|
|
|
759
|
+
@dataclass
|
|
760
|
+
class BudgetAccount:
|
|
761
|
+
"""Schema definition for BudgetAccount (not used for instances).
|
|
762
|
+
|
|
763
|
+
Federal account x fiscal year budget rollup. ``/api/budget/accounts/`` is
|
|
764
|
+
shape-on-demand: which fields appear depends on the ``?shape=`` query param,
|
|
765
|
+
so every field is optional. Mirrors the API ``BudgetAccount`` schema.
|
|
766
|
+
"""
|
|
767
|
+
|
|
768
|
+
id: int | None = None
|
|
769
|
+
federal_account_symbol: str | None = None
|
|
770
|
+
fiscal_year: int | None = None
|
|
771
|
+
agency_code: str | None = None
|
|
772
|
+
agency_name: str | None = None
|
|
773
|
+
bureau_name: str | None = None
|
|
774
|
+
account_title: str | None = None
|
|
775
|
+
bea_category: str | None = None
|
|
776
|
+
on_off_budget: str | None = None
|
|
777
|
+
subfunction_code: str | None = None
|
|
778
|
+
# Lifecycle
|
|
779
|
+
requested_ba: Decimal | None = None
|
|
780
|
+
enacted_ba: Decimal | None = None
|
|
781
|
+
apportioned: Decimal | None = None
|
|
782
|
+
obligated_total: Decimal | None = None
|
|
783
|
+
outlayed_total: Decimal | None = None
|
|
784
|
+
unobligated_balance: Decimal | None = None
|
|
785
|
+
# Contract / assistance / unlinked breakdown
|
|
786
|
+
contract_obligated: Decimal | None = None
|
|
787
|
+
contract_outlayed: Decimal | None = None
|
|
788
|
+
n_contracts: int | None = None
|
|
789
|
+
n_unique_contract_recipients: int | None = None
|
|
790
|
+
assistance_obligated: Decimal | None = None
|
|
791
|
+
assistance_outlayed: Decimal | None = None
|
|
792
|
+
n_grants: int | None = None
|
|
793
|
+
n_unique_grant_recipients: int | None = None
|
|
794
|
+
unlinked_obligated: Decimal | None = None
|
|
795
|
+
contract_share_of_obligated: Decimal | None = None
|
|
796
|
+
contract_share_of_obligated_capped: Decimal | None = None
|
|
797
|
+
contract_share_capped_flag: bool | None = None
|
|
798
|
+
assistance_share_of_obligated: Decimal | None = None
|
|
799
|
+
assistance_share_of_obligated_capped: Decimal | None = None
|
|
800
|
+
assistance_share_capped_flag: bool | None = None
|
|
801
|
+
# Forward-look
|
|
802
|
+
next_year_requested_ba: Decimal | None = None
|
|
803
|
+
ba_growth_next_year: Decimal | None = None
|
|
804
|
+
ba_growth_next_year_pct: Decimal | None = None
|
|
805
|
+
# Ratios
|
|
806
|
+
enacted_to_requested_pct: Decimal | None = None
|
|
807
|
+
enacted_to_requested_pct_capped: Decimal | None = None
|
|
808
|
+
enacted_to_requested_pct_capped_flag: bool | None = None
|
|
809
|
+
apportioned_to_enacted_pct: Decimal | None = None
|
|
810
|
+
apportioned_to_enacted_pct_capped: Decimal | None = None
|
|
811
|
+
apportioned_to_enacted_pct_capped_flag: bool | None = None
|
|
812
|
+
obligated_to_apportioned_pct: Decimal | None = None
|
|
813
|
+
obligated_to_apportioned_pct_capped: Decimal | None = None
|
|
814
|
+
obligated_to_apportioned_pct_capped_flag: bool | None = None
|
|
815
|
+
obligated_to_enacted_pct: Decimal | None = None
|
|
816
|
+
obligated_to_enacted_pct_capped: Decimal | None = None
|
|
817
|
+
obligated_to_enacted_pct_capped_flag: bool | None = None
|
|
818
|
+
outlayed_to_obligated_pct: Decimal | None = None
|
|
819
|
+
outlayed_to_obligated_pct_capped: Decimal | None = None
|
|
820
|
+
outlayed_to_obligated_pct_capped_flag: bool | None = None
|
|
821
|
+
unobligated_pct: Decimal | None = None
|
|
822
|
+
# Trends
|
|
823
|
+
enacted_ba_yoy_pct: Decimal | None = None
|
|
824
|
+
obligated_yoy_pct: Decimal | None = None
|
|
825
|
+
contract_obligated_yoy_pct: Decimal | None = None
|
|
826
|
+
enacted_ba_5yr_cagr: Decimal | None = None
|
|
827
|
+
contract_obligated_5yr_cagr: Decimal | None = None
|
|
828
|
+
# Request-vs-actual
|
|
829
|
+
requested_contractual_services: Decimal | None = None
|
|
830
|
+
requested_personnel_share: Decimal | None = None
|
|
831
|
+
actual_vs_requested_contract: Decimal | None = None
|
|
832
|
+
actual_vs_requested_contract_capped: Decimal | None = None
|
|
833
|
+
actual_vs_requested_contract_capped_flag: bool | None = None
|
|
834
|
+
# Provenance + narrative
|
|
835
|
+
appendix_pdf_url: str | None = None
|
|
836
|
+
account_narrative_excerpt: str | None = None
|
|
837
|
+
top_contract_recipients: list[Any] | None = None
|
|
838
|
+
top_grant_recipients: list[Any] | None = None
|
|
839
|
+
created: str | None = None
|
|
840
|
+
modified: str | None = None
|
|
841
|
+
|
|
842
|
+
|
|
716
843
|
@dataclass
|
|
717
844
|
class PaginatedResponse[T]:
|
|
718
845
|
"""Paginated API response
|
|
@@ -831,6 +958,18 @@ class ShapeConfig:
|
|
|
831
958
|
"key,piid,award_date,obligated,total_contract_value,description,recipient(display_name,uei)"
|
|
832
959
|
)
|
|
833
960
|
|
|
961
|
+
# Default for list_budget_accounts() / get_budget_account()
|
|
962
|
+
# Mirrors the API's BUDGET_ACCOUNT_DEFAULT_SHAPE.
|
|
963
|
+
BUDGET_ACCOUNTS_MINIMAL: Final = (
|
|
964
|
+
"id,federal_account_symbol,fiscal_year,agency_code,agency_name,bureau_name,"
|
|
965
|
+
"account_title,bea_category,on_off_budget,subfunction_code,"
|
|
966
|
+
"requested_ba,enacted_ba,apportioned,obligated_total,outlayed_total,"
|
|
967
|
+
"unobligated_balance,contract_obligated,contract_share_of_obligated_capped,"
|
|
968
|
+
"assistance_obligated,obligated_to_apportioned_pct_capped,"
|
|
969
|
+
"obligated_to_enacted_pct_capped,outlayed_to_obligated_pct_capped,"
|
|
970
|
+
"ba_growth_next_year_pct"
|
|
971
|
+
)
|
|
972
|
+
|
|
834
973
|
# Default for list_organizations()
|
|
835
974
|
ORGANIZATIONS_MINIMAL: Final = "key,fh_key,name,level,type,short_name"
|
|
836
975
|
|
tango/shapes/explicit_schemas.py
CHANGED
|
@@ -1315,12 +1315,8 @@ SUBAWARD_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1315
1315
|
"recipient_dba_name": FieldSchema(
|
|
1316
1316
|
name="recipient_dba_name", type=str, is_optional=True, is_list=False
|
|
1317
1317
|
),
|
|
1318
|
-
"recipient_duns": FieldSchema(
|
|
1319
|
-
|
|
1320
|
-
),
|
|
1321
|
-
"recipient_name": FieldSchema(
|
|
1322
|
-
name="recipient_name", type=str, is_optional=True, is_list=False
|
|
1323
|
-
),
|
|
1318
|
+
"recipient_duns": FieldSchema(name="recipient_duns", type=str, is_optional=True, is_list=False),
|
|
1319
|
+
"recipient_name": FieldSchema(name="recipient_name", type=str, is_optional=True, is_list=False),
|
|
1324
1320
|
"recipient_parent_duns": FieldSchema(
|
|
1325
1321
|
name="recipient_parent_duns", type=str, is_optional=True, is_list=False
|
|
1326
1322
|
),
|
|
@@ -1330,9 +1326,7 @@ SUBAWARD_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1330
1326
|
"recipient_parent_uei": FieldSchema(
|
|
1331
1327
|
name="recipient_parent_uei", type=str, is_optional=True, is_list=False
|
|
1332
1328
|
),
|
|
1333
|
-
"recipient_uei": FieldSchema(
|
|
1334
|
-
name="recipient_uei", type=str, is_optional=True, is_list=False
|
|
1335
|
-
),
|
|
1329
|
+
"recipient_uei": FieldSchema(name="recipient_uei", type=str, is_optional=True, is_list=False),
|
|
1336
1330
|
# Expandable nested objects
|
|
1337
1331
|
"awarding_office": FieldSchema(
|
|
1338
1332
|
name="awarding_office",
|
|
@@ -1493,6 +1487,10 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
|
|
|
1493
1487
|
"Location": LOCATION_SCHEMA,
|
|
1494
1488
|
"PlaceOfPerformance": PLACE_OF_PERFORMANCE_SCHEMA,
|
|
1495
1489
|
"Competition": COMPETITION_SCHEMA,
|
|
1490
|
+
# Alias: CONTRACT_SCHEMA/IDV_SCHEMA reference the competition leaf as
|
|
1491
|
+
# "ContractOrIDVCompetition" (the models.py dataclass name); it is the same
|
|
1492
|
+
# field set as Competition. Register both so nested shape selection resolves.
|
|
1493
|
+
"ContractOrIDVCompetition": COMPETITION_SCHEMA,
|
|
1496
1494
|
"ParentAward": PARENT_AWARD_SCHEMA,
|
|
1497
1495
|
"LegislativeMandates": LEGISLATIVE_MANDATES_SCHEMA,
|
|
1498
1496
|
"SubawardsSummary": SUBAWARDS_SUMMARY_SCHEMA,
|
tango/webhooks/cli.py
CHANGED
|
@@ -12,7 +12,7 @@ import json
|
|
|
12
12
|
import sys
|
|
13
13
|
import threading
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import Any, cast
|
|
16
16
|
|
|
17
17
|
try:
|
|
18
18
|
import click
|
|
@@ -197,7 +197,7 @@ def simulate_cmd(
|
|
|
197
197
|
from tango import TangoClient
|
|
198
198
|
|
|
199
199
|
client = TangoClient(api_key=api_key, base_url=base_url)
|
|
200
|
-
payload = client.get_webhook_sample_payload(event_type=event_type)
|
|
200
|
+
payload = cast("dict[str, Any]", client.get_webhook_sample_payload(event_type=event_type))
|
|
201
201
|
else:
|
|
202
202
|
payload = {"events": [{"event_type": "tango.cli.simulated"}]}
|
|
203
203
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tango-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Python SDK for the Tango API
|
|
5
5
|
Project-URL: Homepage, https://github.com/makegov/tango-python
|
|
6
6
|
Project-URL: Documentation, https://docs.makegov.com/tango-python
|
|
@@ -48,15 +48,16 @@ Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
|
48
48
|
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
|
|
49
49
|
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
50
50
|
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
51
|
-
Provides-Extra: notebooks
|
|
52
|
-
Requires-Dist: ipykernel>=6.25.0; extra == 'notebooks'
|
|
53
|
-
Requires-Dist: jupyter>=1.0.0; extra == 'notebooks'
|
|
54
51
|
Provides-Extra: webhooks
|
|
55
52
|
Requires-Dist: click>=8.1; extra == 'webhooks'
|
|
56
53
|
Description-Content-Type: text/markdown
|
|
57
54
|
|
|
58
55
|
# Tango Python SDK
|
|
59
56
|
|
|
57
|
+
[](https://pypi.org/project/tango-python/)
|
|
58
|
+
[](https://pypi.org/project/tango-python/)
|
|
59
|
+
[](LICENSE)
|
|
60
|
+
|
|
60
61
|
A modern Python SDK for the [Tango API](https://tango.makegov.com) by MakeGov, featuring dynamic response shaping and comprehensive type hints.
|
|
61
62
|
|
|
62
63
|
## Features
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
tango/__init__.py,sha256=
|
|
2
|
-
tango/client.py,sha256=
|
|
1
|
+
tango/__init__.py,sha256=3H_QVdoCe1ziPTZePlkIoA7j1jcZ11gotzZ2NslWms0,1912
|
|
2
|
+
tango/client.py,sha256=CAejxkwhyMwisVMGSdVqlKGkSNfNT7G60MtZb6BtEYk,143984
|
|
3
3
|
tango/exceptions.py,sha256=aRvDm0dUCEtNDfRVYCX7SEDdd1WlIVVY6sN78Tzo-a0,3114
|
|
4
|
-
tango/models.py,sha256
|
|
4
|
+
tango/models.py,sha256=QqUPtO7HJJDUaJDAMUkzvqR7pj1YIr8BetbS4palxc0,34261
|
|
5
5
|
tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
|
|
6
|
-
tango/shapes/explicit_schemas.py,sha256=
|
|
6
|
+
tango/shapes/explicit_schemas.py,sha256=8HgxKpAi2U80JP_MnABnW1JpYJA05aoOZSJsJIcyIWQ,71421
|
|
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
|
|
@@ -11,12 +11,12 @@ tango/shapes/parser.py,sha256=-2Ap5jgeAvKsKtA-MaXPGE6PRB93GPV8GK99Z0geW_s,27468
|
|
|
11
11
|
tango/shapes/schema.py,sha256=VRPOB1sBdjFyimNchrZKIpTHn83CyX4RfU9077aQtIU,14136
|
|
12
12
|
tango/shapes/types.py,sha256=27jrAE0VIdrKaLjR_FK71hfIIGX2Tg3ex7REEBV1TFE,1301
|
|
13
13
|
tango/webhooks/__init__.py,sha256=3bbiiGoB3s5iqqmQceroN0-MCSm-ZOZQx3M6JAknIUo,774
|
|
14
|
-
tango/webhooks/cli.py,sha256=
|
|
14
|
+
tango/webhooks/cli.py,sha256=f_vQbJ2AeSQjKnQo7MxHFyUB2SAKcgHYmGQULS0isqQ,13858
|
|
15
15
|
tango/webhooks/receiver.py,sha256=5yhsVhlLyoxmOCGvmbynWAIlDB2OaCPVf1H4GA1SxmU,9279
|
|
16
16
|
tango/webhooks/signing.py,sha256=92Ee-0B6PR7ZkvY3Np3gzl88-mtfKkh-I7lxqCe2lGw,2374
|
|
17
17
|
tango/webhooks/simulate.py,sha256=g2Osa0FYU5mJuon07T2aUCtmkUoTEzsY261tlp76fF0,3165
|
|
18
|
-
tango_python-1.
|
|
19
|
-
tango_python-1.
|
|
20
|
-
tango_python-1.
|
|
21
|
-
tango_python-1.
|
|
22
|
-
tango_python-1.
|
|
18
|
+
tango_python-1.1.1.dist-info/METADATA,sha256=lUFYfhe3_JPafL82HboUatKTDzWWVeemou58w5ofYFo,20599
|
|
19
|
+
tango_python-1.1.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
20
|
+
tango_python-1.1.1.dist-info/entry_points.txt,sha256=kGLUbglUjuaAqEFvOZ1QuSW0vWb6VeSpCIFKaOFkKoQ,50
|
|
21
|
+
tango_python-1.1.1.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
|
|
22
|
+
tango_python-1.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|