-
Notifications
You must be signed in to change notification settings - Fork 986
feat: add delete_project functionality and integrate projects_app in CLI #1511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
abhinav-1305
wants to merge
5
commits into
julep-ai:dev
Choose a base branch
from
abhinav-1305:fix-delete
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
72588f8
feat: add delete_project functionality and integrate projects_app in CLI
abhinav-1305 75a23b9
Update src/cli/src/julep_cli/projects.py
abhinav-1305 e96d01c
feat: wrap delete_project query in a transaction for atomicity
abhinav-1305 2c76db4
Merge branch 'fix-delete' of https://github.com/abhinav-1305/julep in…
abhinav-1305 0e36365
Merge branch 'dev' into fix-delete
creatorrr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
src/agents-api/agents_api/queries/projects/delete_project.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| """ | ||
| This module contains the functionality for deleting projects from the PostgreSQL database. | ||
| It constructs and executes SQL queries to remove project records and associated data. | ||
| """ | ||
|
|
||
| from uuid import UUID | ||
|
|
||
| from beartype import beartype | ||
|
|
||
| from ...autogen.openapi_model import ResourceDeletedResponse | ||
| from ...common.utils.datetime import utcnow | ||
| from ...common.utils.db_exceptions import common_db_exceptions | ||
| from ...metrics.counters import query_metrics | ||
| from ..utils import pg_query, rewrap_exceptions, wrap_in_class | ||
|
|
||
| # Delete project query that handles RESTRICT constraints by deleting associations first | ||
| delete_project_query = """ | ||
| -- First check if the project exists and is not the default project | ||
| DO $$ | ||
| BEGIN | ||
| IF NOT EXISTS ( | ||
| SELECT 1 FROM projects | ||
| WHERE developer_id = $1 AND project_id = $2 | ||
| ) THEN | ||
| RAISE EXCEPTION 'Project not found'; | ||
| END IF; | ||
|
|
||
| IF EXISTS ( | ||
| SELECT 1 FROM projects | ||
| WHERE developer_id = $1 AND project_id = $2 AND canonical_name = 'default' | ||
| ) THEN | ||
| RAISE EXCEPTION 'Cannot delete default project'; | ||
| END IF; | ||
| END $$; | ||
|
|
||
| -- Delete all project associations to handle RESTRICT constraints | ||
| DELETE FROM project_agents WHERE project_id = $2 AND developer_id = $1; | ||
| DELETE FROM project_users WHERE project_id = $2 AND developer_id = $1; | ||
| DELETE FROM project_files WHERE project_id = $2 AND developer_id = $1; | ||
|
|
||
| -- Then delete the project itself | ||
| DELETE FROM projects | ||
| WHERE developer_id = $1 | ||
| AND project_id = $2 | ||
| RETURNING project_id; | ||
| """ | ||
|
|
||
|
|
||
| @rewrap_exceptions(common_db_exceptions("project", ["delete"])) | ||
| @wrap_in_class( | ||
| ResourceDeletedResponse, | ||
| one=True, | ||
| transform=lambda d: { | ||
| "id": d["project_id"], | ||
| "deleted_at": utcnow(), | ||
| "jobs": [], | ||
| }, | ||
| ) | ||
| @query_metrics("delete_project") | ||
| @pg_query | ||
| @beartype | ||
| async def delete_project( | ||
| *, | ||
| developer_id: UUID, | ||
| project_id: UUID, | ||
| ) -> tuple[str, list]: | ||
| """ | ||
| Deletes a project and all its associations. | ||
|
|
||
| Args: | ||
| developer_id: The developer's UUID | ||
| project_id: The project's UUID | ||
|
|
||
| Returns: | ||
| tuple[str, list]: SQL query and parameters | ||
|
|
||
| Raises: | ||
| Exception: If project not found or is the default project | ||
| """ | ||
| return ( | ||
| delete_project_query, | ||
| [developer_id, project_id], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| # ruff: noqa: F401 | ||
|
|
||
| from .create_project import create_project | ||
| from .delete_project import delete_project | ||
| from .list_projects import list_projects | ||
| from .router import router |
30 changes: 30 additions & 0 deletions
30
src/agents-api/agents_api/routers/projects/delete_project.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| from typing import Annotated | ||
| from uuid import UUID | ||
|
|
||
| from fastapi import Depends | ||
| from starlette.status import HTTP_202_ACCEPTED | ||
|
|
||
| from ...autogen.openapi_model import ResourceDeletedResponse | ||
| from ...dependencies.developer_id import get_developer_id | ||
| from ...queries.projects.delete_project import delete_project as delete_project_query | ||
| from .router import router | ||
|
|
||
|
|
||
| @router.delete("/projects/{project_id}", status_code=HTTP_202_ACCEPTED, tags=["projects"]) | ||
| async def delete_project( | ||
| project_id: UUID, | ||
| x_developer_id: Annotated[UUID, Depends(get_developer_id)], | ||
| ) -> ResourceDeletedResponse: | ||
| """Delete a project. | ||
|
|
||
| Args: | ||
| project_id: ID of the project to delete | ||
| x_developer_id: Developer ID from header | ||
|
|
||
| Returns: | ||
| ResourceDeletedResponse: The deleted project information | ||
| """ | ||
| return await delete_project_query( | ||
| developer_id=x_developer_id, | ||
| project_id=project_id, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| """ | ||
| Test cases for project deletion functionality. | ||
| """ | ||
|
|
||
| import pytest | ||
| from uuid import uuid4 | ||
|
|
||
| from agents_api.queries.projects.delete_project import delete_project | ||
| from tests.fixtures import pg_dsn, test_developer, test_project | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_delete_project_success(dsn=pg_dsn, developer=test_developer, project=test_project): | ||
| """Test that a project can be successfully deleted.""" | ||
| # Create a new project to delete (not the default one) | ||
| from agents_api.queries.projects.create_project import create_project | ||
| from agents_api.autogen.openapi_model import CreateProjectRequest | ||
|
|
||
| create_data = CreateProjectRequest( | ||
| name="Test Project to Delete", | ||
| canonical_name="test-delete-project", | ||
| metadata={"test": True} | ||
| ) | ||
|
|
||
| created_project = await create_project( | ||
| developer_id=developer.developer_id, | ||
| data=create_data | ||
| ) | ||
|
|
||
| # Delete the project | ||
| result = await delete_project( | ||
| developer_id=developer.developer_id, | ||
| project_id=created_project.id | ||
| ) | ||
|
|
||
| # Verify the result | ||
| assert result.id == created_project.id | ||
| assert result.deleted_at is not None | ||
| assert result.jobs == [] | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_delete_project_not_found(dsn=pg_dsn, developer=test_developer): | ||
| """Test that deleting a non-existent project raises an appropriate error.""" | ||
| non_existent_project_id = uuid4() | ||
|
|
||
| with pytest.raises(Exception) as exc_info: | ||
| await delete_project( | ||
| developer_id=developer.developer_id, | ||
| project_id=non_existent_project_id | ||
| ) | ||
|
|
||
| # The exact exception type and message may vary based on the database layer | ||
| assert exc_info.value is not None | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_delete_project_wrong_developer(dsn=pg_dsn, developer=test_developer, project=test_project): | ||
| """Test that deleting a project with wrong developer ID raises an error.""" | ||
| wrong_developer_id = uuid4() | ||
|
|
||
| with pytest.raises(Exception) as exc_info: | ||
| await delete_project( | ||
| developer_id=wrong_developer_id, | ||
| project_id=project.id | ||
| ) | ||
|
|
||
| # The exact exception type and message may vary based on the database layer | ||
| assert exc_info.value is not None |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| """ | ||
| CLI commands for project management. | ||
| """ | ||
|
|
||
| from typing import Annotated | ||
| from uuid import UUID | ||
|
|
||
| import typer | ||
| from rich.progress import Progress, SpinnerColumn, TextColumn | ||
| from rich.text import Text | ||
|
|
||
| from .app import console, error_console, projects_app | ||
| from .utils import get_julep_client | ||
|
|
||
|
|
||
| @projects_app.command() | ||
| def delete( | ||
| project_id: Annotated[str, typer.Option("--id", help="ID of the project to delete")], | ||
| yes: Annotated[ | ||
| bool, | ||
| typer.Option("--yes", "-y", help="Skip confirmation prompt"), | ||
| ] = False, | ||
| ): | ||
| """Delete an existing project. | ||
|
|
||
| This command will delete a project and all its associations (agents, users, files). | ||
| The default project cannot be deleted. | ||
| """ | ||
| if not yes: | ||
| confirm = typer.confirm(f"Are you sure you want to delete project '{project_id}'?") | ||
| if not confirm: | ||
| console.print(Text("Project deletion cancelled.", style="bold yellow"), highlight=True) | ||
| raise typer.Exit() | ||
|
|
||
| try: | ||
| client = get_julep_client() | ||
| except Exception as e: | ||
| error_console.print(Text(f"Error initializing Julep client: {e}", style="bold red")) | ||
| raise typer.Exit(1) | ||
|
|
||
| with Progress( | ||
| SpinnerColumn(), | ||
| TextColumn("[progress.description]{task.description}"), | ||
| transient=True, | ||
| console=console, | ||
| ) as progress: | ||
| delete_project_task = progress.add_task("Deleting project...", start=False) | ||
| progress.start_task(delete_project_task) | ||
|
|
||
| try: | ||
| client.projects.delete(project_id) | ||
| progress.update(delete_project_task, completed=True) | ||
| except Exception as e: | ||
| progress.update(delete_project_task, completed=True) | ||
| error_console.print(Text(f"Failed to delete project: {e}", style="bold red")) | ||
| raise typer.Exit(1) | ||
|
|
||
| console.print(Text("Project deleted successfully.", style="bold green"), highlight=True) | ||
|
|
||
|
|
||
| @projects_app.command() | ||
| def list(): | ||
| """List all projects for the current developer.""" | ||
| try: | ||
| client = get_julep_client() | ||
| except Exception as e: | ||
| error_console.print(Text(f"Error initializing Julep client: {e}", style="bold red")) | ||
| raise typer.Exit(1) | ||
|
|
||
| with Progress( | ||
| SpinnerColumn(), | ||
| TextColumn("[progress.description]{task.description}"), | ||
| transient=True, | ||
| console=console, | ||
| ) as progress: | ||
| list_projects_task = progress.add_task("Fetching projects...", start=False) | ||
| progress.start_task(list_projects_task) | ||
|
|
||
| try: | ||
| projects = client.projects.list() | ||
| progress.update(list_projects_task, completed=True) | ||
| except Exception as e: | ||
| progress.update(list_projects_task, completed=True) | ||
| error_console.print(Text(f"Failed to fetch projects: {e}", style="bold red")) | ||
| raise typer.Exit(1) | ||
|
|
||
| if not projects: | ||
| console.print(Text("No projects found.", style="bold yellow"), highlight=True) | ||
| return | ||
|
|
||
| from rich.table import Table | ||
|
|
||
| table = Table(title="Projects", show_header=True, header_style="bold magenta") | ||
| table.add_column("ID", style="dim", width=36) | ||
| table.add_column("Name", style="bold") | ||
| table.add_column("Canonical Name", style="dim") | ||
| table.add_column("Created", style="dim") | ||
|
|
||
| for project in projects: | ||
| table.add_row( | ||
| str(project.id), | ||
| project.name, | ||
| project.canonical_name, | ||
| project.created_at.strftime("%Y-%m-%d %H:%M:%S") if project.created_at else "N/A" | ||
| ) | ||
|
|
||
| console.print(table, highlight=True) | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.