init: new structure + fix lint errors
This commit is contained in:
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user