shreerangss's picture
Add object storage and document ingestion lifecycle
c72325f
Raw History Blame Contribute Delete
5.12 kB
"""Ownership-safe persistence helpers for source-document object metadata."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from sqlalchemy import select
from backend.app.storage.base import document_pdf_object_key, validate_object_key
from persistence.ownership import require_owned_document
class DocumentObjectMetadataError(ValueError):
"""Raised when document object metadata is invalid or internally inconsistent."""
@dataclass(frozen=True)
class StoredDocumentObjectMetadata:
document_id: str
user_id: str
object_key: str
content_type: str
byte_size: int
def _validate_metadata(
*,
document_id: str,
object_key: str,
content_type: str,
byte_size: int,
) -> tuple[str, str, int]:
validated_key = validate_object_key(object_key)
expected_key = document_pdf_object_key(document_id)
if validated_key != expected_key:
raise DocumentObjectMetadataError(
"Document object key does not match the document's generated storage key."
)
normalized_content_type = content_type.strip() if isinstance(content_type, str) else ""
if not normalized_content_type or len(normalized_content_type) > 255:
raise DocumentObjectMetadataError("Document content type is invalid.")
if isinstance(byte_size, bool) or not isinstance(byte_size, int) or byte_size <= 0:
raise DocumentObjectMetadataError("Document byte size must be a positive integer.")
return validated_key, normalized_content_type, byte_size
def document_object_metadata_from_row(document: object) -> Optional[StoredDocumentObjectMetadata]:
"""Validate and project object metadata from an already-loaded document row."""
values = (
getattr(document, "object_key", None),
getattr(document, "content_type", None),
getattr(document, "byte_size", None),
)
if values == (None, None, None):
return None
if any(value is None for value in values):
raise DocumentObjectMetadataError("Document object metadata is incomplete.")
object_key, content_type, byte_size = _validate_metadata(
document_id=str(getattr(document, "id")),
object_key=str(values[0]),
content_type=str(values[1]),
byte_size=values[2],
)
return StoredDocumentObjectMetadata(
document_id=str(getattr(document, "id")),
user_id=str(getattr(document, "user_id")),
object_key=object_key,
content_type=content_type,
byte_size=byte_size,
)
def attach_owned_document_object_metadata(
*,
user_id: str,
document_id: str,
object_key: str,
content_type: str,
byte_size: int,
) -> StoredDocumentObjectMetadata:
"""Attach generated object metadata after enforcing document ownership."""
from persistence.db import SessionLocal
with SessionLocal() as session:
try:
document = require_owned_document(
session,
user_id=user_id,
document_id=document_id,
)
validated_key, normalized_content_type, validated_byte_size = _validate_metadata(
document_id=str(document.id),
object_key=object_key,
content_type=content_type,
byte_size=byte_size,
)
document.object_key = validated_key
document.content_type = normalized_content_type
document.byte_size = validated_byte_size
session.commit()
return StoredDocumentObjectMetadata(
document_id=str(document.id),
user_id=str(document.user_id),
object_key=validated_key,
content_type=normalized_content_type,
byte_size=validated_byte_size,
)
except Exception:
session.rollback()
raise
def get_owned_document_object_metadata(
*,
user_id: str,
document_id: str,
) -> Optional[StoredDocumentObjectMetadata]:
"""Load metadata only after resolving a document in the user's namespace."""
from persistence.db import SessionLocal
with SessionLocal() as session:
document = require_owned_document(
session,
user_id=user_id,
document_id=document_id,
)
return document_object_metadata_from_row(document)
def get_document_object_metadata(
*,
document_id: str,
) -> Optional[StoredDocumentObjectMetadata]:
"""Internal lookup for a future worker that receives only a document ID."""
from persistence.db import SessionLocal
from persistence.models import Document
if not isinstance(document_id, str) or not document_id.strip():
raise DocumentObjectMetadataError("Document ID must not be empty.")
with SessionLocal() as session:
document = session.execute(
select(Document).where(Document.id == document_id)
).scalar_one_or_none()
if document is None:
return None
return document_object_metadata_from_row(document)