tango-python 1.3.0__py3-none-any.whl → 1.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 CHANGED
@@ -50,7 +50,7 @@ from .webhooks import (
50
50
  )
51
51
  from .webhooks.receiver import Delivery, WebhookReceiver
52
52
 
53
- __version__ = "1.3.0"
53
+ __version__ = "1.5.0"
54
54
  __all__ = [
55
55
  "TangoClient",
56
56
  "TangoAPIError",
tango/client.py CHANGED
@@ -491,6 +491,7 @@ class TangoClient:
491
491
  for ag in (self._parse_agency(agency) for agency in data["results"])
492
492
  if ag is not None
493
493
  ],
494
+ meta=data.get("meta"),
494
495
  )
495
496
 
496
497
  def get_agency(self, code: str) -> Agency:
@@ -521,6 +522,7 @@ class TangoClient:
521
522
  next=data.get("next"),
522
523
  previous=data.get("previous"),
523
524
  results=data.get("results", []),
525
+ meta=data.get("meta"),
524
526
  )
525
527
 
526
528
  def get_office(self, code: str) -> dict[str, Any]:
@@ -573,6 +575,7 @@ class TangoClient:
573
575
  next=data.get("next"),
574
576
  previous=data.get("previous"),
575
577
  results=results,
578
+ meta=data.get("meta"),
576
579
  )
577
580
 
578
581
  def get_organization(
@@ -785,6 +788,7 @@ class TangoClient:
785
788
  previous=data.get("previous"),
786
789
  results=results,
787
790
  cursor=data.get("cursor"),
791
+ meta=data.get("meta"),
788
792
  )
789
793
 
790
794
  def get_contract(
@@ -846,6 +850,7 @@ class TangoClient:
846
850
  previous=data.get("previous"),
847
851
  results=results,
848
852
  cursor=data.get("cursor"),
853
+ meta=data.get("meta"),
849
854
  )
850
855
 
851
856
  def get_contract_transactions(
@@ -868,6 +873,7 @@ class TangoClient:
868
873
  previous=data.get("previous"),
869
874
  results=data.get("results") or [],
870
875
  cursor=data.get("cursor"),
876
+ meta=data.get("meta"),
871
877
  )
872
878
 
873
879
  # ============================================================================
@@ -971,6 +977,7 @@ class TangoClient:
971
977
  previous=data.get("previous"),
972
978
  results=results,
973
979
  page_metadata=data.get("page_metadata"),
980
+ meta=data.get("meta"),
974
981
  )
975
982
 
976
983
  def get_idv(
@@ -1072,6 +1079,7 @@ class TangoClient:
1072
1079
  previous=data.get("previous"),
1073
1080
  results=results,
1074
1081
  page_metadata=data.get("page_metadata"),
1082
+ meta=data.get("meta"),
1075
1083
  )
1076
1084
 
1077
1085
  def list_idv_child_idvs(
@@ -1116,6 +1124,7 @@ class TangoClient:
1116
1124
  previous=data.get("previous"),
1117
1125
  results=results,
1118
1126
  page_metadata=data.get("page_metadata"),
1127
+ meta=data.get("meta"),
1119
1128
  )
1120
1129
 
1121
1130
  def list_idv_transactions(
@@ -1132,6 +1141,7 @@ class TangoClient:
1132
1141
  previous=data.get("previous"),
1133
1142
  results=data.get("results") or [],
1134
1143
  page_metadata=data.get("page_metadata"),
1144
+ meta=data.get("meta"),
1135
1145
  )
1136
1146
 
1137
1147
  def list_otas(
@@ -1214,6 +1224,7 @@ class TangoClient:
1214
1224
  results=results,
1215
1225
  cursor=data.get("cursor"),
1216
1226
  page_metadata=data.get("page_metadata"),
1227
+ meta=data.get("meta"),
1217
1228
  )
1218
1229
 
