Skip to content

Add webhooks commands - #41

Open
awoods187 wants to merge 3 commits into
dualentry:mainfrom
awoods187:andyw/dev-12112-cli-no-commands-for-webhooks-8-ops-incl-topics-and
Open

awoods187 wants to merge 3 commits into
dualentry:mainfrom
awoods187:andyw/dev-12112-cli-no-commands-for-webhooks-8-ops-incl-topics-and

Conversation

@awoods187

@awoods187 awoods187 commented Oct 30, 2026 •

Copy link
Copy Markdown

Closes DEV-12112

Stacked on #39. This branch is built on #39's commit, so until #39 merges the diff below also shows its changes. Only the top commit ("Add webhooks commands") belongs to this PR. Merge #39 first. The dependency is the resume_flags mechanism #39 adds, which webhooks list --all uses to repeat --active/--inactive in its resume hint.

Summary

The public API has a webhooks resource with eight operations: create, list, get, PUT, PATCH, delete, list topics, and list deliveries. The README links to the webhook documentation, but no CLI command could register a webhook, choose its topics, or check what was delivered. This PR adds dualentry webhooks.

dualentry webhooks topics
dualentry webhooks create --url https://hooks.example.com/dualentry -t invoice.created -t bill.posted
dualentry webhooks deliveries <uuid>
dualentry webhooks update <uuid> --active

Changes

  • Seven commands covering all eight operations: topics, list, get, create, update, delete, deliveries. update uses PATCH. The API's PUT accepts the same optional fields and runs the same update code, so a separate command would add nothing.
  • Signing secret: only the create response includes it. Human output shows it in its own panel with a reminder that it is never shown again, and -o json keeps it for scripts.
  • No-topics warning: events are only delivered to webhooks subscribed to their topic, so a webhook with no topics receives nothing. create and update --clear-topics print a warning; the API still allows it, so the CLI does too.
  • update changes only what is passed. --topic replaces the whole subscription list, and the help says so. --topic with --clear-topics, or an update that changes nothing, is a usage error. --active also clears an auto-disable after repeated failures, and the help says that too.
  • IDs are validated as UUIDs before any request is sent.
  • --all resume hints: list --all repeats --search (shell-quoted) and --active/--inactive. deliveries --all prints a resume command for its own shape (webhooks deliveries <uuid> --all --offset N), through the shared truncation warning, which now accepts a ready-made command.
  • Output: the list shows a one-word state, and get shows when and why a webhook was auto-disabled. The deliveries table shows times in the local timezone, not UTC with the offset cut off. All tables were checked at 80 columns. The formatters are typed with TypedDicts that match the API schemas.
  • README workflow example and Developers table entry. CHANGELOG entry under Unreleased.

Test plan

  • Unit tests pass (uv run pytest): 344 passed on Python 3.11, 3.12 and 3.13 in CI-equivalent environments. The 137 skipped are integration tests that need X_API_KEY.
  • Linter passes (uv run ruff check src/ tests/ and uv run ruff format --check src/ tests/), with uv sync --locked
  • Changelog check passes, both against main and for this commit alone against Add classifications-lines list command and fix CSV ID filters #39
  • Manually tested with dualentry <command> against a live test organization; see Artifacts.

tests/test_webhooks.py (33 tests) covers every command and every update flag, is_active=false being sent, the secret in both output formats, the no-topics warning, rejection before any request (malformed UUID, conflicting or missing options), both resume hints, and local-time rendering with a pinned timezone. Message assertions strip ANSI codes and box borders, so they also pass with CI's color output.

Artifacts

Run against a test organization on production (api.dualentry.com) with a real API key. Every webhook created here pointed at example.com and was deleted afterwards; the org's webhook list is empty again. Signing secrets are redacted.

Found by running it (fixed in 26aeaff)

  • --url help said "HTTPS endpoint", but the API accepted http://example.com/hook. The backend only rejects private and loopback hosts. The help now says "Public http(s) URL … use https in production".
  • get/create/update printed raw UTC with microseconds (2026-10-30T21:22:19.393385+00:00), while deliveries showed local time. All of them now show local time.

Lifecycle

$ dualentry webhooks topics
core.account/created
core.account/deleted
core.account/updated
core.amortization/created
...

$ dualentry webhooks list
No results.

