generated from john/python-template
This commit is contained in:
@@ -72,19 +72,17 @@ class DocumentPeopleResponse(ApiModel):
|
||||
|
||||
|
||||
def _document_type_to_read(item: DocumentType) -> DocumentTypeRead:
|
||||
return DocumentTypeRead(
|
||||
id=item.id,
|
||||
label=item.label,
|
||||
is_active=item.is_active,
|
||||
)
|
||||
item_id, label, is_active = _registry_read_values(item)
|
||||
return DocumentTypeRead(id=item_id, label=label, is_active=is_active)
|
||||
|
||||
|
||||
def _person_role_to_read(item: PersonRole) -> PersonRoleRead:
|
||||
return PersonRoleRead(
|
||||
id=item.id,
|
||||
label=item.label,
|
||||
is_active=item.is_active,
|
||||
)
|
||||
item_id, label, is_active = _registry_read_values(item)
|
||||
return PersonRoleRead(id=item_id, label=label, is_active=is_active)
|
||||
|
||||
|
||||
def _registry_read_values(item: DocumentType | PersonRole) -> tuple[UUID, str, bool]:
|
||||
return item.id, item.label, item.is_active
|
||||
|
||||
|
||||
def _document_person_to_read(item: DocumentPerson) -> DocumentPersonRead:
|
||||
|
||||
@@ -26,6 +26,7 @@ from ..errors import AppError
|
||||
from ..errors import ErrorCategory
|
||||
from .base import ServiceBase
|
||||
from .registry import RegistryService
|
||||
from .registry import RegistrySummary
|
||||
from .source_media import lookup_source_mime_type
|
||||
from .source_media import supported_source_formats
|
||||
|
||||
@@ -71,15 +72,7 @@ class DocumentTypeRegistry(RegistryService[DocumentType]):
|
||||
return col(Document.document_type_id)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DocumentTypeSummary:
|
||||
"""Settings read model for a Document Type and its usage count."""
|
||||
|
||||
id: UUID
|
||||
label: str
|
||||
is_active: bool
|
||||
is_built_in: bool
|
||||
document_count: int
|
||||
type DocumentTypeSummary = RegistrySummary
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -377,12 +370,12 @@ class DocumentService(ServiceBase):
|
||||
"""List Document Types alphabetically with current usage counts."""
|
||||
rows = await self._document_types.list_entries_with_counts(session=session)
|
||||
return [
|
||||
DocumentTypeSummary(
|
||||
RegistrySummary(
|
||||
id=document_type.id,
|
||||
label=document_type.label,
|
||||
is_active=document_type.is_active,
|
||||
is_built_in=document_type.semantic_key is not None,
|
||||
document_count=document_count,
|
||||
reference_count=document_count,
|
||||
)
|
||||
for document_type, document_count in rows
|
||||
]
|
||||
|
||||
@@ -52,6 +52,36 @@ async def write_media_bytes(
|
||||
return stored_path
|
||||
|
||||
|
||||
async def persist_named_media(
|
||||
*,
|
||||
root: Path,
|
||||
filename: str,
|
||||
file_bytes: bytes,
|
||||
error: type[AppError],
|
||||
failure_message: str,
|
||||
failure_suggestion: str,
|
||||
log_label: str,
|
||||
namespace: Path | str | None = None,
|
||||
filename_stem: str | None = None,
|
||||
preserve_original_name: bool = False,
|
||||
) -> Path:
|
||||
"""Resolve a target directory/name and persist media bytes safely."""
|
||||
target_dir = root if namespace is None else root / Path(namespace)
|
||||
stored_name = Path(filename).name if preserve_original_name else build_stored_filename(
|
||||
filename=filename,
|
||||
filename_stem=filename_stem,
|
||||
)
|
||||
return await write_media_bytes(
|
||||
target_dir=target_dir,
|
||||
stored_name=stored_name,
|
||||
file_bytes=file_bytes,
|
||||
error=error,
|
||||
failure_message=failure_message,
|
||||
failure_suggestion=failure_suggestion,
|
||||
log_label=log_label,
|
||||
)
|
||||
|
||||
|
||||
def _write(stored_path: Path, file_bytes: bytes) -> None:
|
||||
stored_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
stored_path.write_bytes(file_bytes)
|
||||
|
||||
@@ -28,9 +28,9 @@ from ..db.models import PersonRole
|
||||
from ..errors import AppError
|
||||
from ..errors import ErrorCategory
|
||||
from .base import ServiceBase
|
||||
from .media_storage import build_stored_filename
|
||||
from .media_storage import write_media_bytes
|
||||
from .media_storage import persist_named_media
|
||||
from .registry import RegistryService
|
||||
from .registry import RegistrySummary
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -83,15 +83,7 @@ def normalize_family_search_id(value: str | None) -> str | None:
|
||||
return normalized
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PersonRoleSummary:
|
||||
"""Settings read model for a Person Role and its usage count."""
|
||||
|
||||
id: UUID
|
||||
label: str
|
||||
is_active: bool
|
||||
is_built_in: bool
|
||||
link_count: int
|
||||
type PersonRoleSummary = RegistrySummary
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -241,12 +233,12 @@ class PeopleService(ServiceBase):
|
||||
"""List Person Roles alphabetically with current link counts."""
|
||||
rows = await self._person_roles.list_entries_with_counts(session=session)
|
||||
return [
|
||||
PersonRoleSummary(
|
||||
RegistrySummary(
|
||||
id=role.id,
|
||||
label=role.label,
|
||||
is_active=role.is_active,
|
||||
is_built_in=role.semantic_key is not None,
|
||||
link_count=link_count,
|
||||
reference_count=link_count,
|
||||
)
|
||||
for role, link_count in rows
|
||||
]
|
||||
@@ -543,9 +535,10 @@ async def store_person_portrait(
|
||||
)
|
||||
|
||||
runtime_settings = settings or get_settings()
|
||||
return await write_media_bytes(
|
||||
target_dir=runtime_settings.upload_dir / "persons" / str(person_id),
|
||||
stored_name=build_stored_filename(filename=filename),
|
||||
return await persist_named_media(
|
||||
root=runtime_settings.upload_dir,
|
||||
namespace=Path("persons") / str(person_id),
|
||||
filename=filename,
|
||||
file_bytes=file_bytes,
|
||||
error=PersonMediaError,
|
||||
failure_message="Failed to persist Person portrait media",
|
||||
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Protocol
|
||||
from uuid import UUID
|
||||
@@ -43,6 +44,27 @@ class RegistryEntry(Protocol):
|
||||
def __init__(self, /, **data: Any) -> None: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RegistrySummary:
|
||||
"""Shared settings read model for label-keyed registries and usage counts."""
|
||||
|
||||
id: UUID
|
||||
label: str
|
||||
is_active: bool
|
||||
is_built_in: bool
|
||||
reference_count: int
|
||||
|
||||
@property
|
||||
def document_count(self) -> int:
|
||||
"""Backward-compatible alias for document-type settings consumers."""
|
||||
return self.reference_count
|
||||
|
||||
@property
|
||||
def link_count(self) -> int:
|
||||
"""Backward-compatible alias for person-role settings consumers."""
|
||||
return self.reference_count
|
||||
|
||||
|
||||
class RegistryService[ModelT: RegistryEntry](ServiceBase):
|
||||
"""Generic create/read/update/delete behavior for a registry table.
|
||||
|
||||
|
||||
@@ -24,8 +24,7 @@ from ..db.models import Source
|
||||
from ..db.session import SessionFactory
|
||||
from ..db.session import session_scope
|
||||
from .errors import TranscriptionError
|
||||
from .media_storage import build_stored_filename
|
||||
from .media_storage import write_media_bytes
|
||||
from .media_storage import persist_named_media
|
||||
from .normalization import normalize_orientation_async
|
||||
from .sources import build_prompt_execution
|
||||
from .sources import source_mime_type
|
||||
@@ -375,9 +374,11 @@ async def store_source_file(
|
||||
file_bytes = normalized.content
|
||||
|
||||
upload_dir = runtime_settings.upload_dir
|
||||
stored_path = await write_media_bytes(
|
||||
target_dir=upload_dir if relative_directory is None else upload_dir / relative_directory,
|
||||
stored_name=build_stored_filename(filename=filename, filename_stem=filename_stem),
|
||||
stored_path = await persist_named_media(
|
||||
root=upload_dir,
|
||||
namespace=relative_directory,
|
||||
filename=filename,
|
||||
filename_stem=filename_stem,
|
||||
file_bytes=file_bytes,
|
||||
error=SourceStorageError,
|
||||
failure_message="Failed to persist Source file",
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TypeVar
|
||||
|
||||
from nicegui import ui
|
||||
|
||||
from transcription.errors import AppError
|
||||
@@ -9,6 +14,33 @@ from transcription.errors import ErrorCategory
|
||||
from transcription.errors import canonical_error_category
|
||||
from transcription.errors import classify_unexpected_error
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class UiActionOutcome[T]:
|
||||
"""Result of a UI action wrapper."""
|
||||
|
||||
ok: bool
|
||||
value: T | None = None
|
||||
|
||||
|
||||
async def run_ui_action(
|
||||
*,
|
||||
operation: str,
|
||||
title: str,
|
||||
action: Callable[[], Awaitable[T]],
|
||||
passthrough: tuple[type[Exception], ...] = (),
|
||||
) -> UiActionOutcome[T]:
|
||||
"""Run an async UI action and render consistent errors."""
|
||||
try:
|
||||
return UiActionOutcome(ok=True, value=await action())
|
||||
except Exception as exc:
|
||||
if isinstance(exc, passthrough):
|
||||
raise
|
||||
show_error(exc, title=title, operation=operation)
|
||||
return UiActionOutcome(ok=False, value=None)
|
||||
|
||||
|
||||
def to_app_error(exc: Exception, *, operation: str) -> AppError:
|
||||
"""Normalize any exception for consistent UI display."""
|
||||
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
from transcription.config import Settings
|
||||
from transcription.config import get_settings
|
||||
from transcription.errors import AppError
|
||||
from transcription.services.media_storage import write_media_bytes
|
||||
from transcription.services.media_storage import persist_named_media
|
||||
|
||||
HOME_PAGE_MARKDOWN_NAME = "homepage.md"
|
||||
SUPPORTED_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff"}
|
||||
@@ -68,9 +68,10 @@ async def store_homepage_image(
|
||||
msg = "Homepage image filename is required"
|
||||
raise ValueError(msg)
|
||||
|
||||
return await write_media_bytes(
|
||||
target_dir=homepage_dir(settings),
|
||||
stored_name=safe_name,
|
||||
return await persist_named_media(
|
||||
root=homepage_dir(settings),
|
||||
filename=safe_name,
|
||||
preserve_original_name=True,
|
||||
file_bytes=file_bytes,
|
||||
error=HomepageStorageError,
|
||||
failure_message="Failed to persist homepage image",
|
||||
|
||||
@@ -26,6 +26,7 @@ from transcription.ui.components.confirm_delete import render_delete_actions
|
||||
from transcription.ui.components.confirm_delete import render_delete_blocked_notice
|
||||
from transcription.ui.components.data_display import archival_badge
|
||||
from transcription.ui.components.data_display import metadata_row
|
||||
from transcription.ui.components.error_presenter import run_ui_action
|
||||
from transcription.ui.components.error_presenter import show_error
|
||||
from transcription.ui.components.formatters import compact_date
|
||||
from transcription.ui.components.formatters import parse_iso_date
|
||||
@@ -77,8 +78,13 @@ def register_page() -> None: # noqa: PLR0915
|
||||
requested_person_id = parse_uuid(request.query_params.get("person_id"))
|
||||
staged_links: list[StagedLinkedPerson] = []
|
||||
if requested_person_id is not None and any(person.id == requested_person_id for person in people):
|
||||
try:
|
||||
author_role = await people_service.read_person_role_by_semantic_key(AUTHOR_ROLE_SEMANTIC_KEY)
|
||||
author_role_outcome = await run_ui_action(
|
||||
operation="documents.create.preselect",
|
||||
title="Author role unavailable",
|
||||
action=lambda: people_service.read_person_role_by_semantic_key(AUTHOR_ROLE_SEMANTIC_KEY),
|
||||
)
|
||||
if author_role_outcome.ok and author_role_outcome.value is not None:
|
||||
author_role = author_role_outcome.value
|
||||
if author_role.is_active:
|
||||
staged_links.append(StagedLinkedPerson(person_id=requested_person_id, role_id=author_role.id))
|
||||
else:
|
||||
@@ -86,8 +92,6 @@ def register_page() -> None: # noqa: PLR0915
|
||||
"The Author role is inactive, so the Person could not be preselected.",
|
||||
type="warning",
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Author role unavailable", operation="documents.create.preselect")
|
||||
elif request.query_params.get("person_id"):
|
||||
ui.notify("The requested person could not be preselected.", type="warning")
|
||||
linked_people = LinkedPeopleEditor(
|
||||
@@ -127,16 +131,19 @@ def register_page() -> None: # noqa: PLR0915
|
||||
archive_identifier=(form.archive.value or "").strip() or None,
|
||||
)
|
||||
|
||||
try:
|
||||
created = await create_document_with_people(
|
||||
created_outcome = await run_ui_action(
|
||||
operation="documents.create",
|
||||
title="Create failed",
|
||||
action=lambda: create_document_with_people(
|
||||
document=candidate,
|
||||
links=linked_people.values(),
|
||||
documents=document_service,
|
||||
people=people_service,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Create failed", operation="documents.create")
|
||||
),
|
||||
)
|
||||
if not created_outcome.ok or created_outcome.value is None:
|
||||
return
|
||||
created = created_outcome.value
|
||||
|
||||
ui.notify("Document created", type="positive")
|
||||
if return_to == "jobs_new":
|
||||
@@ -162,15 +169,18 @@ def register_page() -> None: # noqa: PLR0915
|
||||
icon="note_add",
|
||||
).classes("ui-btn-primary")
|
||||
|
||||
try:
|
||||
documents = sorted(
|
||||
await document_service.list_documents(),
|
||||
key=lambda item: item.created_at,
|
||||
reverse=True,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Load failed", operation="documents.list")
|
||||
documents_outcome = await run_ui_action(
|
||||
operation="documents.list",
|
||||
title="Load failed",
|
||||
action=document_service.list_documents,
|
||||
)
|
||||
if not documents_outcome.ok:
|
||||
return
|
||||
documents = sorted(
|
||||
documents_outcome.value or (),
|
||||
key=lambda item: item.created_at,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
rows = [
|
||||
DocumentTableRow(
|
||||
@@ -349,15 +359,17 @@ def register_page() -> None: # noqa: PLR0915
|
||||
updated_at=document.updated_at,
|
||||
)
|
||||
|
||||
try:
|
||||
await update_document_with_people(
|
||||
save_outcome = await run_ui_action(
|
||||
operation="documents.edit.save",
|
||||
title="Save failed",
|
||||
action=lambda: update_document_with_people(
|
||||
document=candidate,
|
||||
links=linked_people.values(),
|
||||
documents=document_service,
|
||||
people=people_service,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Save failed", operation="documents.edit.save")
|
||||
),
|
||||
)
|
||||
if not save_outcome.ok:
|
||||
return
|
||||
|
||||
ui.notify("Document updated", type="positive")
|
||||
|
||||
@@ -28,6 +28,7 @@ from transcription.ui.components.confirm_delete import render_delete_actions
|
||||
from transcription.ui.components.confirm_delete import render_delete_blocked_notice
|
||||
from transcription.ui.components.data_display import archival_badge
|
||||
from transcription.ui.components.data_display import metadata_row
|
||||
from transcription.ui.components.error_presenter import run_ui_action
|
||||
from transcription.ui.components.error_presenter import show_error
|
||||
from transcription.ui.components.formatters import parse_uuid
|
||||
from transcription.ui.components.guards import parsed_record_id
|
||||
@@ -98,15 +99,18 @@ def register_page() -> None: # noqa: PLR0915
|
||||
)
|
||||
|
||||
requested_source_id = parse_uuid(request.query_params.get("source_id"))
|
||||
try:
|
||||
locked_source = (
|
||||
await sources_service.read_source_detail(requested_source_id)
|
||||
locked_source_outcome = await run_ui_action(
|
||||
operation="jobs.retranscribe.load",
|
||||
title="Source unavailable",
|
||||
action=(
|
||||
(lambda: sources_service.read_source_detail(requested_source_id))
|
||||
if requested_source_id is not None
|
||||
else None
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Source unavailable", operation="jobs.retranscribe.load")
|
||||
else _none_async
|
||||
),
|
||||
)
|
||||
if not locked_source_outcome.ok:
|
||||
return
|
||||
locked_source = locked_source_outcome.value
|
||||
documents = await documents_service.list_documents()
|
||||
if not documents:
|
||||
_render_no_documents_card()
|
||||
@@ -156,21 +160,24 @@ def register_page() -> None: # noqa: PLR0915
|
||||
|
||||
async def submit_create() -> None:
|
||||
if locked_source is not None:
|
||||
try:
|
||||
services = ServiceBundle(
|
||||
documents=documents_service,
|
||||
jobs=JobService(session_factory=session_factory, settings=settings),
|
||||
sources=SourceService(session_factory=session_factory, settings=settings),
|
||||
)
|
||||
result_job = await create_source_retranscription_job(
|
||||
services = ServiceBundle(
|
||||
documents=documents_service,
|
||||
jobs=JobService(session_factory=session_factory, settings=settings),
|
||||
sources=SourceService(session_factory=session_factory, settings=settings),
|
||||
)
|
||||
result_outcome = await run_ui_action(
|
||||
operation="jobs.retranscribe",
|
||||
title="Create job failed",
|
||||
action=lambda: create_source_retranscription_job(
|
||||
source_id=locked_source.id,
|
||||
model=str(model_input.value),
|
||||
services=services,
|
||||
settings=settings,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Create job failed", operation="jobs.retranscribe")
|
||||
),
|
||||
)
|
||||
if not result_outcome.ok or result_outcome.value is None:
|
||||
return
|
||||
result_job = result_outcome.value
|
||||
resolve_worker_notifier(request.app.state).notify()
|
||||
ui.notify(f"Created retranscription job {result_job.id}", type="positive")
|
||||
ui.navigate.to(f"/jobs/{result_job.id}")
|
||||
@@ -189,17 +196,20 @@ def register_page() -> None: # noqa: PLR0915
|
||||
ui.notify("Selected document id is invalid.", type="warning")
|
||||
return
|
||||
|
||||
try:
|
||||
result = await create_job_for_document(
|
||||
create_outcome = await run_ui_action(
|
||||
operation="jobs.create",
|
||||
title="Create job failed",
|
||||
action=lambda: create_job_for_document(
|
||||
document_id=document_id,
|
||||
source_files=uploaded_files,
|
||||
provider=(provider_input.value or None),
|
||||
model=(model_input.value or None),
|
||||
session_factory=session_factory,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Create job failed", operation="jobs.create")
|
||||
),
|
||||
)
|
||||
if not create_outcome.ok or create_outcome.value is None:
|
||||
return
|
||||
result = create_outcome.value
|
||||
|
||||
resolve_worker_notifier(request.app.state).notify()
|
||||
ui.notify(f"Created job {result.job_id}", type="positive")
|
||||
@@ -250,12 +260,15 @@ def register_page() -> None: # noqa: PLR0915
|
||||
timer.cancel()
|
||||
timer_holder[0] = None
|
||||
|
||||
try:
|
||||
current_job[0] = await jobs_service.read_job(job_id=parsed_job_id)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
refresh_outcome = await run_ui_action(
|
||||
operation="jobs.detail.refresh",
|
||||
title="Auto-refresh failed",
|
||||
action=lambda: jobs_service.read_job(job_id=parsed_job_id),
|
||||
)
|
||||
if not refresh_outcome.ok or refresh_outcome.value is None:
|
||||
stop_refresh()
|
||||
show_error(exc, title="Auto-refresh failed", operation="jobs.detail.refresh")
|
||||
return
|
||||
current_job[0] = refresh_outcome.value
|
||||
|
||||
render_detail.refresh()
|
||||
if current_job[0].status not in {JobStatus.QUEUED, JobStatus.PROCESSING}:
|
||||
@@ -567,3 +580,7 @@ def _render_job_document_links(job: Job) -> None:
|
||||
|
||||
def _latest_prompt_name(job: Job) -> str | None:
|
||||
return job.prompt_name
|
||||
|
||||
|
||||
async def _none_async() -> None:
|
||||
return None
|
||||
|
||||
@@ -21,6 +21,7 @@ from transcription.ui.components.app_shell import render_navigation_header
|
||||
from transcription.ui.components.cards import archival_card
|
||||
from transcription.ui.components.confirm_delete import render_delete_actions
|
||||
from transcription.ui.components.data_display import metadata_row
|
||||
from transcription.ui.components.error_presenter import run_ui_action
|
||||
from transcription.ui.components.error_presenter import show_error
|
||||
from transcription.ui.components.formatters import compact_date
|
||||
from transcription.ui.components.formatters import family_search_url
|
||||
@@ -77,15 +78,18 @@ def register_page() -> None: # noqa: PLR0915
|
||||
icon="person_add",
|
||||
).classes("ui-btn-primary")
|
||||
|
||||
try:
|
||||
people = sorted(
|
||||
await people_service.list_people(),
|
||||
key=lambda item: item.created_at,
|
||||
reverse=True,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Load failed", operation="people.list")
|
||||
people_outcome = await run_ui_action(
|
||||
operation="people.list",
|
||||
title="Load failed",
|
||||
action=people_service.list_people,
|
||||
)
|
||||
if not people_outcome.ok:
|
||||
return
|
||||
people = sorted(
|
||||
people_outcome.value or (),
|
||||
key=lambda item: item.created_at,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
rows = [
|
||||
PersonTableRow(
|
||||
@@ -139,11 +143,14 @@ def register_page() -> None: # noqa: PLR0915
|
||||
family_search_id=(form.family_search_id.value or "").strip() or None,
|
||||
)
|
||||
|
||||
try:
|
||||
created = await people_service.create_person(candidate)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Create failed", operation="people.create")
|
||||
create_outcome = await run_ui_action(
|
||||
operation="people.create",
|
||||
title="Create failed",
|
||||
action=lambda: people_service.create_person(candidate),
|
||||
)
|
||||
if not create_outcome.ok or create_outcome.value is None:
|
||||
return
|
||||
created = create_outcome.value
|
||||
|
||||
ui.notify("Person created", type="positive")
|
||||
ui.navigate.to(f"/people/{created.id}")
|
||||
@@ -256,10 +263,12 @@ def register_page() -> None: # noqa: PLR0915
|
||||
updated_at=person.updated_at,
|
||||
)
|
||||
|
||||
try:
|
||||
await people_service.update_person(candidate)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Save failed", operation="people.edit.save")
|
||||
save_outcome = await run_ui_action(
|
||||
operation="people.edit.save",
|
||||
title="Save failed",
|
||||
action=lambda: people_service.update_person(candidate),
|
||||
)
|
||||
if not save_outcome.ok:
|
||||
return
|
||||
|
||||
ui.notify("Person updated", type="positive")
|
||||
|
||||
@@ -13,7 +13,7 @@ from transcription.services.people import PeopleService
|
||||
from transcription.services.prompts import PromptStore
|
||||
from transcription.ui.components.app_shell import render_navigation_header
|
||||
from transcription.ui.components.cards import archival_card
|
||||
from transcription.ui.components.error_presenter import show_error
|
||||
from transcription.ui.components.error_presenter import run_ui_action
|
||||
from transcription.ui.components.primitives import destructive_button
|
||||
from transcription.ui.components.primitives import render_empty_state
|
||||
from transcription.ui.components.primitives import section_header_row
|
||||
@@ -45,11 +45,14 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
ui.label("Types are listed alphabetically. Select one row to edit or delete it.").classes(
|
||||
"text-xs ui-text-muted mb-3"
|
||||
)
|
||||
try:
|
||||
document_types = await documents.list_document_type_summaries()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Document Types unavailable", operation="settings.types.list")
|
||||
document_types_outcome = await run_ui_action(
|
||||
operation="settings.types.list",
|
||||
title="Document Types unavailable",
|
||||
action=documents.list_document_type_summaries,
|
||||
)
|
||||
if not document_types_outcome.ok:
|
||||
return
|
||||
document_types = document_types_outcome.value or ()
|
||||
|
||||
if not document_types:
|
||||
render_empty_state("No Document Types are configured.", extra_classes="mt-3")
|
||||
@@ -75,17 +78,22 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
label: str,
|
||||
is_active: bool,
|
||||
) -> bool:
|
||||
try:
|
||||
async def _save_type() -> None:
|
||||
if item_id is None:
|
||||
await documents.create_document_type(label=label, is_active=is_active)
|
||||
else:
|
||||
await documents.update_document_type(
|
||||
item_id,
|
||||
label=label,
|
||||
is_active=is_active,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Document Type save failed", operation="settings.types.save")
|
||||
return
|
||||
await documents.update_document_type(
|
||||
item_id,
|
||||
label=label,
|
||||
is_active=is_active,
|
||||
)
|
||||
|
||||
save_outcome = await run_ui_action(
|
||||
operation="settings.types.save",
|
||||
title="Document Type save failed",
|
||||
action=_save_type,
|
||||
)
|
||||
if not save_outcome.ok:
|
||||
return False
|
||||
ui.notify("Document Type saved", type="positive")
|
||||
render_document_types.refresh()
|
||||
@@ -131,10 +139,12 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
if selected is None:
|
||||
ui.notify("Select one Document Type to delete.", type="warning")
|
||||
return
|
||||
try:
|
||||
await documents.delete_document_type(UUID(str(selected["id"])))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Document Type deletion failed", operation="settings.types.delete")
|
||||
delete_outcome = await run_ui_action(
|
||||
operation="settings.types.delete",
|
||||
title="Document Type deletion failed",
|
||||
action=lambda: documents.delete_document_type(UUID(str(selected["id"]))),
|
||||
)
|
||||
if not delete_outcome.ok:
|
||||
return
|
||||
ui.notify("Document Type deleted", type="positive")
|
||||
render_document_types.refresh()
|
||||
@@ -153,11 +163,14 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
"Roles are listed alphabetically. Built-ins cannot be deleted; "
|
||||
"referenced custom roles must be deactivated."
|
||||
).classes("text-xs ui-text-muted mb-3")
|
||||
try:
|
||||
roles = await people.list_person_role_summaries()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Person Roles unavailable", operation="settings.roles.list")
|
||||
roles_outcome = await run_ui_action(
|
||||
operation="settings.roles.list",
|
||||
title="Person Roles unavailable",
|
||||
action=people.list_person_role_summaries,
|
||||
)
|
||||
if not roles_outcome.ok:
|
||||
return
|
||||
roles = roles_outcome.value or ()
|
||||
if not roles:
|
||||
render_empty_state("No Person Roles are configured.", extra_classes="mt-3")
|
||||
rows = [
|
||||
@@ -182,13 +195,18 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
label: str,
|
||||
is_active: bool,
|
||||
) -> bool:
|
||||
try:
|
||||
async def _save_role() -> None:
|
||||
if item_id is None:
|
||||
await people.create_person_role(label=label, is_active=is_active)
|
||||
else:
|
||||
await people.update_person_role(item_id, label=label, is_active=is_active)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Person Role save failed", operation="settings.roles.save")
|
||||
return
|
||||
await people.update_person_role(item_id, label=label, is_active=is_active)
|
||||
|
||||
save_outcome = await run_ui_action(
|
||||
operation="settings.roles.save",
|
||||
title="Person Role save failed",
|
||||
action=_save_role,
|
||||
)
|
||||
if not save_outcome.ok:
|
||||
return False
|
||||
ui.notify("Person Role saved", type="positive")
|
||||
render_person_roles.refresh()
|
||||
@@ -234,10 +252,12 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
if selected is None:
|
||||
ui.notify("Select one Person Role to delete.", type="warning")
|
||||
return
|
||||
try:
|
||||
await people.delete_person_role(UUID(str(selected["id"])))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Person Role deletion failed", operation="settings.roles.delete")
|
||||
delete_outcome = await run_ui_action(
|
||||
operation="settings.roles.delete",
|
||||
title="Person Role deletion failed",
|
||||
action=lambda: people.delete_person_role(UUID(str(selected["id"]))),
|
||||
)
|
||||
if not delete_outcome.ok:
|
||||
return
|
||||
ui.notify("Person Role deleted", type="positive")
|
||||
render_person_roles.refresh()
|
||||
@@ -250,25 +270,31 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
destructive_button("Delete", icon="delete", on_click=delete_selected_role)
|
||||
|
||||
@ui.refreshable
|
||||
def render_prompts() -> None:
|
||||
async def render_prompts() -> None:
|
||||
with archival_card("Prompts"):
|
||||
ui.label("Only existing Markdown prompts can be edited. Changes affect future Jobs only.").classes(
|
||||
"text-xs ui-text-muted mb-3"
|
||||
)
|
||||
try:
|
||||
summaries = prompts.list_prompts()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Prompts unavailable", operation="settings.prompts.list")
|
||||
summaries_outcome = await run_ui_action(
|
||||
operation="settings.prompts.list",
|
||||
title="Prompts unavailable",
|
||||
action=lambda: _list_prompts(prompts),
|
||||
)
|
||||
if not summaries_outcome.ok:
|
||||
return
|
||||
summaries = summaries_outcome.value or ()
|
||||
|
||||
if not summaries:
|
||||
render_empty_state("No editable Markdown prompts were found.")
|
||||
for summary in summaries:
|
||||
try:
|
||||
content = prompts.read_prompt(summary.name)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title=f"{summary.name} unavailable", operation="settings.prompts.read")
|
||||
read_outcome = await run_ui_action(
|
||||
operation="settings.prompts.read",
|
||||
title=f"{summary.name} unavailable",
|
||||
action=lambda name=summary.name: _read_prompt(prompts, name),
|
||||
)
|
||||
if not read_outcome.ok:
|
||||
continue
|
||||
content = read_outcome.value or ""
|
||||
|
||||
with ui.column().classes("w-full gap-2 py-3 ui-header-divider"):
|
||||
with section_header_row():
|
||||
@@ -283,28 +309,28 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
.classes("w-full")
|
||||
)
|
||||
|
||||
def save_prompt(
|
||||
async def save_prompt(
|
||||
*,
|
||||
name: str = summary.name,
|
||||
field: Any = editor,
|
||||
) -> None:
|
||||
try:
|
||||
prompts.write_prompt(name, str(field.value or ""))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Prompt save failed", operation="settings.prompts.write")
|
||||
save_outcome = await run_ui_action(
|
||||
operation="settings.prompts.write",
|
||||
title="Prompt save failed",
|
||||
action=lambda: _write_prompt(prompts, name, str(field.value or "")),
|
||||
)
|
||||
if not save_outcome.ok:
|
||||
return
|
||||
ui.notify(f"{name} saved for future Jobs", type="positive")
|
||||
render_prompts.refresh()
|
||||
|
||||
def recover_prompt(*, name: str = summary.name) -> None:
|
||||
try:
|
||||
prompts.recover_prompt(name)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(
|
||||
exc,
|
||||
title="Prompt recovery failed",
|
||||
operation="settings.prompts.recover",
|
||||
)
|
||||
async def recover_prompt(*, name: str = summary.name) -> None:
|
||||
recover_outcome = await run_ui_action(
|
||||
operation="settings.prompts.recover",
|
||||
title="Prompt recovery failed",
|
||||
action=lambda: _recover_prompt(prompts, name),
|
||||
)
|
||||
if not recover_outcome.ok:
|
||||
return
|
||||
ui.notify(f"{name} restored from its previous version", type="positive")
|
||||
render_prompts.refresh()
|
||||
@@ -321,7 +347,7 @@ def register_page(*, settings: Settings) -> None: # noqa: PLR0915
|
||||
|
||||
await render_document_types()
|
||||
await render_person_roles()
|
||||
render_prompts()
|
||||
await render_prompts()
|
||||
|
||||
|
||||
def _selected_table_row(table: Any) -> dict[str, Any] | None:
|
||||
@@ -329,3 +355,19 @@ def _selected_table_row(table: Any) -> dict[str, Any] | None:
|
||||
if len(selected) != 1:
|
||||
return None
|
||||
return selected[0]
|
||||
|
||||
|
||||
async def _list_prompts(prompts: PromptStore) -> tuple[Any, ...]:
|
||||
return prompts.list_prompts()
|
||||
|
||||
|
||||
async def _read_prompt(prompts: PromptStore, name: str) -> str:
|
||||
return prompts.read_prompt(name)
|
||||
|
||||
|
||||
async def _write_prompt(prompts: PromptStore, name: str, content: str) -> None:
|
||||
prompts.write_prompt(name, content)
|
||||
|
||||
|
||||
async def _recover_prompt(prompts: PromptStore, name: str) -> None:
|
||||
prompts.recover_prompt(name)
|
||||
|
||||
@@ -25,6 +25,7 @@ from transcription.ui.components.confirm_delete import render_delete_actions
|
||||
from transcription.ui.components.confirm_delete import render_delete_blocked_notice
|
||||
from transcription.ui.components.data_display import archival_badge
|
||||
from transcription.ui.components.data_display import metadata_row
|
||||
from transcription.ui.components.error_presenter import run_ui_action
|
||||
from transcription.ui.components.error_presenter import show_error
|
||||
from transcription.ui.components.formatters import parse_uuid
|
||||
from transcription.ui.components.guards import parsed_record_id
|
||||
@@ -62,11 +63,14 @@ def register_page() -> None: # noqa: PLR0915
|
||||
elif parsed_job_id is not None:
|
||||
header_title = "Sources for Job"
|
||||
|
||||
try:
|
||||
sources = await sources_service.list_sources_detail(document_id=parsed_doc_id, job_id=parsed_job_id)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Load failed", operation="sources.list")
|
||||
sources_outcome = await run_ui_action(
|
||||
operation="sources.list",
|
||||
title="Load failed",
|
||||
action=lambda: sources_service.list_sources_detail(document_id=parsed_doc_id, job_id=parsed_job_id),
|
||||
)
|
||||
if not sources_outcome.ok:
|
||||
return
|
||||
sources = sources_outcome.value or ()
|
||||
|
||||
render_navigation_header(current_path="/sources")
|
||||
|
||||
@@ -424,11 +428,14 @@ def _transport_display(latest_attempt: LatestExecutionAttempt) -> dict[str, obje
|
||||
|
||||
|
||||
async def _download_evidence(*, source_id: UUID, evidence_service: EvidenceService) -> None:
|
||||
try:
|
||||
payload = await evidence_service.build_evidence_export(source_id=source_id)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Export failed", operation="sources.evidence_export")
|
||||
payload_outcome = await run_ui_action(
|
||||
operation="sources.evidence_export",
|
||||
title="Export failed",
|
||||
action=lambda: evidence_service.build_evidence_export(source_id=source_id),
|
||||
)
|
||||
if not payload_outcome.ok or payload_outcome.value is None:
|
||||
return
|
||||
payload = payload_outcome.value
|
||||
ui.download(
|
||||
json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False).encode("utf-8"),
|
||||
filename=f"source-{source_id}-evidence-v1.json",
|
||||
@@ -594,13 +601,15 @@ def _render_machine_candidates(
|
||||
)
|
||||
|
||||
async def promote(candidate_id: UUID = attempt.id) -> None:
|
||||
try:
|
||||
await evidence_service.promote_machine_attempt(
|
||||
promote_outcome = await run_ui_action(
|
||||
operation="sources.candidate.promote",
|
||||
title="Promotion failed",
|
||||
action=lambda: evidence_service.promote_machine_attempt(
|
||||
source_id=source.id,
|
||||
execution_attempt_id=candidate_id,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
show_error(exc, title="Promotion failed", operation="sources.candidate.promote")
|
||||
),
|
||||
)
|
||||
if not promote_outcome.ok:
|
||||
return
|
||||
ui.notify("Preferred machine transcription updated", type="positive")
|
||||
ui.navigate.to(f"/sources/{source.id}")
|
||||
|
||||
Reference in New Issue
Block a user