1219
1230
  def get_ota(
@@ -1319,6 +1330,7 @@ class TangoClient:
1319
1330
  results=results,
1320
1331
  cursor=data.get("cursor"),
1321
1332
  page_metadata=data.get("page_metadata"),
1333
+ meta=data.get("meta"),
1322
1334
  )
1323
1335
 
1324
1336
  def get_otidv(
@@ -1395,6 +1407,7 @@ class TangoClient:
1395
1407
  results=results,
1396
1408
  cursor=data.get("cursor"),
1397
1409
  page_metadata=data.get("page_metadata"),
1410
+ meta=data.get("meta"),
1398
1411
  )
1399
1412
 
1400
1413
  def list_subawards(
@@ -1449,6 +1462,7 @@ class TangoClient:
1449
1462
  next=data.get("next"),
1450
1463
  previous=data.get("previous"),
1451
1464
  results=results,
1465
+ meta=data.get("meta"),
1452
1466
  )
1453
1467
 
1454
1468
  def get_subaward(
@@ -1533,6 +1547,7 @@ class TangoClient:
1533
1547
  next=data.get("next"),
1534
1548
  previous=data.get("previous"),
1535
1549
  results=results,
1550
+ meta=data.get("meta"),
1536
1551
  )
1537
1552
 
1538
1553
  def get_gsa_elibrary_contract(
@@ -1639,6 +1654,7 @@ class TangoClient:
1639
1654
  next=data.get("next"),
1640
1655
  previous=data.get("previous"),
1641
1656
  results=results,
1657
+ meta=data.get("meta"),
1642
1658
  )
1643
1659
 
1644
1660
  def get_itdashboard_investment(
@@ -1808,6 +1824,7 @@ class TangoClient:
1808
1824
  next=data.get("next"),
1809
1825
  previous=data.get("previous"),
1810
1826
  results=results,
1827
+ meta=data.get("meta"),
1811
1828
  )
1812
1829
 
1813
1830
  def get_vehicle(
@@ -1889,6 +1906,7 @@ class TangoClient:
1889
1906
  next=data.get("next"),
1890
1907
  previous=data.get("previous"),
1891
1908
  results=results,
1909
+ meta=data.get("meta"),
1892
1910
  )
1893
1911
 
1894
1912
  def list_vehicle_orders(
@@ -1937,6 +1955,7 @@ class TangoClient:
1937
1955
  next=data.get("next"),
1938
1956
  previous=data.get("previous"),
1939
1957
  results=results,
1958
+ meta=data.get("meta"),
1940
1959
  )
1941
1960
 
1942
1961
  # Business Types endpoints
@@ -1971,6 +1990,7 @@ class TangoClient:
1971
1990
  next=data.get("next"),
1972
1991
  previous=data.get("previous"),
1973
1992
  results=results,
1993
+ meta=data.get("meta"),
1974
1994
  )
1975
1995
 
1976
1996
  def list_naics(
@@ -2022,6 +2042,7 @@ class TangoClient:
2022
2042
  next=data.get("next"),
2023
2043
  previous=data.get("previous"),
2024
2044
  results=data.get("results", []),
2045
+ meta=data.get("meta"),
2025
2046
  )
2026
2047
 
2027
2048
  # Entity endpoints
@@ -2108,6 +2129,7 @@ class TangoClient:
2108
2129
  next=data.get("next"),
2109
2130
  previous=data.get("previous"),
2110
2131
  results=results,
2132
+ meta=data.get("meta"),
2111
2133
  )
2112
2134
 
2113
2135
  def get_entity(
@@ -2164,6 +2186,7 @@ class TangoClient:
2164
2186
  next=data.get("next"),
2165
2187
  previous=data.get("previous"),
2166
2188
  results=list(data.get("results") or []),
2189
+ meta=data.get("meta"),
2167
2190
  )
2168
2191
 
2169
2192
  # Forecast endpoints
@@ -2256,6 +2279,7 @@ class TangoClient:
2256
2279
  next=data.get("next"),