$ dualentry webhooks create --url https://example.com/cli-artifact-hook --topic core.classification/created
╭────────────────────────────────────────────────╮
│ WEBHOOK  https://example.com/cli-artifact-hook │
╰────────────────────────────────────────────────╯
UUID:               b1d191ad-e968-4162-a966-4bc3d1ef10d1
State:              active
Topics:             core.classification/created
Created:            2026-10-30 17:28
Modified:           2026-10-30 17:28
╭──────────────────────── Signing secret ─────────────────────────╮
│ whsec_<redacted>                                                │
│                                                                 │
│ Save this signing secret now. DualEntry will not show it again. │
╰─────────────────────────────────────────────────────────────────╯

$ dualentry webhooks create --url http://example.com/hook
...
Topics:             none: receives no events
Warning: this webhook has no topics, so it will receive no events. Add some with `dualentry webhooks update --topic`.

$ dualentry webhooks create --url https://example.com/hook --topic core.nope/created
  ✗ Error: Validation error: {'topics': ['Unknown topic: core.nope/created']}

$ dualentry webhooks update <uuid> --topic core.classification/created --inactive
State:              inactive
Topics:             core.classification/created

$ dualentry webhooks update <uuid>
│ nothing to change; pass --url, --topic, --clear-topics, --active, or --inactive │

$ dualentry webhooks get not-a-uuid
│ Invalid value for 'webhook_id': 'not-a-uuid' is not a valid UUID. │

A real delivery

I reactivated the webhook and created a classification to fire core.classification/created. example.com doesn't accept POSTs, so the delivery records a 405:

$ dualentry webhooks deliveries <uuid>
                               Webhook Deliveries
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Created          ┃ Topic           ┃ Status ┃ Tries ┃ HTTP ┃ Error           ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ 2026-10-30 17:23 │ core.classifica │ failed │     1 │  405 │ Non-2xx         │
│                  │ tion/created    │        │       │      │ response: 405   │
└──────────────────┴─────────────────┴────────┴───────┴──────┴─────────────────┘
Showing 1 of 1

$ dualentry webhooks deliveries <uuid> -o json
{
  "items": [
    {
      "event_id": "1b29e03b-e8ff-5fd2-87c7-21d22855bca9",
      "topic": "core.classification/created",
      "status": "failed",
      "attempts": 1,
      "response_status": 405,
      "last_error": "Non-2xx response: 405",
      "created_at": "2026-10-30T21:23:39.792603+00:00",
      "delivered_at": null
    }
  ],
  "count": 1
}

$ dualentry webhooks delete <uuid>
Webhook <uuid> deleted, with its delivery history.

$ dualentry webhooks get <uuid>
  ✗ Error: Resource not found. Check the ID or number and try again.

$ dualentry webhooks list
No results.

Live integration suite

133 passed. The 4 failures also fail on main (429 without backoff; 403 on vat-rates/product-tax-codes get), so nothing new.

Merge note

This PR touches the same CHANGELOG lines and output.py area as #40, so whichever merges second needs a small conflict resolved.

Add `dualentry classifications-lines list`, a read-only list of
classification values across every classification. It filters by
--classification, --id, --name, and --active/--inactive, and prints
each line with its parent classification.

Fix comma-separated ID filters, shared with statistical-journals:
- Values were sent to the API as a nested list (`number=[1, 2]`), so
  multi-value filters never matched. They are now sent flat and the
  flag can be repeated (`--id 1,2 --id 3`).
- An empty value (`--id ""`) is now a usage error instead of silently
  dropping the filter and listing every row.

Fix the `list --all` resume hint for commands with their own filters:
it now repeats every filter, spells boolean filters as on/off flags,
and shell-quotes values containing spaces.
Add `dualentry webhooks` with topics, list, get, create, update, delete,
and deliveries, covering every operation of the webhooks API.

- create shows the signing secret in its own panel with a reminder that
  it is never shown again; JSON output keeps it for scripts.
- create and update --clear-topics warn when a webhook has no topics,
  since it then receives no events.
- update changes only what is passed; --topic replaces the subscription
  list, and --active also clears an auto-disable.
- Webhook IDs are validated as UUIDs before any request.
- list --all repeats --search and --active/--inactive in its resume
  hint, and deliveries --all prints a resume command for its own shape.
