codeframe-ai 0.9.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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Standardized API response models for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
This module provides consistent response and error formats for all v2 API endpoints.
|
|
4
|
+
|
|
5
|
+
Standard Response Format:
|
|
6
|
+
{
|
|
7
|
+
"success": true,
|
|
8
|
+
"data": { ... },
|
|
9
|
+
"message": "Optional human-readable message"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Standard Error Format:
|
|
13
|
+
{
|
|
14
|
+
"error": "Error description",
|
|
15
|
+
"detail": "Additional context",
|
|
16
|
+
"code": "ERROR_CODE"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
from codeframe.ui.response_models import ApiResponse, api_response, ApiError
|
|
21
|
+
|
|
22
|
+
@router.get("/items")
|
|
23
|
+
async def list_items() -> ApiResponse[list[Item]]:
|
|
24
|
+
items = get_items()
|
|
25
|
+
return api_response(items, message="Retrieved 5 items")
|
|
26
|
+
|
|
27
|
+
# For errors, raise HTTPException with ApiError detail
|
|
28
|
+
raise HTTPException(status_code=404, detail=ApiError(
|
|
29
|
+
error="Item not found",
|
|
30
|
+
detail=f"No item with id {item_id}",
|
|
31
|
+
code="ITEM_NOT_FOUND"
|
|
32
|
+
).model_dump())
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
36
|
+
|
|
37
|
+
from pydantic import BaseModel, Field
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ============================================================================
|
|
43
|
+
# Standard Response Models
|
|
44
|
+
# ============================================================================
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ApiResponse(BaseModel, Generic[T]):
|
|
48
|
+
"""Standard API response wrapper.
|
|
49
|
+
|
|
50
|
+
All successful responses should use this format for consistency.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
success: bool = Field(default=True, description="Whether the request succeeded")
|
|
54
|
+
data: T = Field(..., description="Response payload")
|
|
55
|
+
message: Optional[str] = Field(default=None, description="Optional human-readable message")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ApiError(BaseModel):
|
|
59
|
+
"""Standard API error format.
|
|
60
|
+
|
|
61
|
+
Use this for HTTPException details to ensure consistent error responses.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
error: str = Field(..., description="Error description")
|
|
65
|
+
detail: Optional[str] = Field(default=None, description="Additional context")
|
|
66
|
+
code: str = Field(..., description="Machine-readable error code")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
70
|
+
"""Standard paginated response wrapper."""
|
|
71
|
+
|
|
72
|
+
success: bool = Field(default=True)
|
|
73
|
+
data: list[T] = Field(..., description="List of items")
|
|
74
|
+
total: int = Field(..., description="Total number of items")
|
|
75
|
+
page: int = Field(..., description="Current page number")
|
|
76
|
+
page_size: int = Field(..., description="Items per page")
|
|
77
|
+
message: Optional[str] = Field(default=None)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ============================================================================
|
|
81
|
+
# Helper Functions
|
|
82
|
+
# ============================================================================
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def api_response(
|
|
86
|
+
data: Any,
|
|
87
|
+
message: Optional[str] = None,
|
|
88
|
+
) -> dict:
|
|
89
|
+
"""Create a standard API response dict.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
data: Response payload
|
|
93
|
+
message: Optional human-readable message
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dict in standard response format
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
return api_response({"id": 1, "name": "test"}, message="Created successfully")
|
|
100
|
+
"""
|
|
101
|
+
response = {
|
|
102
|
+
"success": True,
|
|
103
|
+
"data": data,
|
|
104
|
+
}
|
|
105
|
+
if message:
|
|
106
|
+
response["message"] = message
|
|
107
|
+
return response
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def api_error(
|
|
111
|
+
error: str,
|
|
112
|
+
code: str,
|
|
113
|
+
detail: Optional[str] = None,
|
|
114
|
+
) -> dict:
|
|
115
|
+
"""Create a standard API error dict for HTTPException detail.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
error: Error description
|
|
119
|
+
code: Machine-readable error code
|
|
120
|
+
detail: Additional context
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dict in standard error format
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
raise HTTPException(
|
|
127
|
+
status_code=404,
|
|
128
|
+
detail=api_error("Not found", "ITEM_NOT_FOUND", f"No item {id}")
|
|
129
|
+
)
|
|
130
|
+
"""
|
|
131
|
+
result = {
|
|
132
|
+
"error": error,
|
|
133
|
+
"code": code,
|
|
134
|
+
}
|
|
135
|
+
if detail:
|
|
136
|
+
result["detail"] = detail
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ============================================================================
|
|
141
|
+
# Common Error Codes
|
|
142
|
+
# ============================================================================
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ErrorCodes:
|
|
146
|
+
"""Standard error codes for consistent error handling."""
|
|
147
|
+
|
|
148
|
+
# Resource errors (4xx)
|
|
149
|
+
NOT_FOUND = "NOT_FOUND"
|
|
150
|
+
ALREADY_EXISTS = "ALREADY_EXISTS"
|
|
151
|
+
INVALID_REQUEST = "INVALID_REQUEST"
|
|
152
|
+
VALIDATION_ERROR = "VALIDATION_ERROR"
|
|
153
|
+
|
|
154
|
+
# Authorization errors
|
|
155
|
+
UNAUTHORIZED = "UNAUTHORIZED"
|
|
156
|
+
FORBIDDEN = "FORBIDDEN"
|
|
157
|
+
|
|
158
|
+
# State errors
|
|
159
|
+
INVALID_STATE = "INVALID_STATE"
|
|
160
|
+
CONFLICT = "CONFLICT"
|
|
161
|
+
|
|
162
|
+
# Execution errors
|
|
163
|
+
EXECUTION_FAILED = "EXECUTION_FAILED"
|
|
164
|
+
TIMEOUT = "TIMEOUT"
|
|
165
|
+
|
|
166
|
+
# Server errors (5xx)
|
|
167
|
+
INTERNAL_ERROR = "INTERNAL_ERROR"
|
|
168
|
+
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ============================================================================
|
|
172
|
+
# Common Success Messages
|
|
173
|
+
# ============================================================================
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def created_message(resource: str, id: Any = None) -> str:
|
|
177
|
+
"""Generate a standard created message."""
|
|
178
|
+
if id:
|
|
179
|
+
return f"{resource} created successfully (id: {id})"
|
|
180
|
+
return f"{resource} created successfully"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def updated_message(resource: str, id: Any = None) -> str:
|
|
184
|
+
"""Generate a standard updated message."""
|
|
185
|
+
if id:
|
|
186
|
+
return f"{resource} updated successfully (id: {id})"
|
|
187
|
+
return f"{resource} updated successfully"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def deleted_message(resource: str, id: Any = None) -> str:
|
|
191
|
+
"""Generate a standard deleted message."""
|
|
192
|
+
if id:
|
|
193
|
+
return f"{resource} deleted successfully (id: {id})"
|
|
194
|
+
return f"{resource} deleted successfully"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def retrieved_message(resource: str, count: Optional[int] = None) -> str:
|
|
198
|
+
"""Generate a standard retrieval message."""
|
|
199
|
+
if count is not None:
|
|
200
|
+
return f"Retrieved {count} {resource}(s)"
|
|
201
|
+
return f"{resource} retrieved successfully"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared helpers for v2 routers."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def atomic_write_json(path: Path, payload: dict) -> None:
|
|
10
|
+
"""Write JSON via per-call unique temp-file + os.replace.
|
|
11
|
+
|
|
12
|
+
A unique temp name is required so concurrent writers to the same target
|
|
13
|
+
do not collide on a shared `.tmp` suffix.
|
|
14
|
+
"""
|
|
15
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
17
|
+
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
|
|
18
|
+
)
|
|
19
|
+
try:
|
|
20
|
+
with os.fdopen(fd, "w") as f:
|
|
21
|
+
f.write(json.dumps(payload, indent=2))
|
|
22
|
+
os.replace(tmp_name, path)
|
|
23
|
+
except Exception:
|
|
24
|
+
# Clean up the temp file on failure so we don't leak it.
|
|
25
|
+
try:
|
|
26
|
+
os.unlink(tmp_name)
|
|
27
|
+
except OSError:
|
|
28
|
+
pass
|
|
29
|
+
raise
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""V2 Batches router - delegates to core/conductor module.
|
|
2
|
+
|
|
3
|
+
This module provides v2-style API endpoints for batch execution management.
|
|
4
|
+
Batches represent coordinated execution of multiple tasks.
|
|
5
|
+
|
|
6
|
+
Routes:
|
|
7
|
+
GET /api/v2/batches - List batches
|
|
8
|
+
GET /api/v2/batches/{id} - Get batch status
|
|
9
|
+
POST /api/v2/batches/{id}/stop - Stop a running batch
|
|
10
|
+
POST /api/v2/batches/{id}/resume - Resume a batch
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from codeframe.core.workspace import Workspace
|
|
20
|
+
from codeframe.lib.rate_limiter import rate_limit_standard
|
|
21
|
+
from codeframe.core import conductor
|
|
22
|
+
from codeframe.core.conductor import BatchStatus
|
|
23
|
+
from codeframe.ui.dependencies import get_v2_workspace
|
|
24
|
+
from codeframe.ui.response_models import api_error, ErrorCodes
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
router = APIRouter(prefix="/api/v2/batches", tags=["batches-v2"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ============================================================================
|
|
32
|
+
# Request/Response Models
|
|
33
|
+
# ============================================================================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BatchResponse(BaseModel):
|
|
37
|
+
"""Response for a single batch."""
|
|
38
|
+
|
|
39
|
+
id: str
|
|
40
|
+
workspace_id: str
|
|
41
|
+
task_ids: list[str]
|
|
42
|
+
status: str
|
|
43
|
+
strategy: str
|
|
44
|
+
max_parallel: int
|
|
45
|
+
on_failure: str
|
|
46
|
+
started_at: Optional[str]
|
|
47
|
+
completed_at: Optional[str]
|
|
48
|
+
results: dict[str, str] # task_id -> RunStatus value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BatchListResponse(BaseModel):
|
|
52
|
+
"""Response for batch list."""
|
|
53
|
+
|
|
54
|
+
batches: list[BatchResponse]
|
|
55
|
+
total: int
|
|
56
|
+
by_status: dict[str, int]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StopBatchRequest(BaseModel):
|
|
60
|
+
"""Request for stopping a batch."""
|
|
61
|
+
|
|
62
|
+
force: bool = Field(False, description="Force stop by terminating running processes")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ResumeBatchRequest(BaseModel):
|
|
66
|
+
"""Request for resuming a batch."""
|
|
67
|
+
|
|
68
|
+
force: bool = Field(False, description="Re-run all tasks including completed ones")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ============================================================================
|
|
72
|
+
# Helper Functions
|
|
73
|
+
# ============================================================================
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _batch_to_response(batch: conductor.BatchRun) -> BatchResponse:
|
|
77
|
+
"""Convert a BatchRun to a BatchResponse."""
|
|
78
|
+
return BatchResponse(
|
|
79
|
+
id=batch.id,
|
|
80
|
+
workspace_id=batch.workspace_id,
|
|
81
|
+
task_ids=batch.task_ids,
|
|
82
|
+
status=batch.status.value,
|
|
83
|
+
strategy=batch.strategy,
|
|
84
|
+
max_parallel=batch.max_parallel,
|
|
85
|
+
on_failure=batch.on_failure.value,
|
|
86
|
+
started_at=batch.started_at.isoformat() if batch.started_at else None,
|
|
87
|
+
completed_at=batch.completed_at.isoformat() if batch.completed_at else None,
|
|
88
|
+
results={k: v.value if hasattr(v, 'value') else str(v) for k, v in batch.results.items()},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ============================================================================
|
|
93
|
+
# Endpoints
|
|
94
|
+
# ============================================================================
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.get("", response_model=BatchListResponse)
|
|
98
|
+
@rate_limit_standard()
|
|
99
|
+
async def list_batches(
|
|
100
|
+
request: Request,
|
|
101
|
+
status: Optional[str] = Query(None, description="Filter by status (PENDING, RUNNING, COMPLETED, PARTIAL, FAILED, CANCELLED)"),
|
|
102
|
+
limit: int = Query(20, ge=1, le=100),
|
|
103
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
104
|
+
) -> BatchListResponse:
|
|
105
|
+
"""List batches in the workspace.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
status: Optional status filter
|
|
109
|
+
limit: Maximum batches to return
|
|
110
|
+
workspace: v2 Workspace
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of batches with counts by status
|
|
114
|
+
"""
|
|
115
|
+
# Parse status filter
|
|
116
|
+
status_filter = None
|
|
117
|
+
if status:
|
|
118
|
+
try:
|
|
119
|
+
status_filter = BatchStatus(status.upper())
|
|
120
|
+
except ValueError:
|
|
121
|
+
raise HTTPException(
|
|
122
|
+
status_code=400,
|
|
123
|
+
detail=api_error(
|
|
124
|
+
f"Invalid status: {status}",
|
|
125
|
+
ErrorCodes.VALIDATION_ERROR,
|
|
126
|
+
f"Valid values: {[s.value for s in BatchStatus]}",
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Get batches
|
|
131
|
+
batch_list = conductor.list_batches(workspace, status=status_filter, limit=limit)
|
|
132
|
+
|
|
133
|
+
# Calculate counts by status
|
|
134
|
+
all_batches = conductor.list_batches(workspace, limit=1000)
|
|
135
|
+
status_counts: dict[str, int] = {}
|
|
136
|
+
for batch in all_batches:
|
|
137
|
+
status_val = batch.status.value
|
|
138
|
+
status_counts[status_val] = status_counts.get(status_val, 0) + 1
|
|
139
|
+
|
|
140
|
+
return BatchListResponse(
|
|
141
|
+
batches=[_batch_to_response(b) for b in batch_list],
|
|
142
|
+
total=len(batch_list),
|
|
143
|
+
by_status=status_counts,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@router.get("/{batch_id}", response_model=BatchResponse)
|
|
148
|
+
@rate_limit_standard()
|
|
149
|
+
async def get_batch(
|
|
150
|
+
request: Request,
|
|
151
|
+
batch_id: str,
|
|
152
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
153
|
+
) -> BatchResponse:
|
|
154
|
+
"""Get a specific batch by ID.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
batch_id: Batch identifier
|
|
158
|
+
workspace: v2 Workspace
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Batch details
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
HTTPException: 404 if batch not found
|
|
165
|
+
"""
|
|
166
|
+
batch = conductor.get_batch(workspace, batch_id)
|
|
167
|
+
|
|
168
|
+
if not batch:
|
|
169
|
+
raise HTTPException(
|
|
170
|
+
status_code=404,
|
|
171
|
+
detail=api_error("Batch not found", ErrorCodes.NOT_FOUND, f"No batch with id {batch_id}"),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return _batch_to_response(batch)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@router.post("/{batch_id}/stop", response_model=BatchResponse)
|
|
178
|
+
@rate_limit_standard()
|
|
179
|
+
async def stop_batch(
|
|
180
|
+
request: Request,
|
|
181
|
+
batch_id: str,
|
|
182
|
+
body: StopBatchRequest = None,
|
|
183
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
184
|
+
) -> BatchResponse:
|
|
185
|
+
"""Stop a running batch.
|
|
186
|
+
|
|
187
|
+
Graceful stop (force=False):
|
|
188
|
+
- Marks batch as CANCELLED
|
|
189
|
+
- Running tasks finish naturally
|
|
190
|
+
|
|
191
|
+
Force stop (force=True):
|
|
192
|
+
- Sends SIGTERM to running processes
|
|
193
|
+
- Immediate termination
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
request: HTTP request for rate limiting
|
|
197
|
+
batch_id: Batch to stop
|
|
198
|
+
body: Stop options
|
|
199
|
+
workspace: v2 Workspace
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Updated batch
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
HTTPException:
|
|
206
|
+
- 404: Batch not found
|
|
207
|
+
- 400: Batch not in stoppable state
|
|
208
|
+
"""
|
|
209
|
+
force = body.force if body else False
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
batch = conductor.stop_batch(workspace, batch_id, force=force)
|
|
213
|
+
return _batch_to_response(batch)
|
|
214
|
+
|
|
215
|
+
except ValueError as e:
|
|
216
|
+
error_msg = str(e)
|
|
217
|
+
if "not found" in error_msg.lower():
|
|
218
|
+
raise HTTPException(
|
|
219
|
+
status_code=404,
|
|
220
|
+
detail=api_error("Batch not found", ErrorCodes.NOT_FOUND, error_msg),
|
|
221
|
+
)
|
|
222
|
+
raise HTTPException(
|
|
223
|
+
status_code=400,
|
|
224
|
+
detail=api_error("Cannot stop batch", ErrorCodes.INVALID_STATE, error_msg),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@router.post("/{batch_id}/resume", response_model=BatchResponse)
|
|
229
|
+
@rate_limit_standard()
|
|
230
|
+
async def resume_batch(
|
|
231
|
+
request: Request,
|
|
232
|
+
batch_id: str,
|
|
233
|
+
body: ResumeBatchRequest = None,
|
|
234
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
235
|
+
) -> BatchResponse:
|
|
236
|
+
"""Resume a batch by re-running failed/blocked tasks.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
request: HTTP request for rate limiting
|
|
240
|
+
batch_id: Batch to resume
|
|
241
|
+
body: Resume options
|
|
242
|
+
workspace: v2 Workspace
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Updated batch
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
HTTPException:
|
|
249
|
+
- 404: Batch not found
|
|
250
|
+
- 400: Batch not in resumable state
|
|
251
|
+
"""
|
|
252
|
+
force = body.force if body else False
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
batch = conductor.resume_batch(workspace, batch_id, force=force)
|
|
256
|
+
return _batch_to_response(batch)
|
|
257
|
+
|
|
258
|
+
except ValueError as e:
|
|
259
|
+
error_msg = str(e)
|
|
260
|
+
if "not found" in error_msg.lower():
|
|
261
|
+
raise HTTPException(
|
|
262
|
+
status_code=404,
|
|
263
|
+
detail=api_error("Batch not found", ErrorCodes.NOT_FOUND, error_msg),
|
|
264
|
+
)
|
|
265
|
+
raise HTTPException(
|
|
266
|
+
status_code=400,
|
|
267
|
+
detail=api_error("Cannot resume batch", ErrorCodes.INVALID_STATE, error_msg),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"Failed to resume batch {batch_id}: {e}", exc_info=True)
|
|
272
|
+
raise HTTPException(
|
|
273
|
+
status_code=500,
|
|
274
|
+
detail=api_error("Resume failed", ErrorCodes.EXECUTION_FAILED, str(e)),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@router.post("/{batch_id}/cancel", response_model=BatchResponse)
|
|
279
|
+
@rate_limit_standard()
|
|
280
|
+
async def cancel_batch(
|
|
281
|
+
request: Request,
|
|
282
|
+
batch_id: str,
|
|
283
|
+
workspace: Workspace = Depends(get_v2_workspace),
|
|
284
|
+
) -> BatchResponse:
|
|
285
|
+
"""Cancel a running batch.
|
|
286
|
+
|
|
287
|
+
Similar to stop but with explicit cancel semantics.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
batch_id: Batch to cancel
|
|
291
|
+
workspace: v2 Workspace
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Updated batch
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
HTTPException:
|
|
298
|
+
- 404: Batch not found
|
|
299
|
+
- 400: Batch not in cancellable state
|
|
300
|
+
"""
|
|
301
|
+
try:
|
|
302
|
+
batch = conductor.cancel_batch(workspace, batch_id)
|
|
303
|
+
return _batch_to_response(batch)
|
|
304
|
+
|
|
305
|
+
except ValueError as e:
|
|
306
|
+
error_msg = str(e)
|
|
307
|
+
if "not found" in error_msg.lower():
|
|
308
|
+
raise HTTPException(
|
|
309
|
+
status_code=404,
|
|
310
|
+
detail=api_error("Batch not found", ErrorCodes.NOT_FOUND, error_msg),
|
|
311
|
+
)
|
|
312
|
+
raise HTTPException(
|
|
313
|
+
status_code=400,
|
|
314
|
+
detail=api_error("Cannot cancel batch", ErrorCodes.INVALID_STATE, error_msg),
|
|
315
|
+
)
|