Skip to content

Commit 9dd92a6

Browse files
committed
Initial CRUD API for workspaces
This adds a simple and unimplemented REST API for workspaces. Workspaces will be the base for all other resources in terms of REST resource mapping, so these go first. These are initially left entirely unimplemented as #600 needs to merge Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent dceeef8 commit 9dd92a6

File tree

7 files changed

+160
-11
lines changed

7 files changed

+160
-11
lines changed

src/codegate/api/__init__.py

Whitespace-only changes.

src/codegate/api/v1.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from fastapi import APIRouter
2+
from fastapi.exceptions import HTTPException
3+
from fastapi.routing import APIRoute
4+
from pydantic import ValidationError
5+
6+
from codegate.api import v1_models
7+
from codegate.db.connection import WorkspaceAlreadyExistsError
8+
from codegate.pipeline.workspace import commands as wscmd
9+
10+
v1 = APIRouter()
11+
wscrud = wscmd.WorkspaceCrud()
12+
13+
14+
def uniq_name(route: APIRoute):
15+
return f"v1_{route.name}"
16+
17+
18+
@v1.get("/workspaces", tags=["Workspaces"], generate_unique_id_function=uniq_name)
19+
async def list_workspaces() -> v1_models.ListWorkspacesResponse:
20+
"""List all workspaces."""
21+
wslist = await wscrud.get_workspaces()
22+
23+
resp = v1_models.ListWorkspacesResponse.from_db_workspaces(wslist)
24+
25+
return resp
26+
27+
28+
@v1.get("/workspaces/active", tags=["Workspaces"], generate_unique_id_function=uniq_name)
29+
async def list_active_workspaces() -> v1_models.ListActiveWorkspacesResponse:
30+
"""List all active workspaces.
31+
32+
In it's current form, this function will only return one workspace. That is,
33+
the globally active workspace."""
34+
activews = await wscrud.get_active_workspace()
35+
36+
resp = v1_models.ListActiveWorkspacesResponse.from_db_workspaces(activews)
37+
38+
return resp
39+
40+
41+
@v1.post("/workspaces/active", tags=["Workspaces"], generate_unique_id_function=uniq_name)
42+
async def activate_workspace(request: v1_models.ActivateWorkspaceRequest, status_code=204):
43+
"""Activate a workspace by name."""
44+
activated = await wscrud.activate_workspace(request.name)
45+
46+
# TODO: Refactor
47+
if not activated:
48+
return HTTPException(status_code=409, detail="Workspace already active")
49+
50+
return None
51+
52+
53+
@v1.post("/workspaces", tags=["Workspaces"], generate_unique_id_function=uniq_name, status_code=201)
54+
async def create_workspace(request: v1_models.CreateWorkspaceRequest):
55+
"""Create a new workspace."""
56+
# Input validation is done in the model
57+
try:
58+
created = await wscrud.add_workspace(request.name, should_raise=True)
59+
except ValidationError as e:
60+
raise HTTPException(status_code=400,
61+
detail=str(e))
62+
except WorkspaceAlreadyExistsError:
63+
raise HTTPException(status_code=409,
64+
detail="Workspace already exists")
65+
66+
if not created:
67+
raise HTTPException(status_code=500, detail="Failed to create workspace")
68+
69+
return v1_models.Workspace(name=request.name)
70+
71+
72+
73+
@v1.delete("/workspaces/{workspace_name}", tags=["Workspaces"],
74+
generate_unique_id_function=uniq_name, status_code=204)
75+
async def delete_workspace(workspace_name: str):
76+
"""Delete a workspace by name."""
77+
raise NotImplementedError

src/codegate/api/v1_models.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from typing import Any, List, Optional
2+
3+
import pydantic
4+
5+
from codegate.db import models as db_models
6+
7+
8+
class Workspace(pydantic.BaseModel):
9+
name: str
10+
is_active: bool
11+
12+
class ActiveWorkspace(Workspace):
13+
# TODO: use a more specific type for last_updated
14+
last_updated: Any
15+
16+
class ListWorkspacesResponse(pydantic.BaseModel):
17+
workspaces: list[Workspace]
18+
19+
@classmethod
20+
def from_db_workspaces(
21+
cls, db_workspaces: List[db_models.WorkspaceActive])-> "ListWorkspacesResponse":
22+
return cls(workspaces=[
23+
Workspace(name=ws.name, is_active=ws.active_workspace_id is not None)
24+
for ws in db_workspaces])
25+
26+
class ListActiveWorkspacesResponse(pydantic.BaseModel):
27+
workspaces: list[ActiveWorkspace]
28+
29+
@classmethod
30+
def from_db_workspaces(
31+
cls, ws: Optional[db_models.ActiveWorkspace]) -> "ListActiveWorkspacesResponse":
32+
if ws is None:
33+
return cls(workspaces=[])
34+
return cls(workspaces=[
35+
ActiveWorkspace(name=ws.name,
36+
is_active=True,
37+
last_updated=ws.last_update)
38+
])
39+
40+
class CreateWorkspaceRequest(pydantic.BaseModel):
41+
name: str
42+
43+
class ActivateWorkspaceRequest(pydantic.BaseModel):
44+
name: str

src/codegate/db/connection.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
alert_queue = asyncio.Queue()
3131
fim_cache = FimCache()
3232

33+
class WorkspaceAlreadyExistsError(Exception):
34+
pass
35+
3336

