Skip to content

Commit 7e4b963

Browse files
authored
Initial CRUD API for workspaces (#620)
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 7e4b963

File tree

7 files changed

+137
-9
lines changed

7 files changed

+137
-9
lines changed

src/codegate/api/__init__.py

Whitespace-only changes.

src/codegate/api/v1.py

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

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

33-
3433
class DbCodeGate:
3534
_instance = None
3635

@@ -256,7 +255,13 @@ async def add_workspace(self, workspace_name: str) -> Optional[Workspace]:
256255
RETURNING *
257256
"""
258257
)
259-
added_workspace = await self._execute_update_pydantic_model(workspace, sql)
258+
try:
259+
added_workspace = await self._execute_update_pydantic_model(
260+
workspace, sql)
261+
except Exception as e:
262+
logger.error(f"Failed to add workspace: {workspace_name}.", error=str(e))
263+
return None
264+
260265
return added_workspace
261266

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

src/codegate/pipeline/workspace/commands.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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:
@@ -18,15 +18,22 @@ async def add_workspace(self, new_workspace_name: str) -> bool:
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)
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)