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
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, Float, ForeignKey, JSON, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from cpv3.db.base import Base, BaseModelMixin
class Job(Base, BaseModelMixin):
__tablename__ = "jobs"
broker_id: Mapped[str] = mapped_column(String(255))
user_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="RESTRICT"), nullable=True, index=True
)
project_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("projects.id", ondelete="RESTRICT"),
nullable=True,
index=True,
)
input_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
output_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
status: Mapped[str] = mapped_column(String(16), default="PENDING")
job_type: Mapped[str] = mapped_column(String(32), default="PENDING")
project_pct: Mapped[float | None] = mapped_column(Float, nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
current_message: Mapped[str | None] = mapped_column(Text, nullable=True)
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class JobEvent(Base, BaseModelMixin):
__tablename__ = "job_events"
job_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("jobs.id", ondelete="CASCADE"), index=True
)
event_type: Mapped[str] = mapped_column(String(64))
payload: Mapped[dict | None] = mapped_column(JSON, nullable=True)
+109
View File
@@ -0,0 +1,109 @@
from __future__ import annotations
import uuid
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.jobs.models import Job, JobEvent
from cpv3.modules.jobs.schemas import (
JobCreate,
JobEventCreate,
JobEventUpdate,
JobUpdate,
)
from cpv3.modules.users.models import User
class JobRepository:
"""Repository for Job database operations."""
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def list_all(self, *, requester: User) -> list[Job]:
stmt: Select[tuple[Job]] = select(Job).where(Job.is_active.is_(True))
if not requester.is_staff:
stmt = stmt.where(Job.user_id == requester.id)
result = await self._session.execute(stmt.order_by(Job.created_at.desc()))
return list(result.scalars().all())
async def get_by_id(self, job_id: uuid.UUID) -> Job | None:
result = await self._session.execute(
select(Job).where(Job.id == job_id).where(Job.is_active.is_(True))
)
return result.scalar_one_or_none()
async def create(self, *, requester: User, data: JobCreate) -> Job:
job = Job(
user_id=requester.id,
broker_id=data.broker_id,
project_id=data.project_id,
input_data=data.input_data,
status=data.status,
job_type=data.job_type,
)
self._session.add(job)
await self._session.commit()
await self._session.refresh(job)
return job
async def update(self, job: Job, data: JobUpdate) -> Job:
for key, value in data.model_dump(exclude_unset=True).items():
if value is not None:
setattr(job, key, value)
await self._session.commit()
await self._session.refresh(job)
return job
async def deactivate(self, job: Job) -> None:
job.is_active = False
await self._session.commit()
class JobEventRepository:
"""Repository for JobEvent database operations."""
def __init__(self, session: AsyncSession) -> None:
self._session = session
async def list_all(self) -> list[JobEvent]:
result = await self._session.execute(
select(JobEvent)
.where(JobEvent.is_active.is_(True))
.order_by(JobEvent.created_at.desc())
)
return list(result.scalars().all())
async def get_by_id(self, event_id: uuid.UUID) -> JobEvent | None:
result = await self._session.execute(
select(JobEvent)
.where(JobEvent.id == event_id)
.where(JobEvent.is_active.is_(True))
)
return result.scalar_one_or_none()
async def create(self, data: JobEventCreate) -> JobEvent:
event = JobEvent(
job_id=data.job_id, event_type=data.event_type, payload=data.payload
)
self._session.add(event)
await self._session.commit()
await self._session.refresh(event)
return event
async def update(self, event: JobEvent, data: JobEventUpdate) -> JobEvent:
for key, value in data.model_dump(exclude_unset=True).items():
if value is not None:
setattr(event, key, value)
await self._session.commit()
await self._session.refresh(event)
return event
async def deactivate(self, event: JobEvent) -> None:
event.is_active = False
await self._session.commit()
+168
View File
@@ -0,0 +1,168 @@
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.jobs.schemas import (
JobCreate,
JobEventCreate,
JobEventRead,
JobEventUpdate,
JobRead,
JobUpdate,
)
from cpv3.modules.jobs.service import JobService
from cpv3.modules.users.models import User
jobs_router = APIRouter(prefix="/api/jobs", tags=["jobs"])
events_router = APIRouter(prefix="/api/jobs", tags=["events"])
@jobs_router.get("/jobs/", response_model=list[JobRead])
async def list_jobs_endpoint(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[JobRead]:
service = JobService(db)
jobs = await service.list_jobs(requester=current_user)
return [JobRead.model_validate(j) for j in jobs]
@jobs_router.post("/jobs/", response_model=JobRead, status_code=status.HTTP_201_CREATED)
async def create_job_endpoint(
body: JobCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> JobRead:
service = JobService(db)
job = await service.create_job(requester=current_user, data=body)
return JobRead.model_validate(job)
@jobs_router.get("/jobs/{job_id}/", response_model=JobRead)
async def retrieve_job_endpoint(
job_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> JobRead:
service = JobService(db)
job = await service.get_job(job_id)
if job is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and job.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
return JobRead.model_validate(job)
@jobs_router.patch("/jobs/{job_id}/", response_model=JobRead)
async def patch_job_endpoint(
job_id: uuid.UUID,
body: JobUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> JobRead:
service = JobService(db)
job = await service.get_job(job_id)
if job is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and job.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
job = await service.update_job(job, body)
return JobRead.model_validate(job)
@jobs_router.delete("/jobs/{job_id}/", status_code=status.HTTP_204_NO_CONTENT)
async def delete_job_endpoint(
job_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Response:
service = JobService(db)
job = await service.get_job(job_id)
if job is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
if not current_user.is_staff and job.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
await service.deactivate_job(job)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@events_router.get("/events/", response_model=list[JobEventRead])
async def list_events_endpoint(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> list[JobEventRead]:
_ = current_user
service = JobService(db)
events = await service.list_job_events()
return [JobEventRead.model_validate(e) for e in events]
@events_router.post("/events/", response_model=JobEventRead, status_code=status.HTTP_201_CREATED)
async def create_event_endpoint(
body: JobEventCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> JobEventRead:
_ = current_user
service = JobService(db)
event = await service.create_job_event(body)
return JobEventRead.model_validate(event)
@events_router.get("/events/{event_id}/", response_model=JobEventRead)
async def retrieve_event_endpoint(
event_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> JobEventRead:
_ = current_user
service = JobService(db)
event = await service.get_job_event(event_id)
if event is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
return JobEventRead.model_validate(event)
@events_router.patch("/events/{event_id}/", response_model=JobEventRead)
async def patch_event_endpoint(
event_id: uuid.UUID,
body: JobEventUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> JobEventRead:
_ = current_user
service = JobService(db)
event = await service.get_job_event(event_id)
if event is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
event = await service.update_job_event(event, body)
return JobEventRead.model_validate(event)
@events_router.delete("/events/{event_id}/", status_code=status.HTTP_204_NO_CONTENT)
async def delete_event_endpoint(
event_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Response:
_ = current_user
service = JobService(db)
event = await service.get_job_event(event_id)
if event is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
await service.deactivate_job_event(event)
return Response(status_code=status.HTTP_204_NO_CONTENT)
+74
View File
@@ -0,0 +1,74 @@
from __future__ import annotations
from datetime import datetime
from typing import Literal
from uuid import UUID
from cpv3.common.schemas import Schema
JobStatusEnum = Literal["PENDING", "RUNNING", "FAILED", "CANCELLED", "DONE"]
JobTypeEnum = Literal["PENDING", "RUNNING", "FAILED", "CANCELLED", "DONE"]
class JobRead(Schema):
id: UUID
broker_id: str
user_id: UUID | None
project_id: UUID | None
input_data: dict | None
output_data: dict | None
status: JobStatusEnum
job_type: JobTypeEnum
project_pct: float | None
error_message: str | None
current_message: str | None
started_at: datetime | None
finished_at: datetime | None
is_active: bool
created_at: datetime
updated_at: datetime
class JobCreate(Schema):
broker_id: str
project_id: UUID | None = None
input_data: dict | None = None
status: JobStatusEnum = "PENDING"
job_type: JobTypeEnum = "PENDING"
class JobUpdate(Schema):
output_data: dict | None = None
status: JobStatusEnum | None = None
project_pct: float | None = None
error_message: str | None = None
current_message: str | None = None
started_at: datetime | None = None
finished_at: datetime | None = None
class JobEventRead(Schema):
id: UUID
job_id: UUID
event_type: str
payload: dict | None
is_active: bool
created_at: datetime
updated_at: datetime
class JobEventCreate(Schema):
job_id: UUID
event_type: str
payload: dict | None = None
class JobEventUpdate(Schema):
payload: dict | None = None
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
from cpv3.modules.jobs.models import Job, JobEvent
from cpv3.modules.jobs.repository import JobEventRepository, JobRepository
from cpv3.modules.jobs.schemas import (
JobCreate,
JobEventCreate,
JobEventUpdate,
JobUpdate,
)
from cpv3.modules.users.models import User
class JobService:
"""Service for job business logic and orchestration."""
def __init__(self, session: AsyncSession) -> None:
self._job_repo = JobRepository(session)
self._event_repo = JobEventRepository(session)
async def list_jobs(self, *, requester: User) -> list[Job]:
return await self._job_repo.list_all(requester=requester)
async def get_job(self, job_id: uuid.UUID) -> Job | None:
return await self._job_repo.get_by_id(job_id)
async def create_job(self, *, requester: User, data: JobCreate) -> Job:
return await self._job_repo.create(requester=requester, data=data)
async def update_job(self, job: Job, data: JobUpdate) -> Job:
return await self._job_repo.update(job, data)
async def deactivate_job(self, job: Job) -> None:
await self._job_repo.deactivate(job)
async def list_job_events(self) -> list[JobEvent]:
return await self._event_repo.list_all()
async def get_job_event(self, event_id: uuid.UUID) -> JobEvent | None:
return await self._event_repo.get_by_id(event_id)
async def create_job_event(self, data: JobEventCreate) -> JobEvent:
return await self._event_repo.create(data)
async def update_job_event(self, event: JobEvent, data: JobEventUpdate) -> JobEvent:
return await self._event_repo.update(event, data)
async def deactivate_job_event(self, event: JobEvent) -> None:
await self._event_repo.deactivate(event)