- Delivery times are shown in the local timezone instead of as UTC with
  the offset cut off.
get/create/update printed raw UTC with microseconds while the delivery
list showed local time. The API accepts any public http(s) URL, not only
https.
@awoods187

Copy link
Copy Markdown
Author
Screenshot 2026-10-30 at 5 58 30 PM andywoods@Andys-MacBook-Air dualentry-cli % uv run dualentry webhooks create \ > --url https://webhook.site/6f248f36-2b67-42b1-9888-151fd6449633

⚠ Update available: 0.1.18 → 0.1.20
brew upgrade dualentry or uv tool upgrade dualentry-cli

╭────────────────────────────────────────────────────────────────────╮
│ WEBHOOK https://webhook.site/6f248f36-2b67-42b1-9888-151fd6449633 │
╰────────────────────────────────────────────────────────────────────╯
UUID: d4e139bb-9541-42ed-9d60-8da6f52f9612
State: active
Topics: none: receives no events
Created: 2026-10-30 17:55
Modified: 2026-10-30 17:55
╭──────────────────────── Signing secret ─────────────────────────╮
│ whsec__VW3SxdHRXn7DC2nEq3thEk7vw8PR4dQEsnu1oz9pcw │
│ │
│ Save this signing secret now. DualEntry will not show it again. │
╰─────────────────────────────────────────────────────────────────╯
Warning: this webhook has no topics, so it will receive no events. Add some with dualentry webhooks update --topic.
andywoods@Andys-MacBook-Air dualentry-cli % W=d4e139bb-9541-42ed-9d60-8da6f52f9612
andywoods@Andys-MacBook-Air dualentry-cli % uv run dualentry webhooks update $W --topic core.classification/created --topic core.classification/updated

⚠ Update available: 0.1.18 → 0.1.20
brew upgrade dualentry or uv tool upgrade dualentry-cli

╭────────────────────────────────────────────────────────────────────╮
│ WEBHOOK https://webhook.site/6f248f36-2b67-42b1-9888-151fd6449633 │
╰────────────────────────────────────────────────────────────────────╯
UUID: d4e139bb-9541-42ed-9d60-8da6f52f9612
State: active
Topics: core.classification/created
core.classification/updated
Created: 2026-10-30 17:55
Modified: 2026-10-30 17:57
andywoods@Andys-MacBook-Air dualentry-cli % echo '{"name":"webhook manual test","is_active":false,"selections":[],"required_for_records":[],"parent_id":null}' > /tmp/cls.json
andywoods@Andys-MacBook-Air dualentry-cli % uv run dualentry classifications create --file /tmp/cls.json

⚠ Update available: 0.1.18 → 0.1.20
brew upgrade dualentry or uv tool upgrade dualentry-cli

╭─────────────────────────────────────╮
│ CLASSIFICATION webhook manual test │
╰─────────────────────────────────────╯
ID: 16914
Name: webhook manual test
andywoods@Andys-MacBook-Air dualentry-cli % uv run dualentry webhooks deliveries $W

⚠ Update available: 0.1.18 → 0.1.20
brew upgrade dualentry or uv tool upgrade dualentry-cli

                             Webhook Deliveries

┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┳━━━━━━┳━━━━━━━┓
┃ Created ┃ Topic ┃ Status ┃ Tries ┃ HTTP ┃ Error ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━╇━━━━━━╇━━━━━━━┩
│ 2026-10-30 17:57 │ core.classification/created │ delivered │ 1 │ 200 │ - │
└──────────────────┴─────────────────────────────┴───────────┴───────┴──────┴───────┘

Showing 1 of 1
andywoods@Andys-MacBook-Air dualentry-cli % uv run dualentry webhooks deliveries $W -o json

⚠ Update available: 0.1.18 → 0.1.20
brew upgrade dualentry or uv tool upgrade dualentry-cli

{
"items": [
{
"event_id": "b34bc7d7-1c0b-5450-822d-de21175eb2e0",
"topic": "core.classification/created",
"status": "delivered",
"attempts": 1,
"response_status": 200,
"last_error": "",
"created_at": "2026-10-30T21:57:36.546994+00:00",
"delivered_at": "2026-10-30T21:57:36.965665+00:00"
}
],
"count": 1
}

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