2257
2280
  previous=data.get("previous"),
2258
2281
  results=results,
2282
+ meta=data.get("meta"),
2259
2283
  )
2260
2284
 
2261
2285
  def get_forecast(
@@ -2379,6 +2403,7 @@ class TangoClient:
2379
2403
  next=data.get("next"),
2380
2404
  previous=data.get("previous"),
2381
2405
  results=results,
2406
+ meta=data.get("meta"),
2382
2407
  )
2383
2408
 
2384
2409
  def get_opportunity(
@@ -2493,6 +2518,7 @@ class TangoClient:
2493
2518
  next=data.get("next"),
2494
2519
  previous=data.get("previous"),
2495
2520
  results=results,
2521
+ meta=data.get("meta"),
2496
2522
  )
2497
2523
 
2498
2524
  def get_notice(
@@ -2540,6 +2566,7 @@ class TangoClient:
2540
2566
  filed_date_before: str | None = None,
2541
2567
  decision_date_after: str | None = None,
2542
2568
  decision_date_before: str | None = None,
2569
+ naics_code: str | None = None,
2543
2570
  search: str | None = None,
2544
2571
  ) -> PaginatedResponse:
2545
2572
  """
@@ -2569,6 +2596,7 @@ class TangoClient:
2569
2596
  filed_date_before: Filed date on or before
2570
2597
  decision_date_after: Decision date on or after
2571
2598
  decision_date_before: Decision date on or before
2599
+ naics_code: Filter by the solicitation's NAICS code (e.g. 541519)
2572
2600
  search: Full-text search over protest searchable fields
2573
2601
  """
2574
2602
  params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
@@ -2594,6 +2622,7 @@ class TangoClient:
2594
2622
  ("filed_date_before", filed_date_before),
2595
2623
  ("decision_date_after", decision_date_after),
2596
2624
  ("decision_date_before", decision_date_before),
2625
+ ("naics_code", naics_code),
2597
2626
  ("search", search),
2598
2627
  ):
2599
2628
  if val is not None:
@@ -2611,6 +2640,7 @@ class TangoClient:
2611
2640
  next=data.get("next"),
2612
2641
  previous=data.get("previous"),
2613
2642
  results=results,
2643
+ meta=data.get("meta"),
2614
2644
  )
2615
2645
 
2616
2646
  def get_protest(
@@ -2747,6 +2777,7 @@ class TangoClient:
2747
2777
  next=data.get("next"),
2748
2778
  previous=data.get("previous"),
2749
2779
  results=results,
2780
+ meta=data.get("meta"),
2750
2781
  )
2751
2782
 
2752
2783
  def get_dibbs_rfq(
@@ -2866,6 +2897,7 @@ class TangoClient:
2866
2897
  next=data.get("next"),
2867
2898
  previous=data.get("previous"),
2868
2899
  results=results,
2900
+ meta=data.get("meta"),
2869
2901
  )
2870
2902
 
2871
2903
  def get_dibbs_rfp(
@@ -3003,6 +3035,7 @@ class TangoClient:
3003
3035
  next=data.get("next"),
3004
3036
  previous=data.get("previous"),
3005
3037
  results=results,
3038
+ meta=data.get("meta"),
3006
3039
  )
3007
3040
 
3008
3041
  def get_dibbs_award(
@@ -3148,6 +3181,7 @@ class TangoClient:
3148
3181
  next=data.get("next"),
3149
3182
  previous=data.get("previous"),
3150
3183
  results=results,
3184
+ meta=data.get("meta"),
3151
3185
  )
3152
3186
 
3153
3187
  def get_exclusion(
@@ -3275,6 +3309,7 @@ class TangoClient:
3275
3309
  next=data.get("next"),
3276
3310
  previous=data.get("previous"),
3277
3311
  results=results,
3312
+ meta=data.get("meta"),
3278
3313
  )
3279
3314
 
