Skip to content

Add webhooks signing helpers, local receiver, and tango CLI#23

Closed
makegov-mark[bot] wants to merge 8 commits intomainfrom
feature/webhooks-cli
Closed

Add webhooks signing helpers, local receiver, and tango CLI#23
makegov-mark[bot] wants to merge 8 commits intomainfrom
feature/webhooks-cli

Conversation

@makegov-mark
Copy link
Copy Markdown
Contributor

@makegov-mark makegov-mark Bot commented May 7, 2026

Summary

Adds a complete webhook tooling surface to tango-python for developers building integrations against the Tango API — signing helpers, a local receiver class, a command-line tool, and management commands for the underlying endpoints and subscriptions.

A dev with TANGO_API_KEY can now go from zero to receiving real Tango deliveries entirely from the shell, and can develop / test their handler offline with no Tango involvement at all.

What ships

tango.webhooks subpackage

  • Signing helpers (pure stdlib, no extras required): verify_signature, generate_signature, parse_signature_header. Mirrors the canonical Tango server scheme (X-Tango-Signature: sha256=<hex> HMAC-SHA256 over raw body) byte-for-byte.
  • WebhookReceiver — context-manager-friendly local HTTP receiver for tests and development. Verifies signatures, optional forward_to mirroring, in-memory delivery history with cap, optional on_delivery callback. Returns JSON error bodies ({"ok": false, "error": "invalid_signature"}) instead of stdlib HTML pages.
  • simulate.sign(payload, secret) -> SignedRequest — produce the exact wire form a Tango delivery would have. Useful in pytest fixtures.
  • simulate.deliver(...) — sign and POST to a target URL.

tango[webhooks] extra

A new optional extra (`pip install 'tango-python[webhooks]'`, adds `click`) installs a `tango` console script with the full webhook lifecycle:

```bash

Discovery

tango webhooks list-event-types
tango webhooks fetch-sample --event-type entities.updated

Local development

tango webhooks listen --port 8011 --secret $SECRET
tango webhooks simulate --secret $SECRET --event-type entities.updated # sign+print
tango webhooks simulate --secret $SECRET --event-type entities.updated
--to http://127.0.0.1:8011/tango/webhooks # also POST

Configuration (the piece that previously required dropping into Python)

tango webhooks endpoints create|list|get|delete
tango webhooks subscriptions create|list|get|delete

Real end-to-end test

tango webhooks trigger
```

Documentation

  • New `docs/WEBHOOKS.md` — single comprehensive guide: install, concepts, zero-to-receiving quickstart, full CLI reference, and programmatic patterns for `WebhookReceiver` / `simulate.sign` / `simulate.deliver` in pytest. Troubleshooting appendix.
  • `docs/API_REFERENCE.md` — filled in `get_webhook_subscription` (was missing), replaced the inline hand-rolled HMAC snippet with a pointer to `tango.webhooks.verify_signature`, added a new "Webhook tooling" section cataloging every importable from the new subpackage.
  • `README.md` — new "Webhook Tooling" section under Advanced Features; new guide linked from the Documentation index.

Why

Devs integrating Tango webhooks shouldn't have to clone the Tango server repo to find a test harness. The tooling that lived in `tango/tools/webhook_lab/` was already half-duplicating SDK calls (`client.py` reimplemented `/api/webhooks/subscriptions/`); putting the tooling alongside the SDK lets it use the SDK directly and ships a cohesive integration story for partner developers.

Test plan

  • `uv run pytest tests/test_webhooks_*.py` — 35 new tests pass
  • Full unit suite green on macOS / Linux / Windows × Python 3.12 / 3.13 (CI confirmed)
  • `uv run mypy tango/webhooks/` — clean
  • `uv run ruff check tango/webhooks/ tests/test_webhooks_*.py` — clean
  • Coverage on new code: signing 100%, receiver 96%, simulate 94%
  • Empirically verified `listen` / `simulate` / round-trip / JSON error bodies / forwarding chain end-to-end

Scope notes / decisions to confirm

  • Console script name `tango`. Clean and short, but `tango-scripts` reuses the bare name elsewhere in the org. Flagged in CHANGELOG as revisitable before release. If you'd rather lock in `tango-cli` or `tango-webhooks`, this is the easy moment.
  • No browser UI. The `webhook_lab` HTML/templates/static did not migrate. Headless CLI + in-memory `deliveries` list cover the testing use case. The lab in `tango/` can stay or be retired — not touched in this PR.
  • The lab's `client.py` in `tango/` still duplicates SDK methods. Recommended follow-up: either delete the lab or refactor it to depend on `tango-python`. Out of scope here.
  • Server-side relay (so `listen` can work without external tunneling tools) intentionally not attempted. Existing tunneling tools cover the use case; a relay would need server-side work in `tango/`.
  • `subscriptions create` is single-record only for now — multi-record needs a `--config-file` option or a more elaborate flag scheme. The Python SDK's `create_webhook_subscription` accepts arbitrary payloads.
  • No `update` commands in the CLI. Endpoints/subscriptions are usually delete-and-recreate during dev, and partial-update semantics get awkward via flags.

Risks

  • New public API surface (`tango.webhooks.*`, top-level re-exports). Per CLAUDE.md these are now contract — renames need deprecation aliases.
  • Adds an optional dependency (`click`); base install unchanged.
  • The `tango` console script name is global — see the scope note above.

Next steps

  • Decide console script name before the next release.
  • Follow-up PR in `tango/` to retire or refactor `tools/webhook_lab/`.
  • Possible follow-up: lift the workflow guide section out of `docs/WEBHOOKS.md` into the public mkdocs site (`makegov/docs`) once the API surface settles.

