feat(14-04): add analysis API routes and typed schemas

- POST /analysis/connections/{id}/estimate: scope estimate without bytes (T-14-04)
- POST /analysis/connections/{id}/jobs: enqueue job, returns job_id (ANALYZE-01)
- GET /analysis/jobs, GET /analysis/jobs/{id}: job status list and detail
- POST /analysis/jobs/{id}/cancel: batch cancel (queued items immediate; running cooperative)
- POST /analysis/jobs/{id}/items/{item_id}/cancel|skip|retry: per-item controls (ANALYZE-05)
- AnalysisEstimateRequest/Out, AnalysisEnqueueRequest/Out, AnalysisJobOut: typed schemas
- AnalysisJobOut: both simple (waiting/working/done/skipped/failed) and per-stage counts
- All routes use get_regular_user — admin blocked (T-14-01)
- Response schemas exclude credentials_enc, object_key, raw provider URLs (T-14-02)
- 14/14 test_cloud_analysis_api.py pass; 7 new analysis security tests pass (33/33 total)
This commit is contained in:
curo1305
2026-06-23 15:50:37 +02:00
parent 3f26cd2059
commit ba48d625dd
3 changed files with 845 additions and 10 deletions
+140
View File
@@ -255,3 +255,143 @@ class CacheSettingsUpdateRequest(BaseModel):
ge=1,
description="Preferred byte ceiling for the local byte cache",
)
# ── Phase 14 analysis request schemas ─────────────────────────────────────────
class AnalysisEstimateRequest(BaseModel):
"""Request body for POST /analysis/connections/{id}/estimate.
scope: "file" | "selection" | "folder" | "connection"
provider_item_ids: Required for file/selection/folder scope. Omitted for connection scope.
recursive: Expand folder subtree recursively (folder scope only; connection always recursive).
"""
scope: str = Field(..., description="file | selection | folder | connection")
provider_item_ids: Optional[List[str]] = Field(
default=None,
description="Provider item IDs for file/selection/folder scope",
)
recursive: bool = Field(
default=False,
description="Expand folder children recursively (folder scope)",
)
class AnalysisEnqueueRequest(BaseModel):
"""Request body for POST /analysis/connections/{id}/jobs.
scope: "file" | "selection" | "folder" | "connection"
provider_item_ids: Required for file/selection/folder scope.
recursive: Expand folder subtree recursively.
failure_behavior: "pause_batch" (default) | "continue_item" (D-11).
"""
scope: str = Field(..., description="file | selection | folder | connection")
provider_item_ids: Optional[List[str]] = Field(default=None)
recursive: bool = Field(default=False)
failure_behavior: str = Field(
default="pause_batch",
description="pause_batch | continue_item",
)
# ── Phase 14 analysis response schemas ────────────────────────────────────────
class AnalysisEstimateOut(BaseModel):
"""Estimate response — no credentials, bytes, or object_key (T-14-02).
supported_count: Files that can be analysed.
unsupported_count: Items that cannot be analysed (unsupported type, folder).
total_provider_bytes: Sum of provider_size across supported items.
recursive: Whether the estimate was recursive.
is_partial: True when metadata expansion was incomplete.
scope_kind: Echo of the requested scope.
"""
supported_count: int = 0
unsupported_count: int = 0
total_provider_bytes: int = 0
recursive: bool = False
is_partial: bool = False
scope_kind: str
class AnalysisJobOut(BaseModel):
"""Job status response — no credentials or object_key (T-14-02).
Exposes both simple and detailed aggregate counts. Caller requests detail
via ?detail=true query parameter.
Simple (default): waiting_count, working_count, done_count,
skipped_count, failed_count, total_count.
Detailed: queued_count, downloading_count, extracting_count,
classifying_count, indexed_count, already_current_count,
cancelled_count, failed_count, unsupported_count.
"""
job_id: str
connection_id: str
scope_kind: str
status: str
failure_behavior: str
recursive: bool = False
total_count: int = 0
# Simple labels (always present)
waiting_count: int = 0
working_count: int = 0
done_count: int = 0
skipped_count: int = 0
failed_count: int = 0
# Detailed labels (always present — 0 when detail=false, per-stage values when detail=true)
queued_count: int = 0
downloading_count: int = 0
extracting_count: int = 0
classifying_count: int = 0
indexed_count: int = 0
already_current_count: int = 0
cancelled_count: int = 0
unsupported_count: int = 0
created_at: Optional[datetime] = None
started_at: Optional[datetime] = None
finished_at: Optional[datetime] = None
class AnalysisEnqueueOut(BaseModel):
"""Enqueue response — returns a stable job_id for status polling (ANALYZE-01).
No credentials or object_key in response (T-14-02).
"""
job_id: str
status: str
total_count: int = 0
queued_count: int = 0
already_current_count: int = 0
unsupported_count: int = 0
class AnalysisJobItemOut(BaseModel):
"""Per-item job status — no credentials, object_key, or raw provider data (T-14-02)."""
id: str
cloud_item_id: str
provider_item_id: str
status: str
error_code: Optional[str] = None
error_message: Optional[str] = None
retry_count: int = 0
created_at: Optional[datetime] = None
finished_at: Optional[datetime] = None
class AnalysisControlOut(BaseModel):
"""Generic control result for cancel/skip/retry operations."""
kind: str # "cancelled" | "skipped" | "retried"
reason: Optional[str] = None
job_id: Optional[str] = None
item_id: Optional[str] = None