3280
3315
  def get_sbir_topic(
@@ -3395,6 +3430,7 @@ class TangoClient:
3395
3430
  next=data.get("next"),
3396
3431
  previous=data.get("previous"),
3397
3432
  results=results,
3433
+ meta=data.get("meta"),
3398
3434
  )
3399
3435
 
3400
3436
  def get_sbir_solicitation(
@@ -3769,6 +3805,7 @@ class TangoClient:
3769
3805
  next=data.get("next"),
3770
3806
  previous=data.get("previous"),
3771
3807
  results=results,
3808
+ meta=data.get("meta"),
3772
3809
  )
3773
3810
 
3774
3811
  def get_budget_account(
@@ -3820,6 +3857,7 @@ class TangoClient:
3820
3857
  next=data.get("next"),
3821
3858
  previous=data.get("previous"),
3822
3859
  results=data.get("results") or [],
3860
+ meta=data.get("meta"),
3823
3861
  )
3824
3862
 
3825
3863
  def get_budget_account_recipients(
@@ -3847,6 +3885,7 @@ class TangoClient:
3847
3885
  next=data.get("next"),
3848
3886
  previous=data.get("previous"),
3849
3887
  results=data.get("results") or [],
3888
+ meta=data.get("meta"),
3850
3889
  )
3851
3890
 
3852
3891
  # Grant endpoints
@@ -3940,6 +3979,7 @@ class TangoClient:
3940
3979
  next=data.get("next"),
3941
3980
  previous=data.get("previous"),
3942
3981
  results=results,
3982
+ meta=data.get("meta"),
3943
3983
  )
3944
3984
 
3945
3985
  def get_grant(
@@ -4032,6 +4072,7 @@ class TangoClient:
4032
4072
  next=data.get("next"),
4033
4073
  previous=data.get("previous"),
4034
4074
  results=results,
4075
+ meta=data.get("meta"),
4035
4076
  )
4036
4077
 
4037
4078
  def create_webhook_endpoint(
@@ -4216,6 +4257,7 @@ class TangoClient:
4216
4257
  next=data.get("next"),
4217
4258
  previous=data.get("previous"),
4218
4259
  results=results,
4260
+ meta=data.get("meta"),
4219
4261
  )
4220
4262
 
4221
4263
  def get_webhook_alert(self, alert_id: str) -> WebhookAlert:
@@ -4430,6 +4472,7 @@ class TangoClient:
4430
4472
  next=data.get("next"),
4431
4473
  previous=data.get("previous"),
4432
4474
  results=list(data.get("results") or []),
4475
+ meta=data.get("meta"),
4433
4476
  )
4434
4477
 
4435
4478
  def get_department(self, code: str) -> dict[str, Any]:
@@ -4445,8 +4488,13 @@ class TangoClient:
4445
4488
  shape: str | None = None,
4446
4489
  flat: bool = False,
4447
4490
  flat_lists: bool = False,
4491
+ has_awards: bool | None = None,
4448
4492
  ) -> PaginatedResponse[dict[str, Any]]:
4449
- """List Product Service Codes (`/api/psc/`)."""
4493
+ """List Product Service Codes (`/api/psc/`).
4494
+
4495
+ Args:
4496
+ has_awards: When True, return only codes with contract award history.
4497
+ """
4450
4498
  params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
4451
4499
  if shape:
4452
4500
  params["shape"] = shape
@@ -4454,12 +4502,15 @@ class TangoClient:
4454
4502
  params["flat"] = "true"
4455
4503
  if flat_lists:
4456
4504
  params["flat_lists"] = "true"
4505
+ if has_awards is not None:
4506
+ params["has_awards"] = "true" if has_awards else "false"
4457
4507
  data = self._get("/api/psc/", params)
4458
4508
  return PaginatedResponse(
4459
4509
  count=int(data.get("count", 0)),
4460
4510
  next=data.get("next"),
4461
4511
  previous=data.get("previous"),
4462
4512
  results=list(data.get("results") or []),
4513
+ meta=data.get("meta"),
4463
4514
  )
4464
4515
 
4465
4516
  def get_psc(
@@ -4560,6 +4611,7 @@ class TangoClient:
4560
4611
  next=data.get("next"),
4561
4612
  previous=data.get("previous"),
4562
4613
  results=list(data.get("results") or []),
4614
+ meta=data.get("meta"),
4563
4615
  )
4564
4616
 
4565
4617
  def get_assistance_listing(
@@ -4606,6 +4658,7 @@ class TangoClient:
4606
4658
  next=data.get("next"),
4607
4659
  previous=data.get("previous"),
4608
4660
  results=list(data.get("results") or []),
4661
+ meta=data.get("meta"),
4609
4662
  )
4610
4663
 
4611
4664
  def get_mas_sin(
@@ -4675,6 +4728,7 @@ class TangoClient:
4675
4728
  results=results,
4676
4729
  cursor=data.get("cursor"),
4677
4730
  page_metadata=data.get("page_metadata"),
4731
+ meta=data.get("meta"),
4678
4732
  )
4679
4733
 
4680
4734
  def list_entity_contracts(
@@ -4850,6 +4904,7 @@ class TangoClient:
4850
4904
  next=data.get("next"),
4851
4905
  previous=data.get("previous"),
4852
4906
  results=results,
4907
+ meta=data.get("meta"),
4853
4908
  )
4854
4909
 
4855
4910
  def list_entity_lcats(
@@ -4880,6 +4935,7 @@ class TangoClient:
4880
4935
  next=data.get("next"),
4881
4936
  previous=data.get("previous"),
4882
4937
  results=list(data.get("results") or []),
4938
+ meta=data.get("meta"),
4883
4939
  )
4884
4940
 
4885
4941
  def get_entity_metrics(self, uei: str, months: int, period_grouping: str) -> dict[str, Any]:
@@ -4920,6 +4976,7 @@ class TangoClient:
4920
4976
  next=data.get("next"),
4921
4977
  previous=data.get("previous"),
4922
4978
  results=list(data.get("results") or []),
4979
+ meta=data.get("meta"),
4923
4980
  )
4924
4981
 
4925
4982
  # ============================================================================
@@ -4977,6 +5034,7 @@ class TangoClient:
4977
5034
  results=results,
4978
5035
  cursor=data.get("cursor"),
4979
5036
  page_metadata=data.get("page_metadata"),
5037
+ meta=data.get("meta"),
4980
5038
  )
4981
5039
 
