Files
bookshelf/src/logic/__init__.py
Petr Polezhaev 084d1aebd5 Initial commit
Photo-based book cataloger with AI identification.
Room → Cabinet → Shelf → Book hierarchy; FastAPI + SQLite backend;
vanilla JS SPA; OpenAI-compatible plugin system for boundary
detection, text recognition, and archive search.
2026-03-09 14:17:13 +03:00

109 lines
4.1 KiB
Python

"""Logic package: plugin dispatch orchestration and public re-exports."""
import asyncio
import dataclasses
from typing import Any
import plugins as plugin_registry
from errors import InvalidPluginEntityError, PluginNotFoundError, PluginTargetMismatchError
from models import PluginLookupResult
from logic.archive import run_archive_searcher, run_archive_searcher_bg
from logic.batch import archive_executor, batch_executor, batch_state, process_book_sync, run_batch
from logic.boundaries import book_spine_source, bounds_for_index, run_boundary_detector, shelf_source
from logic.identification import (
AI_FIELDS,
apply_ai_result,
build_query,
compute_status,
dismiss_field,
run_book_identifier,
run_text_recognizer,
save_user_fields,
)
from logic.images import prep_img_b64, crop_save, serve_crop
__all__ = [
"AI_FIELDS",
"apply_ai_result",
"archive_executor",
"batch_executor",
"batch_state",
"book_spine_source",
"bounds_for_index",
"build_query",
"compute_status",
"crop_save",
"dismiss_field",
"dispatch_plugin",
"process_book_sync",
"run_archive_searcher",
"run_archive_searcher_bg",
"run_batch",
"run_book_identifier",
"run_boundary_detector",
"run_text_recognizer",
"save_user_fields",
"serve_crop",
"shelf_source",
"prep_img_b64",
]
async def dispatch_plugin(
plugin_id: str,
lookup: PluginLookupResult,
entity_type: str,
entity_id: str,
loop: asyncio.AbstractEventLoop,
) -> dict[str, Any]:
"""Validate plugin/entity compatibility, run the plugin, and trigger auto-queue follow-ups.
Args:
plugin_id: The plugin ID string (used in error reporting).
lookup: Discriminated tuple from plugins.get_plugin(); (None, None) if not found.
entity_type: Entity type string (e.g. 'cabinets', 'shelves', 'books').
entity_id: ID of the entity to operate on.
loop: Running event loop for executor dispatch.
Returns:
dataclasses.asdict() of the updated entity row.
Raises:
PluginNotFoundError: If lookup is (None, None).
InvalidPluginEntityError: If the entity_type is not compatible with the plugin category.
PluginTargetMismatchError: If a boundary_detector plugin's target mismatches the entity.
"""
match lookup:
case (None, None):
raise PluginNotFoundError(plugin_id)
case ("boundary_detector", plugin):
if entity_type not in ("cabinets", "shelves"):
raise InvalidPluginEntityError("boundary_detector", entity_type)
if entity_type == "cabinets" and plugin.target != "shelves":
raise PluginTargetMismatchError(plugin.plugin_id, "shelves", plugin.target)
if entity_type == "shelves" and plugin.target != "books":
raise PluginTargetMismatchError(plugin.plugin_id, "books", plugin.target)
result = await loop.run_in_executor(None, run_boundary_detector, plugin, entity_type, entity_id)
return dataclasses.asdict(result)
case ("text_recognizer", plugin):
if entity_type != "books":
raise InvalidPluginEntityError("text_recognizer", entity_type)
result = await loop.run_in_executor(None, run_text_recognizer, plugin, entity_id)
for ap in plugin_registry.get_auto_queue("archive_searchers"):
loop.run_in_executor(archive_executor, run_archive_searcher_bg, ap, entity_id)
return dataclasses.asdict(result)
case ("book_identifier", plugin):
if entity_type != "books":
raise InvalidPluginEntityError("book_identifier", entity_type)
result = await loop.run_in_executor(None, run_book_identifier, plugin, entity_id)
return dataclasses.asdict(result)
case ("archive_searcher", plugin):
if entity_type != "books":
raise InvalidPluginEntityError("archive_searcher", entity_type)
result = await loop.run_in_executor(archive_executor, run_archive_searcher, plugin, entity_id)
return dataclasses.asdict(result)