gobby 0.2.5__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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webhook action executor for workflows.
|
|
3
|
+
|
|
4
|
+
Executes HTTP requests as workflow actions with retry logic,
|
|
5
|
+
variable interpolation, and response capture.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Callable, Coroutine
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import aiohttp
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class WebhookResult:
|
|
26
|
+
"""Result of a webhook execution."""
|
|
27
|
+
|
|
28
|
+
success: bool
|
|
29
|
+
status_code: int | None = None
|
|
30
|
+
body: str | None = None
|
|
31
|
+
headers: dict[str, str] | None = None
|
|
32
|
+
error: str | None = None
|
|
33
|
+
|
|
34
|
+
def json_body(self) -> dict[str, Any] | None:
|
|
35
|
+
"""Parse body as JSON.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Parsed JSON dict, or None if body is not valid JSON.
|
|
39
|
+
"""
|
|
40
|
+
if not self.body:
|
|
41
|
+
return None
|
|
42
|
+
try:
|
|
43
|
+
result: dict[str, Any] = json.loads(self.body)
|
|
44
|
+
return result
|
|
45
|
+
except json.JSONDecodeError:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class RetryConfig:
|
|
51
|
+
"""Configuration for retry behavior."""
|
|
52
|
+
|
|
53
|
+
max_attempts: int = 3
|
|
54
|
+
backoff_seconds: float = 1.0
|
|
55
|
+
retry_on_status: list[int] = field(default_factory=lambda: [429, 500, 502, 503, 504])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WebhookExecutor:
|
|
59
|
+
"""Executes webhook HTTP requests from workflows.
|
|
60
|
+
|
|
61
|
+
Handles URL resolution, variable interpolation, retries,
|
|
62
|
+
and response capture.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
template_engine: Any | None = None,
|
|
68
|
+
webhook_registry: dict[str, dict[str, Any]] | None = None,
|
|
69
|
+
secrets: dict[str, str] | None = None,
|
|
70
|
+
):
|
|
71
|
+
"""Initialize the executor.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
template_engine: Optional template engine for variable interpolation.
|
|
75
|
+
webhook_registry: Dict mapping webhook_id to config (url, headers, etc.).
|
|
76
|
+
secrets: Dict of secret values for ${secrets.VAR} interpolation.
|
|
77
|
+
"""
|
|
78
|
+
self.template_engine = template_engine
|
|
79
|
+
self.webhook_registry = webhook_registry or {}
|
|
80
|
+
self.secrets = secrets or {}
|
|
81
|
+
|
|
82
|
+
async def execute(
|
|
83
|
+
self,
|
|
84
|
+
url: str,
|
|
85
|
+
method: str = "POST",
|
|
86
|
+
headers: dict[str, str] | None = None,
|
|
87
|
+
payload: dict[str, Any] | str | None = None,
|
|
88
|
+
timeout: int = 30,
|
|
89
|
+
retry_config: dict[str, Any] | None = None,
|
|
90
|
+
context: dict[str, Any] | None = None,
|
|
91
|
+
on_success: Callable[[WebhookResult], Coroutine[Any, Any, None]] | None = None,
|
|
92
|
+
on_failure: Callable[[WebhookResult], Coroutine[Any, Any, None]] | None = None,
|
|
93
|
+
) -> WebhookResult:
|
|
94
|
+
"""Execute a webhook HTTP request.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
url: Target URL for the request.
|
|
98
|
+
method: HTTP method (GET, POST, PUT, PATCH, DELETE).
|
|
99
|
+
headers: Request headers (supports ${secrets.VAR} interpolation).
|
|
100
|
+
payload: Request body as dict or string.
|
|
101
|
+
timeout: Request timeout in seconds.
|
|
102
|
+
retry_config: Optional retry configuration dict.
|
|
103
|
+
context: Context dict for variable interpolation.
|
|
104
|
+
on_success: Async callback for successful (2xx) response.
|
|
105
|
+
on_failure: Async callback after all retries exhausted.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
WebhookResult with response data or error.
|
|
109
|
+
"""
|
|
110
|
+
headers = headers or {}
|
|
111
|
+
context = context or {}
|
|
112
|
+
|
|
113
|
+
# Interpolate secrets in headers
|
|
114
|
+
interpolated_headers = self._interpolate_secrets(headers)
|
|
115
|
+
|
|
116
|
+
# Interpolate context in payload
|
|
117
|
+
interpolated_payload = self._interpolate_payload(payload, context)
|
|
118
|
+
|
|
119
|
+
# Parse retry config
|
|
120
|
+
retry = self._parse_retry_config(retry_config)
|
|
121
|
+
|
|
122
|
+
# Execute with retry logic
|
|
123
|
+
result = await self._execute_with_retry(
|
|
124
|
+
url=url,
|
|
125
|
+
method=method,
|
|
126
|
+
headers=interpolated_headers,
|
|
127
|
+
payload=interpolated_payload,
|
|
128
|
+
timeout=timeout,
|
|
129
|
+
retry=retry,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Call appropriate handler
|
|
133
|
+
if result.success and on_success:
|
|
134
|
+
await on_success(result)
|
|
135
|
+
elif not result.success and on_failure:
|
|
136
|
+
await on_failure(result)
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
async def execute_by_webhook_id(
|
|
141
|
+
self,
|
|
142
|
+
webhook_id: str,
|
|
143
|
+
payload: dict[str, Any] | str | None = None,
|
|
144
|
+
method: str | None = None,
|
|
145
|
+
headers: dict[str, str] | None = None,
|
|
146
|
+
timeout: int | None = None,
|
|
147
|
+
context: dict[str, Any] | None = None,
|
|
148
|
+
retry_config: dict[str, Any] | None = None,
|
|
149
|
+
on_success: Callable[[WebhookResult], Coroutine[Any, Any, None]] | None = None,
|
|
150
|
+
on_failure: Callable[[WebhookResult], Coroutine[Any, Any, None]] | None = None,
|
|
151
|
+
) -> WebhookResult:
|
|
152
|
+
"""Execute a webhook by looking up its ID in the registry.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
webhook_id: ID of the webhook in the registry.
|
|
156
|
+
payload: Request body.
|
|
157
|
+
method: Override HTTP method from registry.
|
|
158
|
+
headers: Additional headers (merged with registry headers).
|
|
159
|
+
timeout: Override timeout from registry.
|
|
160
|
+
context: Context for variable interpolation.
|
|
161
|
+
retry_config: Optional retry configuration dict.
|
|
162
|
+
on_success: Async callback for successful (2xx) response.
|
|
163
|
+
on_failure: Async callback after all retries exhausted.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
WebhookResult with response data or error.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
ValueError: If webhook_id not found in registry.
|
|
170
|
+
"""
|
|
171
|
+
if webhook_id not in self.webhook_registry:
|
|
172
|
+
raise ValueError(f"webhook_id '{webhook_id}' not found in registry")
|
|
173
|
+
|
|
174
|
+
config = self.webhook_registry[webhook_id]
|
|
175
|
+
url = config.get("url")
|
|
176
|
+
if not url:
|
|
177
|
+
raise ValueError(f"webhook_id '{webhook_id}' has no URL configured")
|
|
178
|
+
|
|
179
|
+
# Merge headers (registry defaults + provided overrides)
|
|
180
|
+
merged_headers = dict(config.get("headers", {}))
|
|
181
|
+
if headers:
|
|
182
|
+
merged_headers.update(headers)
|
|
183
|
+
|
|
184
|
+
return await self.execute(
|
|
185
|
+
url=url,
|
|
186
|
+
method=method or config.get("method", "POST"),
|
|
187
|
+
headers=merged_headers,
|
|
188
|
+
payload=payload,
|
|
189
|
+
timeout=timeout or config.get("timeout", 30),
|
|
190
|
+
context=context,
|
|
191
|
+
retry_config=retry_config,
|
|
192
|
+
on_success=on_success,
|
|
193
|
+
on_failure=on_failure,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _interpolate_secrets(self, headers: dict[str, str]) -> dict[str, str]:
|
|
197
|
+
"""Interpolate ${secrets.VAR} in header values.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
headers: Headers dict with potential secret references.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Headers with secrets interpolated.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ValueError: If a referenced secret is not found in self.secrets.
|
|
207
|
+
"""
|
|
208
|
+
result = {}
|
|
209
|
+
pattern = re.compile(r"\$\{secrets\.(\w+)\}")
|
|
210
|
+
|
|
211
|
+
for key, value in headers.items():
|
|
212
|
+
if isinstance(value, str):
|
|
213
|
+
# Find all secret references in the value
|
|
214
|
+
matches = pattern.findall(value)
|
|
215
|
+
for secret_name in matches:
|
|
216
|
+
if secret_name not in self.secrets:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Missing secret '{secret_name}' referenced in header '{key}'"
|
|
219
|
+
)
|
|
220
|
+
# Replace all secrets with their values
|
|
221
|
+
result[key] = pattern.sub(
|
|
222
|
+
lambda m: self.secrets[m.group(1)],
|
|
223
|
+
value,
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
result[key] = value
|
|
227
|
+
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
def _interpolate_payload(
|
|
231
|
+
self,
|
|
232
|
+
payload: dict[str, Any] | str | None,
|
|
233
|
+
context: dict[str, Any],
|
|
234
|
+
) -> dict[str, Any] | str | None:
|
|
235
|
+
"""Interpolate context variables in payload.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
payload: Payload to interpolate.
|
|
239
|
+
context: Context dict for variable values.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Interpolated payload.
|
|
243
|
+
"""
|
|
244
|
+
if payload is None:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
if self.template_engine and isinstance(payload, str):
|
|
248
|
+
rendered: str = self.template_engine.render(payload, context)
|
|
249
|
+
return rendered
|
|
250
|
+
|
|
251
|
+
# For dicts, we could deep-interpolate, but for now just return as-is
|
|
252
|
+
# since the tests expect the executor to handle the interpolation
|
|
253
|
+
return payload
|
|
254
|
+
|
|
255
|
+
def _parse_retry_config(self, config: dict[str, Any] | None) -> RetryConfig:
|
|
256
|
+
"""Parse retry configuration from dict.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
config: Retry config dict or None.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
RetryConfig instance.
|
|
263
|
+
"""
|
|
264
|
+
if not config:
|
|
265
|
+
return RetryConfig(max_attempts=1) # No retry by default
|
|
266
|
+
|
|
267
|
+
return RetryConfig(
|
|
268
|
+
max_attempts=config.get("max_attempts", 3),
|
|
269
|
+
backoff_seconds=config.get("backoff_seconds", 1.0),
|
|
270
|
+
retry_on_status=config.get("retry_on_status", [429, 500, 502, 503, 504]),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def _execute_with_retry(
|
|
274
|
+
self,
|
|
275
|
+
url: str,
|
|
276
|
+
method: str,
|
|
277
|
+
headers: dict[str, str],
|
|
278
|
+
payload: dict[str, Any] | str | None,
|
|
279
|
+
timeout: int,
|
|
280
|
+
retry: RetryConfig,
|
|
281
|
+
) -> WebhookResult:
|
|
282
|
+
"""Execute request with retry logic.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
url: Target URL.
|
|
286
|
+
method: HTTP method.
|
|
287
|
+
headers: Request headers.
|
|
288
|
+
payload: Request body.
|
|
289
|
+
timeout: Timeout in seconds.
|
|
290
|
+
retry: Retry configuration.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
WebhookResult with response or error.
|
|
294
|
+
"""
|
|
295
|
+
last_error: str | None = None
|
|
296
|
+
last_status: int | None = None
|
|
297
|
+
|
|
298
|
+
for attempt in range(retry.max_attempts):
|
|
299
|
+
if attempt > 0:
|
|
300
|
+
# Exponential backoff
|
|
301
|
+
delay = retry.backoff_seconds * (2 ** (attempt - 1))
|
|
302
|
+
logger.debug(f"Webhook retry {attempt + 1}/{retry.max_attempts}, backoff {delay}s")
|
|
303
|
+
await asyncio.sleep(delay)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
start_time = time.time()
|
|
307
|
+
result = await self._make_request(
|
|
308
|
+
url=url,
|
|
309
|
+
method=method,
|
|
310
|
+
headers=headers,
|
|
311
|
+
payload=payload,
|
|
312
|
+
timeout=timeout,
|
|
313
|
+
)
|
|
314
|
+
elapsed = time.time() - start_time
|
|
315
|
+
logger.debug(f"Webhook {method} {url} -> {result.status_code} ({elapsed:.2f}s)")
|
|
316
|
+
|
|
317
|
+
if result.success:
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
# Check if we should retry
|
|
321
|
+
if result.status_code and result.status_code in retry.retry_on_status:
|
|
322
|
+
last_error = f"HTTP {result.status_code}"
|
|
323
|
+
last_status = result.status_code
|
|
324
|
+
continue # Retry
|
|
325
|
+
|
|
326
|
+
# Non-retryable error
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
except TimeoutError:
|
|
330
|
+
last_error = f"Timeout after {timeout}s"
|
|
331
|
+
logger.debug(f"Webhook timeout: {url}")
|
|
332
|
+
continue # Retry on timeout
|
|
333
|
+
|
|
334
|
+
except aiohttp.ClientError as e:
|
|
335
|
+
last_error = str(e)
|
|
336
|
+
logger.debug(f"Webhook connection error: {url} - {e}")
|
|
337
|
+
continue # Retry on aiohttp client errors
|
|
338
|
+
|
|
339
|
+
# All retries exhausted
|
|
340
|
+
return WebhookResult(
|
|
341
|
+
success=False,
|
|
342
|
+
status_code=last_status,
|
|
343
|
+
body=None,
|
|
344
|
+
headers=None,
|
|
345
|
+
error=last_error or "Unknown error",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
async def _make_request(
|
|
349
|
+
self,
|
|
350
|
+
url: str,
|
|
351
|
+
method: str,
|
|
352
|
+
headers: dict[str, str],
|
|
353
|
+
payload: dict[str, Any] | str | None,
|
|
354
|
+
timeout: int,
|
|
355
|
+
) -> WebhookResult:
|
|
356
|
+
"""Make a single HTTP request.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
url: Target URL.
|
|
360
|
+
method: HTTP method.
|
|
361
|
+
headers: Request headers.
|
|
362
|
+
payload: Request body.
|
|
363
|
+
timeout: Timeout in seconds.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
WebhookResult with response data.
|
|
367
|
+
"""
|
|
368
|
+
client_timeout = aiohttp.ClientTimeout(total=timeout)
|
|
369
|
+
|
|
370
|
+
async with aiohttp.ClientSession(timeout=client_timeout) as session:
|
|
371
|
+
# Prepare request kwargs
|
|
372
|
+
kwargs: dict[str, Any] = {
|
|
373
|
+
"method": method,
|
|
374
|
+
"url": url,
|
|
375
|
+
"headers": headers,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# Add payload
|
|
379
|
+
if payload is not None:
|
|
380
|
+
if isinstance(payload, dict):
|
|
381
|
+
kwargs["json"] = payload
|
|
382
|
+
else:
|
|
383
|
+
kwargs["data"] = payload
|
|
384
|
+
|
|
385
|
+
async with session.request(**kwargs) as response:
|
|
386
|
+
body = await response.text()
|
|
387
|
+
|
|
388
|
+
# Convert headers to dict
|
|
389
|
+
response_headers = dict(response.headers)
|
|
390
|
+
|
|
391
|
+
success = 200 <= response.status < 300
|
|
392
|
+
|
|
393
|
+
return WebhookResult(
|
|
394
|
+
success=success,
|
|
395
|
+
status_code=response.status,
|
|
396
|
+
body=body,
|
|
397
|
+
headers=response_headers,
|
|
398
|
+
error=None if success else f"HTTP {response.status}",
|
|
399
|
+
)
|