Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **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`.

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.
- **`alerts.ebuy_request.match` is an alertable event type.** `create_webhook_alert(query_type="ebuy_request", ...)` works.

## [1.8.0] - 2026-09-28

### Added
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ protests = client.list_protests(source_system="gao", outcome="Sustained", limit=
protest = client.get_protest("CASE_UUID")
```

### GSA eBuy

```python
access = client.get_ebuy_access() # scoped to the schedule contracts linked to your account
requests = client.list_ebuy_requests(status="Open", sin="54151S", limit=25)
request = client.get_ebuy_request("RFQ1835158")
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1) # short-lived signed URL
```

### GSA eLibrary Contracts

```python
Expand Down
3 changes: 1 addition & 2 deletions contracts/shape_coverage_baseline.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
{
"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.",
"count": 15,
"count": 14,
"known_gaps": [
"unmapped_resource|agencies_contracts_awarding|(root)|None",
"unmapped_resource|agencies_contracts_funding|(root)|None",
"unmapped_resource|contracts_subawards|(root)|None",
"unmapped_resource|ebuy/requests|(root)|None",
"unmapped_resource|entities_contracts|(root)|None",
"unmapped_resource|entities_idvs|(root)|None",
"unmapped_resource|entities_lcats|(root)|None",
Expand Down
115 changes: 115 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Complete reference for all Tango Python SDK methods and functionality.
- [Protests](#protests)
- [Contract Appeals](#contract-appeals)
- [Federal Register](#federal-register)
- [GSA eBuy](#gsa-ebuy)
- [Budget](#budget)
- [Business Types](#business-types)
- [NAICS](#naics)
Expand Down Expand Up @@ -1602,6 +1603,118 @@ forecast = client.get_sled_forecast("FORECAST_UUID")

---

## GSA eBuy

GSA eBuy requests for quotes, proposals and information (RFQs, RFPs, RFIs), keyed by `rfq_id`.

**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".

### list_ebuy_requests()

List requests with filtering and shaping.

```python
requests = client.list_ebuy_requests(
page=1,
limit=25,
shape=ShapeConfig.EBUY_REQUESTS_MINIMAL,
# Filter parameters (all optional)
search=None,
rfq_id=None,
reference_number=None,
request_type=None,
status=None,
sin=None,
schedule=None,
buyer_agency=None,
agency=None,
contract_number=None,
issue_date_after=None,
issue_date_before=None,
close_date_after=None,
close_date_before=None,
ordering=None,
)
```

**Filter Parameters:**
- `search` - Full-text search over the title, description, reference number, request id and attachment text. Results rank by relevance unless `ordering` is given
- `rfq_id` - Exact request id, e.g. `"RFQ1835158"`
- `reference_number` - The buyer's own solicitation number; dashes are ignored
- `request_type` - `"RFQ"`, `"RFP"` or `"RFI"`
- `status` - `"Open"` or `"Cancelled"`, as last seen (see the note below)
- `sin` - Special Item Number, e.g. `"54151S"`
- `schedule` - GSA schedule
- `buyer_agency` - The buyer agency as eBuy names it (free text)
- `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
- `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
- `issue_date_after` / `issue_date_before` - Issue date range (`YYYY-MM-DD`, inclusive)
- `close_date_after` / `close_date_before` - Close date range (`YYYY-MM-DD`, inclusive)
- `ordering` - `issue_date` (the default, as `-issue_date`), `close_date`, `last_seen` or `modified`; prefix `-` for descending

String filters accept several values joined with `|` (OR).

**Returns:** [PaginatedResponse](#paginatedresponse) with request dictionaries

**Example:**
```python
requests = client.list_ebuy_requests(status="Open", sin="54151S", ordering="close_date")

for req in requests.results:
print(f"{req['rfq_id']} closes {req['close_date']}: {req['title']}")
print(f" last seen {req['last_seen']}")
```

**Notes:**
- **`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.
- The contract number a request was posted under is never returned in any payload.
- `buyer_agency_code` and some other buyer and contact fields are sparse on older requests.

### get_ebuy_request()

Get a single request by `rfq_id`.

