From 0623a93b2081c19d5ab2695fae03a58d2f8534d5 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Mon, 20 Jan 2025 14:29:35 +0200 Subject: [PATCH 1/5] Implement basic soft deletion for workspaces this adds a `deleted_at` column to workspaces that implements a basic soft-deletion mechanism. All relevant queries have been modified to reflect this. At the moment, there is no hard deletion of workspaces; this will be implemented in the future. We also have no way of showing "archived" or "soft-deleted" workspaces. This will come in due time. Signed-off-by: Juan Antonio Osorio --- .../versions/8e4b4b8d1a88_add_soft_delete.py | 30 +++++++++++++++++++ src/codegate/api/v1.py | 10 +++++-- src/codegate/db/connection.py | 17 ++++++++++- src/codegate/pipeline/cli/commands.py | 20 +++++++++++++ src/codegate/workspaces/crud.py | 29 +++++++++++++++--- 5 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 migrations/versions/8e4b4b8d1a88_add_soft_delete.py diff --git a/migrations/versions/8e4b4b8d1a88_add_soft_delete.py b/migrations/versions/8e4b4b8d1a88_add_soft_delete.py new file mode 100644 index 00000000..ad8138a3 --- /dev/null +++ b/migrations/versions/8e4b4b8d1a88_add_soft_delete.py @@ -0,0 +1,30 @@ +"""add soft delete + +Revision ID: 8e4b4b8d1a88 +Revises: 5c2f3eee5f90 +Create Date: 2025-01-20 14:08:40.851647 + +""" + +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "8e4b4b8d1a88" +down_revision: Union[str, None] = "5c2f3eee5f90" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute( + """ + ALTER TABLE workspaces + ADD COLUMN deleted_at DATETIME DEFAULT NULL; + """ + ) + + +def downgrade() -> None: + op.execute("ALTER TABLE workspaces DROP COLUMN deleted_at;") diff --git a/src/codegate/api/v1.py b/src/codegate/api/v1.py index 73a8d195..a6065c0f 100644 --- a/src/codegate/api/v1.py +++ b/src/codegate/api/v1.py @@ -79,8 +79,14 @@ async def create_workspace(request: v1_models.CreateWorkspaceRequest) -> v1_mode "/workspaces/{workspace_name}", tags=["Workspaces"], generate_unique_id_function=uniq_name, - status_code=204, ) async def delete_workspace(workspace_name: str): """Delete a workspace by name.""" - raise NotImplementedError + try: + _ = await wscrud.soft_delete_workspace(workspace_name) + except crud.WorkspaceDoesNotExistError: + return HTTPException(status_code=404, detail="Workspace does not exist") + except Exception: + return HTTPException(status_code=500, detail="Internal server error") + + return Response(status_code=204) diff --git a/src/codegate/db/connection.py b/src/codegate/db/connection.py index cca4e691..6e014916 100644 --- a/src/codegate/db/connection.py +++ b/src/codegate/db/connection.py @@ -304,6 +304,20 @@ async def update_session(self, session: Session) -> Optional[Session]: active_session = await self._execute_update_pydantic_model(session, sql, should_raise=True) return active_session + async def soft_delete_workspace(self, workspace: Workspace) -> Optional[Workspace]: + sql = text( + """ + UPDATE workspaces + SET deleted_at = CURRENT_TIMESTAMP + WHERE id = :id + RETURNING * + """ + ) + deleted_workspace = await self._execute_update_pydantic_model( + workspace, sql, should_raise=True + ) + return deleted_workspace + class DbReader(DbCodeGate): @@ -401,6 +415,7 @@ async def get_workspaces(self) -> List[WorkspaceActive]: w.id, w.name, s.active_workspace_id FROM workspaces w LEFT JOIN sessions s ON w.id = s.active_workspace_id + WHERE w.deleted_at IS NULL """ ) workspaces = await self._execute_select_pydantic_model(WorkspaceActive, sql) @@ -412,7 +427,7 @@ async def get_workspace_by_name(self, name: str) -> Optional[Workspace]: SELECT id, name, system_prompt FROM workspaces - WHERE name = :name + WHERE name = :name AND deleted_at IS NULL """ ) conditions = {"name": name} diff --git a/src/codegate/pipeline/cli/commands.py b/src/codegate/pipeline/cli/commands.py index 8902f409..d62e6f8b 100644 --- a/src/codegate/pipeline/cli/commands.py +++ b/src/codegate/pipeline/cli/commands.py @@ -153,6 +153,7 @@ def subcommands(self) -> Dict[str, Callable[[List[str]], Awaitable[str]]]: "list": self._list_workspaces, "add": self._add_workspace, "activate": self._activate_workspace, + "remove": self._remove_workspace, } async def _list_workspaces(self, flags: Dict[str, str], args: List[str]) -> str: @@ -211,6 +212,25 @@ async def _activate_workspace(self, flags: Dict[str, str], args: List[str]) -> s return "An error occurred while activating the workspace" return f"Workspace **{workspace_name}** has been activated" + async def _remove_workspace(self, args: List[str]) -> str: + """ + Remove a workspace + """ + if args is None or len(args) == 0: + return "Please provide a name. Use `codegate workspace remove workspace_name`" + + workspace_name = args[0] + if not workspace_name: + return "Please provide a name. Use `codegate workspace remove workspace_name`" + + try: + await self.workspace_crud.soft_delete_workspace(workspace_name) + except crud.WorkspaceDoesNotExistError: + return f"Workspace **{workspace_name}** does not exist" + except Exception: + return "An error occurred while removing the workspace" + return f"Workspace **{workspace_name}** has been removed" + @property def help(self) -> str: return ( diff --git a/src/codegate/workspaces/crud.py b/src/codegate/workspaces/crud.py index 2b44466d..5ec9a09a 100644 --- a/src/codegate/workspaces/crud.py +++ b/src/codegate/workspaces/crud.py @@ -68,10 +68,6 @@ async def _is_workspace_active( async def activate_workspace(self, workspace_name: str): """ Activate a workspace - - Will return: - - True if the workspace was activated - - False if the workspace is already active or does not exist """ is_active, session, workspace = await self._is_workspace_active(workspace_name) if is_active: @@ -100,6 +96,31 @@ async def update_workspace_system_prompt( updated_workspace = await db_recorder.update_workspace(workspace_update) return updated_workspace + async def soft_delete_workspace(self, workspace_name: str): + """ + Soft delete a workspace + """ + if workspace_name == "": + raise WorkspaceCrudError("Workspace name cannot be empty.") + if workspace_name == "default": + raise WorkspaceCrudError("Cannot delete default workspace.") + + selected_workspace = await self._db_reader.get_workspace_by_name(workspace_name) + if not selected_workspace: + raise WorkspaceDoesNotExistError(f"Workspace {workspace_name} does not exist.") + + # Check if workspace is active, if it is, make the default workspace active + active_workspace = await self._db_reader.get_active_workspace() + if active_workspace and active_workspace.id == selected_workspace.id: + _ = await self.activate_workspace("default") + + db_recorder = DbRecorder() + try: + _ = await db_recorder.soft_delete_workspace(selected_workspace) + except Exception: + raise WorkspaceCrudError(f"Error deleting workspace {workspace_name}") + return + async def get_workspace_by_name(self, workspace_name: str) -> Workspace: workspace = await self._db_reader.get_workspace_by_name(workspace_name) if not workspace: From 6f4e2164d6251c2957de49baf81bd4b0dde0b24e Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Mon, 20 Jan 2025 16:09:09 +0200 Subject: [PATCH 2/5] Add merge migration Signed-off-by: Juan Antonio Osorio --- ..._merging_system_prompt_and_soft_deletes.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 migrations/versions/e6227073183d_merging_system_prompt_and_soft_deletes.py diff --git a/migrations/versions/e6227073183d_merging_system_prompt_and_soft_deletes.py b/migrations/versions/e6227073183d_merging_system_prompt_and_soft_deletes.py new file mode 100644 index 00000000..64dd0717 --- /dev/null +++ b/migrations/versions/e6227073183d_merging_system_prompt_and_soft_deletes.py @@ -0,0 +1,23 @@ +"""merging system prompt and soft-deletes + +Revision ID: e6227073183d +Revises: 8e4b4b8d1a88, a692c8b52308 +Create Date: 2025-01-20 16:08:40.645298 + +""" + +from typing import Sequence, Union + +# revision identifiers, used by Alembic. +revision: str = "e6227073183d" +down_revision: Union[str, None] = ("8e4b4b8d1a88", "a692c8b52308") +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass From e1613ead7764c29736042645486230e6c90dfd02 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Mon, 20 Jan 2025 16:32:37 +0200 Subject: [PATCH 3/5] Fix exceptions in delete handler Signed-off-by: Juan Antonio Osorio --- src/codegate/api/v1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codegate/api/v1.py b/src/codegate/api/v1.py index a6065c0f..363e2f47 100644 --- a/src/codegate/api/v1.py +++ b/src/codegate/api/v1.py @@ -85,8 +85,8 @@ async def delete_workspace(workspace_name: str): try: _ = await wscrud.soft_delete_workspace(workspace_name) except crud.WorkspaceDoesNotExistError: - return HTTPException(status_code=404, detail="Workspace does not exist") + raise HTTPException(status_code=404, detail="Workspace does not exist") except Exception: - return HTTPException(status_code=500, detail="Internal server error") + raise HTTPException(status_code=500, detail="Internal server error") return Response(status_code=204) From b068f41f24b1f668edcd5eb331fecacff2a7d5b6 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Mon, 20 Jan 2025 17:14:01 +0200 Subject: [PATCH 4/5] Don't delete active workspace Signed-off-by: Juan Antonio Osorio --- src/codegate/workspaces/crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegate/workspaces/crud.py b/src/codegate/workspaces/crud.py index 5ec9a09a..9fcc63de 100644 --- a/src/codegate/workspaces/crud.py +++ b/src/codegate/workspaces/crud.py @@ -112,7 +112,7 @@ async def soft_delete_workspace(self, workspace_name: str): # Check if workspace is active, if it is, make the default workspace active active_workspace = await self._db_reader.get_active_workspace() if active_workspace and active_workspace.id == selected_workspace.id: - _ = await self.activate_workspace("default") + raise WorkspaceCrudError("Cannot delete active workspace.") db_recorder = DbRecorder() try: From 7ff329b9544f6a9caf1b25f5a556237796927646 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Mon, 20 Jan 2025 17:19:06 +0200 Subject: [PATCH 5/5] Add missing flags Signed-off-by: Juan Antonio Osorio --- src/codegate/pipeline/cli/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegate/pipeline/cli/commands.py b/src/codegate/pipeline/cli/commands.py index d62e6f8b..233188ba 100644 --- a/src/codegate/pipeline/cli/commands.py +++ b/src/codegate/pipeline/cli/commands.py @@ -212,7 +212,7 @@ async def _activate_workspace(self, flags: Dict[str, str], args: List[str]) -> s return "An error occurred while activating the workspace" return f"Workspace **{workspace_name}** has been activated" - async def _remove_workspace(self, args: List[str]) -> str: + async def _remove_workspace(self, flags: Dict[str, str], args: List[str]) -> str: """ Remove a workspace """ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy