tango-python 0.3.0__py3-none-any.whl → 0.4.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
@@ -26,7 +26,7 @@ from .shapes import (
26
26
  TypeGenerator,
27
27
  )
28
28
 
29
- __version__ = "0.3.0"
29
+ __version__ = "0.4.0"
30
30
  __all__ = [
31
31
  "TangoClient",
32
32
  "TangoAPIError",
tango/client.py CHANGED
@@ -17,6 +17,8 @@ from tango.exceptions import (
17
17
  )
18
18
  from tango.models import (
19
19
  IDV,
20
+ OTA,
21
+ OTIDV,
20
22
  Agency,
21
23
  BusinessType,
22
24
  Contract,
@@ -26,9 +28,11 @@ from tango.models import (
26
28
  Location,
27
29
  Notice,
28
30
  Opportunity,
31
+ Organization,
29
32
  PaginatedResponse,
30
33
  SearchFilters,
31
34
  ShapeConfig,
35
+ Subaward,
32
36
  Vehicle,
33
37
  WebhookEndpoint,
34
38
  WebhookEventType,
@@ -362,9 +366,13 @@ class TangoClient:
362
366
  # ============================================================================
363
367
 
364
368
  # Agency endpoints
365
- def list_agencies(self, page: int = 1, limit: int = 25) -> PaginatedResponse:
369
+ def list_agencies(
370
+ self, page: int = 1, limit: int = 25, search: str | None = None
371
+ ) -> PaginatedResponse:
366
372
  """List all agencies"""
367
- params = {"page": page, "limit": min(limit, 100)}
373
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
374
+ if search:
375
+ params["search"] = search
368
376
  data = self._get("/api/agencies/", params)
369
377
  return PaginatedResponse(
370
378
  count=data["count"],
@@ -389,6 +397,96 @@ class TangoClient:
389
397
  raise TangoNotFoundError(f"Agency '{code}' not found", 404)
390
398
  return agency
391
399
 
400
+ def list_offices(
401
+ self,
402
+ page: int = 1,
403
+ limit: int = 25,
404
+ search: str | None = None,
405
+ ) -> PaginatedResponse:
406
+ """List offices (`/api/offices/`)."""
407
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
408
+ if search is not None:
409
+ params["search"] = search
410
+ data = self._get("/api/offices/", params)
411
+ return PaginatedResponse(
412
+ count=data.get("count", 0),
413
+ next=data.get("next"),
414
+ previous=data.get("previous"),
415
+ results=data.get("results", []),
416
+ )
417
+
418
+ def get_office(self, code: str) -> dict[str, Any]:
419
+ """Get a single office by code (`/api/offices/{code}/`)."""
420
+ return self._get(f"/api/offices/{code}/")
421
+
422
+ def list_organizations(
423
+ self,
424
+ page: int = 1,
425
+ limit: int = 25,
426
+ shape: str | None = None,
427
+ flat: bool = False,
428
+ flat_lists: bool = False,
429
+ cgac: str | None = None,
430
+ include_inactive: bool | None = None,
431
+ level: int | None = None,
432
+ parent: str | None = None,
433
+ search: str | None = None,
434
+ type: str | None = None,
435
+ ) -> PaginatedResponse:
436
+ """List organizations (`/api/organizations/`)."""
437
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
438
+ if shape is None:
439
+ shape = ShapeConfig.ORGANIZATIONS_MINIMAL
440
+ if shape:
441
+ params["shape"] = shape
442
+ if flat:
443
+ params["flat"] = "true"
444
+ if flat_lists:
445
+ params["flat_lists"] = "true"
446
+ if cgac is not None:
447
+ params["cgac"] = cgac
448
+ if include_inactive is not None:
449
+ params["include_inactive"] = include_inactive
450
+ if level is not None:
451
+ params["level"] = level
452
+ if parent is not None:
453
+ params["parent"] = parent
454
+ if search is not None:
455
+ params["search"] = search
456
+ if type is not None:
457
+ params["type"] = type
458
+ data = self._get("/api/organizations/", params)
459
+ results = [
460
+ self._parse_response_with_shape(obj, shape, Organization, flat, flat_lists)
461
+ for obj in data.get("results", [])
462
+ ]
463
+ return PaginatedResponse(
464
+ count=data.get("count", 0),
465
+ next=data.get("next"),
466
+ previous=data.get("previous"),
467
+ results=results,
468
+ )
469
+
470
+ def get_organization(
471
+ self,
472
+ fh_key: str,
473
+ shape: str | None = None,
474
+ flat: bool = False,
475
+ flat_lists: bool = False,
476
+ ) -> Any:
477
+ """Get a single organization by fh_key (`/api/organizations/{fh_key}/`)."""
478
+ params: dict[str, Any] = {}
479
+ if shape is None:
480
+ shape = ShapeConfig.ORGANIZATIONS_MINIMAL
481
+ if shape:
482
+ params["shape"] = shape
483
+ if flat:
484
+ params["flat"] = "true"
485
+ if flat_lists:
486
+ params["flat_lists"] = "true"
487
+ data = self._get(f"/api/organizations/{fh_key}/", params)
488
+ return self._parse_response_with_shape(data, shape, Organization, flat, flat_lists)
489
+
392
490
  # Contract endpoints
393
491
  def list_contracts(
394
492
  self,
@@ -398,7 +496,7 @@ class TangoClient:
398
496
  flat: bool = False,
399
497
  flat_lists: bool = False,
400
498
  filters: SearchFilters | dict[str, Any] | None = None,
401
- **kwargs,
499
+ **kwargs: Any,
402
500
  ) -> PaginatedResponse:
403
501
  """
404
502
  List contracts with optional filtering
@@ -600,7 +698,7 @@ class TangoClient:
600
698
  flat: bool = False,
601
699
  flat_lists: bool = False,
602
700
  joiner: str = ".",
603
- **filters,
701
+ **filters: Any,
604
702
  ) -> PaginatedResponse:
605
703
  """
606
704
  List IDVs (indefinite delivery vehicles) with keyset pagination.
@@ -675,7 +773,7 @@ class TangoClient:
675
773
  flat_lists: bool = False,
676
774
  joiner: str = ".",
677
775
  filters: SearchFilters | dict[str, Any] | None = None,
678
- **kwargs,
776
+ **kwargs: Any,
679
777
  ) -> PaginatedResponse:
680
778
  """
681
779
  List child awards (contracts) under an IDV (`/api/idvs/{key}/awards/`).
@@ -751,7 +849,7 @@ class TangoClient:
751
849
  flat: bool = False,
752
850
  flat_lists: bool = False,
753
851
  joiner: str = ".",
754
- **filters,
852
+ **filters: Any,
755
853
  ) -> PaginatedResponse:
756
854
  """List child IDVs under an IDV (`/api/idvs/{key}/idvs/`)."""
757
855
  params: dict[str, Any] = {"limit": min(limit, 100)}
@@ -828,6 +926,268 @@ class TangoClient:
828
926
  page_metadata=data.get("page_metadata"),
829
927
  )
830
928
 
929
+ def list_otas(
930
+ self,
931
+ limit: int = 25,
932
+ cursor: str | None = None,
933
+ shape: str | None = None,
934
+ flat: bool = False,
935
+ flat_lists: bool = False,
936
+ joiner: str = ".",
937
+ award_date: str | None = None,
938
+ award_date_gte: str | None = None,
939
+ award_date_lte: str | None = None,
940
+ awarding_agency: str | None = None,
941
+ expiring_gte: str | None = None,
942
+ expiring_lte: str | None = None,
943
+ fiscal_year: int | None = None,
944
+ fiscal_year_gte: int | None = None,
945
+ fiscal_year_lte: int | None = None,
946
+ funding_agency: str | None = None,
947
+ ordering: str | None = None,
948
+ piid: str | None = None,
949
+ pop_end_date_gte: str | None = None,
950
+ pop_end_date_lte: str | None = None,
951
+ pop_start_date_gte: str | None = None,
952
+ pop_start_date_lte: str | None = None,
953
+ psc: str | None = None,
954
+ recipient: str | None = None,
955
+ search: str | None = None,
956
+ uei: str | None = None,
957
+ ) -> PaginatedResponse:
958
+ """List OTAs (Other Transaction Agreements) (`/api/otas/`). Keyset pagination."""
959
+ params: dict[str, Any] = {"limit": min(limit, 100)}
960
+ if cursor:
961
+ params["cursor"] = cursor
962
+ if shape is None:
963
+ shape = ShapeConfig.OTAS_MINIMAL
964
+ if shape:
965
+ params["shape"] = shape
966
+ if flat:
967
+ params["flat"] = "true"
968
+ if joiner:
969
+ params["joiner"] = joiner
970
+ if flat_lists:
971
+ params["flat_lists"] = "true"
972
+ for key, val in (
973
+ ("award_date", award_date),
974
+ ("award_date_gte", award_date_gte),
975
+ ("award_date_lte", award_date_lte),
976
+ ("awarding_agency", awarding_agency),
977
+ ("expiring_gte", expiring_gte),
978
+ ("expiring_lte", expiring_lte),
979
+ ("fiscal_year", fiscal_year),
980
+ ("fiscal_year_gte", fiscal_year_gte),
981
+ ("fiscal_year_lte", fiscal_year_lte),
982
+ ("funding_agency", funding_agency),
983
+ ("ordering", ordering),
984
+ ("piid", piid),
985
+ ("pop_end_date_gte", pop_end_date_gte),
986
+ ("pop_end_date_lte", pop_end_date_lte),
987
+ ("pop_start_date_gte", pop_start_date_gte),
988
+ ("pop_start_date_lte", pop_start_date_lte),
989
+ ("psc", psc),
990
+ ("recipient", recipient),
991
+ ("search", search),
992
+ ("uei", uei),
993
+ ):
994
+ if val is not None:
995
+ params[key] = val
996
+ data = self._get("/api/otas/", params)
997
+ raw_results = data.get("results") or []
998
+ results = [
999
+ self._parse_response_with_shape(obj, shape, OTA, flat, flat_lists, joiner=joiner)
1000
+ for obj in raw_results
1001
+ ]
1002
+ return PaginatedResponse(
1003
+ count=int(data.get("count") or len(results)),
1004
+ next=data.get("next"),
1005
+ previous=data.get("previous"),
1006
+ results=results,
1007
+ cursor=data.get("cursor"),
1008
+ page_metadata=data.get("page_metadata"),
1009
+ )
1010
+
1011
+ def get_ota(
1012
+ self,
1013
+ key: str,
1014
+ shape: str | None = None,
1015
+ flat: bool = False,
1016
+ flat_lists: bool = False,
1017
+ joiner: str = ".",
1018
+ ) -> Any:
1019
+ """Get a single OTA by key (`/api/otas/{key}/`)."""
1020
+ params: dict[str, Any] = {}
1021
+ if shape is None:
1022
+ shape = ShapeConfig.OTAS_MINIMAL
1023
+ if shape:
1024
+ params["shape"] = shape
1025
+ if flat:
1026
+ params["flat"] = "true"
1027
+ if joiner:
1028
+ params["joiner"] = joiner
1029
+ if flat_lists:
1030
+ params["flat_lists"] = "true"
1031
+ data = self._get(f"/api/otas/{key}/", params)
1032
+ return self._parse_response_with_shape(data, shape, OTA, flat, flat_lists, joiner=joiner)
1033
+
1034
+ def list_otidvs(
1035
+ self,
1036
+ limit: int = 25,
1037
+ cursor: str | None = None,
1038
+ shape: str | None = None,
1039
+ flat: bool = False,
1040
+ flat_lists: bool = False,
1041
+ joiner: str = ".",
1042
+ award_date: str | None = None,
1043
+ award_date_gte: str | None = None,
1044
+ award_date_lte: str | None = None,
1045
+ awarding_agency: str | None = None,
1046
+ expiring_gte: str | None = None,
1047
+ expiring_lte: str | None = None,
1048
+ fiscal_year: int | None = None,
1049
+ fiscal_year_gte: int | None = None,
1050
+ fiscal_year_lte: int | None = None,
1051
+ funding_agency: str | None = None,
1052
+ ordering: str | None = None,
1053
+ piid: str | None = None,
1054
+ pop_end_date_gte: str | None = None,
1055
+ pop_end_date_lte: str | None = None,
1056
+ pop_start_date_gte: str | None = None,
1057
+ pop_start_date_lte: str | None = None,
1058
+ psc: str | None = None,
1059
+ recipient: str | None = None,
1060
+ search: str | None = None,
1061
+ uei: str | None = None,
1062
+ ) -> PaginatedResponse:
1063
+ """List OTIDVs (Other Transaction IDVs) (`/api/otidvs/`). Keyset pagination."""
1064
+ params: dict[str, Any] = {"limit": min(limit, 100)}
1065
+ if cursor:
1066
+ params["cursor"] = cursor
1067
+ if shape is None:
1068
+ shape = ShapeConfig.OTIDVS_MINIMAL
1069
+ if shape:
1070
+ params["shape"] = shape
1071
+ if flat:
1072
+ params["flat"] = "true"
1073
+ if joiner:
1074
+ params["joiner"] = joiner
1075
+ if flat_lists:
1076
+ params["flat_lists"] = "true"
1077
+ for key, val in (
1078
+ ("award_date", award_date),
1079
+ ("award_date_gte", award_date_gte),
1080
+ ("award_date_lte", award_date_lte),
1081
+ ("awarding_agency", awarding_agency),
1082
+ ("expiring_gte", expiring_gte),
1083
+ ("expiring_lte", expiring_lte),
1084
+ ("fiscal_year", fiscal_year),
1085
+ ("fiscal_year_gte", fiscal_year_gte),
1086
+ ("fiscal_year_lte", fiscal_year_lte),
1087
+ ("funding_agency", funding_agency),
1088
+ ("ordering", ordering),
1089
+ ("piid", piid),
1090
+ ("pop_end_date_gte", pop_end_date_gte),
1091
+ ("pop_end_date_lte", pop_end_date_lte),
1092
+ ("pop_start_date_gte", pop_start_date_gte),
1093
+ ("pop_start_date_lte", pop_start_date_lte),
1094
+ ("psc", psc),
1095
+ ("recipient", recipient),
1096
+ ("search", search),
1097
+ ("uei", uei),
1098
+ ):
1099
+ if val is not None:
1100
+ params[key] = val
1101
+ data = self._get("/api/otidvs/", params)
1102
+ raw_results = data.get("results") or []
1103
+ results = [
1104
+ self._parse_response_with_shape(obj, shape, OTIDV, flat, flat_lists, joiner=joiner)
1105
+ for obj in raw_results
1106
+ ]
1107
+ return PaginatedResponse(
1108
+ count=int(data.get("count") or len(results)),
1109
+ next=data.get("next"),
1110
+ previous=data.get("previous"),
1111
+ results=results,
1112
+ cursor=data.get("cursor"),
1113
+ page_metadata=data.get("page_metadata"),
1114
+ )
1115
+
1116
+ def get_otidv(
1117
+ self,
1118
+ key: str,
1119
+ shape: str | None = None,
1120
+ flat: bool = False,
1121
+ flat_lists: bool = False,
1122
+ joiner: str = ".",
1123
+ ) -> Any:
1124
+ """Get a single OTIDV by key (`/api/otidvs/{key}/`)."""
1125
+ params: dict[str, Any] = {}
1126
+ if shape is None:
1127
+ shape = ShapeConfig.OTIDVS_MINIMAL
1128
+ if shape:
1129
+ params["shape"] = shape
1130
+ if flat:
1131
+ params["flat"] = "true"
1132
+ if joiner:
1133
+ params["joiner"] = joiner
1134
+ if flat_lists:
1135
+ params["flat_lists"] = "true"
1136
+ data = self._get(f"/api/otidvs/{key}/", params)
1137
+ return self._parse_response_with_shape(data, shape, OTIDV, flat, flat_lists, joiner=joiner)
1138
+
1139
+ def list_subawards(
1140
+ self,
1141
+ page: int = 1,
1142
+ limit: int = 25,
1143
+ shape: str | None = None,
1144
+ flat: bool = False,
1145
+ flat_lists: bool = False,
1146
+ award_key: str | None = None,
1147
+ awarding_agency: str | None = None,
1148
+ fiscal_year: int | None = None,
1149
+ fiscal_year_gte: int | None = None,
1150
+ fiscal_year_lte: int | None = None,
1151
+ funding_agency: str | None = None,
1152
+ prime_uei: str | None = None,
1153
+ recipient: str | None = None,
1154
+ sub_uei: str | None = None,
1155
+ ) -> PaginatedResponse:
1156
+ """List subawards (`/api/subawards/`)."""
1157
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1158
+ if shape is None:
1159
+ shape = ShapeConfig.SUBAWARDS_MINIMAL
1160
+ if shape:
1161
+ params["shape"] = shape
1162
+ if flat:
1163
+ params["flat"] = "true"
1164
+ if flat_lists:
1165
+ params["flat_lists"] = "true"
1166
+ for key, val in (
1167
+ ("award_key", award_key),
1168
+ ("awarding_agency", awarding_agency),
1169
+ ("fiscal_year", fiscal_year),
1170
+ ("fiscal_year_gte", fiscal_year_gte),
1171
+ ("fiscal_year_lte", fiscal_year_lte),
1172
+ ("funding_agency", funding_agency),
1173
+ ("prime_uei", prime_uei),
1174
+ ("recipient", recipient),
1175
+ ("sub_uei", sub_uei),
1176
+ ):
1177
+ if val is not None:
1178
+ params[key] = val
1179
+ data = self._get("/api/subawards/", params)
1180
+ results = [
1181
+ self._parse_response_with_shape(obj, shape, Subaward, flat, flat_lists)
1182
+ for obj in data.get("results", [])
1183
+ ]
1184
+ return PaginatedResponse(
1185
+ count=data.get("count", 0),
1186
+ next=data.get("next"),
1187
+ previous=data.get("previous"),
1188
+ results=results,
1189
+ )
1190
+
831
1191
  # ============================================================================
832
1192
  # Vehicles (Awards)
833
1193
  # ============================================================================
@@ -957,6 +1317,42 @@ class TangoClient:
957
1317
  results=[BusinessType(**btype) for btype in data["results"]],
958
1318
  )
959
1319
 
1320
+ def list_naics(
1321
+ self,
1322
+ page: int = 1,
1323
+ limit: int = 25,
1324
+ employee_limit: int | None = None,
1325
+ employee_limit_gte: int | None = None,
1326
+ employee_limit_lte: int | None = None,
1327
+ revenue_limit: int | None = None,
1328
+ revenue_limit_gte: int | None = None,
1329
+ revenue_limit_lte: int | None = None,
1330
+ search: str | None = None,
1331
+ ) -> PaginatedResponse:
1332
+ """List NAICS codes (`/api/naics/`)."""
1333
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1334
+ if employee_limit is not None:
1335
+ params["employee_limit"] = employee_limit
1336
+ if employee_limit_gte is not None:
1337
+ params["employee_limit_gte"] = employee_limit_gte
1338
+ if employee_limit_lte is not None:
1339
+ params["employee_limit_lte"] = employee_limit_lte
1340
+ if revenue_limit is not None:
1341
+ params["revenue_limit"] = revenue_limit
1342
+ if revenue_limit_gte is not None:
1343
+ params["revenue_limit_gte"] = revenue_limit_gte
1344
+ if revenue_limit_lte is not None:
1345
+ params["revenue_limit_lte"] = revenue_limit_lte
1346
+ if search is not None:
1347
+ params["search"] = search
1348
+ data = self._get("/api/naics/", params)
1349
+ return PaginatedResponse(
1350
+ count=data.get("count", 0),
1351
+ next=data.get("next"),
1352
+ previous=data.get("previous"),
1353
+ results=data.get("results", []),
1354
+ )
1355
+
960
1356
  # Entity endpoints
961
1357
  def list_entities(
962
1358
  self,
@@ -966,7 +1362,7 @@ class TangoClient:
966
1362
  flat: bool = False,
967
1363
  flat_lists: bool = False,
968
1364
  search: str | None = None,
969
- **filters,
1365
+ **filters: Any,
970
1366
  ) -> PaginatedResponse:
971
1367
  """
972
1368
  List entities (vendors/recipients)
@@ -980,7 +1376,7 @@ class TangoClient:
980
1376
  search: Search query (maps to 'q' parameter)
981
1377
  **filters: Additional filter parameters (uei, cage_code, etc.)
982
1378
  """
983
- params = {"page": page, "limit": min(limit, 100)}
1379
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
984
1380
 
985
1381
  # Add shape parameter with default minimal shape
986
1382
  if shape is None:
@@ -1046,7 +1442,7 @@ class TangoClient:
1046
1442
  shape: str | None = None,
1047
1443
  flat: bool = False,
1048
1444
  flat_lists: bool = False,
1049
- **filters,
1445
+ **filters: Any,
1050
1446
  ) -> PaginatedResponse:
1051
1447
  """
1052
1448
  List contract forecasts
@@ -1059,7 +1455,7 @@ class TangoClient:
1059
1455
  flat_lists: If True, flatten arrays using indexed keys
1060
1456
  **filters: Additional filter parameters
1061
1457
  """
1062
- params = {"page": page, "limit": min(limit, 100)}
1458
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1063
1459
 
1064
1460
  # Add shape parameter with default minimal shape
1065
1461
  if shape is None:
@@ -1096,7 +1492,7 @@ class TangoClient:
1096
1492
  shape: str | None = None,
1097
1493
  flat: bool = False,
1098
1494
  flat_lists: bool = False,
1099
- **filters,
1495
+ **filters: Any,
1100
1496
  ) -> PaginatedResponse:
1101
1497
  """
1102
1498
  List contract opportunities/solicitations
@@ -1109,7 +1505,7 @@ class TangoClient:
1109
1505
  flat_lists: If True, flatten arrays using indexed keys
1110
1506
  **filters: Additional filter parameters
1111
1507
  """
1112
- params = {"page": page, "limit": min(limit, 100)}
1508
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1113
1509
 
1114
1510
  # Add shape parameter with default minimal shape
1115
1511
  if shape is None:
@@ -1146,7 +1542,7 @@ class TangoClient:
1146
1542
  shape: str | None = None,
1147
1543
  flat: bool = False,
1148
1544
  flat_lists: bool = False,
1149
- **filters,
1545
+ **filters: Any,
1150
1546
  ) -> PaginatedResponse:
1151
1547
  """
1152
1548
  List contract notices
@@ -1161,7 +1557,7 @@ class TangoClient:
1161
1557
  flat_lists: If True, flatten arrays using indexed keys
1162
1558
  **filters: Additional filter parameters
1163
1559
  """
1164
- params = {"page": page, "limit": min(limit, 100)}
1560
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1165
1561
 
1166
1562
  # Add shape parameter with default minimal shape
1167
1563
  if shape is None:
@@ -1198,7 +1594,7 @@ class TangoClient:
1198
1594
  shape: str | None = None,
1199
1595
  flat: bool = False,
1200
1596
  flat_lists: bool = False,
1201
- **filters,
1597
+ **filters: Any,
1202
1598
  ) -> PaginatedResponse:
1203
1599
  """
1204
1600
  List grants
@@ -1213,7 +1609,7 @@ class TangoClient:
1213
1609
  flat_lists: If True, flatten arrays using indexed keys
1214
1610
  **filters: Additional filter parameters
1215
1611
  """
1216
- params = {"page": page, "limit": min(limit, 100)}
1612
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1217
1613
 
1218
1614
  # Add shape parameter with default minimal shape
1219
1615
  if shape is None:
@@ -1242,6 +1638,47 @@ class TangoClient:
1242
1638
  results=results,
1243
1639
  )
1244
1640
 
1641
+ def list_assistance(
1642
+ self,
1643
+ limit: int = 25,
1644
+ cursor: str | None = None,
1645
+ assistance_type: str | None = None,
1646
+ award_key: str | None = None,
1647
+ fiscal_year: int | None = None,
1648
+ fiscal_year_gte: int | None = None,
1649
+ fiscal_year_lte: int | None = None,
1650
+ highly_compensated_officers: str | None = None,
1651
+ recipient: str | None = None,
1652
+ recipient_address: str | None = None,
1653
+ search: str | None = None,
1654
+ ) -> PaginatedResponse:
1655
+ """List assistance (financial assistance) transactions (`/api/assistance/`). Keyset pagination."""
1656
+ params: dict[str, Any] = {"limit": min(limit, 100)}
1657
+ if cursor:
1658
+ params["cursor"] = cursor
1659
+ for key, val in (
1660
+ ("assistance_type", assistance_type),
1661
+ ("award_key", award_key),
1662
+ ("fiscal_year", fiscal_year),
1663
+ ("fiscal_year_gte", fiscal_year_gte),
1664
+ ("fiscal_year_lte", fiscal_year_lte),
1665
+ ("highly_compensated_officers", highly_compensated_officers),
1666
+ ("recipient", recipient),
1667
+ ("recipient_address", recipient_address),
1668
+ ("search", search),
1669
+ ):
1670
+ if val is not None:
1671
+ params[key] = val
1672
+ data = self._get("/api/assistance/", params)
1673
+ return PaginatedResponse(
1674
+ count=int(data.get("count") or len(data.get("results") or [])),
1675
+ next=data.get("next"),
1676
+ previous=data.get("previous"),
1677
+ results=data.get("results", []),
1678
+ cursor=data.get("cursor"),
1679
+ page_metadata=data.get("page_metadata"),
1680
+ )
1681
+
1245
1682
  # ============================================================================
1246
1683
  # Webhooks (v2)
1247
1684
  # ============================================================================
tango/models.py CHANGED
@@ -295,6 +295,50 @@ class IDV:
295
295
  recipient: RecipientProfile | None = None
296
296
 
297
297
 
298
+ @dataclass
299
+ class Organization:
300
+ """Schema definition for Organization (not used for instances)"""
301
+
302
+ key: str
303
+ fh_key: str | None = None
304
+ name: str | None = None
305
+ level: int | None = None
306
+ type: str | None = None
307
+
308
+
309
+ @dataclass
310
+ class OTA:
311
+ """Schema definition for OTA / Other Transaction Agreement (not used for instances)"""
312
+
313
+ key: str
314
+ piid: str | None = None
315
+ award_date: date | None = None
316
+ description: str | None = None
317
+ recipient: RecipientProfile | None = None
318
+
319
+
320
+ @dataclass
321
+ class OTIDV:
322
+ """Schema definition for OTIDV / Other Transaction IDV (not used for instances)"""
323
+
324
+ key: str
325
+ piid: str | None = None
326
+ award_date: date | None = None
327
+ description: str | None = None
328
+ recipient: RecipientProfile | None = None
329
+
330
+
331
+ @dataclass
332
+ class Subaward:
333
+ """Schema definition for Subaward (not used for instances)"""
334
+
335
+ id: str | None = None
336
+ award_key: str | None = None
337
+ prime_uei: str | None = None
338
+ sub_uei: str | None = None
339
+ amount: Decimal | None = None
340
+
341
+
298
342
  @dataclass
299
343
  class Vehicle:
300
344
  """Schema definition for Vehicle (not used for instances)"""
@@ -563,3 +607,20 @@ class ShapeConfig:
563
607
 
564
608
  # Default for list_vehicle_awardees()
565
609
  VEHICLE_AWARDEES_MINIMAL: Final = "uuid,key,piid,award_date,title,order_count,idv_obligations,idv_contracts_value,recipient(display_name,uei)"
610
+
611
+ # Default for list_organizations()
612
+ ORGANIZATIONS_MINIMAL: Final = "key,fh_key,name,level,type,short_name"
613
+
614
+ # Default for list_otas()
615
+ OTAS_MINIMAL: Final = (
616
+ "key,piid,award_date,recipient(display_name,uei),description,total_contract_value,obligated"
617
+ )
618
+
619
+ # Default for list_otidvs()
620
+ OTIDVS_MINIMAL: Final = "key,piid,award_date,recipient(display_name,uei),description,total_contract_value,obligated,idv_type"
621
+
622
+ # Default for list_subawards()
623
+ # Note: API does not accept "id" or "amount" in shape (unknown_field). Use only accepted fields.
624
+ SUBAWARDS_MINIMAL: Final = (
625
+ "award_key,prime_recipient(uei,display_name),subaward_recipient(uei,display_name)"
626
+ )
@@ -975,6 +975,76 @@ VEHICLE_SCHEMA: dict[str, FieldSchema] = {
975
975
  }
976
976
 
977
977
 
978
+ # Organization (agencies hierarchy)
979
+ ORGANIZATION_SCHEMA: dict[str, FieldSchema] = {
980
+ "key": FieldSchema(name="key", type=str, is_optional=True, is_list=False),
981
+ "fh_key": FieldSchema(name="fh_key", type=str, is_optional=False, is_list=False),
982
+ "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
983
+ "short_name": FieldSchema(name="short_name", type=str, is_optional=True, is_list=False),
984
+ "level": FieldSchema(name="level", type=int, is_optional=True, is_list=False),
985
+ "type": FieldSchema(name="type", type=str, is_optional=True, is_list=False),
986
+ }
987
+
988
+ # OTA (Other Transaction Agreement) - IDV-like
989
+ OTA_SCHEMA: dict[str, FieldSchema] = {
990
+ "key": FieldSchema(name="key", type=str, is_optional=False, is_list=False),
991
+ "piid": FieldSchema(name="piid", type=str, is_optional=True, is_list=False),
992
+ "award_date": FieldSchema(name="award_date", type=date, is_optional=True, is_list=False),
993
+ "description": FieldSchema(name="description", type=str, is_optional=True, is_list=False),
994
+ "total_contract_value": FieldSchema(
995
+ name="total_contract_value", type=Decimal, is_optional=True, is_list=False
996
+ ),
997
+ "obligated": FieldSchema(name="obligated", type=Decimal, is_optional=True, is_list=False),
998
+ "recipient": FieldSchema(
999
+ name="recipient",
1000
+ type=dict,
1001
+ is_optional=True,
1002
+ is_list=False,
1003
+ nested_model="RecipientProfile",
1004
+ ),
1005
+ }
1006
+
1007
+ # OTIDV (Other Transaction IDV) - IDV-like
1008
+ OTIDV_SCHEMA: dict[str, FieldSchema] = {
1009
+ "key": FieldSchema(name="key", type=str, is_optional=False, is_list=False),
1010
+ "piid": FieldSchema(name="piid", type=str, is_optional=True, is_list=False),
1011
+ "award_date": FieldSchema(name="award_date", type=date, is_optional=True, is_list=False),
1012
+ "description": FieldSchema(name="description", type=str, is_optional=True, is_list=False),
1013
+ "total_contract_value": FieldSchema(
1014
+ name="total_contract_value", type=Decimal, is_optional=True, is_list=False
1015
+ ),
1016
+ "obligated": FieldSchema(name="obligated", type=Decimal, is_optional=True, is_list=False),
1017
+ "idv_type": FieldSchema(name="idv_type", type=dict, is_optional=True, is_list=False),
1018
+ "recipient": FieldSchema(
1019
+ name="recipient",
1020
+ type=dict,
1021
+ is_optional=True,
1022
+ is_list=False,
1023
+ nested_model="RecipientProfile",
1024
+ ),
1025
+ }
1026
+
1027
+ # Subaward (prime/sub awards)
1028
+ SUBAWARD_SCHEMA: dict[str, FieldSchema] = {
1029
+ "id": FieldSchema(name="id", type=str, is_optional=True, is_list=False),
1030
+ "award_key": FieldSchema(name="award_key", type=str, is_optional=True, is_list=False),
1031
+ "amount": FieldSchema(name="amount", type=Decimal, is_optional=True, is_list=False),
1032
+ "prime_recipient": FieldSchema(
1033
+ name="prime_recipient",
1034
+ type=dict,
1035
+ is_optional=True,
1036
+ is_list=False,
1037
+ nested_model="RecipientProfile",
1038
+ ),
1039
+ "subaward_recipient": FieldSchema(
1040
+ name="subaward_recipient",
1041
+ type=dict,
1042
+ is_optional=True,
1043
+ is_list=False,
1044
+ nested_model="RecipientProfile",
1045
+ ),
1046
+ }
1047
+
978
1048
  # ============================================================================
979
1049
  # SCHEMA REGISTRY MAPPING
980
1050
  # ============================================================================
@@ -1009,6 +1079,11 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
1009
1079
  "CFDANumber": CFDA_NUMBER_SCHEMA,
1010
1080
  "CodeDescription": CODE_DESCRIPTION_SCHEMA,
1011
1081
  "GrantAttachment": GRANT_ATTACHMENT_SCHEMA,
1082
+ # Additional list endpoints
1083
+ "Organization": ORGANIZATION_SCHEMA,
1084
+ "OTA": OTA_SCHEMA,
1085
+ "OTIDV": OTIDV_SCHEMA,
1086
+ "Subaward": SUBAWARD_SCHEMA,
1012
1087
  }
1013
1088
 
1014
1089
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tango-python
3
- Version: 0.3.0
3
+ Version: 0.4.0
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
@@ -487,8 +487,10 @@ tango-python/
487
487
  │ └── quick_start.ipynb # Interactive quick start
488
488
  ├── scripts/ # Utility scripts
489
489
  │ ├── README.md
490
+ │ ├── check_filter_shape_conformance.py # Filter + shape conformance (CI)
490
491
  │ ├── fetch_api_schema.py
491
- └── generate_schemas_from_api.py
492
+ ├── generate_schemas_from_api.py
493
+ │ └── pr_review.py # PR validation (lint, types, tests, conformance)
492
494
  ├── pyproject.toml # Project configuration
493
495
  ├── uv.lock # Dependency lock file
494
496
  ├── LICENSE # MIT License
@@ -526,7 +528,12 @@ Contributions are welcome! Please feel free to submit a Pull Request.
526
528
 
527
529
  1. Fork the repository
528
530
  2. Create your feature branch (`git checkout -b feature/amazing-feature`)
529
- 3. Run tests (`uv run pytest`)
530
- 4. Commit your changes (`git commit -m 'Add amazing feature'`)
531
- 5. Push to the branch (`git push origin feature/amazing-feature`)
532
- 6. Open a Pull Request
531
+ 3. Run lint and format: `uv run ruff format tango/ && uv run ruff check tango/`
532
+ 4. Run type checking: `uv run mypy tango/`
533
+ 5. Run tests: `uv run pytest`
534
+ 6. (Optional) Run [filter and shape conformance](scripts/README.md#filter-and-shape-conformance) if you have the tango API manifest; CI will run it on push/PR
535
+ 7. Commit your changes (`git commit -m 'Add amazing feature'`)
536
+ 8. Push to the branch (`git push origin feature/amazing-feature`)
537
+ 9. Open a Pull Request
538
+
539
+ For a single command that runs formatting, linting, type checking, and tests (and conformance when the manifest is present), use: `uv run python scripts/pr_review.py --mode full`
@@ -1,16 +1,16 @@
1
- tango/__init__.py,sha256=Pf_z5e6houpE5uwd6-UBvWH8geraP0XdvkIQ6ImyDkk,1050
2
- tango/client.py,sha256=BvccO-iHQHMA5xjps8IZbaG1IWXchEBWH2wxZZvnLXs,55501
1
+ tango/__init__.py,sha256=PahQcpEfXRycCJhXaWm2-9AbfcdUKB8CC6EcN3AsBqc,1050
2
+ tango/client.py,sha256=Y3ZsOCHX3zhrx7qjHWnQNIfLXidffxty1pBvY0yVrH0,71719
3
3
  tango/exceptions.py,sha256=JmtbOY0ofBnX24pUErh2XFlTj9dim2ngyboserEGRFw,2226
4
- tango/models.py,sha256=aArNNK_qH-P1diq_boAGBCKSdrNxLEjP0T0IjrpFfaM,17268
4
+ tango/models.py,sha256=uEdNdEN40BAJ9OSZyoRE7L0UemSPzrQto80OZYeCVYE,19008
5
5
  tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
6
- tango/shapes/explicit_schemas.py,sha256=KDU5Xlvs2pM285ZLd-7lOJB7YcB38f8RHQwmRaUgH9o,47003
6
+ tango/shapes/explicit_schemas.py,sha256=32g47TIdrJpNzlJfxg_OKu5H2o1l_3Z7JTDVf2bA4N8,50219
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.3.0.dist-info/METADATA,sha256=XN1DvdP2s1y_S-nwFPiZH43bhTWxSLJkZILuOe4Y5P8,15576
14
- tango_python-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
- tango_python-0.3.0.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
16
- tango_python-0.3.0.dist-info/RECORD,,
13
+ tango_python-0.4.0.dist-info/METADATA,sha256=WKMs-pFRxK6ENOUlI8mb6rUbNF95SuZtcVF5io9wSOQ,16210
14
+ tango_python-0.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
15
+ tango_python-0.4.0.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
16
+ tango_python-0.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any