tango-python 0.5.0__py3-none-any.whl → 0.6.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 +16 -1
- tango/client.py +162 -4
- tango/models.py +43 -6
- tango/shapes/explicit_schemas.py +145 -21
- tango/webhooks/__init__.py +27 -0
- tango/webhooks/cli.py +519 -0
- tango/webhooks/receiver.py +232 -0
- tango/webhooks/signing.py +50 -0
- tango/webhooks/simulate.py +102 -0
- {tango_python-0.5.0.dist-info → tango_python-0.6.0.dist-info}/METADATA +50 -2
- tango_python-0.6.0.dist-info/RECORD +22 -0
- tango_python-0.6.0.dist-info/entry_points.txt +2 -0
- tango_python-0.5.0.dist-info/RECORD +0 -16
- {tango_python-0.5.0.dist-info → tango_python-0.6.0.dist-info}/WHEEL +0 -0
- {tango_python-0.5.0.dist-info → tango_python-0.6.0.dist-info}/licenses/LICENSE +0 -0
tango/__init__.py
CHANGED
|
@@ -15,6 +15,8 @@ from .models import (
|
|
|
15
15
|
RateLimitInfo,
|
|
16
16
|
SearchFilters,
|
|
17
17
|
ShapeConfig,
|
|
18
|
+
Vehicle,
|
|
19
|
+
VehicleMetrics,
|
|
18
20
|
WebhookEndpoint,
|
|
19
21
|
WebhookEventType,
|
|
20
22
|
WebhookEventTypesResponse,
|
|
@@ -28,8 +30,14 @@ from .shapes import (
|
|
|
28
30
|
ShapeParser,
|
|
29
31
|
TypeGenerator,
|
|
30
32
|
)
|
|
33
|
+
from .webhooks import (
|
|
34
|
+
generate_signature,
|
|
35
|
+
parse_signature_header,
|
|
36
|
+
verify_signature,
|
|
37
|
+
)
|
|
38
|
+
from .webhooks.receiver import Delivery, WebhookReceiver
|
|
31
39
|
|
|
32
|
-
__version__ = "0.
|
|
40
|
+
__version__ = "0.6.0"
|
|
33
41
|
__all__ = [
|
|
34
42
|
"TangoClient",
|
|
35
43
|
"TangoAPIError",
|
|
@@ -43,6 +51,8 @@ __all__ = [
|
|
|
43
51
|
"PaginatedResponse",
|
|
44
52
|
"SearchFilters",
|
|
45
53
|
"ShapeConfig",
|
|
54
|
+
"Vehicle",
|
|
55
|
+
"VehicleMetrics",
|
|
46
56
|
"WebhookEndpoint",
|
|
47
57
|
"WebhookEventType",
|
|
48
58
|
"WebhookEventTypesResponse",
|
|
@@ -53,4 +63,9 @@ __all__ = [
|
|
|
53
63
|
"ModelFactory",
|
|
54
64
|
"TypeGenerator",
|
|
55
65
|
"SchemaRegistry",
|
|
66
|
+
"Delivery",
|
|
67
|
+
"WebhookReceiver",
|
|
68
|
+
"generate_signature",
|
|
69
|
+
"parse_signature_header",
|
|
70
|
+
"verify_signature",
|
|
56
71
|
]
|
tango/client.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tango API Client"""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import warnings
|
|
4
5
|
from datetime import date, datetime
|
|
5
6
|
from decimal import Decimal
|
|
6
7
|
from typing import Any
|
|
@@ -123,6 +124,7 @@ class TangoClient:
|
|
|
123
124
|
@staticmethod
|
|
124
125
|
def _parse_rate_limit_headers(headers: httpx.Headers) -> RateLimitInfo:
|
|
125
126
|
"""Extract rate limit info from response headers."""
|
|
127
|
+
|
|
126
128
|
def _int_or_none(val: str | None) -> int | None:
|
|
127
129
|
if val is None:
|
|
128
130
|
return None
|
|
@@ -1447,6 +1449,40 @@ class TangoClient:
|
|
|
1447
1449
|
# Vehicles (Awards)
|
|
1448
1450
|
# ============================================================================
|
|
1449
1451
|
|
|
1452
|
+
@staticmethod
|
|
1453
|
+
def _warn_deprecated_vehicle_shape(shape: str | None) -> None:
|
|
1454
|
+
# Upstream sends `Deprecation: true` for these fields/expansions; warn
|
|
1455
|
+
# callers who request them explicitly so they have time to migrate
|
|
1456
|
+
# before tango publishes a Sunset timeline.
|
|
1457
|
+
from tango.shapes.explicit_schemas import DEPRECATED_VEHICLE_SHAPE_FIELDS
|
|
1458
|
+
|
|
1459
|
+
if not shape:
|
|
1460
|
+
return
|
|
1461
|
+
# Match top-level field tokens, ignoring nesting inside parentheses.
|
|
1462
|
+
depth = 0
|
|
1463
|
+
token = ""
|
|
1464
|
+
tokens: list[str] = []
|
|
1465
|
+
for ch in shape:
|
|
1466
|
+
if ch == "(":
|
|
1467
|
+
depth += 1
|
|
1468
|
+
elif ch == ")":
|
|
1469
|
+
depth = max(0, depth - 1)
|
|
1470
|
+
elif ch == "," and depth == 0:
|
|
1471
|
+
tokens.append(token.strip())
|
|
1472
|
+
token = ""
|
|
1473
|
+
continue
|
|
1474
|
+
token += ch
|
|
1475
|
+
tokens.append(token.strip())
|
|
1476
|
+
used = {t.split("(", 1)[0] for t in tokens} & DEPRECATED_VEHICLE_SHAPE_FIELDS
|
|
1477
|
+
if used:
|
|
1478
|
+
warnings.warn(
|
|
1479
|
+
f"Vehicle shape field(s) {sorted(used)!r} are deprecated upstream "
|
|
1480
|
+
"and may be removed in a future tango API version. The API currently "
|
|
1481
|
+
"returns a `Deprecation: true` header for these.",
|
|
1482
|
+
DeprecationWarning,
|
|
1483
|
+
stacklevel=3,
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1450
1486
|
def list_vehicles(
|
|
1451
1487
|
self,
|
|
1452
1488
|
page: int = 1,
|
|
@@ -1456,12 +1492,46 @@ class TangoClient:
|
|
|
1456
1492
|
flat_lists: bool = False,
|
|
1457
1493
|
joiner: str = ".",
|
|
1458
1494
|
search: str | None = None,
|
|
1495
|
+
vehicle_type: str | None = None,
|
|
1496
|
+
type_of_idc: str | None = None,
|
|
1497
|
+
contract_type: str | None = None,
|
|
1498
|
+
set_aside: str | None = None,
|
|
1499
|
+
who_can_use: str | None = None,
|
|
1500
|
+
naics_code: int | None = None,
|
|
1501
|
+
psc_code: str | None = None,
|
|
1502
|
+
program_acronym: str | None = None,
|
|
1503
|
+
agency: str | None = None,
|
|
1504
|
+
organization_id: str | None = None,
|
|
1505
|
+
total_obligated_min: float | int | Decimal | None = None,
|
|
1506
|
+
total_obligated_max: float | int | Decimal | None = None,
|
|
1507
|
+
idv_count_min: int | None = None,
|
|
1508
|
+
idv_count_max: int | None = None,
|
|
1509
|
+
order_count_min: int | None = None,
|
|
1510
|
+
order_count_max: int | None = None,
|
|
1511
|
+
fiscal_year: int | None = None,
|
|
1512
|
+
award_date_after: str | date | datetime | None = None,
|
|
1513
|
+
award_date_before: str | date | datetime | None = None,
|
|
1514
|
+
last_date_to_order_after: str | date | datetime | None = None,
|
|
1515
|
+
last_date_to_order_before: str | date | datetime | None = None,
|
|
1516
|
+
ordering: str | None = None,
|
|
1459
1517
|
) -> PaginatedResponse:
|
|
1460
|
-
"""List Vehicles (solicitation-centric groupings of IDVs).
|
|
1518
|
+
"""List Vehicles (solicitation-centric groupings of IDVs).
|
|
1519
|
+
|
|
1520
|
+
Multi-value filters (``vehicle_type``, ``type_of_idc``, ``contract_type``,
|
|
1521
|
+
``set_aside``) accept pipe-separated values for OR semantics, e.g.
|
|
1522
|
+
``vehicle_type="A|B|C"``.
|
|
1523
|
+
|
|
1524
|
+
``ordering`` accepts: ``vehicle_obligations``, ``latest_award_date``,
|
|
1525
|
+
``total_obligated``, ``award_date``, ``last_date_to_order``,
|
|
1526
|
+
``fiscal_year``, ``idv_count``, ``order_count``. Prefix with ``-`` for
|
|
1527
|
+
descending.
|
|
1528
|
+
"""
|
|
1461
1529
|
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
|
|
1462
1530
|
|
|
1463
1531
|
if shape is None:
|
|
1464
1532
|
shape = ShapeConfig.VEHICLES_MINIMAL
|
|
1533
|
+
else:
|
|
1534
|
+
self._warn_deprecated_vehicle_shape(shape)
|
|
1465
1535
|
if shape:
|
|
1466
1536
|
params["shape"] = shape
|
|
1467
1537
|
if flat:
|
|
@@ -1471,8 +1541,37 @@ class TangoClient:
|
|
|
1471
1541
|
if flat_lists:
|
|
1472
1542
|
params["flat_lists"] = "true"
|
|
1473
1543
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1544
|
+
for k, val in (
|
|
1545
|
+
("search", search),
|
|
1546
|
+
("vehicle_type", vehicle_type),
|
|
1547
|
+
("type_of_idc", type_of_idc),
|
|
1548
|
+
("contract_type", contract_type),
|
|
1549
|
+
("set_aside", set_aside),
|
|
1550
|
+
("who_can_use", who_can_use),
|
|
1551
|
+
("naics_code", naics_code),
|
|
1552
|
+
("psc_code", psc_code),
|
|
1553
|
+
("program_acronym", program_acronym),
|
|
1554
|
+
("agency", agency),
|
|
1555
|
+
("organization_id", organization_id),
|
|
1556
|
+
("total_obligated_min", total_obligated_min),
|
|
1557
|
+
("total_obligated_max", total_obligated_max),
|
|
1558
|
+
("idv_count_min", idv_count_min),
|
|
1559
|
+
("idv_count_max", idv_count_max),
|
|
1560
|
+
("order_count_min", order_count_min),
|
|
1561
|
+
("order_count_max", order_count_max),
|
|
1562
|
+
("fiscal_year", fiscal_year),
|
|
1563
|
+
("award_date_after", award_date_after),
|
|
1564
|
+
("award_date_before", award_date_before),
|
|
1565
|
+
("last_date_to_order_after", last_date_to_order_after),
|
|
1566
|
+
("last_date_to_order_before", last_date_to_order_before),
|
|
1567
|
+
("ordering", ordering),
|
|
1568
|
+
):
|
|
1569
|
+
if val is None:
|
|
1570
|
+
continue
|
|
1571
|
+
if isinstance(val, (date, datetime)):
|
|
1572
|
+
params[k] = val.isoformat()
|
|
1573
|
+
else:
|
|
1574
|
+
params[k] = val
|
|
1476
1575
|
|
|
1477
1576
|
data = self._get("/api/vehicles/", params)
|
|
1478
1577
|
|
|
@@ -1504,6 +1603,8 @@ class TangoClient:
|
|
|
1504
1603
|
|
|
1505
1604
|
if shape is None:
|
|
1506
1605
|
shape = ShapeConfig.VEHICLES_COMPREHENSIVE
|
|
1606
|
+
else:
|
|
1607
|
+
self._warn_deprecated_vehicle_shape(shape)
|
|
1507
1608
|
if shape:
|
|
1508
1609
|
params["shape"] = shape
|
|
1509
1610
|
if flat:
|
|
@@ -1531,8 +1632,14 @@ class TangoClient:
|
|
|
1531
1632
|
flat: bool = False,
|
|
1532
1633
|
flat_lists: bool = False,
|
|
1533
1634
|
joiner: str = ".",
|
|
1635
|
+
search: str | None = None,
|
|
1534
1636
|
) -> PaginatedResponse:
|
|
1535
|
-
"""List the IDV awardees for a Vehicle (`/api/vehicles/{uuid}/awardees/`).
|
|
1637
|
+
"""List the IDV awardees for a Vehicle (`/api/vehicles/{uuid}/awardees/`).
|
|
1638
|
+
|
|
1639
|
+
``search`` runs entity-aware full-text search across IDV fields
|
|
1640
|
+
(PIID, key, solicitation_identifier, NAICS, PSC, idv_type,
|
|
1641
|
+
fiscal_year) and recipient entity details (name, address).
|
|
1642
|
+
"""
|
|
1536
1643
|
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
|
|
1537
1644
|
|
|
1538
1645
|
if shape is None:
|
|
@@ -1546,6 +1653,9 @@ class TangoClient:
|
|
|
1546
1653
|
if flat_lists:
|
|
1547
1654
|
params["flat_lists"] = "true"
|
|
1548
1655
|
|
|
1656
|
+
if search:
|
|
1657
|
+
params["search"] = search
|
|
1658
|
+
|
|
1549
1659
|
data = self._get(f"/api/vehicles/{uuid}/awardees/", params)
|
|
1550
1660
|
|
|
1551
1661
|
results = [
|
|
@@ -1560,6 +1670,54 @@ class TangoClient:
|
|
|
1560
1670
|
results=results,
|
|
1561
1671
|
)
|
|
1562
1672
|
|
|
1673
|
+
def list_vehicle_orders(
|
|
1674
|
+
self,
|
|
1675
|
+
uuid: str,
|
|
1676
|
+
page: int = 1,
|
|
1677
|
+
limit: int = 25,
|
|
1678
|
+
shape: str | None = None,
|
|
1679
|
+
flat: bool = False,
|
|
1680
|
+
flat_lists: bool = False,
|
|
1681
|
+
joiner: str = ".",
|
|
1682
|
+
ordering: str | None = None,
|
|
1683
|
+
) -> PaginatedResponse:
|
|
1684
|
+
"""List task orders under a Vehicle's IDVs (``/api/vehicles/{uuid}/orders/``).
|
|
1685
|
+
|
|
1686
|
+
Args:
|
|
1687
|
+
ordering: Server-side sort. Allowed: ``award_date`` (default),
|
|
1688
|
+
``obligated``, ``total_contract_value``. Prefix with ``-`` for
|
|
1689
|
+
descending.
|
|
1690
|
+
"""
|
|
1691
|
+
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
|
|
1692
|
+
|
|
1693
|
+
if shape is None:
|
|
1694
|
+
shape = ShapeConfig.VEHICLE_ORDERS_MINIMAL
|
|
1695
|
+
if shape:
|
|
1696
|
+
params["shape"] = shape
|
|
1697
|
+
if flat:
|
|
1698
|
+
params["flat"] = "true"
|
|
1699
|
+
if joiner:
|
|
1700
|
+
params["joiner"] = joiner
|
|
1701
|
+
if flat_lists:
|
|
1702
|
+
params["flat_lists"] = "true"
|
|
1703
|
+
|
|
1704
|
+
if ordering:
|
|
1705
|
+
params["ordering"] = ordering
|
|
1706
|
+
|
|
1707
|
+
data = self._get(f"/api/vehicles/{uuid}/orders/", params)
|
|
1708
|
+
|
|
1709
|
+
results = [
|
|
1710
|
+
self._parse_response_with_shape(order, shape, Contract, flat, flat_lists, joiner=joiner)
|
|
1711
|
+
for order in data["results"]
|
|
1712
|
+
]
|
|
1713
|
+
|
|
1714
|
+
return PaginatedResponse(
|
|
1715
|
+
count=data["count"],
|
|
1716
|
+
next=data.get("next"),
|
|
1717
|
+
previous=data.get("previous"),
|
|
1718
|
+
results=results,
|
|
1719
|
+
)
|
|
1720
|
+
|
|
1563
1721
|
# Business Types endpoints
|
|
1564
1722
|
def list_business_types(self, page: int = 1, limit: int = 25) -> PaginatedResponse:
|
|
1565
1723
|
"""List business types"""
|
tango/models.py
CHANGED
|
@@ -413,6 +413,12 @@ class Vehicle:
|
|
|
413
413
|
uuid: str
|
|
414
414
|
solicitation_identifier: str
|
|
415
415
|
agency_id: str
|
|
416
|
+
is_synthetic_solicitation: bool | None = None
|
|
417
|
+
program_acronym: str | None = None
|
|
418
|
+
description: str | None = None
|
|
419
|
+
idv_count: int | None = None
|
|
420
|
+
total_obligated: Decimal | None = None
|
|
421
|
+
latest_award_date: date | None = None
|
|
416
422
|
solicitation_title: str | None = None
|
|
417
423
|
solicitation_date: date | None = None
|
|
418
424
|
award_date: date | None = None
|
|
@@ -420,6 +426,24 @@ class Vehicle:
|
|
|
420
426
|
fiscal_year: int | None = None
|
|
421
427
|
|
|
422
428
|
|
|
429
|
+
@dataclass
|
|
430
|
+
class VehicleMetrics:
|
|
431
|
+
"""Schema definition for the Vehicle `metrics(*)` expansion (not used for instances)"""
|
|
432
|
+
|
|
433
|
+
avg_offers_received: float | None = None
|
|
434
|
+
award_concentration_hhi: float | None = None
|
|
435
|
+
order_concentration_hhi: float | None = None
|
|
436
|
+
competed_rate: float | None = None
|
|
437
|
+
using_agency_count: int | None = None
|
|
438
|
+
avg_order_value: float | None = None
|
|
439
|
+
max_order_value: float | None = None
|
|
440
|
+
top_recipient_share: float | None = None
|
|
441
|
+
recent_obligations_24mo: float | None = None
|
|
442
|
+
recent_orders_24mo: int | None = None
|
|
443
|
+
days_since_last_order: int | None = None
|
|
444
|
+
obligation_to_ceiling_ratio: float | None = None
|
|
445
|
+
|
|
446
|
+
|
|
423
447
|
@dataclass
|
|
424
448
|
class Entity:
|
|
425
449
|
"""Schema definition for Entity (not used for instances)"""
|
|
@@ -690,21 +714,34 @@ class ShapeConfig:
|
|
|
690
714
|
|
|
691
715
|
# Default for list_vehicles()
|
|
692
716
|
VEHICLES_MINIMAL: Final = (
|
|
693
|
-
"uuid,solicitation_identifier,
|
|
694
|
-
"
|
|
717
|
+
"uuid,solicitation_identifier,is_synthetic_solicitation,program_acronym,"
|
|
718
|
+
"organization_id,organization,vehicle_type,description,"
|
|
719
|
+
"idv_count,awardee_count,order_count,total_obligated,"
|
|
720
|
+
"vehicle_obligations,vehicle_contracts_value,latest_award_date,"
|
|
721
|
+
"solicitation_title,solicitation_date"
|
|
695
722
|
)
|
|
696
723
|
|
|
697
724
|
# Default for get_vehicle()
|
|
698
725
|
VEHICLES_COMPREHENSIVE: Final = (
|
|
699
|
-
"uuid,solicitation_identifier,agency_id,
|
|
700
|
-
"
|
|
701
|
-
"
|
|
702
|
-
"
|
|
726
|
+
"uuid,solicitation_identifier,is_synthetic_solicitation,agency_id,program_acronym,"
|
|
727
|
+
"organization_id,organization(*),vehicle_type,who_can_use,"
|
|
728
|
+
"solicitation_title,solicitation_description,solicitation_date,opportunity_id,"
|
|
729
|
+
"naics_code,psc_code,set_aside,"
|
|
730
|
+
"fiscal_year,award_date,latest_award_date,last_date_to_order,"
|
|
731
|
+
"description,idv_count,awardee_count,order_count,total_obligated,"
|
|
732
|
+
"vehicle_obligations,vehicle_contracts_value,"
|
|
733
|
+
"type_of_idc,contract_type,metrics(*)"
|
|
703
734
|
)
|
|
704
735
|
|
|
705
736
|
# Default for list_vehicle_awardees()
|
|
706
737
|
VEHICLE_AWARDEES_MINIMAL: Final = "uuid,key,piid,award_date,title,order_count,idv_obligations,idv_contracts_value,recipient(display_name,uei)"
|
|
707
738
|
|
|
739
|
+
# Default for list_vehicle_orders()
|
|
740
|
+
VEHICLE_ORDERS_MINIMAL: Final = (
|
|
741
|
+
"key,piid,award_date,obligated,total_contract_value,description,"
|
|
742
|
+
"recipient(display_name,uei)"
|
|
743
|
+
)
|
|
744
|
+
|
|
708
745
|
# Default for list_organizations()
|
|
709
746
|
ORGANIZATIONS_MINIMAL: Final = "key,fh_key,name,level,type,short_name"
|
|
710
747
|
|
tango/shapes/explicit_schemas.py
CHANGED
|
@@ -37,6 +37,25 @@ AWARD_OFFICE_SCHEMA: dict[str, FieldSchema] = {
|
|
|
37
37
|
),
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
# Canonical 7-key office payload returned by the `organization(...)` shape
|
|
41
|
+
# expand on awards, vehicles, forecasts, grants, IT Dashboard, and protests.
|
|
42
|
+
# Resolved deterministically from the resource's organization_id.
|
|
43
|
+
ORGANIZATION_OFFICE_SCHEMA: dict[str, FieldSchema] = {
|
|
44
|
+
"organization_id": FieldSchema(
|
|
45
|
+
name="organization_id", type=str, is_optional=True, is_list=False
|
|
46
|
+
),
|
|
47
|
+
"office_code": FieldSchema(name="office_code", type=str, is_optional=True, is_list=False),
|
|
48
|
+
"office_name": FieldSchema(name="office_name", type=str, is_optional=True, is_list=False),
|
|
49
|
+
"agency_code": FieldSchema(name="agency_code", type=str, is_optional=True, is_list=False),
|
|
50
|
+
"agency_name": FieldSchema(name="agency_name", type=str, is_optional=True, is_list=False),
|
|
51
|
+
"department_code": FieldSchema(
|
|
52
|
+
name="department_code", type=str, is_optional=True, is_list=False
|
|
53
|
+
),
|
|
54
|
+
"department_name": FieldSchema(
|
|
55
|
+
name="department_name", type=str, is_optional=True, is_list=False
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
PERIOD_OF_PERFORMANCE_IDV_SCHEMA: dict[str, FieldSchema] = {
|
|
41
60
|
"start_date": FieldSchema(name="start_date", type=date, is_optional=True, is_list=False),
|
|
42
61
|
"last_date_to_order": FieldSchema(
|
|
@@ -396,6 +415,13 @@ CONTRACT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
396
415
|
"undefinitized_action": FieldSchema(
|
|
397
416
|
name="undefinitized_action", type=str, is_optional=True, is_list=False
|
|
398
417
|
),
|
|
418
|
+
"vehicle": FieldSchema(
|
|
419
|
+
name="vehicle",
|
|
420
|
+
type=dict,
|
|
421
|
+
is_optional=True,
|
|
422
|
+
is_list=False,
|
|
423
|
+
nested_model="Vehicle",
|
|
424
|
+
),
|
|
399
425
|
}
|
|
400
426
|
|
|
401
427
|
|
|
@@ -556,6 +582,13 @@ FORECAST_SCHEMA: dict[str, FieldSchema] = {
|
|
|
556
582
|
"source_system": FieldSchema(name="source_system", type=str, is_optional=False, is_list=False),
|
|
557
583
|
"status": FieldSchema(name="status", type=str, is_optional=True, is_list=False),
|
|
558
584
|
"title": FieldSchema(name="title", type=str, is_optional=False, is_list=False),
|
|
585
|
+
"organization": FieldSchema(
|
|
586
|
+
name="organization",
|
|
587
|
+
type=dict,
|
|
588
|
+
is_optional=True,
|
|
589
|
+
is_list=False,
|
|
590
|
+
nested_model="OrganizationOffice",
|
|
591
|
+
),
|
|
559
592
|
}
|
|
560
593
|
|
|
561
594
|
|
|
@@ -685,6 +718,13 @@ PROTEST_SCHEMA: dict[str, FieldSchema] = {
|
|
|
685
718
|
"dockets": FieldSchema(
|
|
686
719
|
name="dockets", type=dict, is_optional=True, is_list=True, nested_model="ProtestDocket"
|
|
687
720
|
),
|
|
721
|
+
"organization": FieldSchema(
|
|
722
|
+
name="organization",
|
|
723
|
+
type=dict,
|
|
724
|
+
is_optional=True,
|
|
725
|
+
is_list=False,
|
|
726
|
+
nested_model="OrganizationOffice",
|
|
727
|
+
),
|
|
688
728
|
}
|
|
689
729
|
|
|
690
730
|
|
|
@@ -785,6 +825,13 @@ GRANT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
785
825
|
is_list=True,
|
|
786
826
|
nested_model="GrantAttachment",
|
|
787
827
|
),
|
|
828
|
+
"organization": FieldSchema(
|
|
829
|
+
name="organization",
|
|
830
|
+
type=dict,
|
|
831
|
+
is_optional=True,
|
|
832
|
+
is_list=False,
|
|
833
|
+
nested_model="OrganizationOffice",
|
|
834
|
+
),
|
|
788
835
|
}
|
|
789
836
|
|
|
790
837
|
|
|
@@ -838,6 +885,45 @@ VEHICLE_COMPETITION_DETAILS_SCHEMA: dict[str, FieldSchema] = {
|
|
|
838
885
|
}
|
|
839
886
|
|
|
840
887
|
|
|
888
|
+
# Vehicles expose a "metrics(...)" expansion bundling computed metrics.
|
|
889
|
+
VEHICLE_METRICS_SCHEMA: dict[str, FieldSchema] = {
|
|
890
|
+
"avg_offers_received": FieldSchema(
|
|
891
|
+
name="avg_offers_received", type=float, is_optional=True, is_list=False
|
|
892
|
+
),
|
|
893
|
+
"award_concentration_hhi": FieldSchema(
|
|
894
|
+
name="award_concentration_hhi", type=float, is_optional=True, is_list=False
|
|
895
|
+
),
|
|
896
|
+
"order_concentration_hhi": FieldSchema(
|
|
897
|
+
name="order_concentration_hhi", type=float, is_optional=True, is_list=False
|
|
898
|
+
),
|
|
899
|
+
"competed_rate": FieldSchema(name="competed_rate", type=float, is_optional=True, is_list=False),
|
|
900
|
+
"using_agency_count": FieldSchema(
|
|
901
|
+
name="using_agency_count", type=int, is_optional=True, is_list=False
|
|
902
|
+
),
|
|
903
|
+
"avg_order_value": FieldSchema(
|
|
904
|
+
name="avg_order_value", type=float, is_optional=True, is_list=False
|
|
905
|
+
),
|
|
906
|
+
"max_order_value": FieldSchema(
|
|
907
|
+
name="max_order_value", type=float, is_optional=True, is_list=False
|
|
908
|
+
),
|
|
909
|
+
"top_recipient_share": FieldSchema(
|
|
910
|
+
name="top_recipient_share", type=float, is_optional=True, is_list=False
|
|
911
|
+
),
|
|
912
|
+
"recent_obligations_24mo": FieldSchema(
|
|
913
|
+
name="recent_obligations_24mo", type=float, is_optional=True, is_list=False
|
|
914
|
+
),
|
|
915
|
+
"recent_orders_24mo": FieldSchema(
|
|
916
|
+
name="recent_orders_24mo", type=int, is_optional=True, is_list=False
|
|
917
|
+
),
|
|
918
|
+
"days_since_last_order": FieldSchema(
|
|
919
|
+
name="days_since_last_order", type=int, is_optional=True, is_list=False
|
|
920
|
+
),
|
|
921
|
+
"obligation_to_ceiling_ratio": FieldSchema(
|
|
922
|
+
name="obligation_to_ceiling_ratio", type=float, is_optional=True, is_list=False
|
|
923
|
+
),
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
|
|
841
927
|
# IDV schema (used for `/api/idvs/`, and also by Vehicles awardees shaping).
|
|
842
928
|
IDV_SCHEMA: dict[str, FieldSchema] = {
|
|
843
929
|
# Identifiers
|
|
@@ -970,26 +1056,52 @@ VEHICLE_SCHEMA: dict[str, FieldSchema] = {
|
|
|
970
1056
|
"solicitation_identifier": FieldSchema(
|
|
971
1057
|
name="solicitation_identifier", type=str, is_optional=False, is_list=False
|
|
972
1058
|
),
|
|
1059
|
+
"is_synthetic_solicitation": FieldSchema(
|
|
1060
|
+
name="is_synthetic_solicitation", type=bool, is_optional=True, is_list=False
|
|
1061
|
+
),
|
|
973
1062
|
"agency_id": FieldSchema(name="agency_id", type=str, is_optional=False, is_list=False),
|
|
974
1063
|
"organization_id": FieldSchema(
|
|
975
1064
|
name="organization_id", type=str, is_optional=True, is_list=False
|
|
976
1065
|
),
|
|
1066
|
+
# Live awarding-org snapshot — canonical 7-key office payload. Selectable
|
|
1067
|
+
# as the bare leaf (`shape=...,organization` returns the full dict) or as
|
|
1068
|
+
# a sub-selectable expansion (`shape=...,organization(office_code,...)`).
|
|
1069
|
+
"organization": FieldSchema(
|
|
1070
|
+
name="organization",
|
|
1071
|
+
type=dict,
|
|
1072
|
+
is_optional=True,
|
|
1073
|
+
is_list=False,
|
|
1074
|
+
nested_model="OrganizationOffice",
|
|
1075
|
+
),
|
|
977
1076
|
# Choice fields are returned as {code, description} objects.
|
|
978
1077
|
"vehicle_type": FieldSchema(name="vehicle_type", type=dict, is_optional=True, is_list=False),
|
|
1078
|
+
"program_acronym": FieldSchema(
|
|
1079
|
+
name="program_acronym", type=str, is_optional=True, is_list=False
|
|
1080
|
+
),
|
|
979
1081
|
"who_can_use": FieldSchema(name="who_can_use", type=dict, is_optional=True, is_list=False),
|
|
980
1082
|
"type_of_idc": FieldSchema(name="type_of_idc", type=dict, is_optional=True, is_list=False),
|
|
981
1083
|
"contract_type": FieldSchema(name="contract_type", type=dict, is_optional=True, is_list=False),
|
|
1084
|
+
# Deprecated: recomputed from IDVs at request time. Upstream sends `Deprecation: true`.
|
|
982
1085
|
"agency_details": FieldSchema(
|
|
983
1086
|
name="agency_details", type=dict, is_optional=True, is_list=False
|
|
984
1087
|
),
|
|
1088
|
+
"description": FieldSchema(name="description", type=str, is_optional=True, is_list=False),
|
|
985
1089
|
"descriptions": FieldSchema(name="descriptions", type=str, is_optional=True, is_list=True),
|
|
986
1090
|
"fiscal_year": FieldSchema(name="fiscal_year", type=int, is_optional=True, is_list=False),
|
|
987
1091
|
"award_date": FieldSchema(name="award_date", type=date, is_optional=True, is_list=False),
|
|
1092
|
+
"latest_award_date": FieldSchema(
|
|
1093
|
+
name="latest_award_date", type=date, is_optional=True, is_list=False
|
|
1094
|
+
),
|
|
988
1095
|
"last_date_to_order": FieldSchema(
|
|
989
1096
|
name="last_date_to_order", type=date, is_optional=True, is_list=False
|
|
990
1097
|
),
|
|
1098
|
+
# Denormalized rollups
|
|
1099
|
+
"idv_count": FieldSchema(name="idv_count", type=int, is_optional=True, is_list=False),
|
|
991
1100
|
"awardee_count": FieldSchema(name="awardee_count", type=int, is_optional=True, is_list=False),
|
|
992
1101
|
"order_count": FieldSchema(name="order_count", type=int, is_optional=True, is_list=False),
|
|
1102
|
+
"total_obligated": FieldSchema(
|
|
1103
|
+
name="total_obligated", type=Decimal, is_optional=True, is_list=False
|
|
1104
|
+
),
|
|
993
1105
|
"vehicle_obligations": FieldSchema(
|
|
994
1106
|
name="vehicle_obligations", type=Decimal, is_optional=True, is_list=False
|
|
995
1107
|
),
|
|
@@ -1006,6 +1118,7 @@ VEHICLE_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1006
1118
|
"solicitation_date": FieldSchema(
|
|
1007
1119
|
name="solicitation_date", type=date, is_optional=True, is_list=False
|
|
1008
1120
|
),
|
|
1121
|
+
"opportunity_id": FieldSchema(name="opportunity_id", type=str, is_optional=True, is_list=False),
|
|
1009
1122
|
"naics_code": FieldSchema(name="naics_code", type=int, is_optional=True, is_list=False),
|
|
1010
1123
|
"psc_code": FieldSchema(name="psc_code", type=str, is_optional=True, is_list=False),
|
|
1011
1124
|
"set_aside": FieldSchema(name="set_aside", type=str, is_optional=True, is_list=False),
|
|
@@ -1013,6 +1126,14 @@ VEHICLE_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1013
1126
|
"awardees": FieldSchema(
|
|
1014
1127
|
name="awardees", type=dict, is_optional=True, is_list=True, nested_model="IDV"
|
|
1015
1128
|
),
|
|
1129
|
+
"metrics": FieldSchema(
|
|
1130
|
+
name="metrics",
|
|
1131
|
+
type=dict,
|
|
1132
|
+
is_optional=True,
|
|
1133
|
+
is_list=False,
|
|
1134
|
+
nested_model="VehicleMetrics",
|
|
1135
|
+
),
|
|
1136
|
+
# Deprecated expansions (upstream sends `Deprecation: true`).
|
|
1016
1137
|
"opportunity": FieldSchema(
|
|
1017
1138
|
name="opportunity", type=dict, is_optional=True, is_list=False, nested_model="Opportunity"
|
|
1018
1139
|
),
|
|
@@ -1026,6 +1147,14 @@ VEHICLE_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1026
1147
|
}
|
|
1027
1148
|
|
|
1028
1149
|
|
|
1150
|
+
# Top-level vehicle shape fields that upstream marks deprecated (the API sends
|
|
1151
|
+
# `Deprecation: true` and recomputes them from IDVs at request time). The client
|
|
1152
|
+
# emits a `DeprecationWarning` when callers explicitly include these in `shape`.
|
|
1153
|
+
DEPRECATED_VEHICLE_SHAPE_FIELDS: frozenset[str] = frozenset(
|
|
1154
|
+
{"agency_details", "competition_details", "opportunity"}
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
|
|
1029
1158
|
# Organization (agencies hierarchy)
|
|
1030
1159
|
ORGANIZATION_SCHEMA: dict[str, FieldSchema] = {
|
|
1031
1160
|
"key": FieldSchema(name="key", type=str, is_optional=True, is_list=False),
|
|
@@ -1135,18 +1264,10 @@ GSA_ELIBRARY_CONTRACT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1135
1264
|
# IT Dashboard Investment
|
|
1136
1265
|
ITDASHBOARD_INVESTMENT_SCHEMA: dict[str, FieldSchema] = {
|
|
1137
1266
|
"uii": FieldSchema(name="uii", type=str, is_optional=False, is_list=False),
|
|
1138
|
-
"agency_code": FieldSchema(
|
|
1139
|
-
|
|
1140
|
-
),
|
|
1141
|
-
"
|
|
1142
|
-
name="agency_name", type=str, is_optional=True, is_list=False
|
|
1143
|
-
),
|
|
1144
|
-
"bureau_code": FieldSchema(
|
|
1145
|
-
name="bureau_code", type=int, is_optional=True, is_list=False
|
|
1146
|
-
),
|
|
1147
|
-
"bureau_name": FieldSchema(
|
|
1148
|
-
name="bureau_name", type=str, is_optional=True, is_list=False
|
|
1149
|
-
),
|
|
1267
|
+
"agency_code": FieldSchema(name="agency_code", type=int, is_optional=True, is_list=False),
|
|
1268
|
+
"agency_name": FieldSchema(name="agency_name", type=str, is_optional=True, is_list=False),
|
|
1269
|
+
"bureau_code": FieldSchema(name="bureau_code", type=int, is_optional=True, is_list=False),
|
|
1270
|
+
"bureau_name": FieldSchema(name="bureau_name", type=str, is_optional=True, is_list=False),
|
|
1150
1271
|
"investment_title": FieldSchema(
|
|
1151
1272
|
name="investment_title", type=str, is_optional=True, is_list=False
|
|
1152
1273
|
),
|
|
@@ -1167,15 +1288,9 @@ ITDASHBOARD_INVESTMENT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1167
1288
|
# Modeled as opaque dict/list since their inner shapes are dynamic.
|
|
1168
1289
|
"funding": FieldSchema(name="funding", type=dict, is_optional=True, is_list=False),
|
|
1169
1290
|
"details": FieldSchema(name="details", type=dict, is_optional=True, is_list=False),
|
|
1170
|
-
"cio_evaluation": FieldSchema(
|
|
1171
|
-
|
|
1172
|
-
),
|
|
1173
|
-
"contracts": FieldSchema(
|
|
1174
|
-
name="contracts", type=list, is_optional=True, is_list=True
|
|
1175
|
-
),
|
|
1176
|
-
"projects": FieldSchema(
|
|
1177
|
-
name="projects", type=list, is_optional=True, is_list=True
|
|
1178
|
-
),
|
|
1291
|
+
"cio_evaluation": FieldSchema(name="cio_evaluation", type=list, is_optional=True, is_list=True),
|
|
1292
|
+
"contracts": FieldSchema(name="contracts", type=list, is_optional=True, is_list=True),
|
|
1293
|
+
"projects": FieldSchema(name="projects", type=list, is_optional=True, is_list=True),
|
|
1179
1294
|
"cost_pools_towers": FieldSchema(
|
|
1180
1295
|
name="cost_pools_towers", type=list, is_optional=True, is_list=True
|
|
1181
1296
|
),
|
|
@@ -1191,6 +1306,13 @@ ITDASHBOARD_INVESTMENT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1191
1306
|
"operational_analysis": FieldSchema(
|
|
1192
1307
|
name="operational_analysis", type=list, is_optional=True, is_list=True
|
|
1193
1308
|
),
|
|
1309
|
+
"organization": FieldSchema(
|
|
1310
|
+
name="organization",
|
|
1311
|
+
type=dict,
|
|
1312
|
+
is_optional=True,
|
|
1313
|
+
is_list=False,
|
|
1314
|
+
nested_model="OrganizationOffice",
|
|
1315
|
+
),
|
|
1194
1316
|
}
|
|
1195
1317
|
|
|
1196
1318
|
# ============================================================================
|
|
@@ -1200,6 +1322,7 @@ ITDASHBOARD_INVESTMENT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1200
1322
|
EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
|
|
1201
1323
|
"Office": OFFICE_SCHEMA,
|
|
1202
1324
|
"AwardOffice": AWARD_OFFICE_SCHEMA,
|
|
1325
|
+
"OrganizationOffice": ORGANIZATION_OFFICE_SCHEMA,
|
|
1203
1326
|
"Location": LOCATION_SCHEMA,
|
|
1204
1327
|
"PlaceOfPerformance": PLACE_OF_PERFORMANCE_SCHEMA,
|
|
1205
1328
|
"Competition": COMPETITION_SCHEMA,
|
|
@@ -1225,6 +1348,7 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
|
|
|
1225
1348
|
"Vehicle": VEHICLE_SCHEMA,
|
|
1226
1349
|
"IDV": IDV_SCHEMA,
|
|
1227
1350
|
"VehicleCompetitionDetails": VEHICLE_COMPETITION_DETAILS_SCHEMA,
|
|
1351
|
+
"VehicleMetrics": VEHICLE_METRICS_SCHEMA,
|
|
1228
1352
|
# Nested schemas for Grant fields
|
|
1229
1353
|
"CFDANumber": CFDA_NUMBER_SCHEMA,
|
|
1230
1354
|
"CodeDescription": CODE_DESCRIPTION_SCHEMA,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Tango webhooks: signature helpers and developer tooling.
|
|
2
|
+
|
|
3
|
+
The signing helpers (:func:`verify_signature`, :func:`generate_signature`,
|
|
4
|
+
:func:`parse_signature_header`) are pure stdlib and importable from a default
|
|
5
|
+
``pip install tango``. The CLI (``tango webhooks ...``) and the in-process
|
|
6
|
+
:class:`~tango.webhooks.receiver.WebhookReceiver` ship with the
|
|
7
|
+
``tango[webhooks]`` extra.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from tango.webhooks.signing import (
|
|
11
|
+
SIGNATURE_HEADER,
|
|
12
|
+
SIGNATURE_PREFIX,
|
|
13
|
+
generate_signature,
|
|
14
|
+
parse_signature_header,
|
|
15
|
+
verify_signature,
|
|
16
|
+
)
|
|
17
|
+
from tango.webhooks.simulate import SignedRequest, sign
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"SIGNATURE_HEADER",
|
|
21
|
+
"SIGNATURE_PREFIX",
|
|
22
|
+
"SignedRequest",
|
|
23
|
+
"generate_signature",
|
|
24
|
+
"parse_signature_header",
|
|
25
|
+
"sign",
|
|
26
|
+
"verify_signature",
|
|
27
|
+
]
|