MAKEGOV_BOT_SIGNATURE

vdavez and others added 8 commits May 7, 2026 12:22
Tier 1+2 of the Stripe-CLI-style webhook DX migration from tango/tools/
webhook_lab into the SDK.

- tango.webhooks: HMAC-SHA256 signing helpers (verify_signature,
  generate_signature, parse_signature_header). Pure stdlib; importable
  from a default install.
- WebhookReceiver: stdlib-based local listener with optional forwarding,
  delivery history, and on_delivery callback. Usable as a context manager
  inside integration tests.
- simulate.deliver: offline sign+POST helper for driving a receiver
  without provisioning a real subscription.
- New tango[webhooks] extra installs click and a tango console script
  with `webhooks listen|trigger|simulate` subcommands.
- Top-level re-exports: WebhookReceiver, Delivery, verify_signature,
  generate_signature, parse_signature_header.

The script name `tango` is flagged in CHANGELOG as revisitable before
release if it conflicts with sibling tooling (e.g. tango-scripts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The default BaseHTTPRequestHandler error page is HTML, which makes the
receiver's rejection responses inconsistent with tango's own JSON-shaped
API and harder to inspect programmatically (e.g. via simulate's
response_body field).

Now responds with `{"ok": false, "error": "<code>"}` and Content-Type
application/json on 401 (invalid_signature) and 404 (not_found), and
`{"ok": true}` on 200 — explicit Content-Length on all paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The simulate output's `sent_bytes` (length only) and `response_body`
(the receiver's pong) were both confusing for devs who really want to
see the canonical Tango shape they're driving their handler with:

- `sent_bytes` (int) → `sent_payload` (the parsed JSON dict)
- `response_body` → `receiver_response` (clarifies whose body it is)

Two new subcommands close the discovery loop for devs building against
the Tango API:

- `tango webhooks fetch-sample [--event-type X]` — print the canonical
  sample payload Tango emits (read-only, no POST). Wraps the SDK's
  `get_webhook_sample_payload`.
- `tango webhooks list-event-types` — list every event type Tango
  supports with descriptions. Wraps `list_webhook_event_types`.

Together: `list-event-types` → pick one → `fetch-sample` to see the
shape → `simulate --event-type X` to drive the handler with that shape,
all without leaving the shell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A dev exploring "what would Tango send my handler" shouldn't have to
stand up a listener first. Now:

- `simulate --event-type X` (no --to) signs the canonical payload and
  prints the headers + body — no POST. Output includes the literal
  X-Tango-Signature value the handler would see.
- `simulate --event-type X --to URL` keeps the previous behavior:
  signs, POSTs, prints the receiver's response.

Output gained `delivered: bool` so callers can disambiguate the two
modes from the JSON itself.

Refactored simulate to expose a public `simulate.sign(payload, secret)
-> SignedRequest`. Useful in pytest fixtures that want the wire bytes
without spinning a process. `simulate.deliver` now goes through it.

Top-level re-exports: `from tango.webhooks import sign, SignedRequest`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the dev-against-Tango workflow loop — a dev with TANGO_API_KEY
can now go from zero to receiving real deliveries entirely from the
shell. Previously they had to drop into Python to call
create_webhook_endpoint / create_webhook_subscription.

Added:
- `tango webhooks endpoints list|get|create|delete`
- `tango webhooks subscriptions list|get|create|delete`

Notes:
- `subscriptions create` accepts simple flags (--name, --event-type,
  --subject-type, --subject-id) and assembles them into the
  payload.records[] shape Tango expects. For multi-record
  subscriptions, use the SDK's create_webhook_subscription directly.
- `delete` requires confirmation; pass --yes to skip.
- update commands deferred — endpoints/subscriptions are usually
  delete-and-recreate during dev, and partial-update semantics are
  awkward to express via flags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`test_cli_simulate_rejects_both_modes` was passing /dev/null as a
placeholder file. Click validates --payload-file with exists=True
before the command body runs, so on Windows CI the test bailed with
"File '/dev/null' does not exist" before ever reaching the mutual-
exclusion error it was checking for.

Use tmp_path to get a real existing file. macOS/Linux already passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New `docs/WEBHOOKS.md` (~370 lines) — single comprehensive guide for
  developers building against the Tango API: install, concepts, a
  zero-to-receiving quickstart, full CLI reference (every subcommand
  with copy-pasteable examples), and programmatic patterns for
  `WebhookReceiver`, `simulate.sign`, and `simulate.deliver` in pytest
  fixtures and offline development. Includes a troubleshooting
  appendix for the common gotchas (signature drift, missing extra,
  one-endpoint-per-user, etc.).
- `README.md` — new "Webhook Tooling" section under Advanced Features
  with a one-paragraph overview, the install one-liner, the seven CLI
  subcommands at a glance, and the bare-minimum receiver pattern.
  WEBHOOKS.md added to the Documentation index.
- `docs/API_REFERENCE.md` — filled in `get_webhook_subscription`
  (previously missing); replaced the hand-rolled signature snippet
  with a pointer to `tango.webhooks.verify_signature`; added a new
  "Webhook tooling (`tango.webhooks`)" section that catalogs every
  importable from the new subpackage (signing helpers,
  WebhookReceiver, Delivery, sign, SignedRequest, simulate.deliver,
  CLI entry point) with constructor tables and short examples.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Public-audience material shouldn't compare the SDK to a third-party
tool — describe what the CLI does without the analogy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@makegov-mark makegov-mark Bot mentioned this pull request May 7, 2026
8 tasks
@vdavez vdavez closed this May 8, 2026
@vdavez vdavez deleted the feature/webhooks-cli branch May 8, 2026 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant