Introduce reusable ParserProfile and ParseChannel, pair jobs with enqueue flatten, Events admin CRUD, and drop inline/legacy parser-builder UI and aliases. Co-authored-by: Cursor <cursoragent@cursor.com>
165 lines
5.4 KiB
Python
165 lines
5.4 KiB
Python
"""Admin CRUD for reusable parser profiles + generate/preview."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.orm import Session
|
|
|
|
from contracts.heuristic_profile import HeuristicProfile
|
|
|
|
from ..database import get_db
|
|
from ..models import ParseJob, ParserProfile
|
|
from ..schemas import ParserProfileCreate, ParserProfileRead, ParserProfileUpdate
|
|
from ..services import parser_builder as builder
|
|
|
|
router = APIRouter(prefix="/admin/parser-profiles", tags=["parser-profiles"])
|
|
|
|
|
|
class GenerateRequest(BaseModel):
|
|
sample_post: str = Field(min_length=1)
|
|
|
|
|
|
class GenerateResponse(BaseModel):
|
|
profile: dict[str, Any]
|
|
|
|
|
|
class PreviewRequest(BaseModel):
|
|
sample_post: str = Field(min_length=1)
|
|
heuristic_profile: dict[str, Any] | None = None
|
|
# Accept alias used by older builder UI
|
|
profile: dict[str, Any] | None = None
|
|
|
|
|
|
class PreviewResponse(BaseModel):
|
|
fields: dict[str, str]
|
|
|
|
|
|
def _validate_heuristic_profile(raw: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
return HeuristicProfile.model_validate(raw).model_dump()
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail=f"Invalid heuristic_profile: {exc}") from exc
|
|
|
|
|
|
def _profile_status(heuristic_profile: dict | None, explicit: str | None = None) -> str:
|
|
if explicit:
|
|
return explicit
|
|
return "ready" if heuristic_profile else "draft"
|
|
|
|
|
|
@router.get("/target-fields")
|
|
def target_fields():
|
|
return {"fields": builder.get_target_fields()}
|
|
|
|
|
|
@router.post("/generate", response_model=GenerateResponse)
|
|
async def generate_profile(payload: GenerateRequest):
|
|
if not builder.deepseek_enabled():
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="DEEPSEEK_API_KEY is not set on ca-api. Add it to .env for parser generation.",
|
|
)
|
|
try:
|
|
profile = await builder.generate_profile(payload.sample_post)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
except Exception as exc:
|
|
raise HTTPException(
|
|
status_code=502,
|
|
detail=f"DeepSeek generate failed: {exc}",
|
|
) from exc
|
|
return GenerateResponse(profile=profile.model_dump())
|
|
|
|
|
|
@router.post("/preview", response_model=PreviewResponse)
|
|
def preview_profile(payload: PreviewRequest):
|
|
raw = payload.heuristic_profile if payload.heuristic_profile is not None else payload.profile
|
|
if raw is None:
|
|
raise HTTPException(status_code=400, detail="heuristic_profile is required")
|
|
try:
|
|
HeuristicProfile.model_validate(raw)
|
|
fields = builder.preview_with_profile(payload.sample_post, raw)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return PreviewResponse(fields=fields)
|
|
|
|
|
|
@router.get("", response_model=list[ParserProfileRead])
|
|
def list_profiles(db: Session = Depends(get_db)):
|
|
return db.query(ParserProfile).order_by(ParserProfile.id.desc()).all()
|
|
|
|
|
|
@router.post("", response_model=ParserProfileRead, status_code=201)
|
|
def create_profile(payload: ParserProfileCreate, db: Session = Depends(get_db)):
|
|
heuristic = _validate_heuristic_profile(payload.heuristic_profile)
|
|
profile = ParserProfile(
|
|
name=payload.name.strip(),
|
|
sample_post=payload.sample_post or "",
|
|
heuristic_profile=heuristic,
|
|
status=_profile_status(heuristic, payload.status),
|
|
)
|
|
db.add(profile)
|
|
db.commit()
|
|
db.refresh(profile)
|
|
return profile
|
|
|
|
|
|
@router.get("/{profile_id}", response_model=ParserProfileRead)
|
|
def get_profile(profile_id: int, db: Session = Depends(get_db)):
|
|
profile = db.query(ParserProfile).filter(ParserProfile.id == profile_id).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Profile not found")
|
|
return profile
|
|
|
|
|
|
@router.patch("/{profile_id}", response_model=ParserProfileRead)
|
|
def update_profile(
|
|
profile_id: int,
|
|
payload: ParserProfileUpdate,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
profile = db.query(ParserProfile).filter(ParserProfile.id == profile_id).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Profile not found")
|
|
|
|
if payload.name is not None:
|
|
profile.name = payload.name.strip()
|
|
if payload.sample_post is not None:
|
|
profile.sample_post = payload.sample_post
|
|
if "heuristic_profile" in payload.model_fields_set:
|
|
profile.heuristic_profile = _validate_heuristic_profile(payload.heuristic_profile)
|
|
if payload.status is not None:
|
|
profile.status = payload.status
|
|
elif "heuristic_profile" in payload.model_fields_set:
|
|
profile.status = _profile_status(profile.heuristic_profile)
|
|
|
|
db.commit()
|
|
db.refresh(profile)
|
|
return profile
|
|
|
|
|
|
@router.delete("/{profile_id}", status_code=204)
|
|
def delete_profile(profile_id: int, db: Session = Depends(get_db)):
|
|
profile = db.query(ParserProfile).filter(ParserProfile.id == profile_id).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail="Profile not found")
|
|
|
|
linked = (
|
|
db.query(ParseJob)
|
|
.filter(ParseJob.profile_id == profile_id, ParseJob.status.in_(("queued", "running")))
|
|
.count()
|
|
)
|
|
if linked:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="Cannot delete profile used by queued/running jobs",
|
|
)
|
|
|
|
db.delete(profile)
|
|
db.commit()
|