Skip to content

Commit c33db62

Browse files
makegov-mark[bot]vdavezclaude
authored
feat(ebuy): add the GSA eBuy requests resource (#70)
* feat(ebuy): add the GSA eBuy requests resource Adds `list_ebuy_requests()`, `get_ebuy_request()`, `get_ebuy_attachment_url()` and `get_ebuy_access()` over the `/api/ebuy/` resource (Tango API 5.1.0), with `EbuyRequest` / `EbuyAttachment` schemas, an `EbuyAccess` result type and the `EBUY_REQUESTS_MINIMAL` / `EBUY_REQUESTS_COMPREHENSIVE` default shapes. All fourteen of the API's list filters are explicit named parameters. `get_ebuy_attachment_url()` reads the download redirect without following it and returns the short-lived signed URL; a link entry raises `TangoValidationError` carrying the link. The response-status handling in `_request()` moves into `_raise_for_status()` so the attachment method can share it. `ebuy/requests` is now mapped in both conformance gates, and its unmapped-resource shape-coverage baseline entry is removed. `ebuy_request` is documented as an alert query type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(ebuy): raise TangoAttachmentLinkError for link attachments `get_ebuy_attachment_url()` now raises `TangoAttachmentLinkError`, a `TangoValidationError` subclass exposing the link as `.url`, when the entry is an external link rather than a stored document. The `agency` filter on `list_ebuy_requests()` documents that it requires Tango API 5.3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: V. David Zvenyach <dave@zvenyach.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 54b4733 commit c33db62

14 files changed

Lines changed: 944 additions & 66 deletions

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- **GSA eBuy requests** (Tango API 5.1.0). Four methods over the `/api/ebuy/` resource: `list_ebuy_requests()`, `get_ebuy_request()`, `get_ebuy_attachment_url()` and `get_ebuy_access()`, plus `EbuyRequest` / `EbuyAttachment` schemas, an `EbuyAccess` result type, and the `EBUY_REQUESTS_MINIMAL` / `EBUY_REQUESTS_COMPREHENSIVE` defaults. These are the RFQs, RFPs and RFIs posted to GSA eBuy. All fourteen of the API's filters are explicit named parameters, including `search` (which also matches attachment text), `reference_number`, `sin`, `agency` and `contract_number`.
12+
13+
Access is scoped to your account: you see only requests posted under the GSA schedule contracts linked to it, and the endpoints require the Pro tier or above. With no linked contract, `list_ebuy_requests()` returns an empty page rather than an error, so `get_ebuy_access()` reports whether access is enabled, why not (`tier_required` or `no_contract_grant`), and which contracts are linked. `status` is frozen at the last state a request was seen in, because a request that closes stops appearing rather than getting a final row; read `last_seen` for staleness. `get_ebuy_attachment_url()` returns the short-lived signed URL the API redirects to without downloading the document, and raises the new `TangoAttachmentLinkError` (a `TangoValidationError`) with the link on `.url` when the entry is an external link rather than a stored document. The `agency` filter requires Tango API 5.3.0.
14+
- **`alerts.ebuy_request.match` is an alertable event type.** `create_webhook_alert(query_type="ebuy_request", ...)` works.
15+
1016
## [1.8.0] - 2026-09-28
1117

1218
### Added

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ protests = client.list_protests(source_system="gao", outcome="Sustained", limit=
254254
protest = client.get_protest("CASE_UUID")
255255
```
256256

257+
### GSA eBuy
258+
259+
```python
260+
access = client.get_ebuy_access() # scoped to the schedule contracts linked to your account
261+
requests = client.list_ebuy_requests(status="Open", sin="54151S", limit=25)
262+
request = client.get_ebuy_request("RFQ1835158")
263+
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1) # short-lived signed URL
264+
```
265+
257266
### GSA eLibrary Contracts
258267

259268
```python

‎contracts/shape_coverage_baseline.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
{
22
"description": "Known reverse shape-coverage gaps (Tango exposes, SDK schema lacks), accepted as a tracked backlog. check_shape_coverage.py fails only on gaps NOT listed here. Burn down and regenerate with --update-baseline.",
3-
"count": 15,
3+
"count": 14,
44
"known_gaps": [
55
"unmapped_resource|agencies_contracts_awarding|(root)|None",
66
"unmapped_resource|agencies_contracts_funding|(root)|None",
77
"unmapped_resource|contracts_subawards|(root)|None",
8-
"unmapped_resource|ebuy/requests|(root)|None",
98
"unmapped_resource|entities_contracts|(root)|None",
109
"unmapped_resource|entities_idvs|(root)|None",
1110
"unmapped_resource|entities_lcats|(root)|None",

‎docs/API_REFERENCE.md‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Complete reference for all Tango Python SDK methods and functionality.
2424
- [Protests](#protests)
2525
- [Contract Appeals](#contract-appeals)
2626
- [Federal Register](#federal-register)
27+
- [GSA eBuy](#gsa-ebuy)
2728
- [Budget](#budget)
2829
- [Business Types](#business-types)
2930
- [NAICS](#naics)
@@ -1602,6 +1603,118 @@ forecast = client.get_sled_forecast("FORECAST_UUID")
16021603

16031604
---
16041605

1606+
## GSA eBuy
1607+
1608+
GSA eBuy requests for quotes, proposals and information (RFQs, RFPs, RFIs), keyed by `rfq_id`.
1609+
1610+
**Access is scoped to your account.** You see only requests posted under the GSA schedule contracts linked to your account, and the endpoints require the Pro tier or above (below it they return 403). With no linked contract, `list_ebuy_requests()` returns an empty page rather than an error, and `get_ebuy_request()` raises `TangoNotFoundError` for a request outside your scope, the same as for an id that does not exist. Use `get_ebuy_access()` to tell "no access" from "no matches".
1611+
1612+
### list_ebuy_requests()
1613+
1614+
List requests with filtering and shaping.
1615+
1616+
```python
1617+
requests = client.list_ebuy_requests(
1618+
page=1,
1619+
limit=25,
1620+
shape=ShapeConfig.EBUY_REQUESTS_MINIMAL,
1621+
# Filter parameters (all optional)
1622+
search=None,
1623+
rfq_id=None,
1624+
reference_number=None,
1625+
request_type=None,
1626+
status=None,
1627+
sin=None,
1628+
schedule=None,
1629+
buyer_agency=None,
1630+
agency=None,
1631+
contract_number=None,
1632+
issue_date_after=None,
1633+
issue_date_before=None,
1634+
close_date_after=None,
1635+
close_date_before=None,
1636+
ordering=None,
1637+
)
1638+
```
1639+
1640+
**Filter Parameters:**
1641+
- `search` - Full-text search over the title, description, reference number, request id and attachment text. Results rank by relevance unless `ordering` is given
1642+
- `rfq_id` - Exact request id, e.g. `"RFQ1835158"`
1643+
- `reference_number` - The buyer's own solicitation number; dashes are ignored
1644+
- `request_type` - `"RFQ"`, `"RFP"` or `"RFI"`
1645+
- `status` - `"Open"` or `"Cancelled"`, as last seen (see the note below)
1646+
- `sin` - Special Item Number, e.g. `"54151S"`
1647+
- `schedule` - GSA schedule
1648+
- `buyer_agency` - The buyer agency as eBuy names it (free text)
1649+
- `agency` - A Tango agency name, abbreviation, code or organization key, e.g. `"GSA"`. Matches the whole organization subtree, so a department includes its sub-agencies. Requires Tango API 5.3.0
1650+
- `contract_number` - Narrow to requests posted under one of your own linked contracts. A contract not linked to your account returns an empty page, not an error
1651+
- `issue_date_after` / `issue_date_before` - Issue date range (`YYYY-MM-DD`, inclusive)
1652+
- `close_date_after` / `close_date_before` - Close date range (`YYYY-MM-DD`, inclusive)
1653+
- `ordering` - `issue_date` (the default, as `-issue_date`), `close_date`, `last_seen` or `modified`; prefix `-` for descending
1654+
1655+
String filters accept several values joined with `|` (OR).
1656+
1657+
**Returns:** [PaginatedResponse](#paginatedresponse) with request dictionaries
1658+
1659+
**Example:**
1660+
```python
1661+
requests = client.list_ebuy_requests(status="Open", sin="54151S", ordering="close_date")
1662+
1663+
for req in requests.results:
1664+
print(f"{req['rfq_id']} closes {req['close_date']}: {req['title']}")
1665+
print(f" last seen {req['last_seen']}")
1666+
```
1667+
1668+
**Notes:**
1669+
- **`status` is frozen at the last state the request was seen in.** Only currently-active requests are carried, so a request that closes stops appearing rather than getting a final row. `Open` means "open the last time it was seen", not "open now"; read `last_seen` for staleness.
1670+
- The contract number a request was posted under is never returned in any payload.
1671+
- `buyer_agency_code` and some other buyer and contact fields are sparse on older requests.
1672+
1673+
### get_ebuy_request()
1674+
1675+
Get a single request by `rfq_id`.
1676+
1677+
```python
1678+
request = client.get_ebuy_request(
1679+
"RFQ1835158",
1680+
shape=ShapeConfig.EBUY_REQUESTS_COMPREHENSIVE,
1681+
)
1682+
1683+
for attachment in request["attachments"]:
1684+
print(attachment["doc_seq_num"], attachment["doc_name"], attachment["is_link"])
1685+
```
1686+
1687+
The default shape returns every field plus two expands: `organization` (the buyer office, with the same seven keys as other resources' `organization` expand) and `attachments`. Each attachment carries `doc_seq_num`, `doc_name`, `doc_type`, `doc_path`, `is_link` and `doc_session_date`. `is_link=True` means `doc_path` is an outbound URL with no stored document behind it. `amendments`, `line_items` and `addresses` are lists of objects, served as eBuy publishes them.
1688+
1689+
### get_ebuy_attachment_url()
1690+
1691+
Get a short-lived download URL for one stored attachment.
1692+
1693+
```python
1694+
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1)
1695+
```
1696+
1697+
**Returns:** The signed URL the API redirects to, as a string. The SDK reads the redirect without following it, so no document is downloaded. The URL expires after about five minutes: fetch it promptly, and call this again rather than storing it.
1698+
1699+
**Raises:**
1700+
- `TangoAttachmentLinkError` (a `TangoValidationError`) - The entry is an external link (`is_link`), not a stored document. The link is on `error.url`
1701+
- `TangoNotFoundError` - The request is unknown or outside your scope, the attachment does not exist, or its document has not been captured yet
1702+
1703+
### get_ebuy_access()
1704+
1705+
Check whether your account can read eBuy requests.
1706+
1707+
```python
1708+
access = client.get_ebuy_access()
1709+
if not access.enabled:
1710+
print(access.reason) # "tier_required" or "no_contract_grant"
1711+
print(access.contracts) # your own linked contracts, sorted
1712+
```
1713+
1714+
**Returns:** `EbuyAccess` with `enabled` (bool), `reason` (`"tier_required"`, `"no_contract_grant"` or `None`; `tier_required` wins when both apply) and `contracts` (list of str).
1715+
1716+
---
1717+
16051718
## Budget
16061719

16071720
Federal account × fiscal year budget rollups, covering the full budget lifecycle (requested → enacted → apportioned → obligated → outlayed), pre-computed ratios and trends, the contract / assistance / unlinked breakdown, and request-vs-actual spend.
@@ -2401,6 +2514,8 @@ entity = client.get_entity("UEI_KEY", shape=ShapeConfig.ENTITIES_COMPREHENSIVE)
24012514
| `CONTRACT_APPEALS_COMPREHENSIVE` | `get_contract_appeal` | The list fields plus docket_raw, docket_source, decision_date_repaired, decision_type_raw, listing_year, first_listed_at, listed, text_status, text_char_count (omits `decision_text`, which needs an Enterprise plan) |
24022515
| `FEDERAL_REGISTER_MINIMAL` | `list_federal_register_documents` | uuid, document_number, publication_date, type, subtype, title, abstract, action, agencies, cfr_references, citation, significant, comments_close_on, effective_on, html_url, pdf_url |
24032516
| `FEDERAL_REGISTER_COMPREHENSIVE` | `get_federal_register_document` | The list fields plus dates, signing_date, start_page, end_page, volume, docket_ids, dockets, regulation_id_numbers, topics, correction_of, corrections, executive_order_number, presidential_document_number, proclamation_number, comment_url, regulations_dot_gov_url, raw_text_url, body_html_url (omits `full_text`) |
2517+
| `EBUY_REQUESTS_MINIMAL` | `list_ebuy_requests` | rfq_id, request_type, title, schedule, sin, status, buyer_name, buyer_agency, buyer_agency_code, reference_number, issue_date, close_date, attachment_count, link_count, last_seen |
2518+
| `EBUY_REQUESTS_COMPREHENSIVE` | `get_ebuy_request` | Every field, plus the `organization` and `attachments` expands |
24042519
| `BUDGET_ACCOUNTS_MINIMAL` | `list_budget_accounts`, `get_budget_account` | id, federal_account_symbol, fiscal_year, agency_code/name, bureau_name, account_title, bea_category, on_off_budget, subfunction_code, lifecycle (requested/enacted/apportioned/obligated/outlayed/unobligated), contract & assistance rollups, key ratios, next-year growth |
24052520
| `VEHICLE_ORDERS_MINIMAL` | `list_vehicle_orders` | key, piid, award_date, recipient(display_name,uei), total_contract_value, obligated |
24062521
| `ITDASHBOARD_INVESTMENTS_MINIMAL` | `list_itdashboard_investments` | Minimal IT Dashboard investment fields |

‎docs/WEBHOOKS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ tango webhooks list-event-types
9090
# alerts.sled_opportunity.match State/local/education solicitation matched a saved alert
9191
# alerts.contract_appeal.match CBCA/ASBCA appeal decision matched a saved alert
9292
# alerts.federal_register.match Federal Register document matched a saved alert
93+
# alerts.ebuy_request.match GSA eBuy request matched a saved alert
9394
```
9495

9596
This list is served by the API, so it is always current — the SDK does not hardcode

‎scripts/check_filter_shape_conformance.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"protests": "list_protests",
8080
"contract_appeals": "list_contract_appeals",
8181
"federal_register": "list_federal_register_documents",
82+
"ebuy/requests": "list_ebuy_requests",
8283
"offices": "list_offices",
8384
"psc": "list_psc",
8485
"mas_sins": "list_mas_sins",

‎scripts/check_shape_coverage.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"protests": "Protest",
7171
"contract_appeals": "ContractAppeal",
7272
"federal_register": "FederalRegisterDocument",
73+
"ebuy/requests": "EbuyRequest",
7374
"offices": "Office",
7475
"assistance_listings": "AssistanceListing",
7576
"business_types": "BusinessType",

‎tango/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .client import TangoClient
44
from .exceptions import (
55
TangoAPIError,
6+
TangoAttachmentLinkError,
67
TangoAuthError,
78
TangoNotFoundError,
89
TangoRateLimitError,
@@ -14,6 +15,9 @@
1415
DibbsAward,
1516
DibbsRfp,
1617
DibbsRfq,
18+
EbuyAccess,
19+
EbuyAttachment,
20+
EbuyRequest,
1721
Exclusion,
1822
FederalRegisterDocument,
1923
GsaElibraryContract,
@@ -62,13 +66,17 @@
6266
"TangoAuthError",
6367
"TangoNotFoundError",
6468
"TangoValidationError",
69+
"TangoAttachmentLinkError",
6570
"TangoRateLimitError",
6671
"RateLimitInfo",
6772
"ResolveCandidate",
6873
"ResolveResult",
6974
"BudgetAccount",
7075
"ContractAppeal",
7176
"FederalRegisterDocument",
77+
"EbuyAccess",
78+
"EbuyAttachment",
79+
"EbuyRequest",
7280
"DibbsAward",
7381
"DibbsRfp",
7482
"DibbsRfq",

0 commit comments

Comments
 (0)