3437
class DbCodeGate:
3538
_instance = None
@@ -70,7 +73,7 @@ def __init__(self, sqlite_path: Optional[str] = None):
7073
super().__init__(sqlite_path)
7174

7275
async def _execute_update_pydantic_model(
73-
self, model: BaseModel, sql_command: TextClause
76+
self, model: BaseModel, sql_command: TextClause, should_raise: bool = False
7477
) -> Optional[BaseModel]:
7578
"""Execute an update or insert command for a Pydantic model."""
7679
async with self._async_db_engine.begin() as conn:
@@ -85,6 +88,8 @@ async def _execute_update_pydantic_model(
8588
return model_class(**row._asdict())
8689
except Exception as e:
8790
logger.error(f"Failed to update model: {model}.", error=str(e))
91+
if should_raise:
92+
raise e
8893
return None
8994

9095
async def record_request(self, prompt_params: Optional[Prompt] = None) -> Optional[Prompt]:
@@ -242,11 +247,14 @@ async def record_context(self, context: Optional[PipelineContext]) -> None:
242247
except Exception as e:
243248
logger.error(f"Failed to record context: {context}.", error=str(e))
244249

245-
async def add_workspace(self, workspace_name: str) -> Optional[Workspace]:
250+
async def add_workspace(
251+
self, workspace_name: str, should_raise: bool = False) -> Optional[Workspace]:
246252
try:
247253
workspace = Workspace(id=str(uuid.uuid4()), name=workspace_name)
248254
except ValidationError as e:
249255
logger.error(f"Failed to create workspace with name: {workspace_name}: {str(e)}")
256+
if should_raise:
257+
raise e
250258
return None
251259

252260
sql = text(
@@ -256,7 +264,17 @@ async def add_workspace(self, workspace_name: str) -> Optional[Workspace]:
256264
RETURNING *
257265
"""
258266
)
259-
added_workspace = await self._execute_update_pydantic_model(workspace, sql)
267+
try:
268+
added_workspace = await self._execute_update_pydantic_model(
269+
workspace, sql, should_raise=should_raise)
270+
except Exception as e:
271+
logger.error(f"Failed to add workspace: {workspace_name}.", error=str(e))
272+
if should_raise:
273+
# Overwrite the exception; we know that the workspace already exists
274+
raise WorkspaceAlreadyExistsError(
275+
f"Workspace with name {workspace_name} already exists.")
276+
return None
277+
260278
return added_workspace
261279

262280
async def update_session(self, session: Session) -> Optional[Session]:

src/codegate/pipeline/workspace/commands.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
import datetime
2-
from typing import Optional, Tuple
2+
from typing import List, Optional, Tuple
33

44
from codegate.db.connection import DbReader, DbRecorder
5-
from codegate.db.models import Session, Workspace
5+
from codegate.db.models import ActiveWorkspace, Session, Workspace, WorkspaceActive
66

77

88
class WorkspaceCrud:
99

1010
def __init__(self):
1111
self._db_reader = DbReader()
1212

13-
async def add_workspace(self, new_workspace_name: str) -> bool:
13+
async def add_workspace(self, new_workspace_name: str, should_raise: bool = True) -> bool:
1414
"""
1515
Add a workspace
1616
1717
Args:
1818
name (str): The name of the workspace
1919
"""
2020
db_recorder = DbRecorder()
21-
workspace_created = await db_recorder.add_workspace(new_workspace_name)
21+
workspace_created = await db_recorder.add_workspace(
22+
new_workspace_name, should_raise=should_raise)
2223
return bool(workspace_created)
2324

24-
async def get_workspaces(self):
25+
async def get_workspaces(self) -> List[WorkspaceActive]:
2526
"""
2627
Get all workspaces
2728
"""
2829
return await self._db_reader.get_workspaces()
2930

31+
async def get_active_workspace(self) -> Optional[ActiveWorkspace]:
32+
"""
33+
Get the active workspace
34+
"""
35+
return await self._db_reader.get_active_workspace()
36+
3037
async def _is_workspace_active_or_not_exist(
3138
self, workspace_name: str
3239
) -> Tuple[bool, Optional[Session], Optional[Workspace]]:

src/codegate/server.py

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from starlette.middleware.errors import ServerErrorMiddleware
88

99
from codegate import __description__, __version__
10+
from codegate.api.v1 import v1
1011
from codegate.dashboard.dashboard import dashboard_router
1112
from codegate.pipeline.factory import PipelineFactory
1213
from codegate.providers.anthropic.provider import AnthropicProvider
@@ -97,4 +98,7 @@ async def health_check():
9798
app.include_router(system_router)
9899
app.include_router(dashboard_router)
99100

101+
# CodeGate API
102+
app.include_router(v1, prefix="/api/v1", tags=["CodeGate API"])
103+
100104
return app

tests/pipeline/workspace/test_workspace.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import datetime
21
from unittest.mock import AsyncMock, patch
32

43
import pytest
54

6-
from codegate.db.models import Session, Workspace, WorkspaceActive
7-
from codegate.pipeline.workspace.commands import WorkspaceCommands, WorkspaceCrud
5+
from codegate.db.models import WorkspaceActive
6+
from codegate.pipeline.workspace.commands import WorkspaceCommands
87

98

109
@pytest.mark.asyncio

0 commit comments

Comments
 (0)