bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
171 lines
7.3 KiB
Python
171 lines
7.3 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
|
|
from mirage import Workspace
|
|
from mirage.server.clone import clone_workspace_with_override
|
|
from mirage.server.summary import make_detail
|
|
from mirage.server.version.api import (branch, checkout, commit_state,
|
|
diff_live_vs_ref, read_version,
|
|
resolve_ref, status_state, version_diff,
|
|
version_log)
|
|
from mirage.server.version.errors import HeadMovedError, NoSuchBranchError
|
|
from mirage.server.version.state_tree import to_state
|
|
from mirage.server.version.store import VersionStore
|
|
from mirage.workspace.snapshot import to_state_dict
|
|
|
|
from mirage.server.schemas import ( # isort: skip
|
|
BranchRequest, BranchResponse, CheckoutRequest, CloneRequest,
|
|
CommitRequest, CommitResponse, DiffResponse, VersionLogItem,
|
|
WorkspaceDetail)
|
|
|
|
router = APIRouter(prefix="/v1")
|
|
|
|
|
|
async def _state_of(ws: Workspace) -> dict:
|
|
return await to_state_dict(ws)
|
|
|
|
|
|
@router.post("/workspaces/{workspace_id}/commit",
|
|
response_model=CommitResponse)
|
|
async def commit_version(workspace_id: str, req: CommitRequest,
|
|
request: Request) -> CommitResponse:
|
|
registry = request.app.state.registry
|
|
if workspace_id not in registry:
|
|
raise HTTPException(status_code=404, detail="workspace not found")
|
|
entry = registry.get(workspace_id)
|
|
state = await entry.runner.call(_state_of(entry.runner.ws))
|
|
store = await VersionStore.open(request.app.state.version_backend,
|
|
workspace_id)
|
|
try:
|
|
version = await commit_state(store, state, req.branch, req.message)
|
|
except HeadMovedError as e:
|
|
raise HTTPException(status_code=409, detail=str(e))
|
|
except NoSuchBranchError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
return CommitResponse(version=version.decode(), branch=req.branch)
|
|
|
|
|
|
@router.get("/workspaces/{workspace_id}/versions",
|
|
response_model=list[VersionLogItem])
|
|
async def list_versions(
|
|
workspace_id: str, request: Request, branch: str = Query("main")
|
|
) -> list[VersionLogItem]: # noqa: E125
|
|
store = await VersionStore.open(request.app.state.version_backend,
|
|
workspace_id)
|
|
if branch not in await store.branches():
|
|
return []
|
|
entries = await version_log(store, branch)
|
|
return [VersionLogItem(id=e["id"], message=e["message"]) for e in entries]
|
|
|
|
|
|
@router.post("/workspaces/{workspace_id}/branch",
|
|
response_model=BranchResponse,
|
|
status_code=201)
|
|
async def create_branch(workspace_id: str, req: BranchRequest,
|
|
request: Request) -> BranchResponse:
|
|
store = await VersionStore.open(request.app.state.version_backend,
|
|
workspace_id)
|
|
if req.name in await store.branches():
|
|
raise HTTPException(status_code=409,
|
|
detail=f"branch already exists: {req.name!r}")
|
|
try:
|
|
await branch(store, req.name, req.from_branch)
|
|
except KeyError:
|
|
raise HTTPException(status_code=404,
|
|
detail=f"no such branch: {req.from_branch!r}")
|
|
head = await store.head(req.name)
|
|
return BranchResponse(branch=req.name, version=head.decode())
|
|
|
|
|
|
@router.get("/workspaces/{workspace_id}/diff", response_model=DiffResponse)
|
|
async def diff_versions(
|
|
workspace_id: str,
|
|
request: Request,
|
|
a: str | None = Query(None),
|
|
b: str | None = Query(None),
|
|
branch: str = Query("main"),
|
|
) -> DiffResponse: # noqa: E125
|
|
store = await VersionStore.open(request.app.state.version_backend,
|
|
workspace_id)
|
|
state = None
|
|
if a is None or b is None:
|
|
registry = request.app.state.registry
|
|
if workspace_id not in registry:
|
|
raise HTTPException(status_code=404, detail="workspace not found")
|
|
entry = registry.get(workspace_id)
|
|
state = await entry.runner.call(_state_of(entry.runner.ws))
|
|
try:
|
|
if a is not None and b is not None:
|
|
changes = await version_diff(store, await resolve_ref(store, a),
|
|
await resolve_ref(store, b))
|
|
elif a is not None:
|
|
changes = await diff_live_vs_ref(store, state, a)
|
|
else:
|
|
changes = await status_state(store, state, branch)
|
|
except KeyError:
|
|
raise HTTPException(status_code=404, detail="version not found")
|
|
return DiffResponse(**changes)
|
|
|
|
|
|
@router.post("/workspaces/{workspace_id}/checkout",
|
|
response_model=WorkspaceDetail)
|
|
async def checkout_version(workspace_id: str, req: CheckoutRequest,
|
|
request: Request) -> WorkspaceDetail:
|
|
registry = request.app.state.registry
|
|
if workspace_id not in registry:
|
|
raise HTTPException(status_code=404, detail="workspace not found")
|
|
entry = registry.get(workspace_id)
|
|
store = await VersionStore.open(request.app.state.version_backend,
|
|
workspace_id)
|
|
try:
|
|
await entry.runner.call(checkout(store, entry.runner.ws, req.ref))
|
|
except KeyError:
|
|
raise HTTPException(status_code=404,
|
|
detail=f"version not found: {req.ref}")
|
|
return await make_detail(entry)
|
|
|
|
|
|
@router.post("/workspaces/clone",
|
|
response_model=WorkspaceDetail,
|
|
status_code=201)
|
|
async def clone_workspace_version(req: CloneRequest,
|
|
request: Request) -> WorkspaceDetail:
|
|
registry = request.app.state.registry
|
|
if req.id is not None and req.id in registry:
|
|
raise HTTPException(status_code=409,
|
|
detail=f"workspace id already exists: {req.id!r}")
|
|
if req.at is not None:
|
|
store = await VersionStore.open(request.app.state.version_backend,
|
|
req.source_id)
|
|
version = await resolve_ref(store, req.at)
|
|
try:
|
|
entries, meta = await read_version(store, version)
|
|
except KeyError:
|
|
raise HTTPException(status_code=404,
|
|
detail=f"version not found: {req.at}")
|
|
ws = await Workspace.from_state(to_state(entries, meta))
|
|
else:
|
|
if req.source_id not in registry:
|
|
raise HTTPException(status_code=404, detail="workspace not found")
|
|
src = registry.get(req.source_id)
|
|
ws = await src.runner.call(
|
|
clone_workspace_with_override(src.runner.ws, None))
|
|
try:
|
|
entry = registry.add(ws, workspace_id=req.id)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=409, detail=str(e))
|
|
return await make_detail(entry)
|