claude-task-master 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_task_master/__init__.py +1 -1
- claude_task_master/api/__init__.py +98 -0
- claude_task_master/api/models.py +553 -0
- claude_task_master/api/routes.py +1135 -0
- claude_task_master/api/routes_config.py +160 -0
- claude_task_master/api/routes_control.py +278 -0
- claude_task_master/api/routes_webhooks.py +980 -0
- claude_task_master/api/server.py +551 -0
- claude_task_master/auth/__init__.py +89 -0
- claude_task_master/auth/middleware.py +448 -0
- claude_task_master/auth/password.py +332 -0
- claude_task_master/bin/claudetm +1 -1
- claude_task_master/cli.py +4 -0
- claude_task_master/cli_commands/__init__.py +2 -0
- claude_task_master/cli_commands/ci_helpers.py +114 -0
- claude_task_master/cli_commands/control.py +191 -0
- claude_task_master/cli_commands/fix_pr.py +260 -0
- claude_task_master/cli_commands/fix_session.py +174 -0
- claude_task_master/cli_commands/workflow.py +51 -3
- claude_task_master/core/__init__.py +13 -0
- claude_task_master/core/agent_message.py +27 -5
- claude_task_master/core/control.py +466 -0
- claude_task_master/core/orchestrator.py +316 -4
- claude_task_master/core/pr_context.py +7 -2
- claude_task_master/core/prompts_working.py +32 -12
- claude_task_master/core/state.py +84 -2
- claude_task_master/core/state_exceptions.py +9 -6
- claude_task_master/core/workflow_stages.py +160 -21
- claude_task_master/github/client_pr.py +43 -1
- claude_task_master/mcp/auth.py +153 -0
- claude_task_master/mcp/server.py +268 -10
- claude_task_master/mcp/tools.py +281 -0
- claude_task_master/server.py +489 -0
- claude_task_master/webhooks/__init__.py +73 -0
- claude_task_master/webhooks/client.py +703 -0
- claude_task_master/webhooks/config.py +565 -0
- claude_task_master/webhooks/events.py +639 -0
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/METADATA +144 -6
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/RECORD +42 -21
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/entry_points.txt +2 -0
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/WHEEL +0 -0
- {claude_task_master-0.1.1.dist-info → claude_task_master-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
"""REST API routes for webhook management.
|
|
2
|
+
|
|
3
|
+
This module provides CRUD endpoints for managing webhook configurations:
|
|
4
|
+
|
|
5
|
+
Endpoints:
|
|
6
|
+
- GET /webhooks: List all configured webhooks
|
|
7
|
+
- POST /webhooks: Create a new webhook configuration
|
|
8
|
+
- GET /webhooks/{webhook_id}: Get a specific webhook configuration
|
|
9
|
+
- PUT /webhooks/{webhook_id}: Update a webhook configuration
|
|
10
|
+
- DELETE /webhooks/{webhook_id}: Delete a webhook configuration
|
|
11
|
+
- POST /webhooks/test: Send a test webhook to verify configuration
|
|
12
|
+
|
|
13
|
+
Webhooks are stored in the state directory as webhooks.json and are used
|
|
14
|
+
by the orchestrator to send notifications about task lifecycle events.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
from claude_task_master.api.routes_webhooks import create_webhooks_router
|
|
18
|
+
|
|
19
|
+
router = create_webhooks_router()
|
|
20
|
+
app.include_router(router, prefix="/webhooks")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
import uuid
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TYPE_CHECKING, Any
|
|
32
|
+
|
|
33
|
+
from pydantic import BaseModel, Field, field_validator
|
|
34
|
+
|
|
35
|
+
from claude_task_master.webhooks import (
|
|
36
|
+
EventType,
|
|
37
|
+
WebhookClient,
|
|
38
|
+
WebhookDeliveryResult,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from fastapi import APIRouter, Request
|
|
43
|
+
from fastapi.responses import JSONResponse
|
|
44
|
+
|
|
45
|
+
# Import FastAPI - using try/except for graceful degradation
|
|
46
|
+
try:
|
|
47
|
+
from fastapi import APIRouter, Request
|
|
48
|
+
from fastapi.responses import JSONResponse
|
|
49
|
+
|
|
50
|
+
FASTAPI_AVAILABLE = True
|
|
51
|
+
except ImportError:
|
|
52
|
+
FASTAPI_AVAILABLE = False
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
# Webhooks config file name
|
|
57
|
+
WEBHOOKS_FILE = "webhooks.json"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# Request/Response Models
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class WebhookCreateRequest(BaseModel):
|
|
66
|
+
"""Request model for creating a new webhook.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
url: The webhook endpoint URL (must be http:// or https://).
|
|
70
|
+
secret: Optional shared secret for HMAC signature generation.
|
|
71
|
+
events: List of event types to subscribe to. Empty means all events.
|
|
72
|
+
enabled: Whether the webhook is active.
|
|
73
|
+
name: Optional friendly name for the webhook.
|
|
74
|
+
description: Optional description of the webhook's purpose.
|
|
75
|
+
timeout: Request timeout in seconds.
|
|
76
|
+
max_retries: Maximum retry attempts for failed deliveries.
|
|
77
|
+
verify_ssl: Whether to verify SSL certificates.
|
|
78
|
+
headers: Additional HTTP headers to include in requests.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
url: str = Field(
|
|
82
|
+
...,
|
|
83
|
+
min_length=1,
|
|
84
|
+
description="Webhook endpoint URL (must be http:// or https://)",
|
|
85
|
+
examples=["https://example.com/webhook"],
|
|
86
|
+
)
|
|
87
|
+
secret: str | None = Field(
|
|
88
|
+
default=None,
|
|
89
|
+
description="Shared secret for HMAC-SHA256 signature generation",
|
|
90
|
+
)
|
|
91
|
+
events: list[str] | None = Field(
|
|
92
|
+
default=None,
|
|
93
|
+
description="Event types to subscribe to (empty/null = all events)",
|
|
94
|
+
examples=[["task.completed", "pr.created"]],
|
|
95
|
+
)
|
|
96
|
+
enabled: bool = Field(
|
|
97
|
+
default=True,
|
|
98
|
+
description="Whether this webhook is active",
|
|
99
|
+
)
|
|
100
|
+
name: str | None = Field(
|
|
101
|
+
default=None,
|
|
102
|
+
max_length=100,
|
|
103
|
+
description="Optional friendly name for this webhook",
|
|
104
|
+
examples=["Production Slack Notifications"],
|
|
105
|
+
)
|
|
106
|
+
description: str | None = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
max_length=500,
|
|
109
|
+
description="Optional description of this webhook's purpose",
|
|
110
|
+
)
|
|
111
|
+
timeout: float = Field(
|
|
112
|
+
default=30.0,
|
|
113
|
+
ge=1.0,
|
|
114
|
+
le=300.0,
|
|
115
|
+
description="Request timeout in seconds (1-300)",
|
|
116
|
+
)
|
|
117
|
+
max_retries: int = Field(
|
|
118
|
+
default=3,
|
|
119
|
+
ge=0,
|
|
120
|
+
le=10,
|
|
121
|
+
description="Maximum retry attempts for failed deliveries (0-10)",
|
|
122
|
+
)
|
|
123
|
+
verify_ssl: bool = Field(
|
|
124
|
+
default=True,
|
|
125
|
+
description="Whether to verify SSL certificates",
|
|
126
|
+
)
|
|
127
|
+
headers: dict[str, str] = Field(
|
|
128
|
+
default_factory=dict,
|
|
129
|
+
description="Additional HTTP headers to include in requests",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@field_validator("url")
|
|
133
|
+
@classmethod
|
|
134
|
+
def validate_url_scheme(cls, v: str) -> str:
|
|
135
|
+
"""Ensure URL uses http:// or https:// scheme."""
|
|
136
|
+
if not v.startswith(("http://", "https://")):
|
|
137
|
+
raise ValueError("Webhook URL must start with http:// or https://")
|
|
138
|
+
return v
|
|
139
|
+
|
|
140
|
+
@field_validator("events", mode="before")
|
|
141
|
+
@classmethod
|
|
142
|
+
def validate_events(cls, v: Any) -> list[str] | None:
|
|
143
|
+
"""Validate event types."""
|
|
144
|
+
if v is None:
|
|
145
|
+
return None
|
|
146
|
+
if isinstance(v, list):
|
|
147
|
+
if len(v) == 0:
|
|
148
|
+
return None
|
|
149
|
+
valid_events = {e.value for e in EventType}
|
|
150
|
+
for event in v:
|
|
151
|
+
if event not in valid_events:
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"Invalid event type: {event}. Valid types: {sorted(valid_events)}"
|
|
154
|
+
)
|
|
155
|
+
return v
|
|
156
|
+
raise ValueError("Events must be a list or null")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class WebhookUpdateRequest(BaseModel):
|
|
160
|
+
"""Request model for updating an existing webhook.
|
|
161
|
+
|
|
162
|
+
All fields are optional - only provided fields are updated.
|
|
163
|
+
|
|
164
|
+
Attributes:
|
|
165
|
+
url: The webhook endpoint URL.
|
|
166
|
+
secret: Shared secret (set to empty string to remove).
|
|
167
|
+
events: Event types to subscribe to.
|
|
168
|
+
enabled: Whether the webhook is active.
|
|
169
|
+
name: Friendly name for the webhook.
|
|
170
|
+
description: Description of the webhook's purpose.
|
|
171
|
+
timeout: Request timeout in seconds.
|
|
172
|
+
max_retries: Maximum retry attempts.
|
|
173
|
+
verify_ssl: Whether to verify SSL certificates.
|
|
174
|
+
headers: Additional HTTP headers.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
url: str | None = Field(default=None, min_length=1)
|
|
178
|
+
secret: str | None = Field(default=None)
|
|
179
|
+
events: list[str] | None = Field(default=None)
|
|
180
|
+
enabled: bool | None = Field(default=None)
|
|
181
|
+
name: str | None = Field(default=None, max_length=100)
|
|
182
|
+
description: str | None = Field(default=None, max_length=500)
|
|
183
|
+
timeout: float | None = Field(default=None, ge=1.0, le=300.0)
|
|
184
|
+
max_retries: int | None = Field(default=None, ge=0, le=10)
|
|
185
|
+
verify_ssl: bool | None = Field(default=None)
|
|
186
|
+
headers: dict[str, str] | None = Field(default=None)
|
|
187
|
+
|
|
188
|
+
@field_validator("url")
|
|
189
|
+
@classmethod
|
|
190
|
+
def validate_url_scheme(cls, v: str | None) -> str | None:
|
|
191
|
+
"""Ensure URL uses http:// or https:// scheme."""
|
|
192
|
+
if v is not None and not v.startswith(("http://", "https://")):
|
|
193
|
+
raise ValueError("Webhook URL must start with http:// or https://")
|
|
194
|
+
return v
|
|
195
|
+
|
|
196
|
+
@field_validator("events", mode="before")
|
|
197
|
+
@classmethod
|
|
198
|
+
def validate_events(cls, v: Any) -> list[str] | None:
|
|
199
|
+
"""Validate event types."""
|
|
200
|
+
if v is None:
|
|
201
|
+
return None
|
|
202
|
+
if isinstance(v, list):
|
|
203
|
+
if len(v) == 0:
|
|
204
|
+
return [] # Explicitly empty = clear filter
|
|
205
|
+
valid_events = {e.value for e in EventType}
|
|
206
|
+
for event in v:
|
|
207
|
+
if event not in valid_events:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"Invalid event type: {event}. Valid types: {sorted(valid_events)}"
|
|
210
|
+
)
|
|
211
|
+
return v
|
|
212
|
+
raise ValueError("Events must be a list or null")
|
|
213
|
+
|
|
214
|
+
def has_updates(self) -> bool:
|
|
215
|
+
"""Check if any updates were provided."""
|
|
216
|
+
# Check all fields except 'secret' which uses sentinel
|
|
217
|
+
for field_name in self.model_fields.keys():
|
|
218
|
+
value = getattr(self, field_name)
|
|
219
|
+
if value is not None:
|
|
220
|
+
return True
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class WebhookTestRequest(BaseModel):
|
|
225
|
+
"""Request model for testing a webhook.
|
|
226
|
+
|
|
227
|
+
Can test either an existing webhook by ID or a new URL directly.
|
|
228
|
+
|
|
229
|
+
Attributes:
|
|
230
|
+
webhook_id: ID of an existing webhook to test.
|
|
231
|
+
url: URL to test directly (if not using webhook_id).
|
|
232
|
+
secret: Secret for direct URL testing.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
webhook_id: str | None = Field(
|
|
236
|
+
default=None,
|
|
237
|
+
description="ID of an existing webhook to test",
|
|
238
|
+
)
|
|
239
|
+
url: str | None = Field(
|
|
240
|
+
default=None,
|
|
241
|
+
description="URL to test directly (alternative to webhook_id)",
|
|
242
|
+
)
|
|
243
|
+
secret: str | None = Field(
|
|
244
|
+
default=None,
|
|
245
|
+
description="Secret for direct URL testing",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@field_validator("url")
|
|
249
|
+
@classmethod
|
|
250
|
+
def validate_url_scheme(cls, v: str | None) -> str | None:
|
|
251
|
+
"""Ensure URL uses http:// or https:// scheme."""
|
|
252
|
+
if v is not None and not v.startswith(("http://", "https://")):
|
|
253
|
+
raise ValueError("Webhook URL must start with http:// or https://")
|
|
254
|
+
return v
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class WebhookResponse(BaseModel):
|
|
258
|
+
"""Response model for a single webhook.
|
|
259
|
+
|
|
260
|
+
Attributes:
|
|
261
|
+
id: Unique webhook identifier.
|
|
262
|
+
url: Webhook endpoint URL.
|
|
263
|
+
has_secret: Whether a secret is configured (secret itself is not exposed).
|
|
264
|
+
events: List of subscribed event types (null = all events).
|
|
265
|
+
enabled: Whether the webhook is active.
|
|
266
|
+
name: Friendly name.
|
|
267
|
+
description: Description.
|
|
268
|
+
timeout: Request timeout in seconds.
|
|
269
|
+
max_retries: Maximum retry attempts.
|
|
270
|
+
verify_ssl: Whether SSL certificates are verified.
|
|
271
|
+
headers: Additional HTTP headers (values may be masked).
|
|
272
|
+
created_at: When the webhook was created.
|
|
273
|
+
updated_at: When the webhook was last updated.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
id: str
|
|
277
|
+
url: str
|
|
278
|
+
has_secret: bool = False
|
|
279
|
+
events: list[str] | None = None
|
|
280
|
+
enabled: bool = True
|
|
281
|
+
name: str | None = None
|
|
282
|
+
description: str | None = None
|
|
283
|
+
timeout: float = 30.0
|
|
284
|
+
max_retries: int = 3
|
|
285
|
+
verify_ssl: bool = True
|
|
286
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
287
|
+
created_at: datetime | str
|
|
288
|
+
updated_at: datetime | str
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class WebhooksListResponse(BaseModel):
|
|
292
|
+
"""Response model for listing webhooks.
|
|
293
|
+
|
|
294
|
+
Attributes:
|
|
295
|
+
success: Whether the request succeeded.
|
|
296
|
+
webhooks: List of webhook configurations.
|
|
297
|
+
total: Total number of webhooks.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
success: bool = True
|
|
301
|
+
webhooks: list[WebhookResponse]
|
|
302
|
+
total: int
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class WebhookCreateResponse(BaseModel):
|
|
306
|
+
"""Response model for webhook creation.
|
|
307
|
+
|
|
308
|
+
Attributes:
|
|
309
|
+
success: Whether creation succeeded.
|
|
310
|
+
message: Human-readable result message.
|
|
311
|
+
webhook: The created webhook configuration.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
success: bool = True
|
|
315
|
+
message: str
|
|
316
|
+
webhook: WebhookResponse
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class WebhookDeleteResponse(BaseModel):
|
|
320
|
+
"""Response model for webhook deletion.
|
|
321
|
+
|
|
322
|
+
Attributes:
|
|
323
|
+
success: Whether deletion succeeded.
|
|
324
|
+
message: Human-readable result message.
|
|
325
|
+
id: ID of the deleted webhook.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
success: bool = True
|
|
329
|
+
message: str
|
|
330
|
+
id: str
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class WebhookTestResponse(BaseModel):
|
|
334
|
+
"""Response model for webhook test.
|
|
335
|
+
|
|
336
|
+
Attributes:
|
|
337
|
+
success: Whether the test webhook was delivered successfully.
|
|
338
|
+
message: Human-readable result message.
|
|
339
|
+
delivery_result: Details about the delivery attempt.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
success: bool
|
|
343
|
+
message: str
|
|
344
|
+
status_code: int | None = None
|
|
345
|
+
delivery_time_ms: float | None = None
|
|
346
|
+
attempt_count: int = 1
|
|
347
|
+
error: str | None = None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class WebhookErrorResponse(BaseModel):
|
|
351
|
+
"""Error response for webhook endpoints.
|
|
352
|
+
|
|
353
|
+
Attributes:
|
|
354
|
+
success: Always False.
|
|
355
|
+
error: Error type/code.
|
|
356
|
+
message: Human-readable error message.
|
|
357
|
+
detail: Additional error details.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
success: bool = False
|
|
361
|
+
error: str
|
|
362
|
+
message: str
|
|
363
|
+
detail: str | None = None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# =============================================================================
|
|
367
|
+
# Storage Helpers
|
|
368
|
+
# =============================================================================
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _get_webhooks_file(request: Request) -> Path:
|
|
372
|
+
"""Get the webhooks configuration file path.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
request: FastAPI request object.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Path to the webhooks.json file.
|
|
379
|
+
"""
|
|
380
|
+
working_dir: Path = getattr(request.app.state, "working_dir", Path.cwd())
|
|
381
|
+
state_dir = working_dir / ".claude-task-master"
|
|
382
|
+
return state_dir / WEBHOOKS_FILE
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _load_webhooks(webhooks_file: Path) -> dict[str, dict[str, Any]]:
|
|
386
|
+
"""Load webhooks from the configuration file.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
webhooks_file: Path to the webhooks file.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Dictionary mapping webhook IDs to webhook configurations.
|
|
393
|
+
"""
|
|
394
|
+
if not webhooks_file.exists():
|
|
395
|
+
return {}
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
with open(webhooks_file) as f:
|
|
399
|
+
data = json.load(f)
|
|
400
|
+
webhooks: dict[str, dict[str, Any]] = data.get("webhooks", {})
|
|
401
|
+
return webhooks
|
|
402
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
403
|
+
logger.error(f"Failed to load webhooks file: {e}")
|
|
404
|
+
return {}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _save_webhooks(webhooks_file: Path, webhooks: dict[str, dict[str, Any]]) -> None:
|
|
408
|
+
"""Save webhooks to the configuration file.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
webhooks_file: Path to the webhooks file.
|
|
412
|
+
webhooks: Dictionary mapping webhook IDs to configurations.
|
|
413
|
+
"""
|
|
414
|
+
# Ensure state directory exists
|
|
415
|
+
webhooks_file.parent.mkdir(parents=True, exist_ok=True)
|
|
416
|
+
|
|
417
|
+
data = {"webhooks": webhooks, "updated_at": datetime.now().isoformat()}
|
|
418
|
+
|
|
419
|
+
with open(webhooks_file, "w") as f:
|
|
420
|
+
json.dump(data, f, indent=2, default=str)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _generate_webhook_id(url: str) -> str:
|
|
424
|
+
"""Generate a unique webhook ID based on URL hash and UUID.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
url: The webhook URL.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Unique webhook ID string.
|
|
431
|
+
"""
|
|
432
|
+
# Use first 8 chars of URL hash + short UUID for uniqueness
|
|
433
|
+
url_hash = hashlib.sha256(url.encode()).hexdigest()[:8]
|
|
434
|
+
unique_id = str(uuid.uuid4())[:8]
|
|
435
|
+
return f"wh_{url_hash}_{unique_id}"
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _webhook_to_response(webhook_id: str, webhook: dict[str, Any]) -> WebhookResponse:
|
|
439
|
+
"""Convert a stored webhook to response model.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
webhook_id: The webhook ID.
|
|
443
|
+
webhook: The webhook configuration dictionary.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
WebhookResponse model instance.
|
|
447
|
+
"""
|
|
448
|
+
return WebhookResponse(
|
|
449
|
+
id=webhook_id,
|
|
450
|
+
url=webhook["url"],
|
|
451
|
+
has_secret=bool(webhook.get("secret")),
|
|
452
|
+
events=webhook.get("events"),
|
|
453
|
+
enabled=webhook.get("enabled", True),
|
|
454
|
+
name=webhook.get("name"),
|
|
455
|
+
description=webhook.get("description"),
|
|
456
|
+
timeout=webhook.get("timeout", 30.0),
|
|
457
|
+
max_retries=webhook.get("max_retries", 3),
|
|
458
|
+
verify_ssl=webhook.get("verify_ssl", True),
|
|
459
|
+
headers=webhook.get("headers", {}),
|
|
460
|
+
created_at=webhook.get("created_at", datetime.now().isoformat()),
|
|
461
|
+
updated_at=webhook.get("updated_at", datetime.now().isoformat()),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# =============================================================================
|
|
466
|
+
# Webhooks Router
|
|
467
|
+
# =============================================================================
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def create_webhooks_router() -> APIRouter:
|
|
471
|
+
"""Create router for webhook management endpoints.
|
|
472
|
+
|
|
473
|
+
These endpoints allow CRUD operations on webhook configurations
|
|
474
|
+
and testing webhook delivery.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
APIRouter configured with webhook management endpoints.
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
ImportError: If FastAPI is not installed.
|
|
481
|
+
"""
|
|
482
|
+
if not FASTAPI_AVAILABLE:
|
|
483
|
+
raise ImportError(
|
|
484
|
+
"FastAPI not installed. Install with: pip install claude-task-master[api]"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
router = APIRouter(tags=["Webhooks"])
|
|
488
|
+
|
|
489
|
+
# =========================================================================
|
|
490
|
+
# GET /webhooks - List all webhooks
|
|
491
|
+
# =========================================================================
|
|
492
|
+
|
|
493
|
+
@router.get(
|
|
494
|
+
"",
|
|
495
|
+
response_model=WebhooksListResponse,
|
|
496
|
+
responses={
|
|
497
|
+
500: {"model": WebhookErrorResponse, "description": "Internal server error"},
|
|
498
|
+
},
|
|
499
|
+
summary="List Webhooks",
|
|
500
|
+
description="List all configured webhook endpoints.",
|
|
501
|
+
)
|
|
502
|
+
async def list_webhooks(request: Request) -> WebhooksListResponse | JSONResponse:
|
|
503
|
+
"""List all configured webhooks.
|
|
504
|
+
|
|
505
|
+
Returns all webhook configurations without exposing secrets.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
WebhooksListResponse with list of webhooks.
|
|
509
|
+
"""
|
|
510
|
+
try:
|
|
511
|
+
webhooks_file = _get_webhooks_file(request)
|
|
512
|
+
webhooks = _load_webhooks(webhooks_file)
|
|
513
|
+
|
|
514
|
+
webhook_responses = [
|
|
515
|
+
_webhook_to_response(wh_id, wh_data) for wh_id, wh_data in webhooks.items()
|
|
516
|
+
]
|
|
517
|
+
|
|
518
|
+
return WebhooksListResponse(
|
|
519
|
+
success=True,
|
|
520
|
+
webhooks=webhook_responses,
|
|
521
|
+
total=len(webhook_responses),
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
except Exception as e:
|
|
525
|
+
logger.exception("Error listing webhooks")
|
|
526
|
+
return JSONResponse(
|
|
527
|
+
status_code=500,
|
|
528
|
+
content=WebhookErrorResponse(
|
|
529
|
+
error="internal_error",
|
|
530
|
+
message="Failed to list webhooks",
|
|
531
|
+
detail=str(e),
|
|
532
|
+
).model_dump(),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# =========================================================================
|
|
536
|
+
# POST /webhooks - Create webhook
|
|
537
|
+
# =========================================================================
|
|
538
|
+
|
|
539
|
+
@router.post(
|
|
540
|
+
"",
|
|
541
|
+
response_model=WebhookCreateResponse,
|
|
542
|
+
status_code=201,
|
|
543
|
+
responses={
|
|
544
|
+
400: {"model": WebhookErrorResponse, "description": "Invalid request"},
|
|
545
|
+
409: {"model": WebhookErrorResponse, "description": "Webhook already exists"},
|
|
546
|
+
500: {"model": WebhookErrorResponse, "description": "Internal server error"},
|
|
547
|
+
},
|
|
548
|
+
summary="Create Webhook",
|
|
549
|
+
description="Create a new webhook configuration.",
|
|
550
|
+
)
|
|
551
|
+
async def create_webhook(
|
|
552
|
+
request: Request, webhook_request: WebhookCreateRequest
|
|
553
|
+
) -> WebhookCreateResponse | JSONResponse:
|
|
554
|
+
"""Create a new webhook configuration.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
webhook_request: Webhook configuration details.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
WebhookCreateResponse with created webhook.
|
|
561
|
+
"""
|
|
562
|
+
try:
|
|
563
|
+
webhooks_file = _get_webhooks_file(request)
|
|
564
|
+
webhooks = _load_webhooks(webhooks_file)
|
|
565
|
+
|
|
566
|
+
# Check for duplicate URL
|
|
567
|
+
for existing_id, existing_webhook in webhooks.items():
|
|
568
|
+
if existing_webhook["url"] == webhook_request.url:
|
|
569
|
+
return JSONResponse(
|
|
570
|
+
status_code=409,
|
|
571
|
+
content=WebhookErrorResponse(
|
|
572
|
+
error="duplicate_webhook",
|
|
573
|
+
message=f"A webhook with URL '{webhook_request.url}' already exists",
|
|
574
|
+
detail=f"Existing webhook ID: {existing_id}",
|
|
575
|
+
).model_dump(),
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Generate unique ID
|
|
579
|
+
webhook_id = _generate_webhook_id(webhook_request.url)
|
|
580
|
+
|
|
581
|
+
# Ensure ID is unique (very unlikely to collide, but check anyway)
|
|
582
|
+
while webhook_id in webhooks:
|
|
583
|
+
webhook_id = _generate_webhook_id(webhook_request.url + str(uuid.uuid4()))
|
|
584
|
+
|
|
585
|
+
# Create webhook configuration
|
|
586
|
+
now = datetime.now().isoformat()
|
|
587
|
+
webhook_data = {
|
|
588
|
+
"url": webhook_request.url,
|
|
589
|
+
"secret": webhook_request.secret,
|
|
590
|
+
"events": webhook_request.events,
|
|
591
|
+
"enabled": webhook_request.enabled,
|
|
592
|
+
"name": webhook_request.name,
|
|
593
|
+
"description": webhook_request.description,
|
|
594
|
+
"timeout": webhook_request.timeout,
|
|
595
|
+
"max_retries": webhook_request.max_retries,
|
|
596
|
+
"verify_ssl": webhook_request.verify_ssl,
|
|
597
|
+
"headers": webhook_request.headers,
|
|
598
|
+
"created_at": now,
|
|
599
|
+
"updated_at": now,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
# Save webhook
|
|
603
|
+
webhooks[webhook_id] = webhook_data
|
|
604
|
+
_save_webhooks(webhooks_file, webhooks)
|
|
605
|
+
|
|
606
|
+
logger.info(f"Created webhook {webhook_id} for URL: {webhook_request.url}")
|
|
607
|
+
|
|
608
|
+
return WebhookCreateResponse(
|
|
609
|
+
success=True,
|
|
610
|
+
message="Webhook created successfully",
|
|
611
|
+
webhook=_webhook_to_response(webhook_id, webhook_data),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
except Exception as e:
|
|
615
|
+
logger.exception("Error creating webhook")
|
|
616
|
+
return JSONResponse(
|
|
617
|
+
status_code=500,
|
|
618
|
+
content=WebhookErrorResponse(
|
|
619
|
+
error="internal_error",
|
|
620
|
+
message="Failed to create webhook",
|
|
621
|
+
detail=str(e),
|
|
622
|
+
).model_dump(),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# =========================================================================
|
|
626
|
+
# GET /webhooks/{webhook_id} - Get specific webhook
|
|
627
|
+
# =========================================================================
|
|
628
|
+
|
|
629
|
+
@router.get(
|
|
630
|
+
"/{webhook_id}",
|
|
631
|
+
response_model=WebhookResponse,
|
|
632
|
+
responses={
|
|
633
|
+
404: {"model": WebhookErrorResponse, "description": "Webhook not found"},
|
|
634
|
+
500: {"model": WebhookErrorResponse, "description": "Internal server error"},
|
|
635
|
+
},
|
|
636
|
+
summary="Get Webhook",
|
|
637
|
+
description="Get a specific webhook configuration by ID.",
|
|
638
|
+
)
|
|
639
|
+
async def get_webhook(request: Request, webhook_id: str) -> WebhookResponse | JSONResponse:
|
|
640
|
+
"""Get a specific webhook configuration.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
webhook_id: The webhook ID.
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
WebhookResponse with webhook configuration.
|
|
647
|
+
"""
|
|
648
|
+
try:
|
|
649
|
+
webhooks_file = _get_webhooks_file(request)
|
|
650
|
+
webhooks = _load_webhooks(webhooks_file)
|
|
651
|
+
|
|
652
|
+
if webhook_id not in webhooks:
|
|
653
|
+
return JSONResponse(
|
|
654
|
+
status_code=404,
|
|
655
|
+
content=WebhookErrorResponse(
|
|
656
|
+
error="not_found",
|
|
657
|
+
message=f"Webhook '{webhook_id}' not found",
|
|
658
|
+
).model_dump(),
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
return _webhook_to_response(webhook_id, webhooks[webhook_id])
|
|
662
|
+
|
|
663
|
+
except Exception as e:
|
|
664
|
+
logger.exception("Error getting webhook")
|
|
665
|
+
return JSONResponse(
|
|
666
|
+
status_code=500,
|
|
667
|
+
content=WebhookErrorResponse(
|
|
668
|
+
error="internal_error",
|
|
669
|
+
message="Failed to get webhook",
|
|
670
|
+
detail=str(e),
|
|
671
|
+
).model_dump(),
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# =========================================================================
|
|
675
|
+
# PUT /webhooks/{webhook_id} - Update webhook
|
|
676
|
+
# =========================================================================
|
|
677
|
+
|
|
678
|
+
@router.put(
|
|
679
|
+
"/{webhook_id}",
|
|
680
|
+
response_model=WebhookResponse,
|
|
681
|
+
responses={
|
|
682
|
+
400: {"model": WebhookErrorResponse, "description": "Invalid request"},
|
|
683
|
+
404: {"model": WebhookErrorResponse, "description": "Webhook not found"},
|
|
684
|
+
409: {"model": WebhookErrorResponse, "description": "URL conflict"},
|
|
685
|
+
500: {"model": WebhookErrorResponse, "description": "Internal server error"},
|
|
686
|
+
},
|
|
687
|
+
summary="Update Webhook",
|
|
688
|
+
description="Update an existing webhook configuration.",
|
|
689
|
+
)
|
|
690
|
+
async def update_webhook(
|
|
691
|
+
request: Request, webhook_id: str, update_request: WebhookUpdateRequest
|
|
692
|
+
) -> WebhookResponse | JSONResponse:
|
|
693
|
+
"""Update an existing webhook configuration.
|
|
694
|
+
|
|
695
|
+
Only provided fields are updated.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
webhook_id: The webhook ID to update.
|
|
699
|
+
update_request: Fields to update.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
WebhookResponse with updated webhook configuration.
|
|
703
|
+
"""
|
|
704
|
+
try:
|
|
705
|
+
webhooks_file = _get_webhooks_file(request)
|
|
706
|
+
webhooks = _load_webhooks(webhooks_file)
|
|
707
|
+
|
|
708
|
+
if webhook_id not in webhooks:
|
|
709
|
+
return JSONResponse(
|
|
710
|
+
status_code=404,
|
|
711
|
+
content=WebhookErrorResponse(
|
|
712
|
+
error="not_found",
|
|
713
|
+
message=f"Webhook '{webhook_id}' not found",
|
|
714
|
+
).model_dump(),
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
# Check for URL conflict if URL is being updated
|
|
718
|
+
if update_request.url is not None:
|
|
719
|
+
for other_id, other_webhook in webhooks.items():
|
|
720
|
+
if other_id != webhook_id and other_webhook["url"] == update_request.url:
|
|
721
|
+
return JSONResponse(
|
|
722
|
+
status_code=409,
|
|
723
|
+
content=WebhookErrorResponse(
|
|
724
|
+
error="duplicate_webhook",
|
|
725
|
+
message=f"A webhook with URL '{update_request.url}' already exists",
|
|
726
|
+
detail=f"Existing webhook ID: {other_id}",
|
|
727
|
+
).model_dump(),
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# Update fields
|
|
731
|
+
webhook = webhooks[webhook_id]
|
|
732
|
+
|
|
733
|
+
if update_request.url is not None:
|
|
734
|
+
webhook["url"] = update_request.url
|
|
735
|
+
if update_request.secret is not None:
|
|
736
|
+
# Empty string clears the secret
|
|
737
|
+
webhook["secret"] = update_request.secret if update_request.secret else None
|
|
738
|
+
if update_request.events is not None:
|
|
739
|
+
# Empty list clears the filter (all events)
|
|
740
|
+
webhook["events"] = update_request.events if update_request.events else None
|
|
741
|
+
if update_request.enabled is not None:
|
|
742
|
+
webhook["enabled"] = update_request.enabled
|
|
743
|
+
if update_request.name is not None:
|
|
744
|
+
webhook["name"] = update_request.name if update_request.name else None
|
|
745
|
+
if update_request.description is not None:
|
|
746
|
+
webhook["description"] = (
|
|
747
|
+
update_request.description if update_request.description else None
|
|
748
|
+
)
|
|
749
|
+
if update_request.timeout is not None:
|
|
750
|
+
webhook["timeout"] = update_request.timeout
|
|
751
|
+
if update_request.max_retries is not None:
|
|
752
|
+
webhook["max_retries"] = update_request.max_retries
|
|
753
|
+
if update_request.verify_ssl is not None:
|
|
754
|
+
webhook["verify_ssl"] = update_request.verify_ssl
|
|
755
|
+
if update_request.headers is not None:
|
|
756
|
+
webhook["headers"] = update_request.headers
|
|
757
|
+
|
|
758
|
+
webhook["updated_at"] = datetime.now().isoformat()
|
|
759
|
+
|
|
760
|
+
# Save changes
|
|
761
|
+
_save_webhooks(webhooks_file, webhooks)
|
|
762
|
+
|
|
763
|
+
logger.info(f"Updated webhook {webhook_id}")
|
|
764
|
+
|
|
765
|
+
return _webhook_to_response(webhook_id, webhook)
|
|
766
|
+
|
|
767
|
+
except Exception as e:
|
|
768
|
+
logger.exception("Error updating webhook")
|
|
769
|
+
return JSONResponse(
|
|
770
|
+
status_code=500,
|
|
771
|
+
content=WebhookErrorResponse(
|
|
772
|
+
error="internal_error",
|
|
773
|
+
message="Failed to update webhook",
|
|
774
|
+
detail=str(e),
|
|
775
|
+
).model_dump(),
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# =========================================================================
|
|
779
|
+
# DELETE /webhooks/{webhook_id} - Delete webhook
|
|
780
|
+
# =========================================================================
|
|
781
|
+
|
|
782
|
+
@router.delete(
|
|
783
|
+
"/{webhook_id}",
|
|
784
|
+
response_model=WebhookDeleteResponse,
|
|
785
|
+
responses={
|
|
786
|
+
404: {"model": WebhookErrorResponse, "description": "Webhook not found"},
|
|
787
|
+
500: {"model": WebhookErrorResponse, "description": "Internal server error"},
|
|
788
|
+
},
|
|
789
|
+
summary="Delete Webhook",
|
|
790
|
+
description="Delete a webhook configuration.",
|
|
791
|
+
)
|
|
792
|
+
async def delete_webhook(
|
|
793
|
+
request: Request, webhook_id: str
|
|
794
|
+
) -> WebhookDeleteResponse | JSONResponse:
|
|
795
|
+
"""Delete a webhook configuration.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
webhook_id: The webhook ID to delete.
|
|
799
|
+
|
|
800
|
+
Returns:
|
|
801
|
+
WebhookDeleteResponse with deletion result.
|
|
802
|
+
"""
|
|
803
|
+
try:
|
|
804
|
+
webhooks_file = _get_webhooks_file(request)
|
|
805
|
+
webhooks = _load_webhooks(webhooks_file)
|
|
806
|
+
|
|
807
|
+
if webhook_id not in webhooks:
|
|
808
|
+
return JSONResponse(
|
|
809
|
+
status_code=404,
|
|
810
|
+
content=WebhookErrorResponse(
|
|
811
|
+
error="not_found",
|
|
812
|
+
message=f"Webhook '{webhook_id}' not found",
|
|
813
|
+
).model_dump(),
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
# Delete webhook
|
|
817
|
+
del webhooks[webhook_id]
|
|
818
|
+
_save_webhooks(webhooks_file, webhooks)
|
|
819
|
+
|
|
820
|
+
logger.info(f"Deleted webhook {webhook_id}")
|
|
821
|
+
|
|
822
|
+
return WebhookDeleteResponse(
|
|
823
|
+
success=True,
|
|
824
|
+
message="Webhook deleted successfully",
|
|
825
|
+
id=webhook_id,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
except Exception as e:
|
|
829
|
+
logger.exception("Error deleting webhook")
|
|
830
|
+
return JSONResponse(
|
|
831
|
+
status_code=500,
|
|
832
|
+
content=WebhookErrorResponse(
|
|
833
|
+
error="internal_error",
|
|
834
|
+
message="Failed to delete webhook",
|
|
835
|
+
detail=str(e),
|
|
836
|
+
).model_dump(),
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
# =========================================================================
|
|
840
|
+
# POST /webhooks/test - Test webhook
|
|
841
|
+
# =========================================================================
|
|
842
|
+
|
|
843
|
+
@router.post(
|
|
844
|
+
"/test",
|
|
845
|
+
response_model=WebhookTestResponse,
|
|
846
|
+
responses={
|
|
847
|
+
400: {"model": WebhookErrorResponse, "description": "Invalid request"},
|
|
848
|
+
404: {"model": WebhookErrorResponse, "description": "Webhook not found"},
|
|
849
|
+
500: {"model": WebhookErrorResponse, "description": "Internal server error"},
|
|
850
|
+
},
|
|
851
|
+
summary="Test Webhook",
|
|
852
|
+
description="Send a test webhook to verify configuration.",
|
|
853
|
+
)
|
|
854
|
+
async def test_webhook(
|
|
855
|
+
request: Request, test_request: WebhookTestRequest
|
|
856
|
+
) -> WebhookTestResponse | JSONResponse:
|
|
857
|
+
"""Send a test webhook to verify configuration.
|
|
858
|
+
|
|
859
|
+
Can test either an existing webhook by ID or a new URL directly.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
test_request: Test request with webhook_id or url/secret.
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
WebhookTestResponse with delivery result.
|
|
866
|
+
"""
|
|
867
|
+
try:
|
|
868
|
+
# Determine webhook URL and secret
|
|
869
|
+
if test_request.webhook_id:
|
|
870
|
+
# Test existing webhook
|
|
871
|
+
webhooks_file = _get_webhooks_file(request)
|
|
872
|
+
webhooks = _load_webhooks(webhooks_file)
|
|
873
|
+
|
|
874
|
+
if test_request.webhook_id not in webhooks:
|
|
875
|
+
return JSONResponse(
|
|
876
|
+
status_code=404,
|
|
877
|
+
content=WebhookErrorResponse(
|
|
878
|
+
error="not_found",
|
|
879
|
+
message=f"Webhook '{test_request.webhook_id}' not found",
|
|
880
|
+
).model_dump(),
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
webhook = webhooks[test_request.webhook_id]
|
|
884
|
+
url = webhook["url"]
|
|
885
|
+
secret = webhook.get("secret")
|
|
886
|
+
timeout = webhook.get("timeout", 30.0)
|
|
887
|
+
verify_ssl = webhook.get("verify_ssl", True)
|
|
888
|
+
headers = webhook.get("headers", {})
|
|
889
|
+
|
|
890
|
+
elif test_request.url:
|
|
891
|
+
# Test direct URL
|
|
892
|
+
url = test_request.url
|
|
893
|
+
secret = test_request.secret
|
|
894
|
+
timeout = 30.0
|
|
895
|
+
verify_ssl = True
|
|
896
|
+
headers = {}
|
|
897
|
+
|
|
898
|
+
else:
|
|
899
|
+
return JSONResponse(
|
|
900
|
+
status_code=400,
|
|
901
|
+
content=WebhookErrorResponse(
|
|
902
|
+
error="invalid_request",
|
|
903
|
+
message="Either webhook_id or url must be provided",
|
|
904
|
+
).model_dump(),
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
# Create test payload
|
|
908
|
+
event_id = str(uuid.uuid4())
|
|
909
|
+
test_payload = {
|
|
910
|
+
"event_type": "webhook.test",
|
|
911
|
+
"event_id": event_id,
|
|
912
|
+
"timestamp": datetime.now().isoformat(),
|
|
913
|
+
"message": "This is a test webhook from Claude Task Master",
|
|
914
|
+
"test": True,
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
# Send test webhook
|
|
918
|
+
client = WebhookClient(
|
|
919
|
+
url=url,
|
|
920
|
+
secret=secret,
|
|
921
|
+
timeout=timeout,
|
|
922
|
+
max_retries=1, # Only try once for tests
|
|
923
|
+
verify_ssl=verify_ssl,
|
|
924
|
+
headers=headers,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
result: WebhookDeliveryResult = await client.send(
|
|
928
|
+
data=test_payload,
|
|
929
|
+
event_type="webhook.test",
|
|
930
|
+
delivery_id=event_id,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
if result.success:
|
|
934
|
+
return WebhookTestResponse(
|
|
935
|
+
success=True,
|
|
936
|
+
message="Test webhook delivered successfully",
|
|
937
|
+
status_code=result.status_code,
|
|
938
|
+
delivery_time_ms=result.delivery_time_ms,
|
|
939
|
+
attempt_count=result.attempt_count,
|
|
940
|
+
)
|
|
941
|
+
else:
|
|
942
|
+
return WebhookTestResponse(
|
|
943
|
+
success=False,
|
|
944
|
+
message="Test webhook delivery failed",
|
|
945
|
+
status_code=result.status_code,
|
|
946
|
+
delivery_time_ms=result.delivery_time_ms,
|
|
947
|
+
attempt_count=result.attempt_count,
|
|
948
|
+
error=result.error,
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
except Exception as e:
|
|
952
|
+
logger.exception("Error testing webhook")
|
|
953
|
+
return JSONResponse(
|
|
954
|
+
status_code=500,
|
|
955
|
+
content=WebhookErrorResponse(
|
|
956
|
+
error="internal_error",
|
|
957
|
+
message="Failed to test webhook",
|
|
958
|
+
detail=str(e),
|
|
959
|
+
).model_dump(),
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
return router
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
# =============================================================================
|
|
966
|
+
# Exports
|
|
967
|
+
# =============================================================================
|
|
968
|
+
|
|
969
|
+
__all__ = [
|
|
970
|
+
"create_webhooks_router",
|
|
971
|
+
"WebhookCreateRequest",
|
|
972
|
+
"WebhookUpdateRequest",
|
|
973
|
+
"WebhookTestRequest",
|
|
974
|
+
"WebhookResponse",
|
|
975
|
+
"WebhooksListResponse",
|
|
976
|
+
"WebhookCreateResponse",
|
|
977
|
+
"WebhookDeleteResponse",
|
|
978
|
+
"WebhookTestResponse",
|
|
979
|
+
"WebhookErrorResponse",
|
|
980
|
+
]
|