Rescue Paperjet v1 implementation
This commit is contained in:
parent
7ff5c8130d
commit
db9e2ca51b
83 changed files with 6186 additions and 3894 deletions
|
|
@ -1,91 +1,95 @@
|
|||
from typing import Any
|
||||
"""Opaque working-state annotation endpoints."""
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
|
||||
from app.db import get_db
|
||||
from app.models.document import Document
|
||||
from app.models.annotation_state import AnnotationState
|
||||
from app.auth.dependencies import get_current_user, verify_csrf
|
||||
from app.schemas.annotations import AnnotationStateResponse, AnnotationStateUpdateRequest, AnnotationStateUpdateResponse
|
||||
from app.db import get_db
|
||||
from app.models.annotation_state import AnnotationState
|
||||
from app.models.document import Document
|
||||
from app.schemas.annotations import (
|
||||
AnnotationPayload,
|
||||
AnnotationStateResponse,
|
||||
AnnotationStateUpdateRequest,
|
||||
AnnotationStateUpdateResponse,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/documents", tags=["annotations"])
|
||||
|
||||
router = APIRouter(prefix="/documents", tags=["Annotations"])
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
return datetime.now(UTC).isoformat()
|
||||
|
||||
@router.get("/{document_id}/annotations", response_model=AnnotationStateResponse)
|
||||
async def get_annotations(
|
||||
document_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
user_id: Any = Depends(get_current_user)
|
||||
):
|
||||
doc = db.scalar(select(Document).where(Document.id == document_id, Document.deleted_at.is_(None)))
|
||||
if not doc:
|
||||
|
||||
def _utc(value: datetime) -> datetime:
|
||||
return value.replace(tzinfo=UTC) if value.tzinfo is None else value.astimezone(UTC)
|
||||
|
||||
|
||||
def _document_or_404(document_id: str, db: Session) -> Document:
|
||||
document = db.scalar(
|
||||
select(Document).where(Document.id == document_id, Document.deleted_at.is_(None))
|
||||
)
|
||||
if not document:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
state = db.scalar(select(AnnotationState).where(AnnotationState.document_id == document_id))
|
||||
|
||||
return document
|
||||
|
||||
|
||||
def _read_data(state: AnnotationState | None) -> list[AnnotationPayload]:
|
||||
if not state:
|
||||
return AnnotationStateResponse(data=[], updatedAt=datetime.now(timezone.utc))
|
||||
|
||||
return []
|
||||
try:
|
||||
data = json.loads(state.data)
|
||||
except json.JSONDecodeError:
|
||||
data = []
|
||||
|
||||
return AnnotationStateResponse(
|
||||
data=data,
|
||||
updatedAt=datetime.fromisoformat(state.updated_at)
|
||||
)
|
||||
return []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
@router.put("/{document_id}/annotations", response_model=AnnotationStateUpdateResponse, dependencies=[Depends(verify_csrf)])
|
||||
async def update_annotations(
|
||||
|
||||
@router.get("/{document_id}/annotations", response_model=AnnotationStateResponse)
|
||||
def get_annotations(
|
||||
document_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
_user_id: int = Depends(get_current_user),
|
||||
) -> AnnotationStateResponse:
|
||||
_document_or_404(document_id, db)
|
||||
state = db.scalar(select(AnnotationState).where(AnnotationState.document_id == document_id))
|
||||
updated_at = datetime.now(UTC) if not state else _utc(datetime.fromisoformat(state.updated_at))
|
||||
return AnnotationStateResponse(data=_read_data(state), updatedAt=updated_at)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{document_id}/annotations",
|
||||
response_model=AnnotationStateUpdateResponse,
|
||||
dependencies=[Depends(verify_csrf)],
|
||||
)
|
||||
def update_annotations(
|
||||
document_id: str,
|
||||
request: AnnotationStateUpdateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
user_id: Any = Depends(get_current_user)
|
||||
):
|
||||
doc = db.scalar(select(Document).where(Document.id == document_id, Document.deleted_at.is_(None)))
|
||||
if not doc:
|
||||
raise HTTPException(status_code=404, detail="Document not found")
|
||||
|
||||
_user_id: int = Depends(get_current_user),
|
||||
) -> AnnotationStateUpdateResponse:
|
||||
document = _document_or_404(document_id, db)
|
||||
state = db.scalar(select(AnnotationState).where(AnnotationState.document_id == document_id))
|
||||
|
||||
# Check for stale tab
|
||||
|
||||
if state and request.baseUpdatedAt:
|
||||
current_updated_at = datetime.fromisoformat(state.updated_at).replace(tzinfo=timezone.utc)
|
||||
request_updated_at = request.baseUpdatedAt.replace(tzinfo=timezone.utc)
|
||||
# Allow a small grace period for timezone/parsing discrepancies
|
||||
current_updated_at = _utc(datetime.fromisoformat(state.updated_at))
|
||||
request_updated_at = _utc(request.baseUpdatedAt)
|
||||
if abs((current_updated_at - request_updated_at).total_seconds()) > 1.0:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Conflict: annotations have been modified since {request.baseUpdatedAt}"
|
||||
status_code=409,
|
||||
detail=f"Conflict: annotations have been modified since {request.baseUpdatedAt}",
|
||||
)
|
||||
|
||||
now = _now_iso()
|
||||
|
||||
import json
|
||||
|
||||
# serialize request.data list of models to json string
|
||||
data_json = json.dumps([a.model_dump(mode="json", by_alias=True) for a in request.data])
|
||||
|
||||
if not state:
|
||||
state = AnnotationState(
|
||||
document_id=document_id,
|
||||
data=data_json,
|
||||
updated_at=now
|
||||
)
|
||||
db.add(state)
|
||||
else:
|
||||
now = _now_iso()
|
||||
data_json = json.dumps(request.data, separators=(",", ":"))
|
||||
if state:
|
||||
state.data = data_json
|
||||
state.updated_at = now
|
||||
|
||||
# Bump document updated_at
|
||||
doc.updated_at = now
|
||||
|
||||
else:
|
||||
db.add(AnnotationState(document_id=document_id, data=data_json, updated_at=now))
|
||||
document.updated_at = now
|
||||
db.commit()
|
||||
|
||||
return AnnotationStateUpdateResponse(updatedAt=datetime.fromisoformat(now))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue