Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c172154
fix(client): accept json kwarg alias in _post/_patch
infinityplusone May 12, 2026
51b6cfe
feat(client): add ordering param to seven list_* methods
infinityplusone May 12, 2026
f3b6255
feat(webhooks): pass name/endpoint and filter fields to write methods
infinityplusone May 12, 2026
8da90c9
feat(client): add 30+ missing methods for API parity
infinityplusone May 12, 2026
41d62a5
test(client): unit + smoke coverage for API-parity additions
infinityplusone May 12, 2026
a8a1d68
docs: correct README, API_REFERENCE, DEVELOPERS, SHAPES, WEBHOOKS aga…
infinityplusone May 12, 2026
05cd92b
docs: add DYNAMIC_MODELS.md for SDK doc parity
infinityplusone May 12, 2026
7ede733
feat(webhooks): remove subject-based subscription surface (v0.7.0)
infinityplusone May 12, 2026
224878e
fix(webhooks): generate_signature returns prefixed wire form
infinityplusone May 12, 2026
ecdd943
docs(webhooks): sweep dead event-type strings from CLI examples
infinityplusone May 12, 2026
1b3656b
fix(cli): add required --name to `tango webhooks endpoints create`
infinityplusone May 12, 2026
6cb9b91
fix(types): tighten WebhookAlert field types to match server guarantees
infinityplusone May 12, 2026
c81b480
feat(webhooks): type get_webhook_sample_payload return as TypedDict u…
infinityplusone May 12, 2026
c7c7731
docs(webhooks): add --name to CLI examples and correct multi-endpoint…
infinityplusone May 12, 2026
7b233c5
ShapeParser: accept naics(...) / psc(...) as canonical (tango#2266)
infinityplusone May 12, 2026
229f49c
fix(shapes): correct Subaward field list to match server
infinityplusone May 13, 2026
42d760d
docs: port docs-only content from docs.makegov.com (makegov/docs#16)
infinityplusone May 13, 2026
401207e
ci(docs): add docs-dispatch workflow for makegov/docs auto-pull (make…
infinityplusone May 13, 2026
c21637b
chore(release): 1.0.0
infinityplusone May 13, 2026
06e7feb
chore(packaging): whitelist sdist contents to block local-publish leaks
infinityplusone May 13, 2026
132641f
fix: address copilot review + windows webhook-receiver test (PR #25)
infinityplusone May 13, 2026
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
58 changes: 58 additions & 0 deletions .github/workflows/docs-dispatch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Docs dispatch

# Fires on push to main when content that affects the published docs site
# changes (docs/, README, or CHANGELOG). Notifies makegov/docs via
# repository_dispatch so the docs site rebuilds without waiting for someone
# to push to the composer.
#
# tango-python is a `coloc-source` repo: its docs/ folder is the authoritative
# source for the Python SDK pages on docs.makegov.com (see makegov/docs#15).
#
# Required secrets:
# DOCS_DISPATCH_TOKEN — GitHub token with contents:write on makegov/docs.
#
# Required variables (optional):
# DOCS_TARGET_REPO — override the dispatch target (default: makegov/docs).

on:
push:
branches:
- main
paths:
- "docs/**"
- "README.md"
- "CHANGELOG.md"
workflow_dispatch:

jobs:
dispatch:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Detect changed paths
id: changes
run: |
set -euo pipefail
base=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)
external=$(git diff --name-only "$base" HEAD -- 'docs' 'README.md' 'CHANGELOG.md' | paste -sd, -)
{
echo "external=$external"
echo "has_external=$([ -n "$external" ] && echo true || echo false)"
} >> "$GITHUB_OUTPUT"

- name: Dispatch to docs composer (makegov/docs)
if: steps.changes.outputs.has_external == 'true'
env:
GH_TOKEN: ${{ secrets.DOCS_DISPATCH_TOKEN }}
TARGET: ${{ vars.DOCS_TARGET_REPO || 'makegov/docs' }}
run: |
gh api "repos/$TARGET/dispatches" \
-f event_type=external_updated \
-f "client_payload[source_repo]=${{ github.repository }}" \
-f "client_payload[source_ref]=${{ github.sha }}" \
-f "client_payload[changed_paths]=${{ steps.changes.outputs.external }}"
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2026-05-13

> First stable release. `tango-python` is now at full API parity with the
> Tango HTTP surface, the legacy subject-based webhook subscription
> mechanism has been removed in favor of filter alerts, the shape parser
> agrees byte-for-byte with the server's expand-alias handling, and the
> SDK's docs are auto-published to `docs.makegov.com/sdks/python/` via the
> composer pipeline (makegov/docs#15 / makegov/docs#16). From `1.x` on,
> we'll only do breaking changes on a major bump.
>
> Originally tracked as: API parity (PR #25), subject-based webhook
> removal (PR #27 / issue #2275), shape-validator alias support (PR #28 /
> issue #2266), and the docs-only content port (makegov/docs#16).

### Added
- `ordering` parameter on `list_forecasts`, `list_grants`, `list_subawards`, `list_gsa_elibrary_contracts`, and `list_opportunities`. Prefix with `-` for descending. Closes a parity gap with the API surface (these endpoints all accept `?ordering=` server-side).
- `create_webhook_endpoint` accepts `name=` (keyword-only) and now **requires** it. The Tango API enforces unique `(user, name)` on endpoints; omitting `name` returns a 400 server-side, so the SDK raises `TangoValidationError` client-side instead of round-tripping. (0.7.0 — never publicly released — emitted a `DeprecationWarning` instead.)
- `update_webhook_endpoint` accepts `name=` for renaming an endpoint.
- Webhook alerts (filter subscriptions): `list_webhook_alerts`, `get_webhook_alert`, `create_webhook_alert`, `update_webhook_alert`, `delete_webhook_alert` — the canonical write surface over `/api/webhooks/alerts/`. New `WebhookAlert` dataclass exported from the top-level package.
- `resolve(name, target_type, ...)` — POST `/api/resolve/` to rank entity / organization candidates from a free-text name. Returns `ResolveResult` with `ResolveCandidate` entries (both exported).
- `validate(identifier_type, value)` — POST `/api/validate/` to validate the format of a PIID, solicitation number, or UEI. Returns `ValidateResult` (exported).
- Reference data: `list_departments`, `get_department`, `list_psc`, `get_psc`, `get_psc_metrics`, `get_naics`, `get_naics_metrics`, `get_business_type`, `list_assistance_listings`, `get_assistance_listing`, `list_mas_sins`, `get_mas_sin`.
- Entity sub-resources: `list_entity_contracts`, `list_entity_idvs`, `list_entity_otas`, `list_entity_otidvs`, `list_entity_subawards`, `list_entity_lcats`, `get_entity_metrics`. All shape-aware where the underlying endpoint supports shaping.
- IDV sub-resources: `list_idv_lcats`.
- Agency sub-resources: `list_agency_awarding_contracts`, `list_agency_funding_contracts`.
- Misc: `search_opportunity_attachments(q, top_k, include_extracted_text)` for `/api/opportunities/attachment-search/`; `get_version()` for `/api/version/`; `list_api_keys()` for `/api/api-keys/`.

### Changed
- `create_webhook_alert` accepts `endpoint=` (keyword-only). Required for accounts with multiple webhook endpoints; auto-resolves for single-endpoint accounts. Closes the multi-endpoint smoke-test gap (tango#2256).
- `test_webhook_delivery` now sends the canonical `endpoint` body key instead of the deprecated `endpoint_id` alias (tango#2252). The Python kwarg name stays `endpoint_id=` for backwards compatibility; the wire payload is what changed.
- **`generate_signature(body, secret)` now returns the full wire form `"sha256=<hex>"`** instead of bare hex. Callers can assign the return value directly to the `X-Tango-Signature` header without wrapping in a format string. This is a breaking change for code that relied on the bare-hex return; pass it through `parse_signature_header()` to recover the previous form. `verify_signature` accepts both prefixed and bare-hex inputs (unchanged), so receivers continue to work either way.

### Removed
- **Subject-based webhook subscription surface** (tango#2275). Migrate to `create_webhook_alert(...)` and the alerts API.
- Methods: `list_webhook_subscriptions`, `get_webhook_subscription`, `create_webhook_subscription`, `update_webhook_subscription`, `delete_webhook_subscription`.
- Dataclasses: `WebhookSubscription`, `WebhookSubjectTypeDefinition`. Both are no longer exported from the top-level `tango` package — importing them raises `ImportError`.
- Fields: `default_subject_type` removed from `WebhookEventType`; `subject_types` and `subject_type_definitions` removed from `WebhookEventTypesResponse`. The server's `/api/webhooks/event-types/` response no longer carries these.
- CLI: the entire `tango webhooks subscriptions` Click subgroup (`list` / `get` / `create` / `delete`). Use the SDK's `client.create_webhook_alert(...)` etc. directly — there is no CLI subgroup for alerts.
- `ordering` kwarg from `list_notices` and `list_protests`. The notices and protests viewsets reject every `?ordering=` value at runtime (tango#2254); the kwarg silently sent unsupported values. Other five list methods retain `ordering`.

### Fixed
- `TangoClient._post()` and `_patch()` accept both `json_data=` (positional) and `json=` (keyword) for backward compatibility. Internal callers and docs examples that use `json=` no longer fail with `TypeError`. Passing **both** now raises `TangoValidationError` rather than silently preferring one — that ambiguity would hide caller bugs.
- `get_psc_metrics` / `get_naics_metrics` / `get_entity_metrics` docstrings — `period_grouping` values are `"month"` / `"quarter"` / `"year"` (the path-segment values the API accepts), not `"monthly"` / `"quarterly"`.
- `docs/API_REFERENCE.md#get_agency` — example uses `client.get_agency("GSA")` consistently and notes the parameter accepts CGAC / FPDS / short code / abbreviation / canonical name.
- `README.md` Quick Start — `get_agency()` returns an `Agency` dataclass, so the example uses attribute access (`agency.name`) instead of `agency['name']` which would `TypeError`.
- `scripts/smoke_api_parity.py` — `list_business_types(limit=1)` is now wrapped in the `run(...)` helper so a failure on that call records FAIL instead of aborting the smoke run.
- `tango webhooks endpoints create` CLI now accepts and requires `--name` (passed through to `create_webhook_endpoint(name=...)`). Previously the option was absent, meaning the CLI could never set a custom endpoint name and every call would 400 server-side (the server enforces `unique(user, name)`).
- `WebhookAlert.query_type` and `WebhookAlert.filters` tightened from `Optional` to non-optional (`str` and `dict[str, Any]` respectively). Legacy nullable rows were purged by the tango#2275 migration; the server model and serializer guarantee non-null values for all current data. `WebhookAlert.status` narrowed from `str` to `Literal["active", "paused"]` — the server serializer produces exactly those two values.
- **Shape validator agrees with server on `naics(...)` / `psc(...)` expansions.** The client-side `ShapeParser.validate()` previously rejected the canonical `shape=naics(code,description)` form (which the server has always accepted) and also rejected the alias `shape=naics_code(code,description)`. The parser now mirrors the server's `_EXPAND_ALIASES` (introduced in Tango PR makegov/tango#2259) and rewrites `naics_code(...)` / `psc_code(...)` to their canonical `naics(...)` / `psc(...)` form at parse time. Bare scalar leaves (`shape=naics_code` / `shape=psc_code`) are left untouched and still return the raw column value, matching the server. Schemas for `Contract`, `Forecast`, `Opportunity`, `Notice`, and `Vehicle` gained explicit `naics` / `psc` expand entries backed by the existing `CodeDescription` nested model. Fixes makegov/tango#2266.
- **`Subaward` schema matches the server's `SubawardSerializer`.** The previous `SUBAWARD_SCHEMA` declared two fields the server has never exposed (`id`, `amount`) and was missing every real field on the resource — including `piid`, `key`, `awarding_office` / `funding_office` / `place_of_performance` / `subaward_details` / `fsrs_details` / `highly_compensated_officers` / `usaspending_permalink`, and the denormalized `prime_awardee_*` / `recipient_*` lookup columns. Shape strings that referenced any real field (e.g. `shape="piid"`) would fail client-side validation with `unknown_field`, and conversely the SDK happily passed `shape="id"` / `shape="amount"` through to the server, where they were rejected. `SUBAWARD_SCHEMA` is now derived directly from `awards.serializers.subawards.SubawardSerializer` and the resource's runtime `available_fields`. The `Subaward` dataclass in `tango/models.py` was updated to match. New nested schemas `SubawardDetails`, `FsrsDetails`, `SubawardPlaceOfPerformance`, and `HighlyCompensatedOfficer` are registered so the corresponding shape expansions validate end-to-end.

### Documentation
- New `docs/ERRORS.md` — full exception hierarchy, recovery patterns, and the shape-error classes (`ShapeValidationError`, `ShapeParseError`, `TypeGenerationError`, `ModelInstantiationError`). Ported from `docs.makegov.com/sdks/python/errors.md` ahead of the docs-site auto-pull cutover (makegov/docs#15 / makegov/docs#16).
- New `docs/PAGINATION.md` — page-based vs cursor-based strategies, iteration patterns, and the `PaginatedResponse` field reference. Ported from `docs.makegov.com/sdks/python/pagination.md`.
- New `docs/CLIENT.md` — `TangoClient` constructor reference, `rate_limit_info` / `last_response_headers` properties, and retry-semantics note (the SDK has no built-in retry). Ported from `docs.makegov.com/sdks/python/client.md`.

### CI
- New `.github/workflows/docs-dispatch.yml` — fires on push to `main` when `docs/**`, `README.md`, or `CHANGELOG.md` changes and dispatches `external_updated` at `makegov/docs` so the public docs site rebuilds with the latest SDK content. Required for the makegov/docs#15 auto-pull pipeline.

## [0.6.0] - 2026-05-07

### Added
Expand Down
70 changes: 50 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ print(f"Found {agencies.count} agencies")

# Get specific agency
agency = client.get_agency("GSA")
print(f"Agency: {agency['name']}")
print(f"Agency: {agency.name}")

# Search contracts
contracts = client.list_contracts(
limit=10
)
```

## Authentication

Most endpoints require an API key. You can obtain one from the [Tango API portal](https://tango.makegov.com).
Expand Down Expand Up @@ -204,13 +206,13 @@ opportunities = client.list_opportunities(agency="DOD", active=True, limit=25)
### Notices

```python
notices = client.list_notices(agency="DOD", notice_type="award", limit=25)
notices = client.list_notices(agency="DOD", notice_type="Presolicitation", limit=25)
```

### Grants

```python
grants = client.list_grants(agency="HHS", status="forecasted", limit=25)
grants = client.list_grants(agency="HHS", status="F", limit=25) # F = Forecasted
```

### Protests
Expand All @@ -230,12 +232,45 @@ contract = client.get_gsa_elibrary_contract("UUID")
### Reference Data

```python
# Offices, organizations, NAICS, subawards, business types
# Offices, organizations, NAICS, PSC, subawards, business types
offices = client.list_offices(search="acquisitions")
organizations = client.list_organizations(level=1)
naics = client.list_naics(search="software")
get_naics = client.get_naics("541511")
psc = client.list_psc()
subawards = client.list_subawards(prime_uei="UEI123")
business_types = client.list_business_types()
mas_sins = client.list_mas_sins()
assistance = client.list_assistance_listings()
departments = client.list_departments()
```

### Resolve / Validate

```python
# Resolve a name to entity/org candidates
result = client.resolve(name="Lockheed Martin", target_type="entity")
for c in result.candidates:
print(c.identifier, c.display_name)

# Validate an identifier
result = client.validate(identifier_type="uei", value="ABCDEF123456")
```

### IT Dashboard

```python
investments = client.list_itdashboard_investments(search="cloud", limit=25)
investment = client.get_itdashboard_investment("023-000001234")
```

### Entity Sub-resources

```python
contracts = client.list_entity_contracts("ABCDEF123456", limit=25)
idvs = client.list_entity_idvs("ABCDEF123456")
otas = client.list_entity_otas("ABCDEF123456")
metrics = client.get_entity_metrics("ABCDEF123456", months=12, period_grouping="month")
```

## Pagination
Expand All @@ -253,9 +288,9 @@ print(f"Previous page URL: {response.previous}")
for contract in response.results:
print(contract['description'])

# Get next page
# Get next page (contracts use keyset/cursor pagination)
if response.next:
next_response = client.list_contracts(page=2, limit=25)
next_response = client.list_contracts(cursor=response.cursor, limit=25)
```

## Error Handling
Expand All @@ -282,7 +317,7 @@ except TangoNotFoundError:
print("Resource not found")
except TangoValidationError as e:
print(f"Invalid parameters: {e.message}")
print(f"Details: {e.details}")
print(f"Details: {e.response_data}")
except TangoRateLimitError:
print("Rate limit exceeded")
except TangoAPIError as e:
Expand Down Expand Up @@ -314,22 +349,18 @@ contracts = client.list_contracts(

### Flattened Responses

Enable flattening to get dot-notation field names:
The `flat=True` parameter is passed to the API, which returns dot-notation keys in the raw response. The SDK still wraps the result in a `ShapedModel` — access nested fields via attribute or dict syntax, not dot-notation string keys:

```python
contracts = client.list_contracts(
shape="key,piid,recipient(display_name,uei)",
flat=True
)
# Returns: {"key": "...", "piid": "...", "recipient.display_name": "...", "recipient.uei": "..."}

# Flatten arrays with indexed keys
contracts = client.list_contracts(
shape="key,transactions(*)",
flat=True,
flat_lists=True
)
# Returns: {"key": "...", "transactions.0.action_date": "...", "transactions.0.obligated": "..."}
for contract in contracts.results:
# Attribute access
print(contract.recipient.display_name)
# Dict access (nested, not flat string keys)
print(contract['recipient']['display_name'])
```

### Webhook Tooling
Expand All @@ -353,9 +384,8 @@ tango webhooks simulate --secret $SECRET --event-type entities.updated # sign +
tango webhooks simulate --secret $SECRET --event-type entities.updated \
--to http://127.0.0.1:8011/tango/webhooks # also POST

# Manage real subscriptions and endpoints
tango webhooks endpoints create|list|get|delete
tango webhooks subscriptions create|list|get|delete
# Manage delivery endpoints
tango webhooks endpoints create|list|get|delete

# Force a real test delivery from Tango
tango webhooks trigger
Expand Down
Loading
Loading