codex-autorunner 1.0.0__py3-none-any.whl → 1.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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
from fastapi.responses import FileResponse
|
|
9
|
+
|
|
10
|
+
from ....core.filebox import (
|
|
11
|
+
BOXES,
|
|
12
|
+
FileBoxEntry,
|
|
13
|
+
delete_file,
|
|
14
|
+
ensure_structure,
|
|
15
|
+
list_filebox,
|
|
16
|
+
migrate_legacy,
|
|
17
|
+
resolve_file,
|
|
18
|
+
save_file,
|
|
19
|
+
)
|
|
20
|
+
from ....core.hub import HubSupervisor
|
|
21
|
+
from ....core.utils import find_repo_root
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _serialize_entry(entry: FileBoxEntry, *, request: Request) -> dict[str, Any]:
|
|
27
|
+
base = request.scope.get("root_path", "") or ""
|
|
28
|
+
# Provide a download URL that survives base_path rewrites.
|
|
29
|
+
download = f"{base}/api/filebox/{entry.box}/{entry.name}"
|
|
30
|
+
return {
|
|
31
|
+
"name": entry.name,
|
|
32
|
+
"box": entry.box,
|
|
33
|
+
"size": entry.size,
|
|
34
|
+
"modified_at": entry.modified_at,
|
|
35
|
+
"source": entry.source,
|
|
36
|
+
"url": download,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _serialize_listing(
|
|
41
|
+
entries: dict[str, list[FileBoxEntry]], *, request: Request
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
box: [_serialize_entry(e, request=request) for e in files]
|
|
45
|
+
for box, files in entries.items()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _resolve_repo_root(request: Request) -> Path:
|
|
50
|
+
engine = getattr(request.app.state, "engine", None)
|
|
51
|
+
repo_root = getattr(engine, "repo_root", None)
|
|
52
|
+
if isinstance(repo_root, Path):
|
|
53
|
+
return repo_root
|
|
54
|
+
if isinstance(repo_root, str):
|
|
55
|
+
try:
|
|
56
|
+
return Path(repo_root)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return find_repo_root()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_filebox_routes() -> APIRouter:
|
|
63
|
+
router = APIRouter(prefix="/api", tags=["filebox"])
|
|
64
|
+
|
|
65
|
+
@router.get("/filebox")
|
|
66
|
+
def list_box(request: Request) -> dict[str, Any]:
|
|
67
|
+
repo_root = _resolve_repo_root(request)
|
|
68
|
+
ensure_structure(repo_root)
|
|
69
|
+
try:
|
|
70
|
+
migrate_legacy(repo_root)
|
|
71
|
+
except Exception:
|
|
72
|
+
logger.debug("FileBox legacy migration skipped", exc_info=True)
|
|
73
|
+
entries = list_filebox(repo_root)
|
|
74
|
+
return _serialize_listing(entries, request=request)
|
|
75
|
+
|
|
76
|
+
@router.get("/filebox/{box}")
|
|
77
|
+
def list_single_box(box: str, request: Request) -> dict[str, Any]:
|
|
78
|
+
if box not in BOXES:
|
|
79
|
+
raise HTTPException(status_code=400, detail="Invalid box")
|
|
80
|
+
repo_root = _resolve_repo_root(request)
|
|
81
|
+
ensure_structure(repo_root)
|
|
82
|
+
entries = list_filebox(repo_root)
|
|
83
|
+
return {box: _serialize_listing(entries, request=request).get(box, [])}
|
|
84
|
+
|
|
85
|
+
@router.post("/filebox/{box}")
|
|
86
|
+
async def upload_file(box: str, request: Request) -> dict[str, Any]:
|
|
87
|
+
if box not in BOXES:
|
|
88
|
+
raise HTTPException(status_code=400, detail="Invalid box")
|
|
89
|
+
repo_root = _resolve_repo_root(request)
|
|
90
|
+
ensure_structure(repo_root)
|
|
91
|
+
|
|
92
|
+
form = await request.form()
|
|
93
|
+
saved = []
|
|
94
|
+
for filename, file in form.items():
|
|
95
|
+
try:
|
|
96
|
+
data = await file.read()
|
|
97
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
98
|
+
logger.warning("Failed to read upload: %s", exc)
|
|
99
|
+
continue
|
|
100
|
+
try:
|
|
101
|
+
path = save_file(repo_root, box, filename, data)
|
|
102
|
+
saved.append(path.name)
|
|
103
|
+
except ValueError as exc:
|
|
104
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
105
|
+
return {"status": "ok", "saved": saved}
|
|
106
|
+
|
|
107
|
+
@router.get("/filebox/{box}/{filename}")
|
|
108
|
+
def download_file(box: str, filename: str, request: Request):
|
|
109
|
+
if box not in BOXES:
|
|
110
|
+
raise HTTPException(status_code=400, detail="Invalid box")
|
|
111
|
+
repo_root = _resolve_repo_root(request)
|
|
112
|
+
entry = resolve_file(repo_root, box, filename)
|
|
113
|
+
if entry is None:
|
|
114
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
115
|
+
return FileResponse(entry.path, filename=entry.name)
|
|
116
|
+
|
|
117
|
+
@router.delete("/filebox/{box}/{filename}")
|
|
118
|
+
def delete_file_entry(box: str, filename: str, request: Request) -> dict[str, Any]:
|
|
119
|
+
if box not in BOXES:
|
|
120
|
+
raise HTTPException(status_code=400, detail="Invalid box")
|
|
121
|
+
repo_root = _resolve_repo_root(request)
|
|
122
|
+
removed = delete_file(repo_root, box, filename)
|
|
123
|
+
if not removed:
|
|
124
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
125
|
+
return {"status": "ok"}
|
|
126
|
+
|
|
127
|
+
return router
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _resolve_hub_repo_root(request: Request, repo_id: Optional[str]) -> Path:
|
|
131
|
+
supervisor: HubSupervisor | None = getattr(
|
|
132
|
+
request.app.state, "hub_supervisor", None
|
|
133
|
+
)
|
|
134
|
+
if supervisor is None:
|
|
135
|
+
raise HTTPException(status_code=404, detail="Hub supervisor unavailable")
|
|
136
|
+
snapshots = supervisor.list_repos()
|
|
137
|
+
candidates = [
|
|
138
|
+
snap for snap in snapshots if snap.initialized and snap.exists_on_disk
|
|
139
|
+
]
|
|
140
|
+
target = None
|
|
141
|
+
if repo_id:
|
|
142
|
+
target = next((snap for snap in candidates if snap.id == repo_id), None)
|
|
143
|
+
if target is None:
|
|
144
|
+
raise HTTPException(status_code=404, detail="Repo not found")
|
|
145
|
+
else:
|
|
146
|
+
if len(candidates) == 1:
|
|
147
|
+
target = candidates[0]
|
|
148
|
+
if target is None:
|
|
149
|
+
raise HTTPException(status_code=400, detail="repo_id is required")
|
|
150
|
+
return target.path
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _serialize_hub_entry(
|
|
154
|
+
entry: FileBoxEntry, *, request: Request, repo_id: str
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
base = request.scope.get("root_path", "") or ""
|
|
157
|
+
download = f"{base}/hub/filebox/{repo_id}/{entry.box}/{entry.name}"
|
|
158
|
+
return {
|
|
159
|
+
"name": entry.name,
|
|
160
|
+
"box": entry.box,
|
|
161
|
+
"size": entry.size,
|
|
162
|
+
"modified_at": entry.modified_at,
|
|
163
|
+
"source": entry.source,
|
|
164
|
+
"url": download,
|
|
165
|
+
"repo_id": repo_id,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def build_hub_filebox_routes() -> APIRouter:
|
|
170
|
+
router = APIRouter(prefix="/hub/filebox", tags=["filebox"])
|
|
171
|
+
|
|
172
|
+
@router.get("/{repo_id}")
|
|
173
|
+
def list_repo_filebox(repo_id: str, request: Request) -> dict[str, Any]:
|
|
174
|
+
repo_root = _resolve_hub_repo_root(request, repo_id)
|
|
175
|
+
try:
|
|
176
|
+
migrate_legacy(repo_root)
|
|
177
|
+
except Exception:
|
|
178
|
+
logger.debug("Hub FileBox legacy migration skipped", exc_info=True)
|
|
179
|
+
entries = list_filebox(repo_root)
|
|
180
|
+
serialized = {
|
|
181
|
+
box: [
|
|
182
|
+
_serialize_hub_entry(e, request=request, repo_id=repo_id) for e in files
|
|
183
|
+
]
|
|
184
|
+
for box, files in entries.items()
|
|
185
|
+
}
|
|
186
|
+
return serialized
|
|
187
|
+
|
|
188
|
+
@router.post("/{repo_id}/{box}")
|
|
189
|
+
async def hub_upload(repo_id: str, box: str, request: Request) -> dict[str, Any]:
|
|
190
|
+
if box not in BOXES:
|
|
191
|
+
raise HTTPException(status_code=400, detail="Invalid box")
|
|
192
|
+
repo_root = _resolve_hub_repo_root(request, repo_id)
|
|
193
|
+
ensure_structure(repo_root)
|
|
194
|
+
form = await request.form()
|
|
195
|
+
saved = []
|
|
196
|
+
for filename, file in form.items():
|
|
197
|
+
data = await file.read()
|
|
198
|
+
path = save_file(repo_root, box, filename, data)
|
|
199
|
+
saved.append(path.name)
|
|
200
|
+
return {"status": "ok", "saved": saved}
|
|
201
|
+
|
|
202
|
+
@router.get("/{repo_id}/{box}/{filename}")
|
|
203
|
+
def hub_download(repo_id: str, box: str, filename: str, request: Request):
|
|
204
|
+
if box not in BOXES:
|
|
205
|
+
raise HTTPException(status_code=400, detail="Invalid box")
|
|
206
|
+
repo_root = _resolve_hub_repo_root(request, repo_id)
|
|
207
|
+
entry = resolve_file(repo_root, box, filename)
|
|
208
|
+
if entry is None:
|
|
209
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
210
|
+
return FileResponse(entry.path, filename=entry.name)
|
|
211
|
+
|
|
212
|
+
@router.delete("/{repo_id}/{box}/{filename}")
|
|
213
|
+
def hub_delete(
|
|
214
|
+
repo_id: str, box: str, filename: str, request: Request
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
if box not in BOXES:
|
|
217
|
+
raise HTTPException(status_code=400, detail="Invalid box")
|
|
218
|
+
repo_root = _resolve_hub_repo_root(request, repo_id)
|
|
219
|
+
removed = delete_file(repo_root, box, filename)
|
|
220
|
+
if not removed:
|
|
221
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
222
|
+
return {"status": "ok"}
|
|
223
|
+
|
|
224
|
+
return router
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
__all__ = ["build_filebox_routes", "build_hub_filebox_routes"]
|