Source code for app.services.project_service

"""Project CRUD."""

from __future__ import annotations

from sqlalchemy import delete, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.errors import ConflictError, NotFoundError
from app.db.models import Project


[docs] async def create_project( session: AsyncSession, *, tenant_id: str, name: str, description: str | None = None ) -> Project: p = Project(tenant_id=tenant_id, name=name, description=description) session.add(p) try: await session.flush() except IntegrityError as e: await session.rollback() raise ConflictError(f"Project {name!r} already exists") from e return p
[docs] async def get_project(session: AsyncSession, *, tenant_id: str, project_id: str) -> Project: stmt = select(Project).where(Project.tenant_id == tenant_id, Project.project_id == project_id) result = await session.execute(stmt) p = result.scalar_one_or_none() if p is None: raise NotFoundError(f"Project {project_id} not found") return p
[docs] async def list_projects( session: AsyncSession, *, tenant_id: str, page_size: int = 50, page_token: str | None = None, ) -> tuple[list[Project], str | None]: stmt = select(Project).where(Project.tenant_id == tenant_id).order_by(Project.project_id) if page_token: stmt = stmt.where(Project.project_id > page_token) stmt = stmt.limit(page_size + 1) result = await session.execute(stmt) rows = list(result.scalars().all()) next_page_token = None if len(rows) > page_size: next_page_token = rows[page_size - 1].project_id rows = rows[:page_size] return rows, next_page_token
[docs] async def patch_project( session: AsyncSession, *, tenant_id: str, project_id: str, updates: dict, ) -> Project: if not updates: return await get_project(session, tenant_id=tenant_id, project_id=project_id) p = await get_project(session, tenant_id=tenant_id, project_id=project_id) allowed = {"name", "description"} for k, v in updates.items(): if k in allowed: setattr(p, k, v) try: await session.flush() except IntegrityError as e: await session.rollback() raise ConflictError("Project name conflict on update") from e return p
[docs] async def delete_project(session: AsyncSession, *, tenant_id: str, project_id: str) -> None: p = await get_project(session, tenant_id=tenant_id, project_id=project_id) await session.execute(delete(Project).where(Project.project_id == p.project_id))