feat(phase7): add asset classification workflow
This commit is contained in:
@@ -2,6 +2,7 @@ from fastapi import APIRouter
|
||||
|
||||
from app.api.routes.asset_libraries import router as asset_libraries_router
|
||||
from app.api.routes.assets import router as assets_router
|
||||
from app.api.routes.classification_jobs import router as classification_jobs_router
|
||||
from app.api.routes.health import router as health_router
|
||||
from app.api.routes.ingest_jobs import router as ingest_jobs_router
|
||||
from app.api.routes.project_management import router as project_management_router
|
||||
@@ -45,6 +46,13 @@ api_router.include_router(
|
||||
tags=["导入任务"],
|
||||
)
|
||||
|
||||
# Classification Jobs
|
||||
api_router.include_router(
|
||||
classification_jobs_router,
|
||||
prefix="/classification-jobs",
|
||||
tags=["分类任务"],
|
||||
)
|
||||
|
||||
# Upload
|
||||
api_router.include_router(
|
||||
upload_router,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.core.celery_app import celery_app
|
||||
from app.dependencies import get_classification_job_repository
|
||||
from app.schemas.classification_job import ClassificationJobResponse, SubmitClassificationJobRequest
|
||||
from packages.adapters.sqlalchemy_impl import SQLAlchemyClassificationJobRepository
|
||||
from packages.application import SubmitClassificationJobCommand, SubmitClassificationJobUseCase
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/{job_id}", response_model=ClassificationJobResponse)
|
||||
def get_classification_job(
|
||||
job_id: str,
|
||||
classification_job_repository: SQLAlchemyClassificationJobRepository = Depends(get_classification_job_repository),
|
||||
) -> ClassificationJobResponse:
|
||||
job = classification_job_repository.get(job_id)
|
||||
if job is None:
|
||||
raise HTTPException(status_code=404, detail=f"ClassificationJob {job_id} not found")
|
||||
return ClassificationJobResponse(
|
||||
id=job.id,
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
asset_id=job.asset_id,
|
||||
status=job.status.value,
|
||||
classification=job.classification,
|
||||
confidence=job.confidence,
|
||||
error_message=job.error_message,
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=ClassificationJobResponse)
|
||||
def submit_classification_job(
|
||||
request: SubmitClassificationJobRequest,
|
||||
classification_job_repository: SQLAlchemyClassificationJobRepository = Depends(get_classification_job_repository),
|
||||
) -> ClassificationJobResponse:
|
||||
use_case = SubmitClassificationJobUseCase(classification_job_repository)
|
||||
job = use_case.execute(
|
||||
SubmitClassificationJobCommand(
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=request.project_id,
|
||||
asset_id=request.asset_id,
|
||||
)
|
||||
)
|
||||
celery_app.send_task("worker.classify_asset", args=[job.id])
|
||||
return ClassificationJobResponse(
|
||||
id=job.id,
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
asset_id=job.asset_id,
|
||||
status=job.status.value,
|
||||
classification=job.classification,
|
||||
confidence=job.confidence,
|
||||
error_message=job.error_message,
|
||||
)
|
||||
+29
-246
@@ -1,253 +1,36 @@
|
||||
"""
|
||||
依赖注入容器
|
||||
管理所有 Use Cases 和 Repositories 的生命周期
|
||||
"""
|
||||
from packages.adapters.in_memory.user_repository import InMemoryUserRepository
|
||||
from packages.adapters.in_memory.workspace_repository import InMemoryWorkspaceRepository
|
||||
from packages.adapters.in_memory.workspace_member_repository import InMemoryWorkspaceMemberRepository
|
||||
from packages.adapters.in_memory.workspace_invitation_repository import InMemoryWorkspaceInvitationRepository
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from packages.application.auth import (
|
||||
RegisterUserUseCase,
|
||||
LoginUseCase,
|
||||
LogoutUseCase,
|
||||
VerifyEmailUseCase,
|
||||
RequestPasswordResetUseCase,
|
||||
ResetPasswordUseCase,
|
||||
)
|
||||
|
||||
from packages.application.workspace import (
|
||||
CreateWorkspaceUseCase,
|
||||
InviteMemberUseCase,
|
||||
AcceptInvitationUseCase,
|
||||
DeclineInvitationUseCase,
|
||||
RemoveMemberUseCase,
|
||||
LeaveWorkspaceUseCase,
|
||||
UpdateMemberRoleUseCase,
|
||||
ListWorkspacesUseCase,
|
||||
GetWorkspaceDetailUseCase,
|
||||
ListMembersUseCase,
|
||||
UpgradeSubscriptionUseCase,
|
||||
CancelSubscriptionUseCase,
|
||||
)
|
||||
|
||||
from packages.domain.permissions import PermissionChecker
|
||||
from packages.domain.quota import QuotaChecker
|
||||
from packages.adapters.sqlalchemy_impl.asset_library_repository import SQLAlchemyAssetLibraryRepository
|
||||
from packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
|
||||
from packages.adapters.sqlalchemy_impl.classification_job_repository import SQLAlchemyClassificationJobRepository
|
||||
from packages.adapters.sqlalchemy_impl.ingest_job_repository import SQLAlchemyIngestJobRepository
|
||||
from packages.adapters.sqlalchemy_impl.project_repository import SQLAlchemyProjectRepository
|
||||
from packages.adapters.sqlalchemy_impl.session import SessionLocal
|
||||
|
||||
|
||||
class DependencyContainer:
|
||||
"""依赖注入容器"""
|
||||
|
||||
def __init__(self):
|
||||
# Repositories (单例)
|
||||
self._user_repository = None
|
||||
self._workspace_repository = None
|
||||
self._workspace_member_repository = None
|
||||
self._workspace_invitation_repository = None
|
||||
self._project_repository = None
|
||||
|
||||
# Services
|
||||
self._permission_checker = None
|
||||
self._quota_checker = None
|
||||
|
||||
# ==================== Repositories ====================
|
||||
|
||||
@property
|
||||
def user_repository(self):
|
||||
if self._user_repository is None:
|
||||
from apps.api.app.config import settings
|
||||
if settings.USE_IN_MEMORY_DB:
|
||||
from packages.adapters.in_memory.user_repository import InMemoryUserRepository
|
||||
self._user_repository = InMemoryUserRepository()
|
||||
else:
|
||||
from packages.adapters.postgres.user_repository import PostgresUserRepository
|
||||
self._user_repository = PostgresUserRepository(settings.DATABASE_URL)
|
||||
return self._user_repository
|
||||
|
||||
@property
|
||||
def workspace_repository(self):
|
||||
if self._workspace_repository is None:
|
||||
from apps.api.app.config import settings
|
||||
if settings.USE_IN_MEMORY_DB:
|
||||
from packages.adapters.in_memory.workspace_repository import InMemoryWorkspaceRepository
|
||||
self._workspace_repository = InMemoryWorkspaceRepository()
|
||||
else:
|
||||
from packages.adapters.postgres.workspace_repository import PostgresWorkspaceRepository
|
||||
self._workspace_repository = PostgresWorkspaceRepository(settings.DATABASE_URL)
|
||||
return self._workspace_repository
|
||||
|
||||
@property
|
||||
def workspace_member_repository(self):
|
||||
if self._workspace_member_repository is None:
|
||||
from apps.api.app.config import settings
|
||||
if settings.USE_IN_MEMORY_DB:
|
||||
from packages.adapters.in_memory.workspace_member_repository import InMemoryWorkspaceMemberRepository
|
||||
self._workspace_member_repository = InMemoryWorkspaceMemberRepository()
|
||||
else:
|
||||
from packages.adapters.postgres.workspace_member_repository import PostgresWorkspaceMemberRepository
|
||||
self._workspace_member_repository = PostgresWorkspaceMemberRepository(settings.DATABASE_URL)
|
||||
return self._workspace_member_repository
|
||||
|
||||
@property
|
||||
def workspace_invitation_repository(self):
|
||||
if self._workspace_invitation_repository is None:
|
||||
from apps.api.app.config import settings
|
||||
if settings.USE_IN_MEMORY_DB:
|
||||
from packages.adapters.in_memory.workspace_invitation_repository import InMemoryWorkspaceInvitationRepository
|
||||
self._workspace_invitation_repository = InMemoryWorkspaceInvitationRepository()
|
||||
else:
|
||||
from packages.adapters.postgres.workspace_invitation_repository import PostgresWorkspaceInvitationRepository
|
||||
self._workspace_invitation_repository = PostgresWorkspaceInvitationRepository(settings.DATABASE_URL)
|
||||
return self._workspace_invitation_repository
|
||||
|
||||
@property
|
||||
def project_repository(self):
|
||||
# TODO: 实现 InMemoryProjectRepository
|
||||
if self._project_repository is None:
|
||||
from unittest.mock import Mock
|
||||
self._project_repository = Mock()
|
||||
self._project_repository.count_by_workspace = Mock(return_value=0)
|
||||
return self._project_repository
|
||||
|
||||
# ==================== Services ====================
|
||||
|
||||
@property
|
||||
def permission_checker(self):
|
||||
if self._permission_checker is None:
|
||||
self._permission_checker = PermissionChecker(
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
)
|
||||
return self._permission_checker
|
||||
|
||||
@property
|
||||
def quota_checker(self):
|
||||
if self._quota_checker is None:
|
||||
self._quota_checker = QuotaChecker(
|
||||
workspace_repository=self.workspace_repository,
|
||||
project_repository=self.project_repository,
|
||||
)
|
||||
return self._quota_checker
|
||||
|
||||
# ==================== Auth Use Cases ====================
|
||||
|
||||
def get_register_user_use_case(self) -> RegisterUserUseCase:
|
||||
return RegisterUserUseCase(
|
||||
user_repository=self.user_repository,
|
||||
base_url="http://localhost:3000", # TODO: 从配置读取
|
||||
)
|
||||
|
||||
def get_login_use_case(self) -> LoginUseCase:
|
||||
return LoginUseCase(
|
||||
user_repository=self.user_repository,
|
||||
)
|
||||
|
||||
def get_logout_use_case(self) -> LogoutUseCase:
|
||||
return LogoutUseCase()
|
||||
|
||||
def get_verify_email_use_case(self) -> VerifyEmailUseCase:
|
||||
return VerifyEmailUseCase(
|
||||
user_repository=self.user_repository,
|
||||
)
|
||||
|
||||
def get_request_password_reset_use_case(self) -> RequestPasswordResetUseCase:
|
||||
return RequestPasswordResetUseCase(
|
||||
user_repository=self.user_repository,
|
||||
base_url="http://localhost:3000",
|
||||
)
|
||||
|
||||
def get_reset_password_use_case(self) -> ResetPasswordUseCase:
|
||||
return ResetPasswordUseCase(
|
||||
user_repository=self.user_repository,
|
||||
)
|
||||
|
||||
# ==================== Workspace Use Cases ====================
|
||||
|
||||
def get_create_workspace_use_case(self) -> CreateWorkspaceUseCase:
|
||||
return CreateWorkspaceUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
user_repository=self.user_repository,
|
||||
)
|
||||
|
||||
def get_invite_member_use_case(self) -> InviteMemberUseCase:
|
||||
return InviteMemberUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
workspace_invitation_repository=self.workspace_invitation_repository,
|
||||
user_repository=self.user_repository,
|
||||
base_url="http://localhost:3000",
|
||||
)
|
||||
|
||||
def get_accept_invitation_use_case(self) -> AcceptInvitationUseCase:
|
||||
return AcceptInvitationUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
workspace_invitation_repository=self.workspace_invitation_repository,
|
||||
user_repository=self.user_repository,
|
||||
)
|
||||
|
||||
def get_decline_invitation_use_case(self) -> DeclineInvitationUseCase:
|
||||
return DeclineInvitationUseCase(
|
||||
workspace_invitation_repository=self.workspace_invitation_repository,
|
||||
)
|
||||
|
||||
def get_remove_member_use_case(self) -> RemoveMemberUseCase:
|
||||
return RemoveMemberUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
)
|
||||
|
||||
def get_leave_workspace_use_case(self) -> LeaveWorkspaceUseCase:
|
||||
return LeaveWorkspaceUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
)
|
||||
|
||||
def get_update_member_role_use_case(self) -> UpdateMemberRoleUseCase:
|
||||
return UpdateMemberRoleUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
)
|
||||
|
||||
def get_list_workspaces_use_case(self) -> ListWorkspacesUseCase:
|
||||
return ListWorkspacesUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
)
|
||||
|
||||
def get_get_workspace_detail_use_case(self) -> GetWorkspaceDetailUseCase:
|
||||
return GetWorkspaceDetailUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
)
|
||||
|
||||
def get_list_members_use_case(self) -> ListMembersUseCase:
|
||||
return ListMembersUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
user_repository=self.user_repository,
|
||||
)
|
||||
|
||||
def get_upgrade_subscription_use_case(self) -> UpgradeSubscriptionUseCase:
|
||||
return UpgradeSubscriptionUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
)
|
||||
|
||||
def get_cancel_subscription_use_case(self) -> CancelSubscriptionUseCase:
|
||||
return CancelSubscriptionUseCase(
|
||||
workspace_repository=self.workspace_repository,
|
||||
workspace_member_repository=self.workspace_member_repository,
|
||||
)
|
||||
def get_db_session():
|
||||
session: Session = SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
# 全局容器实例
|
||||
_container = None
|
||||
def get_asset_repository(session: Session = next(get_db_session())) -> SQLAlchemyAssetRepository:
|
||||
return SQLAlchemyAssetRepository(session)
|
||||
|
||||
|
||||
def get_container() -> DependencyContainer:
|
||||
"""获取全局依赖容器"""
|
||||
global _container
|
||||
if _container is None:
|
||||
_container = DependencyContainer()
|
||||
return _container
|
||||
def get_asset_library_repository(session: Session = next(get_db_session())) -> SQLAlchemyAssetLibraryRepository:
|
||||
return SQLAlchemyAssetLibraryRepository(session)
|
||||
|
||||
|
||||
def get_ingest_job_repository(session: Session = next(get_db_session())) -> SQLAlchemyIngestJobRepository:
|
||||
return SQLAlchemyIngestJobRepository(session)
|
||||
|
||||
|
||||
def get_classification_job_repository(session: Session = next(get_db_session())) -> SQLAlchemyClassificationJobRepository:
|
||||
return SQLAlchemyClassificationJobRepository(session)
|
||||
|
||||
|
||||
def get_project_repository(session: Session = next(get_db_session())) -> SQLAlchemyProjectRepository:
|
||||
return SQLAlchemyProjectRepository(session)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SubmitClassificationJobRequest(BaseModel):
|
||||
workspace_id: str = Field(..., min_length=1)
|
||||
project_id: str = Field(..., min_length=1)
|
||||
asset_id: str = Field(..., min_length=1)
|
||||
|
||||
|
||||
class ClassificationJobResponse(BaseModel):
|
||||
id: str
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
asset_id: str
|
||||
status: str
|
||||
classification: str
|
||||
confidence: float
|
||||
error_message: str
|
||||
Reference in New Issue
Block a user