183 lines
5.3 KiB
Python
183 lines
5.3 KiB
Python
"""
|
|
Service layer for job processing business logic.
|
|
|
|
This module provides high-level operations for managing processing jobs
|
|
with strict status transition validation and business rules enforcement.
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from app.database.repositories.job_repository import JobRepository
|
|
from app.exceptions.job_exceptions import InvalidStatusTransitionError
|
|
from app.models.job import ProcessingJob, ProcessingStatus
|
|
from app.models.types import PyObjectId
|
|
|
|
|
|
class JobService:
|
|
"""
|
|
Service for processing job business logic operations.
|
|
|
|
Provides high-level job management with strict status transition
|
|
validation and business rule enforcement.
|
|
"""
|
|
|
|
def __init__(self, database):
|
|
"""
|
|
Initialize service with job repository.
|
|
|
|
Args:
|
|
repository: Optional JobRepository instance (creates default if None)
|
|
"""
|
|
self.db = database
|
|
self.repository = JobRepository(database)
|
|
|
|
async def initialize(self):
|
|
await self.repository.initialize()
|
|
return self
|
|
|
|
async def create_job(self, document_id: PyObjectId, task_id: Optional[str] = None) -> ProcessingJob:
|
|
"""
|
|
Create a new processing job.
|
|
|
|
Args:
|
|
document_id: Reference to the file document
|
|
task_id: Optional Celery task UUID
|
|
|
|
Returns:
|
|
The created ProcessingJob
|
|
|
|
Raises:
|
|
JobRepositoryError: If database operation fails
|
|
"""
|
|
return await self.repository.create_job(document_id, task_id)
|
|
|
|
async def get_job_by_id(self, job_id: PyObjectId) -> ProcessingJob:
|
|
"""
|
|
Retrieve a job by its ID.
|
|
|
|
Args:
|
|
job_id: The job ObjectId
|
|
|
|
Returns:
|
|
The ProcessingJob document
|
|
|
|
Raises:
|
|
JobNotFoundError: If job doesn't exist
|
|
JobRepositoryError: If database operation fails
|
|
"""
|
|
return await self.repository.find_job_by_id(job_id)
|
|
|
|
async def mark_job_as_started(self, job_id: PyObjectId) -> ProcessingJob:
|
|
"""
|
|
Mark a job as started (PENDING → PROCESSING).
|
|
|
|
Args:
|
|
job_id: The job ObjectId
|
|
|
|
Returns:
|
|
The updated ProcessingJob
|
|
|
|
Raises:
|
|
JobNotFoundError: If job doesn't exist
|
|
InvalidStatusTransitionError: If job is not in PENDING status
|
|
JobRepositoryError: If database operation fails
|
|
"""
|
|
# Get current job to validate transition
|
|
current_job = await self.repository.find_job_by_id(job_id)
|
|
|
|
# Validate status transition
|
|
if current_job.status != ProcessingStatus.PENDING:
|
|
raise InvalidStatusTransitionError(current_job.status, ProcessingStatus.PROCESSING)
|
|
|
|
# Update status
|
|
return await self.repository.update_job_status(job_id, ProcessingStatus.PROCESSING)
|
|
|
|
async def mark_job_as_completed(self, job_id: PyObjectId) -> ProcessingJob:
|
|
"""
|
|
Mark a job as completed (PROCESSING → COMPLETED).
|
|
|
|
Args:
|
|
job_id: The job ObjectId
|
|
|
|
Returns:
|
|
The updated ProcessingJob
|
|
|
|
Raises:
|
|
JobNotFoundError: If job doesn't exist
|
|
InvalidStatusTransitionError: If job is not in PROCESSING status
|
|
JobRepositoryError: If database operation fails
|
|
"""
|
|
# Get current job to validate transition
|
|
current_job = await self.repository.find_job_by_id(job_id)
|
|
|
|
# Validate status transition
|
|
if current_job.status != ProcessingStatus.PROCESSING:
|
|
raise InvalidStatusTransitionError(current_job.status, ProcessingStatus.COMPLETED)
|
|
|
|
# Update status
|
|
return await self.repository.update_job_status(job_id, ProcessingStatus.COMPLETED)
|
|
|
|
async def mark_job_as_failed(
|
|
self,
|
|
job_id: PyObjectId,
|
|
error_message: Optional[str] = None
|
|
) -> ProcessingJob:
|
|
"""
|
|
Mark a job as failed (PROCESSING → FAILED).
|
|
|
|
Args:
|
|
job_id: The job ObjectId
|
|
error_message: Optional error description
|
|
|
|
Returns:
|
|
The updated ProcessingJob
|
|
|
|
Raises:
|
|
JobNotFoundError: If job doesn't exist
|
|
InvalidStatusTransitionError: If job is not in PROCESSING status
|
|
JobRepositoryError: If database operation fails
|
|
"""
|
|
# Get current job to validate transition
|
|
current_job = await self.repository.find_job_by_id(job_id)
|
|
|
|
# Validate status transition
|
|
if current_job.status != ProcessingStatus.PROCESSING:
|
|
raise InvalidStatusTransitionError(current_job.status, ProcessingStatus.FAILED)
|
|
|
|
# Update status with error message
|
|
return await self.repository.update_job_status(
|
|
job_id,
|
|
ProcessingStatus.FAILED,
|
|
error_message
|
|
)
|
|
|
|
async def delete_job(self, job_id: PyObjectId) -> bool:
|
|
"""
|
|
Delete a job from the database.
|
|
|
|
Args:
|
|
job_id: The job ObjectId
|
|
|
|
Returns:
|
|
True if job was deleted, False if not found
|
|
|
|
Raises:
|
|
JobRepositoryError: If database operation fails
|
|
"""
|
|
return await self.repository.delete_job(job_id)
|
|
|
|
async def get_jobs_by_status(self, status: ProcessingStatus) -> list[ProcessingJob]:
|
|
"""
|
|
Retrieve all jobs with a specific status.
|
|
|
|
Args:
|
|
status: The processing status to filter by
|
|
|
|
Returns:
|
|
List of ProcessingJob documents
|
|
|
|
Raises:
|
|
JobRepositoryError: If database operation fails
|
|
"""
|
|
return await self.repository.get_jobs_by_status(status)
|