From 79c715cadc692d1221ad6d362d3f2123c9856440 Mon Sep 17 00:00:00 2001 From: infinityplusone Date: Tue, 12 May 2026 13:29:33 -0400 Subject: [PATCH] webhooks: remove subject-based subscription surface (v0.7.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tango is dropping subject-based webhook subscriptions (see makegov/tango#2267). This PR mirrors the removal in the Python SDK: list/get/create/update/delete_webhook_subscription methods, the `tango webhooks subscriptions` CLI group, and the subject fields on event-type / subscription / sample-payload models, plus matching tests + docs. Endpoint CRUD, signing helpers, `WebhookReceiver`, `test_delivery`, `get_webhook_sample_payload`, and `list_webhook_event_types` are untouched. SemVer-major (0.6.0 → 0.7.0). Closes makegov/tango#2275 Part of makegov/tango#2267 --- CHANGELOG.md | 13 +++ README.md | 3 +- docs/API_REFERENCE.md | 56 +--------- docs/WEBHOOKS.md | 47 ++------- pyproject.toml | 2 +- tango/__init__.py | 6 +- tango/client.py | 123 +--------------------- tango/models.py | 23 +--- tango/webhooks/cli.py | 107 +------------------ tests/production/test_production_smoke.py | 18 ---- tests/test_client.py | 102 ------------------ tests/test_webhooks_cli.py | 73 +------------ uv.lock | 4 +- 13 files changed, 37 insertions(+), 540 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27f7efe..2bf742a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 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). +## [0.7.0] - 2026-05-12 + +### Removed +- **Subject-based webhook subscriptions.** Mirrors the Tango API deprecation of subject-based subscriptions (see [makegov/tango#2267](https://github.com/makegov/tango/issues/2267)). Removed: + - `TangoClient.list_webhook_subscriptions()`, `get_webhook_subscription()`, `create_webhook_subscription()`, `update_webhook_subscription()`, `delete_webhook_subscription()`. + - `WebhookSubscription` and `WebhookSubjectTypeDefinition` dataclasses (and their `tango` package re-exports). + - `subject_types` and `subject_type_definitions` fields from `WebhookEventTypesResponse`; `default_subject_type` from `WebhookEventType`. + - `tango webhooks subscriptions` CLI group (`list` / `get` / `create` / `delete`). +- Endpoint CRUD, signing helpers, `WebhookReceiver`, `test_webhook_delivery`, `get_webhook_sample_payload`, and `list_webhook_event_types` are unaffected. + +### Notes +- SemVer-major bump (still pre-1.0). Users who were calling `*_webhook_subscription` against an already-migrated tango will see `AttributeError`; against an older tango, the methods were removed before tango's API was. If you need to reach `/api/webhooks/subscriptions/` directly during a migration window, call `client._get` / `_post` / `_patch` / `_delete` with the path explicitly. + ## [0.6.0] - 2026-05-07 ### Added diff --git a/README.md b/README.md index 4964e4e..8d4ea37 100644 --- a/README.md +++ b/README.md @@ -353,9 +353,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 +# Manage delivery endpoints tango webhooks endpoints create|list|get|delete -tango webhooks subscriptions create|list|get|delete # Force a real test delivery from Tango tango webhooks trigger diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 8416e25..f72d4ac 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -1288,67 +1288,19 @@ for code in naics.results: ## Webhooks -Webhook APIs let **Large / Enterprise** users manage subscription filters for outbound Tango webhooks. +Webhook APIs let users manage delivery endpoints for outbound Tango webhooks. > **For testing, signing, and a CLI tool**, see [`docs/WEBHOOKS.md`](WEBHOOKS.md). This section covers SDK method signatures only. ### list_webhook_event_types() -Discover supported `event_type` values and subject types. +Discover supported `event_type` values. ```python info = client.list_webhook_event_types() print(info.event_types[0].event_type) ``` -### list_webhook_subscriptions() - -```python -subs = client.list_webhook_subscriptions(page=1, page_size=25) -``` - -Notes: - -- This endpoint uses `page` + `page_size` (tier-capped) rather than `limit`. - -### get_webhook_subscription() - -```python -sub = client.get_webhook_subscription("SUBSCRIPTION_UUID") -``` - -### create_webhook_subscription() - -```python -sub = client.create_webhook_subscription( - "Track specific vendors", - { - "records": [ - {"event_type": "awards.new_award", "subject_type": "entity", "subject_ids": ["UEI123ABC"]}, - {"event_type": "awards.new_transaction", "subject_type": "entity", "subject_ids": ["UEI123ABC"]}, - ] - }, -) -``` - -Notes: - -- Prefer v2 fields: `subject_type` + `subject_ids`. -- Legacy compatibility: `resource_ids` is accepted as an alias for `subject_ids` (don’t send both). -- Catch-all: `subject_ids: []` means “all subjects” for that record and is **Enterprise-only**. Large tier users must list specific IDs. - -### update_webhook_subscription() - -```python -sub = client.update_webhook_subscription("SUBSCRIPTION_UUID", subscription_name="Updated name") -``` - -### delete_webhook_subscription() - -```python -client.delete_webhook_subscription("SUBSCRIPTION_UUID") -``` - ### list_webhook_endpoints() List your webhook endpoint(s). @@ -1384,7 +1336,7 @@ print(result.success, result.status_code) ### get_webhook_sample_payload() -Fetch Tango-shaped sample deliveries (and sample subscription request bodies). +Fetch Tango-shaped sample deliveries. ```python sample = client.get_webhook_sample_payload(event_type="awards.new_award") @@ -1396,7 +1348,7 @@ print(sample["event_type"]) The API does not currently expose a public `/api/webhooks/deliveries/` or redelivery endpoint. Use: - `test_webhook_delivery()` for connectivity checks -- `get_webhook_sample_payload()` for building handlers + subscription payloads +- `get_webhook_sample_payload()` for building handlers ### Receiving webhooks (signature verification) diff --git a/docs/WEBHOOKS.md b/docs/WEBHOOKS.md index 817b22c..576849d 100644 --- a/docs/WEBHOOKS.md +++ b/docs/WEBHOOKS.md @@ -1,6 +1,6 @@ # Webhooks Guide -This guide covers everything `tango-python` provides for **building, testing, and operating webhook integrations against the Tango API**: signing helpers, a local receiver, a command-line tool, and management commands for the underlying endpoints and subscriptions. +This guide covers everything `tango-python` provides for **building, testing, and operating webhook integrations against the Tango API**: signing helpers, a local receiver, a command-line tool, and management commands for delivery endpoints. If you only need the SDK method signatures, see [`API_REFERENCE.md` § Webhooks](API_REFERENCE.md#webhooks). For the API-level contract (signing scheme, event taxonomy, retry behavior), see the [Tango Webhooks Partner Guide](https://docs.makegov.com/webhooks-user-guide/). @@ -18,7 +18,6 @@ If you only need the SDK method signatures, see [`API_REFERENCE.md` § Webhooks] - [`tango webhooks fetch-sample`](#tango-webhooks-fetch-sample) - [`tango webhooks list-event-types`](#tango-webhooks-list-event-types) - [`tango webhooks endpoints`](#tango-webhooks-endpoints) - - [`tango webhooks subscriptions`](#tango-webhooks-subscriptions) - [Programmatic use](#programmatic-use) - [Signature verification in your handler](#signature-verification-in-your-handler) - [`WebhookReceiver` in pytest fixtures](#webhookreceiver-in-pytest-fixtures) @@ -54,26 +53,24 @@ tango webhooks --help ## Concepts in 60 seconds -Tango webhooks have three pieces of state: +Tango webhooks have two pieces of state: | Concept | What it is | Tango term | |---|---|---| | **Endpoint** | The URL Tango POSTs to, plus a generated signing secret | `WebhookEndpoint` | -| **Subscription** | A filter saying *which events* you want delivered to that endpoint | `WebhookSubscription` | -| **Delivery** | A single signed POST Tango makes when a matching event fires | (the request itself) | +| **Delivery** | A single signed POST Tango makes when an event fires | (the request itself) | A typical setup: 1. **Create an endpoint** (`POST /api/webhooks/endpoints/`) with the public URL of your handler. Tango returns a `secret` — save it; it's used to sign every delivery. -2. **Create one or more subscriptions** (`POST /api/webhooks/subscriptions/`) describing the events your handler cares about (e.g. `entities.updated` for specific UEIs). -3. **Tango POSTs** to your endpoint when matching events fire. The body is JSON; the header `X-Tango-Signature: sha256=` is the HMAC-SHA256 of the raw body bytes keyed by your endpoint's secret. -4. **Your handler verifies the signature**, parses the body, and acts on it. +2. **Tango POSTs** to your endpoint when events fire. The body is JSON; the header `X-Tango-Signature: sha256=` is the HMAC-SHA256 of the raw body bytes keyed by your endpoint's secret. +3. **Your handler verifies the signature**, parses the body, and acts on it. --- ## Quickstart: zero to receiving -Assumes you have a `TANGO_API_KEY` and want to receive entity-update webhooks for a specific UEI. +Assumes you have a `TANGO_API_KEY` and want to receive webhooks. ### 1. See what you can subscribe to @@ -121,12 +118,6 @@ When you're ready for end-to-end testing against Tango itself, expose your local # Use the public URL the tunnel gave you. tango webhooks endpoints create --url https://.ngrok.io/tango/webhooks # Save the `secret` from the response — that's what your handler uses to verify. - -tango webhooks subscriptions create \ - --name "watch UEI ABC123" \ - --event-type entities.updated \ - --subject-type entity \ - --subject-id ABC123 ``` To force a real test delivery from Tango (without waiting for an actual event): @@ -194,7 +185,7 @@ Three sources for the payload (mutually exclusive): | Flag | Source | When to use | |---|---|---| -| `--event-type X` | Fetches the canonical sample for `X` from Tango | You want a realistic body without setting up a subscription | +| `--event-type X` | Fetches the canonical sample for `X` from Tango | You want a realistic body without waiting for a real delivery | | `--payload-file PATH` | Reads a JSON file | You're testing a specific shape (regression, edge case) | | *(neither)* | A built-in placeholder envelope | Smoke-testing the wiring | @@ -239,23 +230,6 @@ tango webhooks endpoints delete ENDPOINT_ID [--yes] `create` returns the generated `secret` once — save it. `delete` prompts for confirmation; `--yes` skips. `--inactive` registers the endpoint disabled (no deliveries until you re-enable it). -### `tango webhooks subscriptions` - -Manage **what Tango delivers**. - -```bash -tango webhooks subscriptions list [--page N] [--page-size N] -tango webhooks subscriptions get SUBSCRIPTION_ID -tango webhooks subscriptions create \ - --name "watch UEI ABC123" \ - --event-type entities.updated \ - --subject-type entity \ - --subject-id ABC123 -tango webhooks subscriptions delete SUBSCRIPTION_ID [--yes] -``` - -`create` builds a single-record subscription (one event type, one subject type, one or more subject IDs). For multi-record subscriptions, call `client.create_webhook_subscription(...)` directly with a hand-crafted `payload` dict. - --- ## Programmatic use @@ -346,7 +320,7 @@ with WebhookReceiver(secret="s").run() as rx: ## Common workflows -### "I'm starting fresh — set me up to receive entity updates" +### "I'm starting fresh — set me up to receive webhooks" ```bash export TANGO_API_KEY=... @@ -354,12 +328,9 @@ export TANGO_API_KEY=... tango webhooks list-event-types # 2. Stand up a tunnel so Tango can reach you ngrok http 8011 & -# 3. Register your endpoint and subscription +# 3. Register your endpoint tango webhooks endpoints create --url https://.ngrok.io/tango/webhooks # (save the `secret` from the response into TANGO_WEBHOOK_SECRET) -tango webhooks subscriptions create \ - --name "entities" --event-type entities.updated \ - --subject-type entity --subject-id # 4. Run the listener pointed at your downstream handler tango webhooks listen --port 8011 --secret $TANGO_WEBHOOK_SECRET \ --forward-to http://localhost:4242/wh diff --git a/pyproject.toml b/pyproject.toml index 7007849..0feba9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "tango-python" -version = "0.6.0" +version = "0.7.0" description = "Python SDK for the Tango API" readme = "README.md" requires-python = ">=3.12" diff --git a/tango/__init__.py b/tango/__init__.py index 814d393..a904d26 100644 --- a/tango/__init__.py +++ b/tango/__init__.py @@ -20,8 +20,6 @@ WebhookEndpoint, WebhookEventType, WebhookEventTypesResponse, - WebhookSubjectTypeDefinition, - WebhookSubscription, WebhookTestDeliveryResult, ) from .shapes import ( @@ -37,7 +35,7 @@ ) from .webhooks.receiver import Delivery, WebhookReceiver -__version__ = "0.6.0" +__version__ = "0.7.0" __all__ = [ "TangoClient", "TangoAPIError", @@ -56,8 +54,6 @@ "WebhookEndpoint", "WebhookEventType", "WebhookEventTypesResponse", - "WebhookSubscription", - "WebhookSubjectTypeDefinition", "WebhookTestDeliveryResult", "ShapeParser", "ModelFactory", diff --git a/tango/client.py b/tango/client.py index 108aa83..7fff363 100644 --- a/tango/client.py +++ b/tango/client.py @@ -42,8 +42,6 @@ WebhookEndpoint, WebhookEventType, WebhookEventTypesResponse, - WebhookSubjectTypeDefinition, - WebhookSubscription, WebhookTestDeliveryResult, ) from tango.shapes import ( @@ -2361,13 +2359,12 @@ def list_grants( # ============================================================================ def list_webhook_event_types(self) -> WebhookEventTypesResponse: - """Discover supported webhook event types and subject types.""" + """Discover supported webhook event types.""" data = self._get("/api/webhooks/event-types/") event_types = [ WebhookEventType( event_type=str(e.get("event_type", "")), - default_subject_type=str(e.get("default_subject_type", "")), description=str(e.get("description", "")), schema_version=int(e.get("schema_version", 1)), ) @@ -2375,123 +2372,7 @@ def list_webhook_event_types(self) -> WebhookEventTypesResponse: if isinstance(e, dict) ] - subject_types = [str(x) for x in (data.get("subject_types") or [])] - - subject_type_definitions = [ - WebhookSubjectTypeDefinition( - subject_type=str(d.get("subject_type", "")), - description=str(d.get("description", "")), - id_format=str(d.get("id_format", "")), - status=str(d.get("status", "active")), - ) - for d in (data.get("subject_type_definitions") or []) - if isinstance(d, dict) - ] - - return WebhookEventTypesResponse( - event_types=event_types, - subject_types=subject_types, - subject_type_definitions=subject_type_definitions, - ) - - def list_webhook_subscriptions( - self, page: int = 1, page_size: int | None = None - ) -> PaginatedResponse[WebhookSubscription]: - """ - List webhook subscriptions for the authenticated user's endpoint. - - Notes: - - This endpoint uses `page` + `page_size` (tier-capped) rather than `limit`. - """ - params: dict[str, Any] = {"page": page} - if page_size is not None: - params["page_size"] = page_size - - data = self._get("/api/webhooks/subscriptions/", params) - results = [ - WebhookSubscription( - id=str(item.get("id", "")), - endpoint=str(item.get("endpoint")) if item.get("endpoint") is not None else None, - subscription_name=str(item.get("subscription_name", "")), - payload=item.get("payload"), - created_at=str(item.get("created_at", "")), - ) - for item in (data.get("results") or []) - if isinstance(item, dict) - ] - - return PaginatedResponse( - count=int(data.get("count", len(results))), - next=data.get("next"), - previous=data.get("previous"), - results=results, - ) - - def get_webhook_subscription(self, subscription_id: str) -> WebhookSubscription: - """Get a single webhook subscription by id (UUID).""" - if not subscription_id: - raise TangoValidationError("Webhook subscription_id is required") - - data = self._get(f"/api/webhooks/subscriptions/{subscription_id}/") - return WebhookSubscription( - id=str(data.get("id", "")), - endpoint=str(data.get("endpoint")) if data.get("endpoint") is not None else None, - subscription_name=str(data.get("subscription_name", "")), - payload=data.get("payload"), - created_at=str(data.get("created_at", "")), - ) - - def create_webhook_subscription( - self, subscription_name: str, payload: dict[str, Any] - ) -> WebhookSubscription: - """Create a webhook subscription.""" - if not subscription_name: - raise TangoValidationError("Webhook subscription_name is required") - - data = self._post( - "/api/webhooks/subscriptions/", - {"subscription_name": subscription_name, "payload": payload}, - ) - - return WebhookSubscription( - id=str(data.get("id", "")), - endpoint=str(data.get("endpoint")) if data.get("endpoint") is not None else None, - subscription_name=str(data.get("subscription_name", "")), - payload=data.get("payload"), - created_at=str(data.get("created_at", "")), - ) - - def update_webhook_subscription( - self, - subscription_id: str, - *, - subscription_name: str | None = None, - payload: dict[str, Any] | None = None, - ) -> WebhookSubscription: - """Patch a webhook subscription.""" - if not subscription_id: - raise TangoValidationError("Webhook subscription_id is required") - - body: dict[str, Any] = {} - if subscription_name is not None: - body["subscription_name"] = subscription_name - if payload is not None: - body["payload"] = payload - - data = self._patch(f"/api/webhooks/subscriptions/{subscription_id}/", body) - return WebhookSubscription( - id=str(data.get("id", "")), - endpoint=str(data.get("endpoint")) if data.get("endpoint") is not None else None, - subscription_name=str(data.get("subscription_name", "")), - payload=data.get("payload"), - created_at=str(data.get("created_at", "")), - ) - - def delete_webhook_subscription(self, subscription_id: str) -> None: - """Delete a webhook subscription.""" - if not subscription_id: - raise TangoValidationError("Webhook subscription_id is required") - self._delete(f"/api/webhooks/subscriptions/{subscription_id}/") + return WebhookEventTypesResponse(event_types=event_types) def get_webhook_endpoint(self, endpoint_id: str) -> WebhookEndpoint: """Get a webhook endpoint by id (UUID).""" diff --git a/tango/models.py b/tango/models.py index 95ca73e..e5a9146 100644 --- a/tango/models.py +++ b/tango/models.py @@ -571,33 +571,13 @@ class APIKey: @dataclass class WebhookEventType: event_type: str - default_subject_type: str description: str schema_version: int -@dataclass -class WebhookSubjectTypeDefinition: - subject_type: str - description: str - id_format: str - status: str - - @dataclass class WebhookEventTypesResponse: event_types: list[WebhookEventType] - subject_types: list[str] - subject_type_definitions: list[WebhookSubjectTypeDefinition] - - -@dataclass -class WebhookSubscription: - id: str - subscription_name: str - payload: dict[str, Any] | None - created_at: str - endpoint: str | None = None @dataclass @@ -738,8 +718,7 @@ class ShapeConfig: # Default for list_vehicle_orders() VEHICLE_ORDERS_MINIMAL: Final = ( - "key,piid,award_date,obligated,total_contract_value,description," - "recipient(display_name,uei)" + "key,piid,award_date,obligated,total_contract_value,description,recipient(display_name,uei)" ) # Default for list_organizations() diff --git a/tango/webhooks/cli.py b/tango/webhooks/cli.py index 6c20d1c..bb36280 100644 --- a/tango/webhooks/cli.py +++ b/tango/webhooks/cli.py @@ -199,7 +199,7 @@ def simulate_cmd( client = TangoClient(api_key=api_key, base_url=base_url) payload = client.get_webhook_sample_payload(event_type=event_type) else: - payload = {"events": [{"event_type": "tango.cli.simulated", "subject_ids": []}]} + payload = {"events": [{"event_type": "tango.cli.simulated"}]} if target_url is None: signed = simulate.sign(payload, secret) @@ -288,7 +288,7 @@ def list_event_types_cmd(api_key: str | None, base_url: str) -> None: # --------------------------------------------------------------------------- -# Endpoint and subscription management +# Endpoint management # --------------------------------------------------------------------------- # # These commands wrap the SDK's CRUD methods. Common --api-key / --base-url @@ -387,109 +387,6 @@ def endpoints_delete_cmd(endpoint_id: str, yes: bool, api_key: str | None, base_ click.echo(json.dumps({"deleted": endpoint_id})) -@webhooks.group("subscriptions") -def subscriptions_group() -> None: - """Manage webhook subscriptions (what Tango delivers).""" - - -@subscriptions_group.command("list") -@click.option("--page", type=int, default=1, show_default=True) -@click.option("--page-size", type=int, default=None) -@_common_api_options -def subscriptions_list_cmd( - page: int, page_size: int | None, api_key: str | None, base_url: str -) -> None: - """List webhook subscriptions configured for your account.""" - from dataclasses import asdict - - client = _tango_client(api_key, base_url) - resp = client.list_webhook_subscriptions(page=page, page_size=page_size) - click.echo( - json.dumps( - { - "count": resp.count, - "results": [asdict(s) for s in resp.results], - }, - indent=2, - sort_keys=True, - ) - ) - - -@subscriptions_group.command("get") -@click.argument("subscription_id") -@_common_api_options -def subscriptions_get_cmd(subscription_id: str, api_key: str | None, base_url: str) -> None: - """Show one subscription by id.""" - from dataclasses import asdict - - client = _tango_client(api_key, base_url) - sub = client.get_webhook_subscription(subscription_id) - click.echo(json.dumps(asdict(sub), indent=2, sort_keys=True)) - - -@subscriptions_group.command("create") -@click.option("--name", "subscription_name", required=True, help="Human-readable name.") -@click.option("--event-type", required=True, help="Event type to subscribe to.") -@click.option( - "--subject-type", - required=True, - help="Subject type (e.g. 'entity', 'opportunity'). See `list-event-types`.", -) -@click.option( - "--subject-id", - "subject_ids", - multiple=True, - required=True, - help="One or more subject ids. Repeat the flag for multiple.", -) -@_common_api_options -def subscriptions_create_cmd( - subscription_name: str, - event_type: str, - subject_type: str, - subject_ids: tuple[str, ...], - api_key: str | None, - base_url: str, -) -> None: - """Create a webhook subscription with a single records[] entry. - - For multi-record subscriptions, use the SDK's - `create_webhook_subscription` directly with a custom payload. - """ - from dataclasses import asdict - - client = _tango_client(api_key, base_url) - sub = client.create_webhook_subscription( - subscription_name=subscription_name, - payload={ - "records": [ - { - "event_type": event_type, - "subject_type": subject_type, - "subject_ids": list(subject_ids), - } - ] - }, - ) - click.echo(json.dumps(asdict(sub), indent=2, sort_keys=True)) - - -@subscriptions_group.command("delete") -@click.argument("subscription_id") -@click.option("--yes", is_flag=True, help="Skip the confirmation prompt.") -@_common_api_options -def subscriptions_delete_cmd( - subscription_id: str, yes: bool, api_key: str | None, base_url: str -) -> None: - """Delete a webhook subscription.""" - if not yes: - click.confirm(f"Delete subscription {subscription_id}?", abort=True) - client = _tango_client(api_key, base_url) - client.delete_webhook_subscription(subscription_id) - click.echo(json.dumps({"deleted": subscription_id})) - - def _print_delivery(delivery: Delivery) -> None: """Default ``listen`` callback: write a one-line summary plus body.""" summary = _summarize(delivery.body_json) diff --git a/tests/production/test_production_smoke.py b/tests/production/test_production_smoke.py index 83ccce4..67236d6 100644 --- a/tests/production/test_production_smoke.py +++ b/tests/production/test_production_smoke.py @@ -684,10 +684,6 @@ def test_list_webhook_event_types(self, production_client): assert hasattr(response, "event_types"), "Response should have event_types" assert isinstance(response.event_types, list), "event_types should be a list" - # Response should have subject_types list - assert hasattr(response, "subject_types"), "Response should have subject_types" - assert isinstance(response.subject_types, list), "subject_types should be a list" - @handle_rate_limit @handle_auth_error def test_list_webhook_endpoints(self, production_client): @@ -701,17 +697,3 @@ def test_list_webhook_endpoints(self, production_client): validate_pagination(response) assert response.count >= 0, "Count should be non-negative" - - @handle_rate_limit - @handle_auth_error - def test_list_webhook_subscriptions(self, production_client): - """Test webhook subscriptions listing - - Validates: - - Webhook subscriptions listing works - - Response parsing is correct - """ - response = production_client.list_webhook_subscriptions() - - validate_pagination(response) - assert response.count >= 0, "Count should be non-negative" diff --git a/tests/test_client.py b/tests/test_client.py index df948cf..ec4f423 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -538,20 +538,10 @@ def test_list_webhook_event_types(self, mock_request): "event_types": [ { "event_type": "awards.new_award", - "default_subject_type": "entity", "description": "", "schema_version": 1, } ], - "subject_types": ["entity"], - "subject_type_definitions": [ - { - "subject_type": "entity", - "description": "Entity UEI", - "id_format": "UEI", - "status": "active", - } - ], } mock_response.content = b'{"event_types": []}' mock_request.return_value = mock_response @@ -560,103 +550,11 @@ def test_list_webhook_event_types(self, mock_request): resp = client.list_webhook_event_types() assert resp.event_types[0].event_type == "awards.new_award" - assert resp.subject_types == ["entity"] - assert resp.subject_type_definitions[0].subject_type == "entity" call_args = mock_request.call_args assert call_args[1]["method"] == "GET" assert call_args[1]["url"].endswith("/api/webhooks/event-types/") - @patch("tango.client.httpx.Client.request") - def test_webhook_subscriptions_crud(self, mock_request): - client = TangoClient(api_key="test-key", base_url="https://example.test") - - # list - list_response = Mock() - list_response.is_success = True - list_response.status_code = 200 - list_response.json.return_value = { - "count": 1, - "next": None, - "previous": None, - "results": [ - { - "id": "sub-1", - "endpoint": "endpoint-1", - "subscription_name": "My sub", - "payload": {"records": []}, - "created_at": "2026-01-01T00:00:00Z", - } - ], - } - list_response.content = b'{"count": 1}' - - # create - create_response = Mock() - create_response.is_success = True - create_response.status_code = 201 - create_response.json.return_value = { - "id": "sub-1", - "endpoint": "endpoint-1", - "subscription_name": "My sub", - "payload": {"records": []}, - "created_at": "2026-01-01T00:00:00Z", - } - create_response.content = b'{"id": "sub-1"}' - - # update - update_response = Mock() - update_response.is_success = True - update_response.status_code = 200 - update_response.json.return_value = { - "id": "sub-1", - "endpoint": "endpoint-1", - "subscription_name": "Updated", - "payload": {"records": []}, - "created_at": "2026-01-01T00:00:00Z", - } - update_response.content = b'{"id": "sub-1"}' - - # delete (204, empty content) - delete_response = Mock() - delete_response.is_success = True - delete_response.status_code = 204 - delete_response.content = b"" - - mock_request.side_effect = [ - list_response, - create_response, - update_response, - delete_response, - ] - - subs = client.list_webhook_subscriptions(page=2, page_size=25) - assert subs.count == 1 - assert subs.results[0].subscription_name == "My sub" - - created = client.create_webhook_subscription("My sub", {"records": []}) - assert created.id == "sub-1" - - updated = client.update_webhook_subscription("sub-1", subscription_name="Updated") - assert updated.subscription_name == "Updated" - - client.delete_webhook_subscription("sub-1") - - # Ensure correct request params/bodies were used - calls = mock_request.call_args_list - assert calls[0][1]["method"] == "GET" - assert calls[0][1]["params"]["page"] == 2 - assert calls[0][1]["params"]["page_size"] == 25 - - assert calls[1][1]["method"] == "POST" - assert calls[1][1]["json"]["subscription_name"] == "My sub" - assert calls[1][1]["json"]["payload"] == {"records": []} - - assert calls[2][1]["method"] == "PATCH" - assert calls[2][1]["json"]["subscription_name"] == "Updated" - - assert calls[3][1]["method"] == "DELETE" - @patch("tango.client.httpx.Client.request") def test_webhook_test_delivery_and_sample_payload(self, mock_request): client = TangoClient(api_key="test-key", base_url="https://example.test") diff --git a/tests/test_webhooks_cli.py b/tests/test_webhooks_cli.py index bacd84f..5c578ff 100644 --- a/tests/test_webhooks_cli.py +++ b/tests/test_webhooks_cli.py @@ -73,7 +73,7 @@ def test_cli_simulate_with_payload_file(tmp_path: object) -> None: import pathlib p = pathlib.Path(str(tmp_path)) / "payload.json" - payload = {"events": [{"event_type": "from.file", "subject_ids": ["S1"]}]} + payload = {"events": [{"event_type": "from.file"}]} p.write_text(json.dumps(payload), encoding="utf-8") runner = CliRunner() @@ -131,19 +131,15 @@ def test_cli_list_event_types_prints_table() -> None: "event_types": [ { "event_type": "entities.updated", - "default_subject_type": "entity", "description": "Entity updated", "schema_version": 1, }, { "event_type": "awards.created", - "default_subject_type": "award", "description": "New award", "schema_version": 1, }, ], - "subject_types": [], - "subject_type_definitions": [], } mock_response = Mock() mock_response.status_code = 200 @@ -253,73 +249,6 @@ def test_cli_endpoints_delete_requires_confirmation() -> None: assert json.loads(result.output) == {"deleted": "ep-1"} -def test_cli_subscriptions_create_builds_records_payload() -> None: - """Verify the `--event-type / --subject-type / --subject-id` flags get folded - into the right `payload.records[0]` shape Tango expects.""" - from unittest.mock import patch - - api = { - "id": "sub-1", - "endpoint": "ep-1", - "subscription_name": "ent-watch", - "payload": { - "records": [ - { - "event_type": "entities.updated", - "subject_type": "entity", - "subject_ids": ["UEI1", "UEI2"], - } - ] - }, - "created_at": "2026-05-07T00:00:00Z", - } - runner = CliRunner() - with patch( - "tango.client.httpx.Client.request", return_value=_mock_response(api) - ) as mock_request: - result = runner.invoke( - main, - [ - "webhooks", - "subscriptions", - "create", - "--name", - "ent-watch", - "--event-type", - "entities.updated", - "--subject-type", - "entity", - "--subject-id", - "UEI1", - "--subject-id", - "UEI2", - "--api-key", - "k", - ], - ) - assert result.exit_code == 0, result.output - # The SDK was called with the constructed payload. - sent_json = mock_request.call_args.kwargs["json"] - assert sent_json["subscription_name"] == "ent-watch" - assert sent_json["payload"]["records"][0]["subject_ids"] == ["UEI1", "UEI2"] - - -def test_cli_subscriptions_list() -> None: - from unittest.mock import patch - - api = { - "count": 0, - "next": None, - "previous": None, - "results": [], - } - runner = CliRunner() - with patch("tango.client.httpx.Client.request", return_value=_mock_response(api)): - result = runner.invoke(main, ["webhooks", "subscriptions", "list", "--api-key", "k"]) - assert result.exit_code == 0, result.output - assert json.loads(result.output) == {"count": 0, "results": []} - - def test_cli_simulate_rejects_both_modes(tmp_path: object) -> None: import pathlib diff --git a/uv.lock b/uv.lock index a9a235b..4314092 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14' and platform_python_implementation == 'PyPy'", @@ -1843,7 +1843,7 @@ wheels = [ [[package]] name = "tango-python" -version = "0.6.0" +version = "0.7.0" source = { editable = "." } dependencies = [ { name = "httpx" },