init: new structure + fix lint errors

This commit is contained in:
Daniil
2026-02-03 02:15:07 +03:00
commit 67e0f22b4f
89 changed files with 7654 additions and 0 deletions
View File
+25
View File
@@ -0,0 +1,25 @@
from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from cpv3.db.base import Base, BaseModelMixin
class Project(Base, BaseModelMixin):
__tablename__ = "projects"
owner_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("users.id", ondelete="RESTRICT"),
index=True,
)
name: Mapped[str] = mapped_column(String(255))
description: Mapped[str | None] = mapped_column(Text, nullable=True)
language: Mapped[str] = mapped_column(String(4), default="auto")
folder: Mapped[str | None] = mapped_column(String(1024), nullable=True)
status: Mapped[str] = mapped_column(String(16), default="DRAFT")
+63
View File
@@ -0,0 +1,63 @@
from __future__ import annotations
import uuid
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.projects.models import Project
from cpv3.modules.projects.schemas import ProjectCreate, ProjectUpdate
from cpv3.modules.users.models import User
class ProjectRepository:
"""Repository for Project database operations."""
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def list_all(self, *, requester: User) -> list[Project]:
stmt: Select[tuple[Project]] = select(Project).where(
Project.is_active.is_(True)
)
if not requester.is_staff:
stmt = stmt.where(Project.owner_id == requester.id)
result = await self._session.execute(stmt.order_by(Project.created_at.desc()))
return list(result.scalars().all())
async def get_by_id(self, project_id: uuid.UUID) -> Project | None:
result = await self._session.execute(
select(Project)
.where(Project.id == project_id)
.where(Project.is_active.is_(True))
)
return result.scalar_one_or_none()
async def create(self, *, requester: User, data: ProjectCreate) -> Project:
project = Project(
owner_id=requester.id,
name=data.name,
description=data.description,
language=data.language,
folder=data.folder,
status=data.status,
)
self._session.add(project)
await self._session.commit()
await self._session.refresh(project)
return project
async def update(self, project: Project, data: ProjectUpdate) -> Project:
for key, value in data.model_dump(exclude_unset=True).items():
if value is not None:
setattr(project, key, value)
await self._session.commit()
await self._session.refresh(project)
return project
async def deactivate(self, project: Project) -> None:
project.is_active = False
await self._session.commit()
+89
View File
@@ -0,0 +1,89 @@
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.infrastructure.auth import get_current_user
from cpv3.db.session import get_db
from cpv3.modules.projects.schemas import ProjectCreate, ProjectRead, ProjectUpdate
from cpv3.modules.projects.service import ProjectService
from cpv3.modules.users.models import User
router = APIRouter(prefix="/api/projects", tags=["Projects"])
@router.get("/", response_model=list[ProjectRead])
async def list_all_projects(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[ProjectRead]:
service = ProjectService(db)
projects = await service.list_projects(requester=current_user)
return [ProjectRead.model_validate(p) for p in projects]
@router.post("/", response_model=ProjectRead, status_code=status.HTTP_201_CREATED)
async def create_project_endpoint(
body: ProjectCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> ProjectRead:
service = ProjectService(db)
project = await service.create_project(requester=current_user, data=body)
return ProjectRead.model_validate(project)
@router.get("/{project_id}/", response_model=ProjectRead)
async def retrieve_project(
project_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> ProjectRead:
service = ProjectService(db)
project = await service.get_project(project_id)
if project is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and project.owner_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
return ProjectRead.model_validate(project)
@router.patch("/{project_id}/", response_model=ProjectRead)
async def patch_project(
project_id: uuid.UUID,
body: ProjectUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> ProjectRead:
service = ProjectService(db)
project = await service.get_project(project_id)
if project is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and project.owner_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
project = await service.update_project(project, body)
return ProjectRead.model_validate(project)
@router.delete("/{project_id}/", status_code=status.HTTP_204_NO_CONTENT)
async def delete_project(
project_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Response:
service = ProjectService(db)
project = await service.get_project(project_id)
if project is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and project.owner_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
await service.deactivate_project(project)
return Response(status_code=status.HTTP_204_NO_CONTENT)
+40
View File
@@ -0,0 +1,40 @@
from __future__ import annotations
from datetime import datetime
from typing import Literal
from uuid import UUID
from cpv3.common.schemas import Schema
ProjectStatusEnum = Literal["DRAFT", "PROCESSING", "DONE", "FAILED"]
class ProjectRead(Schema):
id: UUID
owner_id: UUID
name: str
description: str | None
language: str
folder: str | None
status: ProjectStatusEnum
is_active: bool
created_at: datetime
updated_at: datetime
class ProjectCreate(Schema):
name: str
description: str | None = None
language: str = "auto"
folder: str | None = None
status: ProjectStatusEnum = "DRAFT"
class ProjectUpdate(Schema):
name: str | None = None
description: str | None = None
language: str | None = None
folder: str | None = None
status: ProjectStatusEnum | None = None
+58
View File
@@ -0,0 +1,58 @@
from __future__ import annotations
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.projects.models import Project
from cpv3.modules.projects.repository import ProjectRepository
from cpv3.modules.projects.schemas import ProjectCreate, ProjectUpdate
from cpv3.modules.users.models import User
class ProjectService:
"""Service for project business logic and orchestration."""
def __init__(self, session: AsyncSession) -> None:
self._repo = ProjectRepository(session)
async def list_projects(self, *, requester: User) -> list[Project]:
return await self._repo.list_all(requester=requester)
async def get_project(self, project_id: uuid.UUID) -> Project | None:
return await self._repo.get_by_id(project_id)
async def create_project(self, *, requester: User, data: ProjectCreate) -> Project:
return await self._repo.create(requester=requester, data=data)
async def update_project(self, project: Project, data: ProjectUpdate) -> Project:
return await self._repo.update(project, data)
async def deactivate_project(self, project: Project) -> None:
await self._repo.deactivate(project)
# Legacy function exports for backward compatibility
async def list_projects(session: AsyncSession, *, requester: User) -> list[Project]:
service = ProjectService(session)
return await service.list_projects(requester=requester)
async def get_project(session: AsyncSession, project_id: uuid.UUID) -> Project | None:
service = ProjectService(session)
return await service.get_project(project_id)
async def create_project(session: AsyncSession, *, requester: User, data: ProjectCreate) -> Project:
service = ProjectService(session)
return await service.create_project(requester=requester, data=data)
async def update_project(session: AsyncSession, project: Project, data: ProjectUpdate) -> Project:
service = ProjectService(session)
return await service.update_project(project, data)
async def deactivate_project(session: AsyncSession, project: Project) -> None:
service = ProjectService(session)
await service.deactivate_project(project)