4982
5040
  def list_agency_awarding_contracts(
tango/models.py CHANGED
@@ -1044,7 +1044,15 @@ class PaginatedResponse[T]:
1044
1044
  previous: URL for the previous page of results (None if first page)
1045
1045
  results: List of result items (type depends on shape parameter)
1046
1046
  cursor: Cursor token for cursor-based pagination (None if not available)
1047
- page_metadata: Optional metadata about the current page
1047
+ meta: Response-level metadata the API attached to this page, when present.
1048
+ Currently carries agency-filter diagnostics: ``resolved_filters`` maps
1049
+ each agency filter to the organizations its ``|``-separated tokens
1050
+ resolved to (or ``None``), and ``warnings`` lists human-readable notes
1051
+ about tokens that were dropped or matched loosely. See
1052
+ :meth:`agency_warnings` and :meth:`unresolved_agency_tokens`.
1053
+ page_metadata: Always ``None`` — the API has never emitted a
1054
+ ``page_metadata`` key. Retained so existing attribute access keeps
1055
+ working; use ``meta`` instead.
1048
1056
 
1049
1057
  Examples:
1050
1058
  >>> from tango import TangoClient, ShapeConfig
@@ -1063,8 +1071,92 @@ class PaginatedResponse[T]:
1063
1071
  previous: str | None
1064
1072
  results: list[T]
1065
1073
  cursor: str | None = None
1074
+ meta: dict[str, Any] | None = None
1066
1075
  page_metadata: dict[str, Any] | None = None
1067
1076
 
1077
+ @property
1078
+ def agency_warnings(self) -> list[str]:
1079
+ """Warnings the API raised about agency filters on this request.
1080
+
1081
+ Empty when every supplied agency token resolved cleanly. A non-empty list
1082
+ means part of the filter did not apply, so a small or empty ``results`` is
1083
+ not evidence that no such records exist.
1084
+
1085
+ Examples:
1086
+ >>> response = client.list_contracts(awarding_agency="GSA|NOTANAGENCY")
1087
+ >>> for warning in response.agency_warnings:
1088
+ ... print(warning)
1089
+ Agency filter 'awarding_agency': 'NOTANAGENCY' did not match any organization and was ignored.
1090
+ """
1091
+ if not self.meta:
1092
+ return []
1093
+ warnings = self.meta.get("warnings")
1094
+ return list(warnings) if isinstance(warnings, list) else []
1095
+
1096
+ @property
1097
+ def unresolved_agency_tokens(self) -> dict[str, list[str]]:
1098
+ """Agency tokens that matched no organization, keyed by filter name.
1099
+
1100
+ Empty when everything resolved. Use this to fail loudly in a pipeline
1101
+ rather than treating a silently-narrowed result set as an answer.
1102
+
1103
+ Examples:
1104
+ >>> response = client.list_contracts(awarding_agency="GSA|NOTANAGENCY")
1105
+ >>> response.unresolved_agency_tokens
1106
+ {'awarding_agency': ['NOTANAGENCY']}
1107
+ """
1108
+ if not self.meta:
1109
+ return {}
1110
+ resolved = self.meta.get("resolved_filters")
1111
+ if not isinstance(resolved, dict):
1112
+ return {}
1113
+ dropped: dict[str, list[str]] = {}
1114
+ for filter_name, entries in resolved.items():
1115
+ if not isinstance(entries, list):
1116
+ continue
1117
+ tokens = [
1118
+ entry["token"]
1119
+ for entry in entries
1120
+ if isinstance(entry, dict)
1121
+ and entry.get("resolved") is None
1122
+ and entry.get("token") is not None
1123
+ ]
1124
+ if tokens:
1125
+ dropped[filter_name] = tokens
1126
+ return dropped
1127
+
1128
+ @property
1129
+ def resolved_agencies(self) -> dict[str, list[dict[str, Any]]]:
1130
+ """What each agency token actually resolved to, keyed by filter name.
1131
+
1132
+ Agency resolution is fuzzy, so a token can match an organization the caller
1133
+ did not intend and scope the query to that subtree instead. Checking the
1134
+ resolved ``name`` is the only way to catch that from the client side — an
1135
+ unresolved-token check cannot, because nothing was dropped.
1136
+
1137
+ Examples:
1138
+ >>> response = client.list_contracts(awarding_agency="HUD")
1139
+ >>> [org["name"] for org in response.resolved_agencies["awarding_agency"]]
1140
+ ['Department of Housing and Urban Development']
1141
+ """
1142
+ if not self.meta:
1143
+ return {}
1144
+ resolved = self.meta.get("resolved_filters")
1145
+ if not isinstance(resolved, dict):
1146
+ return {}
1147
+ matched: dict[str, list[dict[str, Any]]] = {}
1148
+ for filter_name, entries in resolved.items():
1149
+ if not isinstance(entries, list):
1150
+ continue
1151
+ orgs = [
1152
+ entry["resolved"]
1153
+ for entry in entries
1154
+ if isinstance(entry, dict) and isinstance(entry.get("resolved"), dict)
1155
+ ]
1156
+ if orgs:
1157
+ matched[filter_name] = orgs
1158
+ return matched
1159
+
1068
1160
 
1069
1161
  class ShapeConfig:
1070
1162
  """Predefined response shape configurations used as defaults for API methods