```python
request = client.get_ebuy_request(
"RFQ1835158",
shape=ShapeConfig.EBUY_REQUESTS_COMPREHENSIVE,
)

for attachment in request["attachments"]:
print(attachment["doc_seq_num"], attachment["doc_name"], attachment["is_link"])
```

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.

### get_ebuy_attachment_url()

Get a short-lived download URL for one stored attachment.

```python
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1)
```

**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.

**Raises:**
- `TangoAttachmentLinkError` (a `TangoValidationError`) - The entry is an external link (`is_link`), not a stored document. The link is on `error.url`
- `TangoNotFoundError` - The request is unknown or outside your scope, the attachment does not exist, or its document has not been captured yet

### get_ebuy_access()

Check whether your account can read eBuy requests.

```python
access = client.get_ebuy_access()
if not access.enabled:
print(access.reason) # "tier_required" or "no_contract_grant"
print(access.contracts) # your own linked contracts, sorted
```

**Returns:** `EbuyAccess` with `enabled` (bool), `reason` (`"tier_required"`, `"no_contract_grant"` or `None`; `tier_required` wins when both apply) and `contracts` (list of str).

---

## Budget

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.
Expand Down Expand Up @@ -2401,6 +2514,8 @@ entity = client.get_entity("UEI_KEY", shape=ShapeConfig.ENTITIES_COMPREHENSIVE)
| `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) |
| `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 |
| `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`) |
| `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 |
| `EBUY_REQUESTS_COMPREHENSIVE` | `get_ebuy_request` | Every field, plus the `organization` and `attachments` expands |
| `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 |
| `VEHICLE_ORDERS_MINIMAL` | `list_vehicle_orders` | key, piid, award_date, recipient(display_name,uei), total_contract_value, obligated |
| `ITDASHBOARD_INVESTMENTS_MINIMAL` | `list_itdashboard_investments` | Minimal IT Dashboard investment fields |
Expand Down
1 change: 1 addition & 0 deletions docs/WEBHOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ tango webhooks list-event-types
# alerts.sled_opportunity.match State/local/education solicitation matched a saved alert
# alerts.contract_appeal.match CBCA/ASBCA appeal decision matched a saved alert
# alerts.federal_register.match Federal Register document matched a saved alert
# alerts.ebuy_request.match GSA eBuy request matched a saved alert
```

This list is served by the API, so it is always current — the SDK does not hardcode
Expand Down
1 change: 1 addition & 0 deletions scripts/check_filter_shape_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"protests": "list_protests",
"contract_appeals": "list_contract_appeals",
"federal_register": "list_federal_register_documents",
"ebuy/requests": "list_ebuy_requests",
"offices": "list_offices",
"psc": "list_psc",
"mas_sins": "list_mas_sins",
Expand Down
1 change: 1 addition & 0 deletions scripts/check_shape_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"protests": "Protest",
"contract_appeals": "ContractAppeal",
"federal_register": "FederalRegisterDocument",
"ebuy/requests": "EbuyRequest",
"offices": "Office",
"assistance_listings": "AssistanceListing",
"business_types": "BusinessType",
Expand Down
8 changes: 8 additions & 0 deletions tango/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .client import TangoClient
from .exceptions import (
TangoAPIError,
TangoAttachmentLinkError,
TangoAuthError,
TangoNotFoundError,
TangoRateLimitError,
Expand All @@ -14,6 +15,9 @@
DibbsAward,
DibbsRfp,
DibbsRfq,
EbuyAccess,
EbuyAttachment,
EbuyRequest,
Exclusion,
FederalRegisterDocument,
GsaElibraryContract,
Expand Down Expand Up @@ -62,13 +66,17 @@
"TangoAuthError",
"TangoNotFoundError",
"TangoValidationError",
"TangoAttachmentLinkError",
"TangoRateLimitError",
"RateLimitInfo",
"ResolveCandidate",
"ResolveResult",
"BudgetAccount",
"ContractAppeal",
"FederalRegisterDocument",
"EbuyAccess",
"EbuyAttachment",
"EbuyRequest",
"DibbsAward",
"DibbsRfp",
"DibbsRfq",
Expand Down
Loading
Loading