37ccaa4fdc
Service organization (19 files moved into 6 subpackages): - services/instance/ — event_bus, health_monitor, lifecycle_hooks - services/config/ — config_profile_resolver - services/git/ — clone, git_operations, git_service - services/build/ — docker_build, manifest_compiler - services/terminal/ — terminal_manager, terminal_session - services/shared/ — correlation, file_service, notification_service, permission_fixer, readiness_probe, ssh_keys, tunnel, workspace_manager API router organization (16 files moved into 6 subpackages): - api/tool/ — tool_instances, tool_types, tool_definitions, tool_types_validation, sessions (extracted from tool_instances) - api/config/ — config_profiles, user_config - api/workspace/ — workspaces, workspace_files, workspace_git, workspace_instances - api/user/ — users, auth, ssh_keys - api/project/ — projects, git_repositories - api/system/ — health, events, notifications, dashboard, terminal, instance_proxy Updated main.py imports and all __init__.py re-exports. Sessions router extracted from tool_instances.py into api/tool/sessions.py. Quality gates: py_compile passed, ruff passed.
276 lines
7.9 KiB
Python
276 lines
7.9 KiB
Python
import os
|
|
import shutil
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.auth.dependencies import (
|
|
_get_owned_project,
|
|
_get_user,
|
|
get_current_user_id,
|
|
get_db_session,
|
|
)
|
|
from src.models import GitRepository
|
|
from src.models.project import Project
|
|
from src.models import SSHKey
|
|
from src.models import ToolInstance
|
|
from src.schemas.project import (
|
|
ProjectCreate,
|
|
ProjectResponse,
|
|
ProjectUpdate,
|
|
SetDefaultSSHKeyRequest,
|
|
)
|
|
|
|
router = APIRouter(prefix="/projects", tags=["projects"])
|
|
|
|
|
|
@router.post(
|
|
"",
|
|
response_model=ProjectResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create a new project",
|
|
description="Create a new project for the authenticated user.",
|
|
)
|
|
async def create_project(
|
|
data: ProjectCreate,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> Project:
|
|
"""Create a new project.
|
|
|
|
Args:
|
|
data: Project creation data including name and optional description.
|
|
user_id: ID of the authenticated user.
|
|
session: Database session.
|
|
|
|
Returns:
|
|
The newly created project.
|
|
"""
|
|
user = await _get_user(session, user_id)
|
|
project = Project(
|
|
name=data.name,
|
|
description=data.description,
|
|
owner_id=user.id,
|
|
default_ssh_key_id=None,
|
|
)
|
|
session.add(project)
|
|
await session.commit()
|
|
await session.refresh(project)
|
|
return project
|
|
|
|
|
|
@router.get(
|
|
"",
|
|
summary="List all projects",
|
|
description="Retrieve all projects owned by the authenticated user with repositories and workspaces.",
|
|
)
|
|
async def list_projects(
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> list[dict]:
|
|
"""List all projects for the authenticated user.
|
|
|
|
Returns projects with nested repositories and workspaces for inline display.
|
|
"""
|
|
user = await _get_user(session, user_id)
|
|
result = await session.execute(
|
|
select(Project)
|
|
.where(Project.owner_id == user.id)
|
|
.order_by(Project.created_at.desc())
|
|
)
|
|
projects = result.scalars().all()
|
|
|
|
from src.models import Workspace
|
|
|
|
enriched = []
|
|
for project in projects:
|
|
repos_result = await session.execute(
|
|
select(GitRepository).where(GitRepository.project_id == project.id)
|
|
)
|
|
repositories = []
|
|
for repo in repos_result.scalars().all():
|
|
ws_result = await session.execute(
|
|
select(Workspace).where(Workspace.repo_id == repo.id)
|
|
)
|
|
workspaces = []
|
|
for ws in ws_result.scalars().all():
|
|
# Count instances
|
|
inst_result = await session.execute(
|
|
select(func.count()).where(ToolInstance.workspace_id == ws.id)
|
|
)
|
|
instance_count = inst_result.scalar() or 0
|
|
workspaces.append(
|
|
{
|
|
"id": str(ws.id),
|
|
"name": ws.name,
|
|
"branch": ws.branch,
|
|
"status": ws.status,
|
|
"instance_count": instance_count,
|
|
}
|
|
)
|
|
|
|
repositories.append(
|
|
{
|
|
"id": str(repo.id),
|
|
"name": repo.name,
|
|
"remote_url": repo.remote_url,
|
|
"workspaces": workspaces,
|
|
}
|
|
)
|
|
|
|
enriched.append(
|
|
{
|
|
"id": str(project.id),
|
|
"name": project.name,
|
|
"description": project.description,
|
|
"owner_id": str(project.owner_id),
|
|
"repositories": repositories,
|
|
"created_at": project.created_at.isoformat()
|
|
if project.created_at
|
|
else None,
|
|
}
|
|
)
|
|
|
|
return enriched
|
|
|
|
|
|
@router.get(
|
|
"/{project_id}",
|
|
response_model=ProjectResponse,
|
|
summary="Get a project",
|
|
description="Retrieve a specific project by ID.",
|
|
)
|
|
async def get_project(
|
|
project_id: uuid.UUID,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> Project:
|
|
"""Get a specific project by ID.
|
|
|
|
Args:
|
|
project_id: UUID of the project to retrieve.
|
|
user_id: ID of the authenticated user.
|
|
session: Database session.
|
|
|
|
Returns:
|
|
The requested project.
|
|
"""
|
|
await _get_user(session, user_id)
|
|
return await _get_owned_project(project_id, user_id, session)
|
|
|
|
|
|
@router.patch(
|
|
"/{project_id}",
|
|
response_model=ProjectResponse,
|
|
summary="Update a project",
|
|
description="Update a project's name or description.",
|
|
)
|
|
async def update_project(
|
|
project_id: uuid.UUID,
|
|
data: ProjectUpdate,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> Project:
|
|
"""Update a project.
|
|
|
|
Args:
|
|
project_id: UUID of the project to update.
|
|
data: Project update data with optional name and description.
|
|
user_id: ID of the authenticated user.
|
|
session: Database session.
|
|
|
|
Returns:
|
|
The updated project.
|
|
"""
|
|
await _get_user(session, user_id)
|
|
project = await _get_owned_project(project_id, user_id, session)
|
|
|
|
if data.name is not None:
|
|
project.name = data.name
|
|
if data.description is not None:
|
|
project.description = data.description
|
|
|
|
await session.commit()
|
|
await session.refresh(project)
|
|
return project
|
|
|
|
|
|
@router.delete(
|
|
"/{project_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Delete a project",
|
|
description="Delete a project and all its associated repositories.",
|
|
)
|
|
async def delete_project(
|
|
project_id: uuid.UUID,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> Response:
|
|
"""Delete a project and all its repositories.
|
|
|
|
Args:
|
|
project_id: UUID of the project to delete.
|
|
user_id: ID of the authenticated user.
|
|
session: Database session.
|
|
|
|
Returns:
|
|
Empty response with 204 status code.
|
|
"""
|
|
await _get_user(session, user_id)
|
|
project = await _get_owned_project(project_id, user_id, session)
|
|
|
|
# Delete repositories from disk and database
|
|
result = await session.execute(
|
|
select(GitRepository).where(GitRepository.project_id == project_id)
|
|
)
|
|
repositories = result.scalars().all()
|
|
for repo in repositories:
|
|
if os.path.exists(repo.path):
|
|
shutil.rmtree(repo.path)
|
|
await session.delete(repo)
|
|
|
|
await session.delete(project)
|
|
await session.commit()
|
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@router.patch(
|
|
"/{project_id}/default-ssh-key",
|
|
response_model=ProjectResponse,
|
|
summary="Set default SSH key",
|
|
description="Set the default SSH key for a project.",
|
|
)
|
|
async def set_default_ssh_key(
|
|
project_id: uuid.UUID,
|
|
data: SetDefaultSSHKeyRequest,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> Project:
|
|
"""Set the default SSH key for a project.
|
|
|
|
Args:
|
|
project_id: UUID of the project.
|
|
data: Request containing the SSH key ID to set as default.
|
|
user_id: ID of the authenticated user.
|
|
session: Database session.
|
|
|
|
Returns:
|
|
The updated project.
|
|
"""
|
|
user = await _get_user(session, user_id)
|
|
project = await _get_owned_project(project_id, user_id, session)
|
|
|
|
ssh_key = await session.get(SSHKey, data.ssh_key_id)
|
|
if ssh_key is None or ssh_key.user_id != user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="invalid ssh key",
|
|
)
|
|
|
|
project.default_ssh_key_id = data.ssh_key_id
|
|
await session.commit()
|
|
await session.refresh(project)
|
|
return project
|