refactor
This commit is contained in:
File diff suppressed because it is too large
Load Diff
900
backend/src/api/routes/dataset_review_pkg/_dependencies.py
Normal file
900
backend/src/api/routes/dataset_review_pkg/_dependencies.py
Normal file
@@ -0,0 +1,900 @@
|
||||
# [DEF:DatasetReviewDependencies:Module]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @LAYER: API
|
||||
# @RATIONALE: Extracted from 2484-line monolith to satisfy INV_7 (400-line module limit).
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.database import get_db
|
||||
from src.core.logger import belief_scope, logger
|
||||
from src.dependencies import (
|
||||
get_config_manager,
|
||||
get_current_user,
|
||||
get_task_manager,
|
||||
has_permission,
|
||||
)
|
||||
from src.models.auth import User
|
||||
from src.models.dataset_review import (
|
||||
AnswerKind,
|
||||
ApprovalState,
|
||||
ArtifactFormat,
|
||||
CandidateStatus,
|
||||
ClarificationSession,
|
||||
DatasetReviewSession,
|
||||
ExecutionMapping,
|
||||
FieldProvenance,
|
||||
MappingMethod,
|
||||
PreviewStatus,
|
||||
QuestionState,
|
||||
ReadinessState,
|
||||
RecommendedAction,
|
||||
SemanticCandidate,
|
||||
SemanticFieldEntry,
|
||||
SessionStatus,
|
||||
)
|
||||
from src.schemas.dataset_review import (
|
||||
ClarificationAnswerDto,
|
||||
ClarificationQuestionDto,
|
||||
ClarificationSessionDto,
|
||||
CompiledPreviewDto,
|
||||
DatasetRunContextDto,
|
||||
ExecutionMappingDto,
|
||||
SemanticFieldEntryDto,
|
||||
SessionDetail,
|
||||
SessionSummary,
|
||||
ValidationFindingDto,
|
||||
)
|
||||
from src.services.dataset_review.clarification_engine import (
|
||||
ClarificationAnswerCommand,
|
||||
ClarificationEngine,
|
||||
ClarificationQuestionPayload,
|
||||
ClarificationStateResult,
|
||||
)
|
||||
from src.services.dataset_review.orchestrator import (
|
||||
DatasetReviewOrchestrator,
|
||||
LaunchDatasetCommand,
|
||||
PreparePreviewCommand,
|
||||
StartSessionCommand,
|
||||
)
|
||||
from src.services.dataset_review.repositories.session_repository import (
|
||||
DatasetReviewSessionRepository,
|
||||
DatasetReviewSessionVersionConflictError,
|
||||
)
|
||||
|
||||
|
||||
# [DEF:StartSessionRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for starting one dataset review session.
|
||||
class StartSessionRequest(BaseModel):
|
||||
source_kind: str = Field(..., pattern="^(superset_link|dataset_selection)$")
|
||||
source_input: str = Field(..., min_length=1)
|
||||
environment_id: str = Field(..., min_length=1)
|
||||
|
||||
|
||||
# [/DEF:StartSessionRequest:Class]
|
||||
|
||||
|
||||
# [DEF:UpdateSessionRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for lifecycle state updates on an existing session.
|
||||
class UpdateSessionRequest(BaseModel):
|
||||
status: SessionStatus
|
||||
note: Optional[str] = None
|
||||
|
||||
|
||||
# [/DEF:UpdateSessionRequest:Class]
|
||||
|
||||
|
||||
# [DEF:SessionCollectionResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Paginated session collection response.
|
||||
class SessionCollectionResponse(BaseModel):
|
||||
items: List[SessionSummary]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
has_next: bool
|
||||
|
||||
|
||||
# [/DEF:SessionCollectionResponse:Class]
|
||||
|
||||
|
||||
# [DEF:ExportArtifactResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Inline export response for documentation or validation outputs.
|
||||
class ExportArtifactResponse(BaseModel):
|
||||
artifact_id: str
|
||||
session_id: str
|
||||
artifact_type: str
|
||||
format: str
|
||||
storage_ref: str
|
||||
created_by_user_id: str
|
||||
created_at: Optional[str] = None
|
||||
content: Dict[str, Any]
|
||||
|
||||
|
||||
# [/DEF:ExportArtifactResponse:Class]
|
||||
|
||||
|
||||
# [DEF:FieldSemanticUpdateRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for field-level semantic candidate acceptance or manual override.
|
||||
class FieldSemanticUpdateRequest(BaseModel):
|
||||
candidate_id: Optional[str] = None
|
||||
verbose_name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
display_format: Optional[str] = None
|
||||
lock_field: bool = False
|
||||
resolution_note: Optional[str] = None
|
||||
|
||||
|
||||
# [/DEF:FieldSemanticUpdateRequest:Class]
|
||||
|
||||
|
||||
# [DEF:FeedbackRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for thumbs up/down feedback.
|
||||
class FeedbackRequest(BaseModel):
|
||||
feedback: str = Field(..., pattern="^(up|down)$")
|
||||
|
||||
|
||||
# [/DEF:FeedbackRequest:Class]
|
||||
|
||||
|
||||
# [DEF:ClarificationAnswerRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for submitting one clarification answer.
|
||||
class ClarificationAnswerRequest(BaseModel):
|
||||
question_id: str = Field(..., min_length=1)
|
||||
answer_kind: AnswerKind
|
||||
answer_value: Optional[str] = None
|
||||
|
||||
|
||||
# [/DEF:ClarificationAnswerRequest:Class]
|
||||
|
||||
|
||||
# [DEF:ClarificationSessionSummaryResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Summary DTO for current clarification session state.
|
||||
class ClarificationSessionSummaryResponse(BaseModel):
|
||||
clarification_session_id: str
|
||||
session_id: str
|
||||
status: str
|
||||
current_question_id: Optional[str] = None
|
||||
resolved_count: int
|
||||
remaining_count: int
|
||||
summary_delta: Optional[str] = None
|
||||
|
||||
|
||||
# [/DEF:ClarificationSessionSummaryResponse:Class]
|
||||
|
||||
|
||||
# [DEF:ClarificationStateResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Response DTO for current clarification state and active question payload.
|
||||
class ClarificationStateResponse(BaseModel):
|
||||
clarification_session: Optional[ClarificationSessionSummaryResponse] = None
|
||||
current_question: Optional[ClarificationQuestionDto] = None
|
||||
|
||||
|
||||
# [/DEF:ClarificationStateResponse:Class]
|
||||
|
||||
|
||||
# [DEF:ClarificationAnswerResultResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Response DTO for one clarification answer mutation result.
|
||||
class ClarificationAnswerResultResponse(BaseModel):
|
||||
clarification_state: ClarificationStateResponse
|
||||
session: SessionSummary
|
||||
changed_findings: List[ValidationFindingDto]
|
||||
|
||||
|
||||
# [/DEF:ClarificationAnswerResultResponse:Class]
|
||||
|
||||
|
||||
# [DEF:FeedbackResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Minimal response DTO for persisted AI feedback actions.
|
||||
class FeedbackResponse(BaseModel):
|
||||
target_id: str
|
||||
feedback: str
|
||||
|
||||
|
||||
# [/DEF:FeedbackResponse:Class]
|
||||
|
||||
|
||||
# [DEF:ApproveMappingRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Optional request DTO for explicit mapping approval audit notes.
|
||||
class ApproveMappingRequest(BaseModel):
|
||||
approval_note: Optional[str] = None
|
||||
|
||||
|
||||
# [/DEF:ApproveMappingRequest:Class]
|
||||
|
||||
|
||||
# [DEF:BatchApproveSemanticItemRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for one batch semantic-approval item.
|
||||
class BatchApproveSemanticItemRequest(BaseModel):
|
||||
field_id: str = Field(..., min_length=1)
|
||||
candidate_id: str = Field(..., min_length=1)
|
||||
lock_field: bool = False
|
||||
|
||||
|
||||
# [/DEF:BatchApproveSemanticItemRequest:Class]
|
||||
|
||||
|
||||
# [DEF:BatchApproveSemanticRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for explicit batch semantic approvals.
|
||||
class BatchApproveSemanticRequest(BaseModel):
|
||||
items: List[BatchApproveSemanticItemRequest] = Field(..., min_length=1)
|
||||
|
||||
|
||||
# [/DEF:BatchApproveSemanticRequest:Class]
|
||||
|
||||
|
||||
# [DEF:BatchApproveMappingRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for explicit batch mapping approvals.
|
||||
class BatchApproveMappingRequest(BaseModel):
|
||||
mapping_ids: List[str] = Field(..., min_length=1)
|
||||
approval_note: Optional[str] = None
|
||||
|
||||
|
||||
# [/DEF:BatchApproveMappingRequest:Class]
|
||||
|
||||
|
||||
# [DEF:PreviewEnqueueResultResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Async preview trigger response exposing only enqueue state.
|
||||
class PreviewEnqueueResultResponse(BaseModel):
|
||||
session_id: str
|
||||
session_version: Optional[int] = None
|
||||
preview_status: str
|
||||
task_id: Optional[str] = None
|
||||
|
||||
|
||||
# [/DEF:PreviewEnqueueResultResponse:Class]
|
||||
|
||||
|
||||
# [DEF:MappingCollectionResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Wrapper for execution mapping list responses.
|
||||
class MappingCollectionResponse(BaseModel):
|
||||
items: List[ExecutionMappingDto]
|
||||
|
||||
|
||||
# [/DEF:MappingCollectionResponse:Class]
|
||||
|
||||
|
||||
# [DEF:UpdateExecutionMappingRequest:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Request DTO for one manual execution-mapping override update.
|
||||
class UpdateExecutionMappingRequest(BaseModel):
|
||||
effective_value: Optional[Any] = None
|
||||
mapping_method: Optional[str] = Field(default=None, pattern="^(manual_override|direct_match|heuristic_match|semantic_match)$")
|
||||
transformation_note: Optional[str] = None
|
||||
|
||||
|
||||
# [/DEF:UpdateExecutionMappingRequest:Class]
|
||||
|
||||
|
||||
# [DEF:LaunchDatasetResponse:Class]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Launch result exposing audited run context and SQL Lab redirect target.
|
||||
class LaunchDatasetResponse(BaseModel):
|
||||
run_context: DatasetRunContextDto
|
||||
redirect_url: str
|
||||
|
||||
|
||||
# [/DEF:LaunchDatasetResponse:Class]
|
||||
|
||||
|
||||
# --- Dependency Injection ---
|
||||
|
||||
# [DEF:_require_auto_review_flag:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Guard US1 dataset review endpoints behind the configured feature flag.
|
||||
def _require_auto_review_flag(config_manager=Depends(get_config_manager)) -> bool:
|
||||
with belief_scope("dataset_review.require_auto_review_flag"):
|
||||
if not config_manager.get_config().settings.ff_dataset_auto_review:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dataset auto review feature is disabled")
|
||||
return True
|
||||
|
||||
|
||||
# [/DEF:_require_auto_review_flag:Function]
|
||||
|
||||
|
||||
# [DEF:_require_clarification_flag:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Guard clarification-specific US2 endpoints behind the configured feature flag.
|
||||
def _require_clarification_flag(config_manager=Depends(get_config_manager)) -> bool:
|
||||
with belief_scope("dataset_review.require_clarification_flag"):
|
||||
if not config_manager.get_config().settings.ff_dataset_clarification:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dataset clarification feature is disabled")
|
||||
return True
|
||||
|
||||
|
||||
# [/DEF:_require_clarification_flag:Function]
|
||||
|
||||
|
||||
# [DEF:_require_execution_flag:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Guard US3 execution endpoints behind the configured feature flag.
|
||||
def _require_execution_flag(config_manager=Depends(get_config_manager)) -> bool:
|
||||
with belief_scope("dataset_review.require_execution_flag"):
|
||||
if not config_manager.get_config().settings.ff_dataset_execution:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dataset execution feature is disabled")
|
||||
return True
|
||||
|
||||
|
||||
# [/DEF:_require_execution_flag:Function]
|
||||
|
||||
|
||||
# [DEF:_get_repository:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Build repository dependency.
|
||||
def _get_repository(db: Session = Depends(get_db)) -> DatasetReviewSessionRepository:
|
||||
return DatasetReviewSessionRepository(db)
|
||||
|
||||
|
||||
# [/DEF:_get_repository:Function]
|
||||
|
||||
|
||||
# [DEF:_get_orchestrator:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Build orchestrator dependency.
|
||||
def _get_orchestrator(
|
||||
repository: DatasetReviewSessionRepository = Depends(_get_repository),
|
||||
config_manager=Depends(get_config_manager),
|
||||
task_manager=Depends(get_task_manager),
|
||||
) -> DatasetReviewOrchestrator:
|
||||
return DatasetReviewOrchestrator(repository=repository, config_manager=config_manager, task_manager=task_manager)
|
||||
|
||||
|
||||
# [/DEF:_get_orchestrator:Function]
|
||||
|
||||
|
||||
# [DEF:_get_clarification_engine:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Build clarification engine dependency.
|
||||
def _get_clarification_engine(
|
||||
repository: DatasetReviewSessionRepository = Depends(_get_repository),
|
||||
) -> ClarificationEngine:
|
||||
return ClarificationEngine(repository=repository)
|
||||
|
||||
|
||||
# [/DEF:_get_clarification_engine:Function]
|
||||
|
||||
|
||||
# --- Serialization Helpers ---
|
||||
|
||||
# [DEF:_serialize_session_summary:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Map session aggregate into stable API summary DTO.
|
||||
def _serialize_session_summary(session: DatasetReviewSession) -> SessionSummary:
|
||||
summary = SessionSummary.model_validate(session, from_attributes=True)
|
||||
summary.session_version = summary.version
|
||||
return summary
|
||||
|
||||
|
||||
# [/DEF:_serialize_session_summary:Function]
|
||||
|
||||
|
||||
# [DEF:_serialize_session_detail:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Map session aggregate into stable API detail DTO.
|
||||
def _serialize_session_detail(session: DatasetReviewSession) -> SessionDetail:
|
||||
detail = SessionDetail.model_validate(session, from_attributes=True)
|
||||
detail.session_version = detail.version
|
||||
return detail
|
||||
|
||||
|
||||
# [/DEF:_serialize_session_detail:Function]
|
||||
|
||||
|
||||
# [DEF:_require_session_version_header:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Read the optimistic-lock session version header.
|
||||
def _require_session_version_header(
|
||||
session_version: int = Header(..., alias="X-Session-Version", ge=0),
|
||||
) -> int:
|
||||
return session_version
|
||||
|
||||
|
||||
# [/DEF:_require_session_version_header:Function]
|
||||
|
||||
|
||||
# [DEF:_build_session_version_conflict_http_exception:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Normalize optimistic-lock conflict errors into HTTP 409 responses.
|
||||
def _build_session_version_conflict_http_exception(exc: DatasetReviewSessionVersionConflictError) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail={"error_code": "session_version_conflict", "message": str(exc), "session_id": exc.session_id, "expected_version": exc.expected_version, "actual_version": exc.actual_version},
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:_build_session_version_conflict_http_exception:Function]
|
||||
|
||||
|
||||
# [DEF:_enforce_session_version:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Convert repository optimistic-lock conflicts into deterministic HTTP 409 responses.
|
||||
def _enforce_session_version(repository, session, expected_version):
|
||||
with belief_scope("_enforce_session_version"):
|
||||
try:
|
||||
repository.require_session_version(session, expected_version)
|
||||
except DatasetReviewSessionVersionConflictError as exc:
|
||||
logger.explore("Dataset review optimistic-lock conflict detected", extra={"session_id": exc.session_id, "expected_version": exc.expected_version, "actual_version": exc.actual_version})
|
||||
raise _build_session_version_conflict_http_exception(exc) from exc
|
||||
return session
|
||||
|
||||
|
||||
# [/DEF:_enforce_session_version:Function]
|
||||
|
||||
|
||||
# [DEF:_get_owned_session_or_404:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve one session for current user or collaborator scope, returning 404 when inaccessible.
|
||||
def _get_owned_session_or_404(repository, session_id, current_user):
|
||||
with belief_scope("_get_owned_session_or_404"):
|
||||
session = repository.load_session_detail(session_id, current_user.id)
|
||||
if session is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")
|
||||
return session
|
||||
|
||||
|
||||
# [/DEF:_get_owned_session_or_404:Function]
|
||||
|
||||
|
||||
# [DEF:_require_owner_mutation_scope:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Enforce owner-only mutation scope.
|
||||
def _require_owner_mutation_scope(session, current_user):
|
||||
with belief_scope("_require_owner_mutation_scope"):
|
||||
if session.user_id != current_user.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only the owner can mutate dataset review state")
|
||||
return session
|
||||
|
||||
|
||||
# [/DEF:_require_owner_mutation_scope:Function]
|
||||
|
||||
|
||||
# [DEF:_prepare_owned_session_mutation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resolve owner-scoped mutation session and enforce optimistic-lock version.
|
||||
def _prepare_owned_session_mutation(repository, session_id, current_user, expected_version):
|
||||
with belief_scope("_prepare_owned_session_mutation"):
|
||||
session = _get_owned_session_or_404(repository, session_id, current_user)
|
||||
_require_owner_mutation_scope(session, current_user)
|
||||
return _enforce_session_version(repository, session, expected_version)
|
||||
|
||||
|
||||
# [/DEF:_prepare_owned_session_mutation:Function]
|
||||
|
||||
|
||||
# [DEF:_commit_owned_session_mutation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Centralize session version bumping and commit semantics.
|
||||
def _commit_owned_session_mutation(repository, session, *, refresh_targets=None):
|
||||
with belief_scope("_commit_owned_session_mutation"):
|
||||
try:
|
||||
repository.commit_session_mutation(session, refresh_targets=refresh_targets)
|
||||
except DatasetReviewSessionVersionConflictError as exc:
|
||||
raise _build_session_version_conflict_http_exception(exc) from exc
|
||||
return session
|
||||
|
||||
|
||||
# [/DEF:_commit_owned_session_mutation:Function]
|
||||
|
||||
|
||||
# [DEF:_record_session_event:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Persist one explicit audit event for an owned mutation endpoint.
|
||||
def _record_session_event(repository, session, current_user, *, event_type, event_summary, event_details=None):
|
||||
repository.event_logger.log_for_session(session, actor_user_id=current_user.id, event_type=event_type, event_summary=event_summary, event_details=event_details or {})
|
||||
|
||||
|
||||
# [/DEF:_record_session_event:Function]
|
||||
|
||||
|
||||
# [DEF:_serialize_semantic_field:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Map one semantic field into stable DTO.
|
||||
def _serialize_semantic_field(field):
|
||||
payload = SemanticFieldEntryDto.model_validate(field, from_attributes=True)
|
||||
session_ref = getattr(field, "session", None)
|
||||
version_value = getattr(session_ref, "version", None)
|
||||
payload.session_version = int(version_value or 0) if version_value is not None else None
|
||||
return payload
|
||||
|
||||
|
||||
# [/DEF:_serialize_semantic_field:Function]
|
||||
|
||||
|
||||
# [DEF:_serialize_execution_mapping:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Map one execution mapping into stable DTO.
|
||||
def _serialize_execution_mapping(mapping):
|
||||
payload = ExecutionMappingDto.model_validate(mapping, from_attributes=True)
|
||||
session_ref = getattr(mapping, "session", None)
|
||||
version_value = getattr(session_ref, "version", None)
|
||||
payload.session_version = int(version_value or 0) if version_value is not None else None
|
||||
return payload
|
||||
|
||||
|
||||
# [/DEF:_serialize_execution_mapping:Function]
|
||||
|
||||
|
||||
# [DEF:_serialize_preview:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Map one preview into stable DTO.
|
||||
def _serialize_preview(preview, *, session_version_fallback=None):
|
||||
payload = CompiledPreviewDto.model_validate(preview, from_attributes=True)
|
||||
session_ref = getattr(preview, "session", None)
|
||||
version_value = getattr(session_ref, "version", None)
|
||||
if version_value is None:
|
||||
version_value = session_version_fallback
|
||||
payload.session_version = int(version_value or 0) if version_value is not None else None
|
||||
return payload
|
||||
|
||||
|
||||
# [/DEF:_serialize_preview:Function]
|
||||
|
||||
|
||||
# [DEF:_serialize_run_context:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Map one run context into stable DTO.
|
||||
def _serialize_run_context(run_context):
|
||||
payload = DatasetRunContextDto.model_validate(run_context, from_attributes=True)
|
||||
session_ref = getattr(run_context, "session", None)
|
||||
version_value = getattr(session_ref, "version", None)
|
||||
payload.session_version = int(version_value or 0) if version_value is not None else None
|
||||
return payload
|
||||
|
||||
|
||||
# [/DEF:_serialize_run_context:Function]
|
||||
|
||||
|
||||
# [DEF:_serialize_clarification_question_payload:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Convert clarification engine payload into API DTO.
|
||||
def _serialize_clarification_question_payload(payload):
|
||||
if payload is None:
|
||||
return None
|
||||
return ClarificationQuestionDto.model_validate({
|
||||
"question_id": payload.question_id, "clarification_session_id": payload.clarification_session_id,
|
||||
"topic_ref": payload.topic_ref, "question_text": payload.question_text,
|
||||
"why_it_matters": payload.why_it_matters, "current_guess": payload.current_guess,
|
||||
"priority": payload.priority, "state": payload.state, "options": payload.options,
|
||||
"answer": None, "created_at": datetime.utcnow(), "updated_at": datetime.utcnow(),
|
||||
})
|
||||
|
||||
|
||||
# [/DEF:_serialize_clarification_question_payload:Function]
|
||||
|
||||
|
||||
# [DEF:_serialize_clarification_state:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Convert clarification engine state into API response.
|
||||
def _serialize_clarification_state(state):
|
||||
return ClarificationStateResponse(
|
||||
clarification_session=ClarificationSessionSummaryResponse(
|
||||
clarification_session_id=state.clarification_session.clarification_session_id,
|
||||
session_id=state.clarification_session.session_id, status=state.clarification_session.status.value,
|
||||
current_question_id=state.clarification_session.current_question_id,
|
||||
resolved_count=state.clarification_session.resolved_count,
|
||||
remaining_count=state.clarification_session.remaining_count,
|
||||
summary_delta=state.clarification_session.summary_delta,
|
||||
),
|
||||
current_question=_serialize_clarification_question_payload(state.current_question),
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:_serialize_clarification_state:Function]
|
||||
|
||||
|
||||
# [DEF:_serialize_empty_clarification_state:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Return empty clarification payload.
|
||||
def _serialize_empty_clarification_state():
|
||||
return ClarificationStateResponse(clarification_session=None, current_question=None)
|
||||
|
||||
|
||||
# [/DEF:_serialize_empty_clarification_state:Function]
|
||||
|
||||
|
||||
# [DEF:_get_latest_clarification_session_or_404:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Resolve the latest clarification aggregate or raise.
|
||||
def _get_latest_clarification_session_or_404(session):
|
||||
if not session.clarification_sessions:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Clarification session not found")
|
||||
return sorted(session.clarification_sessions, key=lambda item: (item.started_at, item.clarification_session_id), reverse=True)[0]
|
||||
|
||||
|
||||
# [/DEF:_get_latest_clarification_session_or_404:Function]
|
||||
|
||||
|
||||
# [DEF:_get_owned_mapping_or_404:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Resolve one execution mapping inside one owned session.
|
||||
def _get_owned_mapping_or_404(session, mapping_id):
|
||||
for mapping in session.execution_mappings:
|
||||
if mapping.mapping_id == mapping_id:
|
||||
return mapping
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Execution mapping not found")
|
||||
|
||||
|
||||
# [/DEF:_get_owned_mapping_or_404:Function]
|
||||
|
||||
|
||||
# [DEF:_get_owned_field_or_404:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Resolve a semantic field inside one owned session.
|
||||
def _get_owned_field_or_404(session, field_id):
|
||||
for field in session.semantic_fields:
|
||||
if field.field_id == field_id:
|
||||
return field
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Semantic field not found")
|
||||
|
||||
|
||||
# [/DEF:_get_owned_field_or_404:Function]
|
||||
|
||||
|
||||
# [DEF:_map_candidate_provenance:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Translate accepted semantic candidate type into stable field provenance.
|
||||
def _map_candidate_provenance(candidate):
|
||||
if str(candidate.match_type.value) == "exact":
|
||||
return FieldProvenance.DICTIONARY_EXACT
|
||||
if str(candidate.match_type.value) == "reference":
|
||||
return FieldProvenance.REFERENCE_IMPORTED
|
||||
if str(candidate.match_type.value) == "generated":
|
||||
return FieldProvenance.AI_GENERATED
|
||||
return FieldProvenance.FUZZY_INFERRED
|
||||
|
||||
|
||||
# [/DEF:_map_candidate_provenance:Function]
|
||||
|
||||
|
||||
# [DEF:_resolve_candidate_source_version:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Resolve the semantic source version for one accepted candidate.
|
||||
def _resolve_candidate_source_version(field, source_id):
|
||||
if not source_id:
|
||||
return None
|
||||
session = getattr(field, "session", None)
|
||||
if session is None:
|
||||
return None
|
||||
for source in getattr(session, "semantic_sources", []) or []:
|
||||
if source.source_id == source_id:
|
||||
return source.source_version
|
||||
return None
|
||||
|
||||
|
||||
# [/DEF:_resolve_candidate_source_version:Function]
|
||||
|
||||
|
||||
# [DEF:_update_semantic_field_state:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Apply field-level semantic manual override or candidate acceptance.
|
||||
# @POST: Manual overrides always set manual provenance plus lock.
|
||||
def _update_semantic_field_state(field, request, changed_by):
|
||||
has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format])
|
||||
selected_candidate = None
|
||||
if request.candidate_id:
|
||||
selected_candidate = next((c for c in field.candidates if c.candidate_id == request.candidate_id), None)
|
||||
if selected_candidate is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Semantic candidate not found")
|
||||
|
||||
if has_manual_override:
|
||||
field.verbose_name = request.verbose_name
|
||||
field.description = request.description
|
||||
field.display_format = request.display_format
|
||||
field.provenance = FieldProvenance.MANUAL_OVERRIDE
|
||||
field.source_id = None
|
||||
field.source_version = None
|
||||
field.confidence_rank = None
|
||||
field.is_locked = True
|
||||
field.has_conflict = False
|
||||
field.needs_review = False
|
||||
field.last_changed_by = changed_by
|
||||
for c in field.candidates:
|
||||
c.status = CandidateStatus.SUPERSEDED
|
||||
return field
|
||||
|
||||
if selected_candidate is not None:
|
||||
field.verbose_name = selected_candidate.proposed_verbose_name
|
||||
field.description = selected_candidate.proposed_description
|
||||
field.display_format = selected_candidate.proposed_display_format
|
||||
field.provenance = _map_candidate_provenance(selected_candidate)
|
||||
field.source_id = selected_candidate.source_id
|
||||
field.source_version = _resolve_candidate_source_version(field, selected_candidate.source_id)
|
||||
field.confidence_rank = selected_candidate.candidate_rank
|
||||
field.is_locked = bool(request.lock_field or field.is_locked)
|
||||
field.has_conflict = len(field.candidates) > 1
|
||||
field.needs_review = False
|
||||
field.last_changed_by = changed_by
|
||||
for c in field.candidates:
|
||||
c.status = CandidateStatus.ACCEPTED if c.candidate_id == selected_candidate.candidate_id else CandidateStatus.SUPERSEDED
|
||||
return field
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Provide candidate_id or at least one manual override field")
|
||||
|
||||
|
||||
# [/DEF:_update_semantic_field_state:Function]
|
||||
|
||||
|
||||
# [DEF:_build_sql_lab_redirect_url:Function]
|
||||
# @COMPLEXITY: 1
|
||||
# @PURPOSE: Build SQL Lab redirect URL.
|
||||
def _build_sql_lab_redirect_url(environment_url, sql_lab_session_ref):
|
||||
base_url = str(environment_url or "").rstrip("/")
|
||||
session_ref = str(sql_lab_session_ref or "").strip()
|
||||
if not base_url:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Superset environment URL is not configured")
|
||||
if not session_ref:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="SQL Lab session reference is missing")
|
||||
return f"{base_url}/superset/sqllab?queryId={session_ref}"
|
||||
|
||||
|
||||
# [/DEF:_build_sql_lab_redirect_url:Function]
|
||||
|
||||
|
||||
# [DEF:_build_documentation_export:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce session documentation export content.
|
||||
def _build_documentation_export(session, export_format):
|
||||
profile = session.profile
|
||||
findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))
|
||||
if export_format == ArtifactFormat.MARKDOWN:
|
||||
lines = [f"# Dataset Review: {session.dataset_ref}", "", f"- Session ID: {session.session_id}", f"- Environment: {session.environment_id}", f"- Readiness: {session.readiness_state.value}", f"- Recommended action: {session.recommended_action.value}", "", "## Business Summary", profile.business_summary if profile else "No profile summary available.", "", "## Findings"]
|
||||
if findings:
|
||||
for f in findings:
|
||||
lines.append(f"- [{f.severity.value}] {f.title}: {f.message}")
|
||||
else:
|
||||
lines.append("- No findings recorded.")
|
||||
return {"storage_ref": f"inline://dataset-review/{session.session_id}/documentation.md", "content": {"markdown": "\n".join(lines)}}
|
||||
content = {"session": _serialize_session_summary(session).model_dump(mode="json"), "profile": profile and {"dataset_name": profile.dataset_name, "business_summary": profile.business_summary, "confidence_state": profile.confidence_state.value, "dataset_type": profile.dataset_type}, "findings": [{"code": f.code, "severity": f.severity.value, "title": f.title, "message": f.message, "resolution_state": f.resolution_state.value} for f in findings]}
|
||||
return {"storage_ref": f"inline://dataset-review/{session.session_id}/documentation.json", "content": content}
|
||||
|
||||
|
||||
# [/DEF:_build_documentation_export:Function]
|
||||
|
||||
|
||||
# [DEF:_build_validation_export:Function]
|
||||
# @COMPLEXITY: 2
|
||||
# @PURPOSE: Produce validation-focused export content.
|
||||
def _build_validation_export(session, export_format):
|
||||
findings = sorted(session.findings, key=lambda item: (item.severity.value, item.code))
|
||||
if export_format == ArtifactFormat.MARKDOWN:
|
||||
lines = [f"# Validation Report: {session.dataset_ref}", "", f"- Session ID: {session.session_id}", f"- Readiness: {session.readiness_state.value}", "", "## Findings"]
|
||||
if findings:
|
||||
for f in findings:
|
||||
lines.append(f"- `{f.code}` [{f.severity.value}] {f.message}")
|
||||
else:
|
||||
lines.append("- No findings recorded.")
|
||||
return {"storage_ref": f"inline://dataset-review/{session.session_id}/validation.md", "content": {"markdown": "\n".join(lines)}}
|
||||
content = {"session_id": session.session_id, "dataset_ref": session.dataset_ref, "readiness_state": session.readiness_state.value, "findings": [{"finding_id": f.finding_id, "area": f.area.value, "severity": f.severity.value, "code": f.code, "title": f.title, "message": f.message, "resolution_state": f.resolution_state.value} for f in findings]}
|
||||
return {"storage_ref": f"inline://dataset-review/{session.session_id}/validation.json", "content": content}
|
||||
|
||||
|
||||
# [/DEF:_build_validation_export:Function]
|
||||
|
||||
|
||||
# [/DEF:DatasetReviewDependencies:Module]
|
||||
923
backend/src/api/routes/dataset_review_pkg/_routes.py
Normal file
923
backend/src/api/routes/dataset_review_pkg/_routes.py
Normal file
@@ -0,0 +1,923 @@
|
||||
# [DEF:DatasetReviewRoutes:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @LAYER: API
|
||||
# @RATIONALE: Extracted from 2484-line monolith to satisfy INV_7 (400-line module limit).
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Optional, Union, cast
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
|
||||
from src.core.logger import belief_scope, logger
|
||||
from src.dependencies import get_config_manager, get_current_user, has_permission
|
||||
from src.models.auth import User
|
||||
from src.models.dataset_review import (
|
||||
ApprovalState,
|
||||
ArtifactFormat,
|
||||
FieldProvenance,
|
||||
MappingMethod,
|
||||
PreviewStatus,
|
||||
ReadinessState,
|
||||
RecommendedAction,
|
||||
SessionStatus,
|
||||
)
|
||||
from src.schemas.dataset_review import (
|
||||
ClarificationAnswerDto,
|
||||
ClarificationStateResponse as _ClarificationStateResponseSchema,
|
||||
CompiledPreviewDto,
|
||||
ExecutionMappingDto,
|
||||
SemanticFieldEntryDto,
|
||||
SessionSummary,
|
||||
ValidationFindingDto,
|
||||
)
|
||||
from src.services.dataset_review.clarification_engine import (
|
||||
ClarificationAnswerCommand,
|
||||
ClarificationStateResult,
|
||||
)
|
||||
from src.services.dataset_review.orchestrator import (
|
||||
LaunchDatasetCommand,
|
||||
PreparePreviewCommand,
|
||||
StartSessionCommand,
|
||||
)
|
||||
from src.services.dataset_review.repositories.session_repository import (
|
||||
DatasetReviewSessionVersionConflictError,
|
||||
)
|
||||
from src.api.routes.dataset_review_pkg._dependencies import (
|
||||
BatchApproveMappingRequest,
|
||||
BatchApproveSemanticRequest,
|
||||
ClarificationAnswerRequest,
|
||||
ClarificationAnswerResultResponse,
|
||||
ClarificationStateResponse,
|
||||
ExportArtifactResponse,
|
||||
FeedbackRequest,
|
||||
FeedbackResponse,
|
||||
FieldSemanticUpdateRequest,
|
||||
LaunchDatasetResponse,
|
||||
MappingCollectionResponse,
|
||||
PreviewEnqueueResultResponse,
|
||||
SessionCollectionResponse,
|
||||
StartSessionRequest,
|
||||
UpdateExecutionMappingRequest,
|
||||
UpdateSessionRequest,
|
||||
_build_documentation_export,
|
||||
_build_sql_lab_redirect_url,
|
||||
_build_validation_export,
|
||||
_commit_owned_session_mutation,
|
||||
_get_clarification_engine,
|
||||
_get_latest_clarification_session_or_404,
|
||||
_get_owned_field_or_404,
|
||||
_get_owned_mapping_or_404,
|
||||
_get_owned_session_or_404,
|
||||
_get_orchestrator,
|
||||
_get_repository,
|
||||
_prepare_owned_session_mutation,
|
||||
_record_session_event,
|
||||
_require_auto_review_flag,
|
||||
_require_clarification_flag,
|
||||
_require_execution_flag,
|
||||
_require_session_version_header,
|
||||
_serialize_clarification_state,
|
||||
_serialize_empty_clarification_state,
|
||||
_serialize_execution_mapping,
|
||||
_serialize_preview,
|
||||
_serialize_run_context,
|
||||
_serialize_semantic_field,
|
||||
_serialize_session_detail,
|
||||
_serialize_session_summary,
|
||||
_update_semantic_field_state,
|
||||
_build_session_version_conflict_http_exception,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/dataset-orchestration", tags=["Dataset Orchestration"])
|
||||
|
||||
|
||||
# [DEF:list_sessions:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: List resumable dataset review sessions for the current user.
|
||||
@router.get(
|
||||
"/sessions",
|
||||
response_model=SessionCollectionResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "READ")),
|
||||
],
|
||||
)
|
||||
async def list_sessions(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.list_sessions"):
|
||||
logger.reason(
|
||||
"Listing dataset review sessions",
|
||||
extra={"user_id": current_user.id, "page": page, "page_size": page_size},
|
||||
)
|
||||
sessions = repository.list_sessions_for_user(current_user.id)
|
||||
start = (page - 1) * page_size
|
||||
end = start + page_size
|
||||
items = [_serialize_session_summary(s) for s in sessions[start:end]]
|
||||
logger.reflect(
|
||||
"Session page assembled",
|
||||
extra={"user_id": current_user.id, "returned": len(items), "total": len(sessions)},
|
||||
)
|
||||
return SessionCollectionResponse(
|
||||
items=items, total=len(sessions), page=page,
|
||||
page_size=page_size, has_next=end < len(sessions),
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:list_sessions:Function]
|
||||
|
||||
|
||||
# [DEF:start_session:Function]
|
||||
# @COMPLEXITY: 4
|
||||
# @PURPOSE: Start a new dataset review session from a Superset link or dataset selection.
|
||||
@router.post(
|
||||
"/sessions",
|
||||
response_model=SessionSummary,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def start_session(
|
||||
request: StartSessionRequest,
|
||||
orchestrator=Depends(_get_orchestrator),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("start_session"):
|
||||
logger.reason(
|
||||
"Starting dataset review session",
|
||||
extra={"user_id": current_user.id, "environment_id": request.environment_id},
|
||||
)
|
||||
try:
|
||||
result = orchestrator.start_session(
|
||||
StartSessionCommand(
|
||||
user=current_user, environment_id=request.environment_id,
|
||||
source_kind=request.source_kind, source_input=request.source_input,
|
||||
)
|
||||
)
|
||||
except ValueError as exc:
|
||||
logger.explore("Session start rejected", extra={"user_id": current_user.id, "error": str(exc)})
|
||||
detail = str(exc)
|
||||
sc = status.HTTP_404_NOT_FOUND if detail == "Environment not found" else status.HTTP_400_BAD_REQUEST
|
||||
raise HTTPException(status_code=sc, detail=detail) from exc
|
||||
logger.reflect("Session started", extra={"session_id": result.session.session_id})
|
||||
return _serialize_session_summary(result.session)
|
||||
|
||||
|
||||
# [/DEF:start_session:Function]
|
||||
|
||||
|
||||
# [DEF:get_session_detail:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Return the full accessible dataset review session aggregate.
|
||||
@router.get(
|
||||
"/sessions/{session_id}",
|
||||
response_model=SessionSummary,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "READ")),
|
||||
],
|
||||
)
|
||||
async def get_session_detail(
|
||||
session_id: str,
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.get_session_detail"):
|
||||
session = _get_owned_session_or_404(repository, session_id, current_user)
|
||||
return _serialize_session_detail(session)
|
||||
|
||||
|
||||
# [/DEF:get_session_detail:Function]
|
||||
|
||||
|
||||
# [DEF:update_session:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Update resumable lifecycle status for an owned session.
|
||||
@router.patch(
|
||||
"/sessions/{session_id}",
|
||||
response_model=SessionSummary,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def update_session(
|
||||
session_id: str,
|
||||
request: UpdateSessionRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("update_session"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
session_record = cast(Any, session)
|
||||
session_record.status = request.status
|
||||
if request.status == SessionStatus.PAUSED:
|
||||
session_record.recommended_action = RecommendedAction.RESUME_SESSION
|
||||
elif request.status in {SessionStatus.ARCHIVED, SessionStatus.CANCELLED, SessionStatus.COMPLETED}:
|
||||
session_record.active_task_id = None
|
||||
_commit_owned_session_mutation(repository, session)
|
||||
_record_session_event(
|
||||
repository, session, current_user,
|
||||
event_type="session_status_updated",
|
||||
event_summary="Dataset review session lifecycle updated",
|
||||
event_details={"status": session_record.status.value, "version": session_record.version},
|
||||
)
|
||||
return _serialize_session_summary(session)
|
||||
|
||||
|
||||
# [/DEF:update_session:Function]
|
||||
|
||||
|
||||
# [DEF:delete_session:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Archive or hard-delete a session owned by the current user.
|
||||
@router.delete(
|
||||
"/sessions/{session_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def delete_session(
|
||||
session_id: str,
|
||||
hard_delete: bool = Query(False),
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("delete_session"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
if hard_delete:
|
||||
_record_session_event(repository, session, current_user, event_type="session_deleted", event_summary="Session hard-deleted", event_details={"hard_delete": True})
|
||||
repository.db.delete(session)
|
||||
repository.db.commit()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
session_record = cast(Any, session)
|
||||
session_record.status = SessionStatus.ARCHIVED
|
||||
session_record.active_task_id = None
|
||||
_commit_owned_session_mutation(repository, session)
|
||||
_record_session_event(repository, session, current_user, event_type="session_archived", event_summary="Session archived", event_details={"hard_delete": False, "version": session_record.version})
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
# [/DEF:delete_session:Function]
|
||||
|
||||
|
||||
# [DEF:export_documentation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Export documentation output for the current session.
|
||||
@router.get(
|
||||
"/sessions/{session_id}/exports/documentation",
|
||||
response_model=ExportArtifactResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "READ")),
|
||||
],
|
||||
)
|
||||
async def export_documentation(
|
||||
session_id: str,
|
||||
format: ArtifactFormat = Query(ArtifactFormat.JSON),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("export_documentation"):
|
||||
if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only json and markdown exports are supported")
|
||||
session = _get_owned_session_or_404(repository, session_id, current_user)
|
||||
payload = _build_documentation_export(session, format)
|
||||
return ExportArtifactResponse(
|
||||
artifact_id=f"documentation-{session.session_id}-{format.value}",
|
||||
session_id=session.session_id, artifact_type="documentation",
|
||||
format=format.value, storage_ref=payload["storage_ref"],
|
||||
created_by_user_id=current_user.id, content=payload["content"],
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:export_documentation:Function]
|
||||
|
||||
|
||||
# [DEF:export_validation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Export validation findings for the current session.
|
||||
@router.get(
|
||||
"/sessions/{session_id}/exports/validation",
|
||||
response_model=ExportArtifactResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "READ")),
|
||||
],
|
||||
)
|
||||
async def export_validation(
|
||||
session_id: str,
|
||||
format: ArtifactFormat = Query(ArtifactFormat.JSON),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("export_validation"):
|
||||
if format not in {ArtifactFormat.JSON, ArtifactFormat.MARKDOWN}:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Only json and markdown exports are supported")
|
||||
session = _get_owned_session_or_404(repository, session_id, current_user)
|
||||
payload = _build_validation_export(session, format)
|
||||
return ExportArtifactResponse(
|
||||
artifact_id=f"validation-{session.session_id}-{format.value}",
|
||||
session_id=session.session_id, artifact_type="validation_report",
|
||||
format=format.value, storage_ref=payload["storage_ref"],
|
||||
created_by_user_id=current_user.id, content=payload["content"],
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:export_validation:Function]
|
||||
|
||||
|
||||
# [DEF:get_clarification_state:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Return the current clarification session summary and active question payload.
|
||||
@router.get(
|
||||
"/sessions/{session_id}/clarification",
|
||||
response_model=ClarificationStateResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_clarification_flag),
|
||||
Depends(has_permission("dataset:session", "READ")),
|
||||
],
|
||||
)
|
||||
async def get_clarification_state(
|
||||
session_id: str,
|
||||
repository=Depends(_get_repository),
|
||||
clarification_engine=Depends(_get_clarification_engine),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("get_clarification_state"):
|
||||
session = _get_owned_session_or_404(repository, session_id, current_user)
|
||||
if not session.clarification_sessions:
|
||||
return _serialize_empty_clarification_state()
|
||||
cs = _get_latest_clarification_session_or_404(session)
|
||||
question = clarification_engine.build_question_payload(session)
|
||||
return _serialize_clarification_state(
|
||||
ClarificationStateResult(clarification_session=cs, current_question=question, session=session, changed_findings=[])
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:get_clarification_state:Function]
|
||||
|
||||
|
||||
# [DEF:resume_clarification:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Resume clarification mode on the highest-priority unresolved question.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/clarification/resume",
|
||||
response_model=ClarificationStateResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_clarification_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def resume_clarification(
|
||||
session_id: str,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
clarification_engine=Depends(_get_clarification_engine),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("resume_clarification"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
cs = _get_latest_clarification_session_or_404(session)
|
||||
question = clarification_engine.build_question_payload(session)
|
||||
return _serialize_clarification_state(
|
||||
ClarificationStateResult(clarification_session=cs, current_question=question, session=session, changed_findings=[])
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:resume_clarification:Function]
|
||||
|
||||
|
||||
# [DEF:record_clarification_answer:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist one clarification answer before advancing the active pointer.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/clarification/answers",
|
||||
response_model=ClarificationAnswerResultResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_clarification_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def record_clarification_answer(
|
||||
session_id: str,
|
||||
request: ClarificationAnswerRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
clarification_engine=Depends(_get_clarification_engine),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.record_clarification_answer"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
try:
|
||||
result = clarification_engine.record_answer(
|
||||
ClarificationAnswerCommand(
|
||||
session=session, question_id=request.question_id,
|
||||
answer_kind=request.answer_kind, answer_value=request.answer_value,
|
||||
user=current_user,
|
||||
)
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
return ClarificationAnswerResultResponse(
|
||||
clarification_state=_serialize_clarification_state(result),
|
||||
session=_serialize_session_summary(result.session),
|
||||
changed_findings=[ValidationFindingDto.model_validate(f, from_attributes=True) for f in result.changed_findings],
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:record_clarification_answer:Function]
|
||||
|
||||
|
||||
# [DEF:update_field_semantic:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Apply one field-level semantic candidate decision or manual override.
|
||||
@router.patch(
|
||||
"/sessions/{session_id}/fields/{field_id}/semantic",
|
||||
response_model=SemanticFieldEntryDto,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def update_field_semantic(
|
||||
session_id: str, field_id: str, request: FieldSemanticUpdateRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.update_field_semantic"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
field = _get_owned_field_or_404(session, field_id)
|
||||
_update_semantic_field_state(field, request, changed_by="user")
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session, refresh_targets=[field])
|
||||
_record_session_event(repository, session, current_user, event_type="semantic_field_updated", event_summary="Semantic field decision persisted", event_details={"field_id": field.field_id, "version": sr.version})
|
||||
return _serialize_semantic_field(field)
|
||||
|
||||
|
||||
# [/DEF:update_field_semantic:Function]
|
||||
|
||||
|
||||
# [DEF:lock_field_semantic:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Lock one semantic field against later automatic overwrite.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/fields/{field_id}/lock",
|
||||
response_model=SemanticFieldEntryDto,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def lock_field_semantic(
|
||||
session_id: str, field_id: str,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.lock_field_semantic"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
field = _get_owned_field_or_404(session, field_id)
|
||||
field.is_locked = True
|
||||
field.last_changed_by = "user"
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session, refresh_targets=[field])
|
||||
_record_session_event(repository, session, current_user, event_type="semantic_field_locked", event_summary="Semantic field lock persisted", event_details={"field_id": field.field_id, "version": sr.version})
|
||||
return _serialize_semantic_field(field)
|
||||
|
||||
|
||||
# [/DEF:lock_field_semantic:Function]
|
||||
|
||||
|
||||
# [DEF:unlock_field_semantic:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Unlock one semantic field so later automated candidate application may replace it.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/fields/{field_id}/unlock",
|
||||
response_model=SemanticFieldEntryDto,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def unlock_field_semantic(
|
||||
session_id: str, field_id: str,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.unlock_field_semantic"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
field = _get_owned_field_or_404(session, field_id)
|
||||
field.is_locked = False
|
||||
field.last_changed_by = "user"
|
||||
if field.provenance == FieldProvenance.MANUAL_OVERRIDE:
|
||||
field.provenance = FieldProvenance.UNRESOLVED
|
||||
field.needs_review = True
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session, refresh_targets=[field])
|
||||
_record_session_event(repository, session, current_user, event_type="semantic_field_unlocked", event_summary="Semantic field unlock persisted", event_details={"field_id": field.field_id, "version": sr.version})
|
||||
return _serialize_semantic_field(field)
|
||||
|
||||
|
||||
# [/DEF:unlock_field_semantic:Function]
|
||||
|
||||
|
||||
# [DEF:approve_batch_semantic_fields:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Approve multiple semantic candidate decisions in one batch.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/fields/semantic/approve-batch",
|
||||
response_model=List[SemanticFieldEntryDto],
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def approve_batch_semantic_fields(
|
||||
session_id: str, request: BatchApproveSemanticRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.approve_batch_semantic_fields"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
updated = []
|
||||
for item in request.items:
|
||||
field = _get_owned_field_or_404(session, item.field_id)
|
||||
_update_semantic_field_state(field, FieldSemanticUpdateRequest(candidate_id=item.candidate_id, lock_field=item.lock_field), changed_by="user")
|
||||
updated.append(field)
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session, refresh_targets=list(updated))
|
||||
_record_session_event(repository, session, current_user, event_type="semantic_fields_batch_approved", event_summary="Batch semantic approval persisted", event_details={"count": len(updated), "version": sr.version})
|
||||
return [_serialize_semantic_field(f) for f in updated]
|
||||
|
||||
|
||||
# [/DEF:approve_batch_semantic_fields:Function]
|
||||
|
||||
|
||||
# [DEF:list_execution_mappings:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Return the current mapping-review set for one accessible session.
|
||||
@router.get(
|
||||
"/sessions/{session_id}/mappings",
|
||||
response_model=MappingCollectionResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_execution_flag),
|
||||
Depends(has_permission("dataset:session", "READ")),
|
||||
],
|
||||
)
|
||||
async def list_execution_mappings(
|
||||
session_id: str,
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.list_execution_mappings"):
|
||||
session = _get_owned_session_or_404(repository, session_id, current_user)
|
||||
return MappingCollectionResponse(items=[_serialize_execution_mapping(m) for m in session.execution_mappings])
|
||||
|
||||
|
||||
# [/DEF:list_execution_mappings:Function]
|
||||
|
||||
|
||||
# [DEF:update_execution_mapping:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist one owner-authorized execution-mapping effective value override.
|
||||
@router.patch(
|
||||
"/sessions/{session_id}/mappings/{mapping_id}",
|
||||
response_model=ExecutionMappingDto,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_execution_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def update_execution_mapping(
|
||||
session_id: str, mapping_id: str, request: UpdateExecutionMappingRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.update_execution_mapping"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
mapping = _get_owned_mapping_or_404(session, mapping_id)
|
||||
if request.effective_value is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="effective_value is required")
|
||||
mapping.effective_value = request.effective_value
|
||||
mapping.mapping_method = MappingMethod(request.mapping_method or MappingMethod.MANUAL_OVERRIDE.value)
|
||||
mapping.transformation_note = request.transformation_note
|
||||
mapping.approval_state = ApprovalState.APPROVED
|
||||
mapping.approved_by_user_id = current_user.id
|
||||
mapping.approved_at = datetime.utcnow()
|
||||
session.last_activity_at = datetime.utcnow()
|
||||
session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW
|
||||
if session.readiness_state in {ReadinessState.MAPPING_REVIEW_NEEDED, ReadinessState.COMPILED_PREVIEW_READY, ReadinessState.RUN_READY, ReadinessState.RUN_IN_PROGRESS}:
|
||||
session.readiness_state = ReadinessState.COMPILED_PREVIEW_READY
|
||||
for preview in session.previews:
|
||||
if preview.preview_status == PreviewStatus.READY:
|
||||
preview.preview_status = PreviewStatus.STALE
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session, refresh_targets=[mapping])
|
||||
_record_session_event(repository, session, current_user, event_type="execution_mapping_updated", event_summary="Mapping override persisted", event_details={"mapping_id": mapping.mapping_id, "version": sr.version})
|
||||
return _serialize_execution_mapping(mapping)
|
||||
|
||||
|
||||
# [/DEF:update_execution_mapping:Function]
|
||||
|
||||
|
||||
# [DEF:approve_execution_mapping:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Explicitly approve a warning-sensitive mapping transformation.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/mappings/{mapping_id}/approve",
|
||||
response_model=ExecutionMappingDto,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_execution_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def approve_execution_mapping(
|
||||
session_id: str, mapping_id: str, request: ApproveMappingRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.approve_execution_mapping"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
mapping = _get_owned_mapping_or_404(session, mapping_id)
|
||||
mapping.approval_state = ApprovalState.APPROVED
|
||||
mapping.approved_by_user_id = current_user.id
|
||||
mapping.approved_at = datetime.utcnow()
|
||||
if request.approval_note:
|
||||
mapping.transformation_note = request.approval_note
|
||||
session.last_activity_at = datetime.utcnow()
|
||||
if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:
|
||||
session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session, refresh_targets=[mapping])
|
||||
_record_session_event(repository, session, current_user, event_type="execution_mapping_approved", event_summary="Mapping approval persisted", event_details={"mapping_id": mapping.mapping_id, "version": sr.version})
|
||||
return _serialize_execution_mapping(mapping)
|
||||
|
||||
|
||||
# [/DEF:approve_execution_mapping:Function]
|
||||
|
||||
|
||||
# [DEF:approve_batch_execution_mappings:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Approve multiple warning-sensitive execution mappings in one batch.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/mappings/approve-batch",
|
||||
response_model=List[ExecutionMappingDto],
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_execution_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def approve_batch_execution_mappings(
|
||||
session_id: str, request: BatchApproveMappingRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.approve_batch_execution_mappings"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
updated = []
|
||||
for mid in list(dict.fromkeys(request.mapping_ids)):
|
||||
mapping = _get_owned_mapping_or_404(session, mid)
|
||||
mapping.approval_state = ApprovalState.APPROVED
|
||||
mapping.approved_by_user_id = current_user.id
|
||||
mapping.approved_at = datetime.utcnow()
|
||||
if request.approval_note:
|
||||
mapping.transformation_note = request.approval_note
|
||||
updated.append(mapping)
|
||||
session.last_activity_at = datetime.utcnow()
|
||||
if session.readiness_state == ReadinessState.MAPPING_REVIEW_NEEDED:
|
||||
session.recommended_action = RecommendedAction.GENERATE_SQL_PREVIEW
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session, refresh_targets=list(updated))
|
||||
_record_session_event(repository, session, current_user, event_type="execution_mappings_batch_approved", event_summary="Batch mapping approval persisted", event_details={"count": len(updated), "version": sr.version})
|
||||
return [_serialize_execution_mapping(m) for m in updated]
|
||||
|
||||
|
||||
# [/DEF:approve_batch_execution_mappings:Function]
|
||||
|
||||
|
||||
# [DEF:trigger_preview_generation:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Trigger Superset-side preview compilation for the current owned execution context.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/preview",
|
||||
response_model=Union[CompiledPreviewDto, PreviewEnqueueResultResponse],
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_execution_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def trigger_preview_generation(
|
||||
session_id: str, response: Response,
|
||||
orchestrator=Depends(_get_orchestrator),
|
||||
repository=Depends(_get_repository),
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.trigger_preview_generation"):
|
||||
_prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
try:
|
||||
result = orchestrator.prepare_launch_preview(
|
||||
PreparePreviewCommand(user=current_user, session_id=session_id, expected_version=session_version)
|
||||
)
|
||||
except DatasetReviewSessionVersionConflictError as exc:
|
||||
raise _build_session_version_conflict_http_exception(exc) from exc
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
sc = status.HTTP_404_NOT_FOUND if detail in {"Session not found", "Environment not found"} else status.HTTP_409_CONFLICT if detail.startswith("Preview blocked:") else status.HTTP_400_BAD_REQUEST
|
||||
raise HTTPException(status_code=sc, detail=detail) from exc
|
||||
if result.preview.preview_status == PreviewStatus.PENDING:
|
||||
response.status_code = status.HTTP_202_ACCEPTED
|
||||
return PreviewEnqueueResultResponse(
|
||||
session_id=result.session.session_id,
|
||||
session_version=int(getattr(result.session, "version", 0) or 0),
|
||||
preview_status=result.preview.preview_status.value, task_id=None,
|
||||
)
|
||||
response.status_code = status.HTTP_200_OK
|
||||
return _serialize_preview(result.preview, session_version_fallback=int(getattr(result.session, "version", 0) or 0))
|
||||
|
||||
|
||||
# [/DEF:trigger_preview_generation:Function]
|
||||
|
||||
|
||||
# [DEF:launch_dataset:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Execute the current owned session launch handoff and return audited SQL Lab run context.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/launch",
|
||||
response_model=LaunchDatasetResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_execution_flag),
|
||||
Depends(has_permission("dataset:execution:launch", "EXECUTE")),
|
||||
],
|
||||
)
|
||||
async def launch_dataset(
|
||||
session_id: str,
|
||||
orchestrator=Depends(_get_orchestrator),
|
||||
repository=Depends(_get_repository),
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
config_manager=Depends(get_config_manager),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.launch_dataset"):
|
||||
_prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
try:
|
||||
result = orchestrator.launch_dataset(
|
||||
LaunchDatasetCommand(user=current_user, session_id=session_id, expected_version=session_version)
|
||||
)
|
||||
except DatasetReviewSessionVersionConflictError as exc:
|
||||
raise _build_session_version_conflict_http_exception(exc) from exc
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
sc = status.HTTP_404_NOT_FOUND if detail in {"Session not found", "Environment not found"} else status.HTTP_409_CONFLICT if detail.startswith("Launch blocked:") else status.HTTP_400_BAD_REQUEST
|
||||
raise HTTPException(status_code=sc, detail=detail) from exc
|
||||
environment = config_manager.get_environment(result.session.environment_id)
|
||||
env_url = getattr(environment, "url", "") if environment is not None else ""
|
||||
return LaunchDatasetResponse(
|
||||
run_context=_serialize_run_context(result.run_context),
|
||||
redirect_url=_build_sql_lab_redirect_url(environment_url=env_url, sql_lab_session_ref=result.run_context.sql_lab_session_ref),
|
||||
)
|
||||
|
||||
|
||||
# [/DEF:launch_dataset:Function]
|
||||
|
||||
|
||||
# [DEF:record_field_feedback:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for AI-assisted semantic field content.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/fields/{field_id}/feedback",
|
||||
response_model=FeedbackResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def record_field_feedback(
|
||||
session_id: str, field_id: str, request: FeedbackRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.record_field_feedback"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
field = _get_owned_field_or_404(session, field_id)
|
||||
field.user_feedback = request.feedback
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session)
|
||||
_record_session_event(repository, session, current_user, event_type="semantic_field_feedback_recorded", event_summary="Feedback persisted", event_details={"field_id": field.field_id, "feedback": request.feedback, "version": sr.version})
|
||||
return FeedbackResponse(target_id=field.field_id, feedback=request.feedback)
|
||||
|
||||
|
||||
# [/DEF:record_field_feedback:Function]
|
||||
|
||||
|
||||
# [DEF:record_clarification_feedback:Function]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Persist thumbs up/down feedback for clarification question/answer content.
|
||||
@router.post(
|
||||
"/sessions/{session_id}/clarification/questions/{question_id}/feedback",
|
||||
response_model=FeedbackResponse,
|
||||
dependencies=[
|
||||
Depends(_require_auto_review_flag),
|
||||
Depends(_require_clarification_flag),
|
||||
Depends(has_permission("dataset:session", "MANAGE")),
|
||||
],
|
||||
)
|
||||
async def record_clarification_feedback(
|
||||
session_id: str, question_id: str, request: FeedbackRequest,
|
||||
session_version: int = Depends(_require_session_version_header),
|
||||
repository=Depends(_get_repository),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
with belief_scope("dataset_review.record_clarification_feedback"):
|
||||
session = _prepare_owned_session_mutation(repository, session_id, current_user, session_version)
|
||||
cs = _get_latest_clarification_session_or_404(session)
|
||||
question = next((q for q in cs.questions if q.question_id == question_id), None)
|
||||
if question is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Clarification question not found")
|
||||
if question.answer is None:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Clarification answer not found")
|
||||
question.answer.user_feedback = request.feedback
|
||||
sr = cast(Any, session)
|
||||
_commit_owned_session_mutation(repository, session)
|
||||
_record_session_event(repository, session, current_user, event_type="clarification_feedback_recorded", event_summary="Feedback persisted", event_details={"question_id": question.question_id, "feedback": request.feedback, "version": sr.version})
|
||||
return FeedbackResponse(target_id=question.question_id, feedback=request.feedback)
|
||||
|
||||
|
||||
# [/DEF:record_clarification_feedback:Function]
|
||||
|
||||
|
||||
# [/DEF:DatasetReviewRoutes:Module]
|
||||
Reference in New Issue
Block a user