tango-python 0.4.0__py3-none-any.whl → 0.4.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 CHANGED
@@ -9,6 +9,7 @@ from .exceptions import (
9
9
  TangoValidationError,
10
10
  )
11
11
  from .models import (
12
+ GsaElibraryContract,
12
13
  PaginatedResponse,
13
14
  SearchFilters,
14
15
  ShapeConfig,
@@ -26,7 +27,7 @@ from .shapes import (
26
27
  TypeGenerator,
27
28
  )
28
29
 
29
- __version__ = "0.4.0"
30
+ __version__ = "0.4.1"
30
31
  __all__ = [
31
32
  "TangoClient",
32
33
  "TangoAPIError",
@@ -34,6 +35,7 @@ __all__ = [
34
35
  "TangoNotFoundError",
35
36
  "TangoValidationError",
36
37
  "TangoRateLimitError",
38
+ "GsaElibraryContract",
37
39
  "PaginatedResponse",
38
40
  "SearchFilters",
39
41
  "ShapeConfig",
tango/client.py CHANGED
@@ -25,6 +25,7 @@ from tango.models import (
25
25
  Entity,
26
26
  Forecast,
27
27
  Grant,
28
+ GsaElibraryContract,
28
29
  Location,
29
30
  Notice,
30
31
  Opportunity,
@@ -496,7 +497,34 @@ class TangoClient:
496
497
  flat: bool = False,
497
498
  flat_lists: bool = False,
498
499
  filters: SearchFilters | dict[str, Any] | None = None,
499
- **kwargs: Any,
500
+ award_date: str | None = None,
501
+ award_date_gte: str | None = None,
502
+ award_date_lte: str | None = None,
503
+ award_type: str | None = None,
504
+ awarding_agency: str | None = None,
505
+ expiring_gte: str | None = None,
506
+ expiring_lte: str | None = None,
507
+ fiscal_year: int | None = None,
508
+ fiscal_year_gte: int | None = None,
509
+ fiscal_year_lte: int | None = None,
510
+ funding_agency: str | None = None,
511
+ obligated_gte: str | None = None,
512
+ obligated_lte: str | None = None,
513
+ ordering: str | None = None,
514
+ piid: str | None = None,
515
+ pop_end_date_gte: str | None = None,
516
+ pop_end_date_lte: str | None = None,
517
+ pop_start_date_gte: str | None = None,
518
+ pop_start_date_lte: str | None = None,
519
+ solicitation_identifier: str | None = None,
520
+ keyword: str | None = None,
521
+ naics_code: str | None = None,
522
+ psc_code: str | None = None,
523
+ recipient_name: str | None = None,
524
+ recipient_uei: str | None = None,
525
+ set_aside_type: str | None = None,
526
+ sort: str | None = None,
527
+ order: str | None = None,
500
528
  ) -> PaginatedResponse:
501
529
  """
502
530
  List contracts with optional filtering
@@ -511,118 +539,61 @@ class TangoClient:
511
539
  flat: If True, flatten nested objects in shaped response using dot notation
512
540
  flat_lists: If True, flatten arrays using indexed keys (e.g., items.0.field)
513
541
  filters: Optional SearchFilters object or dict for backward compatibility.
514
- Filter parameters can also be passed as keyword arguments.
515
- **kwargs: Filter parameters
516
-
517
- Text search:
518
- - keyword: Search contract descriptions (mapped to 'search' API param)
519
-
520
- Date filters:
521
- - award_date_gte: Award date >= (YYYY-MM-DD)
522
- - award_date_lte: Award date <= (YYYY-MM-DD)
523
- - pop_start_date_gte: Period of performance start date >=
524
- - pop_start_date_lte: Period of performance start date <=
525
- - pop_end_date_gte: Period of performance end date >=
526
- - pop_end_date_lte: Period of performance end date <=
527
- - expiring_gte: Expiring on or after date
528
- - expiring_lte: Expiring on or before date
529
-
530
- Party filters:
531
- - awarding_agency: Awarding agency code (e.g., "4700" for GSA)
532
- - funding_agency: Funding agency code
533
- - recipient_name: Vendor/recipient name (mapped to 'recipient' API param)
534
- - recipient_uei: Vendor UEI (mapped to 'uei' API param)
535
-
536
- Classification filters:
537
- - naics_code: NAICS code (mapped to 'naics' API param)
538
- - psc_code: PSC code (mapped to 'psc' API param)
539
- - set_aside_type: Set-aside type (mapped to 'set_aside' API param)
540
-
541
- Type filters:
542
- - fiscal_year: Fiscal year (exact match)
543
- - fiscal_year_gte: Fiscal year >=
544
- - fiscal_year_lte: Fiscal year <=
545
- - award_type: Award type code
546
-
547
- Identifiers:
548
- - piid: Procurement Instrument Identifier
549
- - solicitation_identifier: Solicitation ID
550
-
551
- Sorting:
552
- - sort: Field to sort by (combined with 'order')
553
- - order: Sort order ('asc' or 'desc', default 'asc')
542
+ award_date: Award date (exact match, YYYY-MM-DD)
543
+ award_date_gte: Award date >= (YYYY-MM-DD)
544
+ award_date_lte: Award date <= (YYYY-MM-DD)
545
+ award_type: Award type code
546
+ awarding_agency: Awarding agency code (e.g., "4700" for GSA)
547
+ expiring_gte: Expiring on or after date
548
+ expiring_lte: Expiring on or before date
549
+ fiscal_year: Fiscal year (exact match)
550
+ fiscal_year_gte: Fiscal year >=
551
+ fiscal_year_lte: Fiscal year <=
552
+ funding_agency: Funding agency code
553
+ obligated_gte: Obligated amount >=
554
+ obligated_lte: Obligated amount <=
555
+ ordering: Sort ordering (prefix with '-' for descending)
556
+ piid: Procurement Instrument Identifier
557
+ pop_end_date_gte: Period of performance end date >=
558
+ pop_end_date_lte: Period of performance end date <=
559
+ pop_start_date_gte: Period of performance start date >=
560
+ pop_start_date_lte: Period of performance start date <=
561
+ solicitation_identifier: Solicitation ID
562
+ keyword: Search contract descriptions (mapped to 'search' API param)
563
+ naics_code: NAICS code (mapped to 'naics' API param)
564
+ psc_code: PSC code (mapped to 'psc' API param)
565
+ recipient_name: Vendor/recipient name (mapped to 'recipient' API param)
566
+ recipient_uei: Vendor UEI (mapped to 'uei' API param)
567
+ set_aside_type: Set-aside type (mapped to 'set_aside' API param)
568
+ sort: Field to sort by (combined with 'order' to produce 'ordering')
569
+ order: Sort order ('asc' or 'desc', default 'asc')
554
570
 
555
571
  Examples:
556
- >>> # Simple usage
557
572
  >>> contracts = client.list_contracts(limit=10)
558
-
559
- >>> # With keyword arguments
560
573
  >>> contracts = client.list_contracts(
561
- ... awarding_agency="4700", # GSA
574
+ ... awarding_agency="4700",
562
575
  ... award_date_gte="2023-01-01",
563
- ... limit=25
576
+ ... limit=25,
564
577
  ... )
565
-
566
- >>> # Text search
567
578
  >>> contracts = client.list_contracts(keyword="software development")
568
-
569
- >>> # Pagination with cursor
570
- >>> response = client.list_contracts(limit=25)
571
- >>> if response.cursor:
572
- ... next_page = client.list_contracts(cursor=response.cursor, limit=25)
573
-
574
- >>> # With SearchFilters object (legacy)
575
- >>> filters = SearchFilters(
576
- ... keyword="IT",
577
- ... awarding_agency="4700",
578
- ... fiscal_year=2024
579
- ... )
580
- >>> contracts = client.list_contracts(filters=filters)
581
-
582
- >>> # Using new date range filters
583
- >>> contracts = client.list_contracts(
584
- ... expiring_gte="2025-01-01",
585
- ... expiring_lte="2025-12-31"
586
- ... )
587
579
  """
588
- # Start with pagination parameters
589
- # Use cursor if provided, otherwise use page=1 for first request
590
580
  params: dict[str, Any] = {"limit": min(limit, 100)}
591
581
  if cursor:
592
582
  params["cursor"] = cursor
593
583
  else:
594
- # First page uses page=1, subsequent pages use cursor
595
584
  params["page"] = 1
596
585
 
597
- # Handle filters parameter (backward compatibility)
586
+ # Handle legacy filters parameter (backward compatibility)
598
587
  filter_dict: dict[str, Any] = {}
599
588
  if filters is not None:
600
589
  if hasattr(filters, "to_dict"):
601
- # SearchFilters object
602
590
  filter_dict = filters.to_dict()
603
591
  else:
604
- # dict
605
- filter_dict = filters
592
+ filter_dict = dict(filters)
606
593
 
607
- # Extract limit from filters if using defaults
608
594
  if limit == 25 and "limit" in filter_dict:
609
595
  params["limit"] = min(filter_dict.pop("limit", 25), 100)
610
596
 
611
- # Merge kwargs and filter_dict (kwargs take precedence)
612
- filter_params = {**filter_dict, **kwargs}
613
-
614
- # Explicitly exclude shape-related and pagination parameters from filter_params
615
- # These are handled separately and should not be sent as query parameters
616
- excluded_params = {"shape", "flat", "flat_lists", "cursor", "page"}
617
- for param in excluded_params:
618
- filter_params.pop(param, None)
619
-
620
- # Extract limit from kwargs if provided (override explicit params)
621
- if "limit" in filter_params:
622
- params["limit"] = min(filter_params.pop("limit"), 100)
623
-
624
- # Add shape parameter with default minimal shape
625
- # This is separate from filter parameters and controls response fields, not filtering
626
597
  if shape is None:
627
598
  shape = ShapeConfig.CONTRACTS_MINIMAL
628
599
  if shape:
@@ -632,43 +603,65 @@ class TangoClient:
632
603
  if flat_lists:
633
604
  params["flat_lists"] = "true"
634
605
 
635
- # Process filter parameters - convert award amounts to strings
636
- # Map Python parameter names to API parameter names if needed
637
- # Then update params with all filters (excluding None values)
638
- # This matches the pattern used by other endpoints (params.update(filters))
639
-
640
- # Map Python parameter names to API parameter names
641
- # The API may expect different parameter names than our Python interface
642
606
  api_param_mapping = {
643
- "naics_code": "naics", # API expects 'naics' not 'naics_code'
644
- "keyword": "search", # API expects 'search' not 'keyword'
645
- "psc_code": "psc", # API expects 'psc' not 'psc_code'
646
- "recipient_name": "recipient", # API expects 'recipient' not 'recipient_name'
647
- "recipient_uei": "uei", # API expects 'uei' not 'recipient_uei'
648
- "set_aside_type": "set_aside", # API expects 'set_aside' not 'set_aside_type'
607
+ "naics_code": "naics",
608
+ "keyword": "search",
609
+ "psc_code": "psc",
610
+ "recipient_name": "recipient",
611
+ "recipient_uei": "uei",
612
+ "set_aside_type": "set_aside",
649
613
  }
650
614
 
615
+ # Collect explicit filter params; legacy filter_dict values are used as fallback
616
+ filter_params: dict[str, Any] = {}
617
+ for key, val in (
618
+ ("award_date", award_date),
619
+ ("award_date_gte", award_date_gte),
620
+ ("award_date_lte", award_date_lte),
621
+ ("award_type", award_type),
622
+ ("awarding_agency", awarding_agency),
623
+ ("expiring_gte", expiring_gte),
624
+ ("expiring_lte", expiring_lte),
625
+ ("fiscal_year", fiscal_year),
626
+ ("fiscal_year_gte", fiscal_year_gte),
627
+ ("fiscal_year_lte", fiscal_year_lte),
628
+ ("funding_agency", funding_agency),
629
+ ("obligated_gte", obligated_gte),
630
+ ("obligated_lte", obligated_lte),
631
+ ("ordering", ordering),
632
+ ("piid", piid),
633
+ ("pop_end_date_gte", pop_end_date_gte),
634
+ ("pop_end_date_lte", pop_end_date_lte),
635
+ ("pop_start_date_gte", pop_start_date_gte),
636
+ ("pop_start_date_lte", pop_start_date_lte),
637
+ ("solicitation_identifier", solicitation_identifier),
638
+ ("keyword", keyword),
639
+ ("naics_code", naics_code),
640
+ ("psc_code", psc_code),
641
+ ("recipient_name", recipient_name),
642
+ ("recipient_uei", recipient_uei),
643
+ ("set_aside_type", set_aside_type),
644
+ ):
645
+ if val is not None:
646
+ filter_params[key] = val
647
+
648
+ # Merge: explicit params take precedence over legacy filter_dict
649
+ excluded = {"shape", "flat", "flat_lists", "cursor", "page", "limit"}
650
+ for k, v in filter_dict.items():
651
+ if k not in excluded and k not in filter_params and v is not None:
652
+ filter_params[k] = v
653
+
651
654
  # Handle sort + order → ordering conversion
652
- # API expects single 'ordering' parameter with '-' prefix for descending
653
- sort_field = filter_params.pop("sort", None)
654
- sort_order = filter_params.pop("order", None)
655
- if sort_field:
656
- # Prefix with '-' for descending order
655
+ sort_field = sort or filter_dict.get("sort")
656
+ sort_order = order or filter_dict.get("order")
657
+ if sort_field and "ordering" not in filter_params:
657
658
  prefix = "-" if sort_order == "desc" else ""
658
659
  filter_params["ordering"] = f"{prefix}{sort_field}"
659
660
 
660
- # Apply parameter name mapping and process values
661
- api_params = {}
661
+ # Apply parameter name mapping and add to params
662
662
  for key, value in filter_params.items():
663
- if value is None:
664
- continue # Skip None values
665
- # Map to API parameter name if needed
666
663
  api_key = api_param_mapping.get(key, key)
667
- api_params[api_key] = value
668
-
669
- # Update params with all filter parameters
670
- # This is the same pattern as other endpoints use
671
- params.update(api_params)
664
+ params[api_key] = value
672
665
 
673
666
  data = self._get("/api/contracts/", params)
674
667
 
@@ -698,7 +691,30 @@ class TangoClient:
698
691
  flat: bool = False,
699
692
  flat_lists: bool = False,
700
693
  joiner: str = ".",
701
- **filters: Any,
694
+ award_date: str | None = None,
695
+ award_date_gte: str | None = None,
696
+ award_date_lte: str | None = None,
697
+ awarding_agency: str | None = None,
698
+ expiring_gte: str | None = None,
699
+ expiring_lte: str | None = None,
700
+ fiscal_year: int | None = None,
701
+ fiscal_year_gte: int | None = None,
702
+ fiscal_year_lte: int | None = None,
703
+ funding_agency: str | None = None,
704
+ idv_type: str | None = None,
705
+ last_date_to_order_gte: str | None = None,
706
+ last_date_to_order_lte: str | None = None,
707
+ naics: str | None = None,
708
+ ordering: str | None = None,
709
+ piid: str | None = None,
710
+ pop_start_date_gte: str | None = None,
711
+ pop_start_date_lte: str | None = None,
712
+ psc: str | None = None,
713
+ recipient: str | None = None,
714
+ search: str | None = None,
715
+ set_aside: str | None = None,
716
+ solicitation_identifier: str | None = None,
717
+ uei: str | None = None,
702
718
  ) -> PaginatedResponse:
703
719
  """
704
720
  List IDVs (indefinite delivery vehicles) with keyset pagination.
@@ -721,7 +737,34 @@ class TangoClient:
721
737
  if flat_lists:
722
738
  params["flat_lists"] = "true"
723
739
 
724
- params.update({k: v for k, v in filters.items() if v is not None})
740
+ for key, val in (
741
+ ("award_date", award_date),
742
+ ("award_date_gte", award_date_gte),
743
+ ("award_date_lte", award_date_lte),
744
+ ("awarding_agency", awarding_agency),
745
+ ("expiring_gte", expiring_gte),
746
+ ("expiring_lte", expiring_lte),
747
+ ("fiscal_year", fiscal_year),
748
+ ("fiscal_year_gte", fiscal_year_gte),
749
+ ("fiscal_year_lte", fiscal_year_lte),
750
+ ("funding_agency", funding_agency),
751
+ ("idv_type", idv_type),
752
+ ("last_date_to_order_gte", last_date_to_order_gte),
753
+ ("last_date_to_order_lte", last_date_to_order_lte),
754
+ ("naics", naics),
755
+ ("ordering", ordering),
756
+ ("piid", piid),
757
+ ("pop_start_date_gte", pop_start_date_gte),
758
+ ("pop_start_date_lte", pop_start_date_lte),
759
+ ("psc", psc),
760
+ ("recipient", recipient),
761
+ ("search", search),
762
+ ("set_aside", set_aside),
763
+ ("solicitation_identifier", solicitation_identifier),
764
+ ("uei", uei),
765
+ ):
766
+ if val is not None:
767
+ params[key] = val
725
768
 
726
769
  data = self._get("/api/idvs/", params)
727
770
 
@@ -1188,6 +1231,88 @@ class TangoClient:
1188
1231
  results=results,
1189
1232
  )
1190
1233
 
1234
+ # ============================================================================
1235
+ # GSA eLibrary Contracts
1236
+ # ============================================================================
1237
+
1238
+ def list_gsa_elibrary_contracts(
1239
+ self,
1240
+ page: int = 1,
1241
+ limit: int = 25,
1242
+ shape: str | None = None,
1243
+ flat: bool = False,
1244
+ flat_lists: bool = False,
1245
+ joiner: str = ".",
1246
+ contract_number: str | None = None,
1247
+ key: str | None = None,
1248
+ piid: str | None = None,
1249
+ schedule: str | None = None,
1250
+ search: str | None = None,
1251
+ sin: str | None = None,
1252
+ uei: str | None = None,
1253
+ ) -> PaginatedResponse:
1254
+ """List GSA eLibrary contracts (`/api/gsa_elibrary_contracts/`)."""
1255
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1256
+ if shape is None:
1257
+ shape = ShapeConfig.GSA_ELIBRARY_CONTRACTS_MINIMAL
1258
+ if shape:
1259
+ params["shape"] = shape
1260
+ if flat:
1261
+ params["flat"] = "true"
1262
+ if joiner:
1263
+ params["joiner"] = joiner
1264
+ if flat_lists:
1265
+ params["flat_lists"] = "true"
1266
+ for k, val in (
1267
+ ("contract_number", contract_number),
1268
+ ("key", key),
1269
+ ("piid", piid),
1270
+ ("schedule", schedule),
1271
+ ("search", search),
1272
+ ("sin", sin),
1273
+ ("uei", uei),
1274
+ ):
1275
+ if val is not None:
1276
+ params[k] = val
1277
+ data = self._get("/api/gsa_elibrary_contracts/", params)
1278
+ results = [
1279
+ self._parse_response_with_shape(
1280
+ obj, shape, GsaElibraryContract, flat, flat_lists, joiner=joiner
1281
+ )
1282
+ for obj in data.get("results", [])
1283
+ ]
1284
+ return PaginatedResponse(
1285
+ count=data.get("count", 0),
1286
+ next=data.get("next"),
1287
+ previous=data.get("previous"),
1288
+ results=results,
1289
+ )
1290
+
1291
+ def get_gsa_elibrary_contract(
1292
+ self,
1293
+ uuid: str,
1294
+ shape: str | None = None,
1295
+ flat: bool = False,
1296
+ flat_lists: bool = False,
1297
+ joiner: str = ".",
1298
+ ) -> Any:
1299
+ """Get a single GSA eLibrary contract by UUID (`/api/gsa_elibrary_contracts/{uuid}/`)."""
1300
+ params: dict[str, Any] = {}
1301
+ if shape is None:
1302
+ shape = ShapeConfig.GSA_ELIBRARY_CONTRACTS_MINIMAL
1303
+ if shape:
1304
+ params["shape"] = shape
1305
+ if flat:
1306
+ params["flat"] = "true"
1307
+ if joiner:
1308
+ params["joiner"] = joiner
1309
+ if flat_lists:
1310
+ params["flat_lists"] = "true"
1311
+ data = self._get(f"/api/gsa_elibrary_contracts/{uuid}/", params)
1312
+ return self._parse_response_with_shape(
1313
+ data, shape, GsaElibraryContract, flat, flat_lists, joiner=joiner
1314
+ )
1315
+
1191
1316
  # ============================================================================
1192
1317
  # Vehicles (Awards)
1193
1318
  # ============================================================================
@@ -1362,7 +1487,17 @@ class TangoClient:
1362
1487
  flat: bool = False,
1363
1488
  flat_lists: bool = False,
1364
1489
  search: str | None = None,
1365
- **filters: Any,
1490
+ cage_code: str | None = None,
1491
+ naics: str | None = None,
1492
+ name: str | None = None,
1493
+ psc: str | None = None,
1494
+ purpose_of_registration_code: str | None = None,
1495
+ socioeconomic: str | None = None,
1496
+ state: str | None = None,
1497
+ total_awards_obligated_gte: str | None = None,
1498
+ total_awards_obligated_lte: str | None = None,
1499
+ uei: str | None = None,
1500
+ zip_code: str | None = None,
1366
1501
  ) -> PaginatedResponse:
1367
1502
  """
1368
1503
  List entities (vendors/recipients)
@@ -1373,12 +1508,21 @@ class TangoClient:
1373
1508
  shape: Response shape string (defaults to minimal shape)
1374
1509
  flat: If True, flatten nested objects in shaped response
1375
1510
  flat_lists: If True, flatten arrays using indexed keys
1376
- search: Search query (maps to 'q' parameter)
1377
- **filters: Additional filter parameters (uei, cage_code, etc.)
1511
+ search: Search query
1512
+ cage_code: CAGE code filter
1513
+ naics: NAICS code filter
1514
+ name: Entity name filter
1515
+ psc: PSC code filter
1516
+ purpose_of_registration_code: Purpose of registration code
1517
+ socioeconomic: Socioeconomic status filter
1518
+ state: State filter
1519
+ total_awards_obligated_gte: Total awards obligated >=
1520
+ total_awards_obligated_lte: Total awards obligated <=
1521
+ uei: Unique Entity Identifier
1522
+ zip_code: ZIP code filter
1378
1523
  """
1379
1524
  params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1380
1525
 
1381
- # Add shape parameter with default minimal shape
1382
1526
  if shape is None:
1383
1527
  shape = ShapeConfig.ENTITIES_MINIMAL
1384
1528
  if shape:
@@ -1388,11 +1532,22 @@ class TangoClient:
1388
1532
  if flat_lists:
1389
1533
  params["flat_lists"] = "true"
1390
1534
 
1391
- # Map 'search' parameter to 'q' (query parameter)
1392
- if search:
1393
- params["search"] = search
1394
-
1395
- params.update(filters)
1535
+ for key, val in (
1536
+ ("search", search),
1537
+ ("cage_code", cage_code),
1538
+ ("naics", naics),
1539
+ ("name", name),
1540
+ ("psc", psc),
1541
+ ("purpose_of_registration_code", purpose_of_registration_code),
1542
+ ("socioeconomic", socioeconomic),
1543
+ ("state", state),
1544
+ ("total_awards_obligated_gte", total_awards_obligated_gte),
1545
+ ("total_awards_obligated_lte", total_awards_obligated_lte),
1546
+ ("uei", uei),
1547
+ ("zip_code", zip_code),
1548
+ ):
1549
+ if val is not None:
1550
+ params[key] = val
1396
1551
 
1397
1552
  data = self._get("/api/entities/", params)
1398
1553
 
@@ -1442,7 +1597,19 @@ class TangoClient:
1442
1597
  shape: str | None = None,
1443
1598
  flat: bool = False,
1444
1599
  flat_lists: bool = False,
1445
- **filters: Any,
1600
+ agency: str | None = None,
1601
+ award_date_after: str | None = None,
1602
+ award_date_before: str | None = None,
1603
+ fiscal_year: int | None = None,
1604
+ fiscal_year_gte: int | None = None,
1605
+ fiscal_year_lte: int | None = None,
1606
+ modified_after: str | None = None,
1607
+ modified_before: str | None = None,
1608
+ naics_code: str | None = None,
1609
+ naics_starts_with: str | None = None,
1610
+ search: str | None = None,
1611
+ source_system: str | None = None,
1612
+ status: str | None = None,
1446
1613
  ) -> PaginatedResponse:
1447
1614
  """
1448
1615
  List contract forecasts
@@ -1453,11 +1620,22 @@ class TangoClient:
1453
1620
  shape: Response shape string (defaults to minimal shape)
1454
1621
  flat: If True, flatten nested objects in shaped response
1455
1622
  flat_lists: If True, flatten arrays using indexed keys
1456
- **filters: Additional filter parameters
1623
+ agency: Agency filter
1624
+ award_date_after: Award date after (YYYY-MM-DD)
1625
+ award_date_before: Award date before (YYYY-MM-DD)
1626
+ fiscal_year: Fiscal year (exact match)
1627
+ fiscal_year_gte: Fiscal year >=
1628
+ fiscal_year_lte: Fiscal year <=
1629
+ modified_after: Modified after date
1630
+ modified_before: Modified before date
1631
+ naics_code: NAICS code filter
1632
+ naics_starts_with: NAICS code prefix filter
1633
+ search: Search query
1634
+ source_system: Source system filter
1635
+ status: Status filter
1457
1636
  """
1458
1637
  params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1459
1638
 
1460
- # Add shape parameter with default minimal shape
1461
1639
  if shape is None:
1462
1640
  shape = ShapeConfig.FORECASTS_MINIMAL
1463
1641
  if shape:
@@ -1467,7 +1645,23 @@ class TangoClient:
1467
1645
  if flat_lists:
1468
1646
  params["flat_lists"] = "true"
1469
1647
 
1470
- params.update(filters)
1648
+ for key, val in (
1649
+ ("agency", agency),
1650
+ ("award_date_after", award_date_after),
1651
+ ("award_date_before", award_date_before),
1652
+ ("fiscal_year", fiscal_year),
1653
+ ("fiscal_year_gte", fiscal_year_gte),
1654
+ ("fiscal_year_lte", fiscal_year_lte),
1655
+ ("modified_after", modified_after),
1656
+ ("modified_before", modified_before),
1657
+ ("naics_code", naics_code),
1658
+ ("naics_starts_with", naics_starts_with),
1659
+ ("search", search),
1660
+ ("source_system", source_system),
1661
+ ("status", status),
1662
+ ):
1663
+ if val is not None:
1664
+ params[key] = val
1471
1665
 
1472
1666
  data = self._get("/api/forecasts/", params)
1473
1667
 
@@ -1492,7 +1686,21 @@ class TangoClient:
1492
1686
  shape: str | None = None,
1493
1687
  flat: bool = False,
1494
1688
  flat_lists: bool = False,
1495
- **filters: Any,
1689
+ active: bool | None = None,
1690
+ agency: str | None = None,
1691
+ first_notice_date_after: str | None = None,
1692
+ first_notice_date_before: str | None = None,
1693
+ last_notice_date_after: str | None = None,
1694
+ last_notice_date_before: str | None = None,
1695
+ naics: str | None = None,
1696
+ notice_type: str | None = None,
1697
+ place_of_performance: str | None = None,
1698
+ psc: str | None = None,
1699
+ response_deadline_after: str | None = None,
1700
+ response_deadline_before: str | None = None,
1701
+ search: str | None = None,
1702
+ set_aside: str | None = None,
1703
+ solicitation_number: str | None = None,
1496
1704
  ) -> PaginatedResponse:
1497
1705
  """
1498
1706
  List contract opportunities/solicitations
@@ -1503,11 +1711,24 @@ class TangoClient:
1503
1711
  shape: Response shape string (defaults to minimal shape)
1504
1712
  flat: If True, flatten nested objects in shaped response
1505
1713
  flat_lists: If True, flatten arrays using indexed keys
1506
- **filters: Additional filter parameters
1714
+ active: Filter by active status
1715
+ agency: Agency filter
1716
+ first_notice_date_after: First notice date after
1717
+ first_notice_date_before: First notice date before
1718
+ last_notice_date_after: Last notice date after
1719
+ last_notice_date_before: Last notice date before
1720
+ naics: NAICS code filter
1721
+ notice_type: Notice type filter
1722
+ place_of_performance: Place of performance filter
1723
+ psc: PSC code filter
1724
+ response_deadline_after: Response deadline after
1725
+ response_deadline_before: Response deadline before
1726
+ search: Search query
1727
+ set_aside: Set-aside type filter
1728
+ solicitation_number: Solicitation number filter
1507
1729
  """
1508
1730
  params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1509
1731
 
1510
- # Add shape parameter with default minimal shape
1511
1732
  if shape is None:
1512
1733
  shape = ShapeConfig.OPPORTUNITIES_MINIMAL
1513
1734
  if shape:
@@ -1517,7 +1738,25 @@ class TangoClient:
1517
1738
  if flat_lists:
1518
1739
  params["flat_lists"] = "true"
1519
1740
 
1520
- params.update(filters)
1741
+ for key, val in (
1742
+ ("active", active),
1743
+ ("agency", agency),
1744
+ ("first_notice_date_after", first_notice_date_after),
1745
+ ("first_notice_date_before", first_notice_date_before),
1746
+ ("last_notice_date_after", last_notice_date_after),
1747
+ ("last_notice_date_before", last_notice_date_before),
1748
+ ("naics", naics),
1749
+ ("notice_type", notice_type),
1750
+ ("place_of_performance", place_of_performance),
1751
+ ("psc", psc),
1752
+ ("response_deadline_after", response_deadline_after),
1753
+ ("response_deadline_before", response_deadline_before),
1754
+ ("search", search),
1755
+ ("set_aside", set_aside),
1756
+ ("solicitation_number", solicitation_number),
1757
+ ):
1758
+ if val is not None:
1759
+ params[key] = val
1521
1760
 
1522
1761
  data = self._get("/api/opportunities/", params)
1523
1762
 
@@ -1542,7 +1781,18 @@ class TangoClient:
1542
1781
  shape: str | None = None,
1543
1782
  flat: bool = False,
1544
1783
  flat_lists: bool = False,
1545
- **filters: Any,
1784
+ active: bool | None = None,
1785
+ agency: str | None = None,
1786
+ naics: str | None = None,
1787
+ notice_type: str | None = None,
1788
+ posted_date_after: str | None = None,
1789
+ posted_date_before: str | None = None,
1790
+ psc: str | None = None,
1791
+ response_deadline_after: str | None = None,
1792
+ response_deadline_before: str | None = None,
1793
+ search: str | None = None,
1794
+ set_aside: str | None = None,
1795
+ solicitation_number: str | None = None,
1546
1796
  ) -> PaginatedResponse:
1547
1797
  """
1548
1798
  List contract notices
@@ -1550,16 +1800,24 @@ class TangoClient:
1550
1800
  Args:
1551
1801
  page: Page number
1552
1802
  limit: Results per page (max 100)
1553
- shape: Response shape string (defaults to minimal shape).
1554
- Use None to disable shaping, ShapeConfig.NOTICES_MINIMAL for minimal,
1555
- or provide custom shape string
1803
+ shape: Response shape string (defaults to minimal shape)
1556
1804
  flat: If True, flatten nested objects in shaped response
1557
1805
  flat_lists: If True, flatten arrays using indexed keys
1558
- **filters: Additional filter parameters
1806
+ active: Filter by active status
1807
+ agency: Agency filter
1808
+ naics: NAICS code filter
1809
+ notice_type: Notice type filter
1810
+ posted_date_after: Posted date after
1811
+ posted_date_before: Posted date before
1812
+ psc: PSC code filter
1813
+ response_deadline_after: Response deadline after
1814
+ response_deadline_before: Response deadline before
1815
+ search: Search query
1816
+ set_aside: Set-aside type filter
1817
+ solicitation_number: Solicitation number filter
1559
1818
  """
1560
1819
  params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1561
1820
 
1562
- # Add shape parameter with default minimal shape
1563
1821
  if shape is None:
1564
1822
  shape = ShapeConfig.NOTICES_MINIMAL
1565
1823
  if shape:
@@ -1569,7 +1827,22 @@ class TangoClient:
1569
1827
  if flat_lists:
1570
1828
  params["flat_lists"] = "true"
1571
1829
 
1572
- params.update(filters)
1830
+ for key, val in (
1831
+ ("active", active),
1832
+ ("agency", agency),
1833
+ ("naics", naics),
1834
+ ("notice_type", notice_type),
1835
+ ("posted_date_after", posted_date_after),
1836
+ ("posted_date_before", posted_date_before),
1837
+ ("psc", psc),
1838
+ ("response_deadline_after", response_deadline_after),
1839
+ ("response_deadline_before", response_deadline_before),
1840
+ ("search", search),
1841
+ ("set_aside", set_aside),
1842
+ ("solicitation_number", solicitation_number),
1843
+ ):
1844
+ if val is not None:
1845
+ params[key] = val
1573
1846
 
1574
1847
  data = self._get("/api/notices/", params)
1575
1848
 
@@ -1594,7 +1867,18 @@ class TangoClient:
1594
1867
  shape: str | None = None,
1595
1868
  flat: bool = False,
1596
1869
  flat_lists: bool = False,
1597
- **filters: Any,
1870
+ agency: str | None = None,
1871
+ applicant_types: str | None = None,
1872
+ cfda_number: str | None = None,
1873
+ funding_categories: str | None = None,
1874
+ funding_instruments: str | None = None,
1875
+ opportunity_number: str | None = None,
1876
+ posted_date_after: str | None = None,
1877
+ posted_date_before: str | None = None,
1878
+ response_date_after: str | None = None,
1879
+ response_date_before: str | None = None,
1880
+ search: str | None = None,
1881
+ status: str | None = None,
1598
1882
  ) -> PaginatedResponse:
1599
1883
  """
1600
1884
  List grants
@@ -1602,16 +1886,24 @@ class TangoClient:
1602
1886
  Args:
1603
1887
  page: Page number
1604
1888
  limit: Results per page (max 100)
1605
- shape: Response shape string (defaults to minimal shape).
1606
- Use None to disable shaping, ShapeConfig.GRANTS_MINIMAL for minimal,
1607
- or provide custom shape string
1889
+ shape: Response shape string (defaults to minimal shape)
1608
1890
  flat: If True, flatten nested objects in shaped response
1609
1891
  flat_lists: If True, flatten arrays using indexed keys
1610
- **filters: Additional filter parameters
1892
+ agency: Agency filter
1893
+ applicant_types: Applicant types filter
1894
+ cfda_number: CFDA number filter
1895
+ funding_categories: Funding categories filter
1896
+ funding_instruments: Funding instruments filter
1897
+ opportunity_number: Opportunity number filter
1898
+ posted_date_after: Posted date after
1899
+ posted_date_before: Posted date before
1900
+ response_date_after: Response date after
1901
+ response_date_before: Response date before
1902
+ search: Search query
1903
+ status: Status filter
1611
1904
  """
1612
1905
  params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
1613
1906
 
1614
- # Add shape parameter with default minimal shape
1615
1907
  if shape is None:
1616
1908
  shape = ShapeConfig.GRANTS_MINIMAL
1617
1909
  if shape:
@@ -1621,7 +1913,22 @@ class TangoClient:
1621
1913
  if flat_lists:
1622
1914
  params["flat_lists"] = "true"
1623
1915
 
1624
- params.update(filters)
1916
+ for key, val in (
1917
+ ("agency", agency),
1918
+ ("applicant_types", applicant_types),
1919
+ ("cfda_number", cfda_number),
1920
+ ("funding_categories", funding_categories),
1921
+ ("funding_instruments", funding_instruments),
1922
+ ("opportunity_number", opportunity_number),
1923
+ ("posted_date_after", posted_date_after),
1924
+ ("posted_date_before", posted_date_before),
1925
+ ("response_date_after", response_date_after),
1926
+ ("response_date_before", response_date_before),
1927
+ ("search", search),
1928
+ ("status", status),
1929
+ ):
1930
+ if val is not None:
1931
+ params[key] = val
1625
1932
 
1626
1933
  data = self._get("/api/grants/", params)
1627
1934
 
tango/models.py CHANGED
@@ -339,6 +339,19 @@ class Subaward:
339
339
  amount: Decimal | None = None
340
340
 
341
341
 
342
+ @dataclass
343
+ class GsaElibraryContract:
344
+ """Schema definition for GSA eLibrary Contract (not used for instances)"""
345
+
346
+ uuid: str
347
+ contract_number: str | None = None
348
+ schedule: str | None = None
349
+ cooperative_purchasing: bool | None = None
350
+ disaster_recovery_purchasing: bool | None = None
351
+ file_urls: list[str] | None = None
352
+ sins: list[str] | None = None
353
+
354
+
342
355
  @dataclass
343
356
  class Vehicle:
344
357
  """Schema definition for Vehicle (not used for instances)"""
@@ -624,3 +637,8 @@ class ShapeConfig:
624
637
  SUBAWARDS_MINIMAL: Final = (
625
638
  "award_key,prime_recipient(uei,display_name),subaward_recipient(uei,display_name)"
626
639
  )
640
+
641
+ # Default for list_gsa_elibrary_contracts()
642
+ GSA_ELIBRARY_CONTRACTS_MINIMAL: Final = (
643
+ "uuid,contract_number,schedule,recipient(display_name,uei),idv(key,award_date)"
644
+ )
@@ -1045,6 +1045,42 @@ SUBAWARD_SCHEMA: dict[str, FieldSchema] = {
1045
1045
  ),
1046
1046
  }
1047
1047
 
1048
+ # GSA eLibrary Contract
1049
+ GSA_ELIBRARY_IDV_REF_SCHEMA: dict[str, FieldSchema] = {
1050
+ "key": FieldSchema(name="key", type=str, is_optional=True, is_list=False),
1051
+ "award_date": FieldSchema(name="award_date", type=date, is_optional=True, is_list=False),
1052
+ }
1053
+
1054
+ GSA_ELIBRARY_CONTRACT_SCHEMA: dict[str, FieldSchema] = {
1055
+ "uuid": FieldSchema(name="uuid", type=str, is_optional=False, is_list=False),
1056
+ "contract_number": FieldSchema(
1057
+ name="contract_number", type=str, is_optional=True, is_list=False
1058
+ ),
1059
+ "cooperative_purchasing": FieldSchema(
1060
+ name="cooperative_purchasing", type=bool, is_optional=True, is_list=False
1061
+ ),
1062
+ "disaster_recovery_purchasing": FieldSchema(
1063
+ name="disaster_recovery_purchasing", type=bool, is_optional=True, is_list=False
1064
+ ),
1065
+ "file_urls": FieldSchema(name="file_urls", type=list, is_optional=True, is_list=True),
1066
+ "schedule": FieldSchema(name="schedule", type=str, is_optional=True, is_list=False),
1067
+ "sins": FieldSchema(name="sins", type=list, is_optional=True, is_list=True),
1068
+ "idv": FieldSchema(
1069
+ name="idv",
1070
+ type=dict,
1071
+ is_optional=True,
1072
+ is_list=False,
1073
+ nested_model="GsaElibraryIdvRef",
1074
+ ),
1075
+ "recipient": FieldSchema(
1076
+ name="recipient",
1077
+ type=dict,
1078
+ is_optional=True,
1079
+ is_list=False,
1080
+ nested_model="RecipientProfile",
1081
+ ),
1082
+ }
1083
+
1048
1084
  # ============================================================================
1049
1085
  # SCHEMA REGISTRY MAPPING
1050
1086
  # ============================================================================
@@ -1084,6 +1120,9 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
1084
1120
  "OTA": OTA_SCHEMA,
1085
1121
  "OTIDV": OTIDV_SCHEMA,
1086
1122
  "Subaward": SUBAWARD_SCHEMA,
1123
+ # GSA eLibrary
1124
+ "GsaElibraryContract": GSA_ELIBRARY_CONTRACT_SCHEMA,
1125
+ "GsaElibraryIdvRef": GSA_ELIBRARY_IDV_REF_SCHEMA,
1087
1126
  }
1088
1127
 
1089
1128
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tango-python
3
- Version: 0.4.0
3
+ Version: 0.4.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
@@ -1,16 +1,16 @@
1
- tango/__init__.py,sha256=PahQcpEfXRycCJhXaWm2-9AbfcdUKB8CC6EcN3AsBqc,1050
2
- tango/client.py,sha256=Y3ZsOCHX3zhrx7qjHWnQNIfLXidffxty1pBvY0yVrH0,71719
1
+ tango/__init__.py,sha256=7EdEUgCcK0lDNJWWDgdSWxx8xxryO-fHL-iml9PaYLw,1102
2
+ tango/client.py,sha256=ykeTSh4rI4P3LZFALLfGkl2Yr6jM7NWrq_E4V5vY0nc,84112
3
3
  tango/exceptions.py,sha256=JmtbOY0ofBnX24pUErh2XFlTj9dim2ngyboserEGRFw,2226
4
- tango/models.py,sha256=uEdNdEN40BAJ9OSZyoRE7L0UemSPzrQto80OZYeCVYE,19008
4
+ tango/models.py,sha256=E5jbcQ1CHevTJBD1KOREAqql9bTkft91WaNsL8qbnuA,19575
5
5
  tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
6
- tango/shapes/explicit_schemas.py,sha256=32g47TIdrJpNzlJfxg_OKu5H2o1l_3Z7JTDVf2bA4N8,50219
6
+ tango/shapes/explicit_schemas.py,sha256=iKvmpmeFU8pr4PpTenlpXDKxNbLkuyfUdDc8IV7p4zk,51734
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.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,,
13
+ tango_python-0.4.1.dist-info/METADATA,sha256=QTa_gFDGAc9jj23jv35_0Zy9Js3bXlzK6FV-9OnjLFg,16210
14
+ tango_python-0.4.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
15
+ tango_python-0.4.1.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
16
+ tango_python-0.4.1.dist-info/RECORD,,