procler 0.2.0__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.
- procler/__init__.py +3 -0
- procler/__main__.py +6 -0
- procler/api/__init__.py +5 -0
- procler/api/app.py +261 -0
- procler/api/deps.py +21 -0
- procler/api/routes/__init__.py +5 -0
- procler/api/routes/config.py +290 -0
- procler/api/routes/groups.py +62 -0
- procler/api/routes/logs.py +43 -0
- procler/api/routes/processes.py +185 -0
- procler/api/routes/recipes.py +69 -0
- procler/api/routes/snippets.py +134 -0
- procler/api/routes/ws.py +459 -0
- procler/cli.py +1478 -0
- procler/config/__init__.py +65 -0
- procler/config/changelog.py +148 -0
- procler/config/loader.py +256 -0
- procler/config/schema.py +315 -0
- procler/core/__init__.py +54 -0
- procler/core/context_base.py +117 -0
- procler/core/context_docker.py +384 -0
- procler/core/context_local.py +287 -0
- procler/core/daemon_detector.py +325 -0
- procler/core/events.py +74 -0
- procler/core/groups.py +419 -0
- procler/core/health.py +280 -0
- procler/core/log_tailer.py +262 -0
- procler/core/process_manager.py +1277 -0
- procler/core/recipes.py +330 -0
- procler/core/snippets.py +231 -0
- procler/core/variable_substitution.py +65 -0
- procler/db.py +96 -0
- procler/logging.py +41 -0
- procler/models.py +130 -0
- procler/py.typed +0 -0
- procler/settings.py +29 -0
- procler/static/assets/AboutView-BwZnsfpW.js +4 -0
- procler/static/assets/AboutView-UHbxWXcS.css +1 -0
- procler/static/assets/Code-HTS-H1S6.js +74 -0
- procler/static/assets/ConfigView-CGJcmp9G.css +1 -0
- procler/static/assets/ConfigView-aVtbRDf8.js +1 -0
- procler/static/assets/DashboardView-C5jw9Nsd.css +1 -0
- procler/static/assets/DashboardView-Dab7Cu9v.js +1 -0
- procler/static/assets/DataTable-z39TOAa4.js +746 -0
- procler/static/assets/DescriptionsItem-B2E8YbqJ.js +74 -0
- procler/static/assets/Divider-Dk-6aD2Y.js +42 -0
- procler/static/assets/Empty-MuygEHZM.js +24 -0
- procler/static/assets/Grid-CZ9QVKAT.js +1 -0
- procler/static/assets/GroupsView-BALG7i1X.js +1 -0
- procler/static/assets/GroupsView-gXAI1CVC.css +1 -0
- procler/static/assets/Input-e0xaxoWE.js +259 -0
- procler/static/assets/PhArrowsClockwise.vue-DqDg31az.js +1 -0
- procler/static/assets/PhCheckCircle.vue-Fwj9sh9m.js +1 -0
- procler/static/assets/PhEye.vue-JcPHciC2.js +1 -0
- procler/static/assets/PhPlay.vue-CZm7Gy3u.js +1 -0
- procler/static/assets/PhPlus.vue-yTWqKlSh.js +1 -0
- procler/static/assets/PhStop.vue-DxsqwIki.js +1 -0
- procler/static/assets/PhTrash.vue-DcqQbN1_.js +125 -0
- procler/static/assets/PhXCircle.vue-BXWmrabV.js +1 -0
- procler/static/assets/ProcessDetailView-DDbtIWq9.css +1 -0
- procler/static/assets/ProcessDetailView-DPtdNV-q.js +1 -0
- procler/static/assets/ProcessesView-B3a6Umur.js +1 -0
- procler/static/assets/ProcessesView-goLmghbJ.css +1 -0
- procler/static/assets/RecipesView-D2VxdneD.js +166 -0
- procler/static/assets/RecipesView-DXnFDCK4.css +1 -0
- procler/static/assets/Select-BBR17AHq.js +317 -0
- procler/static/assets/SnippetsView-B3a9q3AI.css +1 -0
- procler/static/assets/SnippetsView-DBCB2yGq.js +1 -0
- procler/static/assets/Spin-BXTjvFUk.js +90 -0
- procler/static/assets/Tag-Bh_qV63A.js +71 -0
- procler/static/assets/changelog-KkTT4H9-.js +1 -0
- procler/static/assets/groups-Zu-_v8ey.js +1 -0
- procler/static/assets/index-BsN-YMXq.css +1 -0
- procler/static/assets/index-BzW1XhyH.js +1282 -0
- procler/static/assets/procler-DOrSB1Vj.js +1 -0
- procler/static/assets/recipes-1w5SseGb.js +1 -0
- procler/static/index.html +17 -0
- procler/static/procler.png +0 -0
- procler-0.2.0.dist-info/METADATA +545 -0
- procler-0.2.0.dist-info/RECORD +83 -0
- procler-0.2.0.dist-info/WHEEL +4 -0
- procler-0.2.0.dist-info/entry_points.txt +2 -0
- procler-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Log retrieval API routes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from ...core import ProcessManager
|
|
9
|
+
from ..deps import get_manager
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LogsResponse(BaseModel):
|
|
15
|
+
"""Response for log queries."""
|
|
16
|
+
|
|
17
|
+
success: bool
|
|
18
|
+
data: dict[str, Any] | None = None
|
|
19
|
+
error: str | None = None
|
|
20
|
+
error_code: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get("/{name}")
|
|
24
|
+
async def get_logs(
|
|
25
|
+
name: str,
|
|
26
|
+
tail: int = Query(default=100, ge=1, le=10000, description="Number of lines to return"),
|
|
27
|
+
since: str | None = Query(default=None, description="Time filter (e.g., '5m', '1h', ISO timestamp)"),
|
|
28
|
+
manager: ProcessManager = Depends(get_manager),
|
|
29
|
+
) -> LogsResponse:
|
|
30
|
+
"""
|
|
31
|
+
Get logs for a process.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: Process name
|
|
35
|
+
tail: Number of lines to return (default: 100, max: 10000)
|
|
36
|
+
since: Time filter (e.g., '5m', '1h', ISO timestamp)
|
|
37
|
+
"""
|
|
38
|
+
result = await manager.logs(name, tail=tail, since=since)
|
|
39
|
+
|
|
40
|
+
if not result["success"]:
|
|
41
|
+
raise HTTPException(status_code=404, detail=result)
|
|
42
|
+
|
|
43
|
+
return LogsResponse(**result)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Process management API routes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from ...core import ProcessManager
|
|
9
|
+
from ..deps import get_manager
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProcessDefineRequest(BaseModel):
|
|
15
|
+
"""Request body for defining a process."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
command: str
|
|
19
|
+
context_type: str = "local"
|
|
20
|
+
container_name: str | None = None
|
|
21
|
+
cwd: str | None = None
|
|
22
|
+
display_name: str | None = None
|
|
23
|
+
tags: list[str] | None = None
|
|
24
|
+
force: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProcessResponse(BaseModel):
|
|
28
|
+
"""Standard response wrapper."""
|
|
29
|
+
|
|
30
|
+
success: bool
|
|
31
|
+
data: dict[str, Any] | None = None
|
|
32
|
+
error: str | None = None
|
|
33
|
+
error_code: str | None = None
|
|
34
|
+
suggestion: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("")
|
|
38
|
+
async def list_processes(manager: ProcessManager = Depends(get_manager)) -> ProcessResponse:
|
|
39
|
+
"""List all process definitions."""
|
|
40
|
+
result = await manager.status()
|
|
41
|
+
return ProcessResponse(**result)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.get("/{name}")
|
|
45
|
+
async def get_process(
|
|
46
|
+
name: str,
|
|
47
|
+
manager: ProcessManager = Depends(get_manager),
|
|
48
|
+
) -> ProcessResponse:
|
|
49
|
+
"""Get a specific process by name."""
|
|
50
|
+
result = await manager.status(name)
|
|
51
|
+
if not result["success"]:
|
|
52
|
+
raise HTTPException(status_code=404, detail=result)
|
|
53
|
+
return ProcessResponse(**result)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("")
|
|
57
|
+
async def create_process(
|
|
58
|
+
request: ProcessDefineRequest,
|
|
59
|
+
manager: ProcessManager = Depends(get_manager),
|
|
60
|
+
) -> ProcessResponse:
|
|
61
|
+
"""Create or update a process definition."""
|
|
62
|
+
from datetime import datetime
|
|
63
|
+
|
|
64
|
+
from sqler.query import SQLerField as F
|
|
65
|
+
|
|
66
|
+
from ...db import init_database
|
|
67
|
+
from ...models import Process
|
|
68
|
+
|
|
69
|
+
init_database()
|
|
70
|
+
|
|
71
|
+
# Check if context is docker and container is missing
|
|
72
|
+
if request.context_type == "docker" and not request.container_name:
|
|
73
|
+
return ProcessResponse(
|
|
74
|
+
success=False,
|
|
75
|
+
error="Container name required for docker context",
|
|
76
|
+
error_code="missing_container",
|
|
77
|
+
suggestion="Provide container_name for docker context",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Check if process already exists
|
|
81
|
+
existing = Process.query().filter(F("name") == request.name).all()
|
|
82
|
+
if existing:
|
|
83
|
+
if not request.force:
|
|
84
|
+
return ProcessResponse(
|
|
85
|
+
success=False,
|
|
86
|
+
error=f"Process '{request.name}' already exists",
|
|
87
|
+
error_code="process_exists",
|
|
88
|
+
suggestion=f"Use force=true to overwrite, or DELETE /api/processes/{request.name} first",
|
|
89
|
+
)
|
|
90
|
+
# Force mode: delete existing and continue
|
|
91
|
+
existing[0].delete()
|
|
92
|
+
|
|
93
|
+
process = Process(
|
|
94
|
+
name=request.name,
|
|
95
|
+
command=request.command,
|
|
96
|
+
context_type=request.context_type,
|
|
97
|
+
container_name=request.container_name,
|
|
98
|
+
cwd=request.cwd,
|
|
99
|
+
display_name=request.display_name,
|
|
100
|
+
tags=request.tags,
|
|
101
|
+
created_at=datetime.now().isoformat(),
|
|
102
|
+
updated_at=datetime.now().isoformat(),
|
|
103
|
+
)
|
|
104
|
+
process.save()
|
|
105
|
+
|
|
106
|
+
return ProcessResponse(
|
|
107
|
+
success=True,
|
|
108
|
+
data={
|
|
109
|
+
"action": "created",
|
|
110
|
+
"process": {
|
|
111
|
+
"id": process._id,
|
|
112
|
+
"name": process.name,
|
|
113
|
+
"command": process.command,
|
|
114
|
+
"context_type": process.context_type,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@router.delete("/{name}")
|
|
121
|
+
async def delete_process(
|
|
122
|
+
name: str,
|
|
123
|
+
manager: ProcessManager = Depends(get_manager),
|
|
124
|
+
) -> ProcessResponse:
|
|
125
|
+
"""Remove a process definition."""
|
|
126
|
+
from sqler.query import SQLerField as F
|
|
127
|
+
|
|
128
|
+
from ...db import init_database
|
|
129
|
+
from ...models import Process
|
|
130
|
+
|
|
131
|
+
init_database()
|
|
132
|
+
|
|
133
|
+
results = Process.query().filter(F("name") == name).all()
|
|
134
|
+
if not results:
|
|
135
|
+
raise HTTPException(
|
|
136
|
+
status_code=404,
|
|
137
|
+
detail={
|
|
138
|
+
"success": False,
|
|
139
|
+
"error": f"Process '{name}' not found",
|
|
140
|
+
"error_code": "process_not_found",
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
results[0].delete()
|
|
145
|
+
return ProcessResponse(
|
|
146
|
+
success=True,
|
|
147
|
+
data={"action": "removed", "name": name},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@router.post("/{name}/start")
|
|
152
|
+
async def start_process(
|
|
153
|
+
name: str,
|
|
154
|
+
manager: ProcessManager = Depends(get_manager),
|
|
155
|
+
) -> ProcessResponse:
|
|
156
|
+
"""Start a process."""
|
|
157
|
+
result = await manager.start(name)
|
|
158
|
+
if not result["success"]:
|
|
159
|
+
raise HTTPException(status_code=400, detail=result)
|
|
160
|
+
return ProcessResponse(**result)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@router.post("/{name}/stop")
|
|
164
|
+
async def stop_process(
|
|
165
|
+
name: str,
|
|
166
|
+
manager: ProcessManager = Depends(get_manager),
|
|
167
|
+
) -> ProcessResponse:
|
|
168
|
+
"""Stop a process."""
|
|
169
|
+
result = await manager.stop(name)
|
|
170
|
+
if not result["success"]:
|
|
171
|
+
raise HTTPException(status_code=400, detail=result)
|
|
172
|
+
return ProcessResponse(**result)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@router.post("/{name}/restart")
|
|
176
|
+
async def restart_process(
|
|
177
|
+
name: str,
|
|
178
|
+
clear_logs: bool = False,
|
|
179
|
+
manager: ProcessManager = Depends(get_manager),
|
|
180
|
+
) -> ProcessResponse:
|
|
181
|
+
"""Restart a process. Use ?clear_logs=true to delete old logs."""
|
|
182
|
+
result = await manager.restart(name, clear_logs=clear_logs)
|
|
183
|
+
if not result["success"]:
|
|
184
|
+
raise HTTPException(status_code=400, detail=result)
|
|
185
|
+
return ProcessResponse(**result)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Recipe management API routes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from ...core.recipes import get_recipe_executor
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RecipeResponse(BaseModel):
|
|
14
|
+
"""Standard response wrapper."""
|
|
15
|
+
|
|
16
|
+
success: bool
|
|
17
|
+
data: dict[str, Any] | None = None
|
|
18
|
+
error: str | None = None
|
|
19
|
+
error_code: str | None = None
|
|
20
|
+
suggestion: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RunRecipeRequest(BaseModel):
|
|
24
|
+
"""Request body for running a recipe."""
|
|
25
|
+
|
|
26
|
+
dry_run: bool = False
|
|
27
|
+
continue_on_error: bool | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get("")
|
|
31
|
+
async def list_recipes() -> RecipeResponse:
|
|
32
|
+
"""List all defined recipes."""
|
|
33
|
+
executor = get_recipe_executor()
|
|
34
|
+
result = executor.list_recipes()
|
|
35
|
+
return RecipeResponse(**result)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get("/{name}")
|
|
39
|
+
async def get_recipe(name: str) -> RecipeResponse:
|
|
40
|
+
"""Get a specific recipe with full details."""
|
|
41
|
+
executor = get_recipe_executor()
|
|
42
|
+
result = executor.get_recipe(name)
|
|
43
|
+
return RecipeResponse(**result)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.post("/{name}/run")
|
|
47
|
+
async def run_recipe(name: str, request: RunRecipeRequest | None = None) -> RecipeResponse:
|
|
48
|
+
"""
|
|
49
|
+
Execute a recipe.
|
|
50
|
+
|
|
51
|
+
Pass dry_run=true to preview what would happen without executing.
|
|
52
|
+
Pass continue_on_error to override the recipe's default error handling.
|
|
53
|
+
"""
|
|
54
|
+
executor = get_recipe_executor()
|
|
55
|
+
req = request or RunRecipeRequest()
|
|
56
|
+
result = await executor.run_recipe(
|
|
57
|
+
name=name,
|
|
58
|
+
dry_run=req.dry_run,
|
|
59
|
+
continue_on_error=req.continue_on_error,
|
|
60
|
+
)
|
|
61
|
+
return RecipeResponse(**result)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.post("/{name}/dry-run")
|
|
65
|
+
async def dry_run_recipe(name: str) -> RecipeResponse:
|
|
66
|
+
"""Preview what a recipe would do without executing."""
|
|
67
|
+
executor = get_recipe_executor()
|
|
68
|
+
result = await executor.run_recipe(name=name, dry_run=True)
|
|
69
|
+
return RecipeResponse(**result)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Snippet management API routes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from ...core import SnippetManager
|
|
9
|
+
from ..deps import get_snippets
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SnippetCreateRequest(BaseModel):
|
|
15
|
+
"""Request body for creating a snippet."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
command: str
|
|
19
|
+
description: str | None = None
|
|
20
|
+
context_type: str = "local"
|
|
21
|
+
container_name: str | None = None
|
|
22
|
+
tags: list[str] | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SnippetResponse(BaseModel):
|
|
26
|
+
"""Standard response wrapper for snippets."""
|
|
27
|
+
|
|
28
|
+
success: bool
|
|
29
|
+
data: dict[str, Any] | None = None
|
|
30
|
+
error: str | None = None
|
|
31
|
+
error_code: str | None = None
|
|
32
|
+
suggestion: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("")
|
|
36
|
+
async def list_snippets(
|
|
37
|
+
tag: str | None = Query(default=None, description="Filter by tag"),
|
|
38
|
+
manager: SnippetManager = Depends(get_snippets),
|
|
39
|
+
) -> SnippetResponse:
|
|
40
|
+
"""List all snippets, optionally filtered by tag."""
|
|
41
|
+
result = manager.list_snippets(tag=tag)
|
|
42
|
+
return SnippetResponse(**result)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.post("")
|
|
46
|
+
async def create_snippet(
|
|
47
|
+
request: SnippetCreateRequest,
|
|
48
|
+
manager: SnippetManager = Depends(get_snippets),
|
|
49
|
+
) -> SnippetResponse:
|
|
50
|
+
"""Create a new snippet."""
|
|
51
|
+
result = manager.save_snippet(
|
|
52
|
+
name=request.name,
|
|
53
|
+
command=request.command,
|
|
54
|
+
description=request.description,
|
|
55
|
+
context_type=request.context_type,
|
|
56
|
+
container_name=request.container_name,
|
|
57
|
+
tags=request.tags,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not result["success"]:
|
|
61
|
+
raise HTTPException(status_code=400, detail=result)
|
|
62
|
+
|
|
63
|
+
return SnippetResponse(**result)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.get("/{name}")
|
|
67
|
+
async def get_snippet(
|
|
68
|
+
name: str,
|
|
69
|
+
manager: SnippetManager = Depends(get_snippets),
|
|
70
|
+
) -> SnippetResponse:
|
|
71
|
+
"""Get a specific snippet by name."""
|
|
72
|
+
from sqler.query import SQLerField as F
|
|
73
|
+
|
|
74
|
+
from ...db import init_database
|
|
75
|
+
from ...models import Snippet
|
|
76
|
+
|
|
77
|
+
init_database()
|
|
78
|
+
|
|
79
|
+
results = Snippet.query().filter(F("name") == name).all()
|
|
80
|
+
if not results:
|
|
81
|
+
raise HTTPException(
|
|
82
|
+
status_code=404,
|
|
83
|
+
detail={
|
|
84
|
+
"success": False,
|
|
85
|
+
"error": f"Snippet '{name}' not found",
|
|
86
|
+
"error_code": "snippet_not_found",
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
snippet = results[0]
|
|
91
|
+
return SnippetResponse(
|
|
92
|
+
success=True,
|
|
93
|
+
data={
|
|
94
|
+
"snippet": {
|
|
95
|
+
"id": snippet._id,
|
|
96
|
+
"name": snippet.name,
|
|
97
|
+
"command": snippet.command,
|
|
98
|
+
"description": snippet.description,
|
|
99
|
+
"context_type": snippet.context_type,
|
|
100
|
+
"container_name": snippet.container_name,
|
|
101
|
+
"tags": snippet.tags or [],
|
|
102
|
+
"created_at": snippet.created_at,
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@router.delete("/{name}")
|
|
109
|
+
async def delete_snippet(
|
|
110
|
+
name: str,
|
|
111
|
+
manager: SnippetManager = Depends(get_snippets),
|
|
112
|
+
) -> SnippetResponse:
|
|
113
|
+
"""Remove a snippet."""
|
|
114
|
+
result = manager.remove_snippet(name)
|
|
115
|
+
|
|
116
|
+
if not result["success"]:
|
|
117
|
+
raise HTTPException(status_code=404, detail=result)
|
|
118
|
+
|
|
119
|
+
return SnippetResponse(**result)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@router.post("/{name}/run")
|
|
123
|
+
async def run_snippet(
|
|
124
|
+
name: str,
|
|
125
|
+
manager: SnippetManager = Depends(get_snippets),
|
|
126
|
+
) -> SnippetResponse:
|
|
127
|
+
"""Run a snippet."""
|
|
128
|
+
result = await manager.run_snippet(name)
|
|
129
|
+
|
|
130
|
+
if not result["success"]:
|
|
131
|
+
status_code = 404 if result.get("error_code") == "snippet_not_found" else 400
|
|
132
|
+
raise HTTPException(status_code=status_code, detail=result)
|
|
133
|
+
|
|
134
|
+
return SnippetResponse(**result)
|