| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- """Bulk reads for pages that show many printers (or archives) at once.
- The Printers page asked for each printer's status, slot presets, AMS labels,
- plugs, sensor readings, firmware and queue with one request per printer, and
- the Archives page for each card's folders with one per archive. On a print
- farm that was hundreds of requests per page load, queued one behind the other
- on the server. The frontend (api/batch.ts) coalesces those per-id calls into
- these.
- Each route answers exactly what its single-id route answers, through the same
- helper. A printer outside the caller's scope, or one that doesn't exist, is
- left out; the client then asks the single route for it, which gives its 404.
- They live under their own prefix so no ``/{id}`` route of another router can
- take them for an id.
- """
- from fastapi import APIRouter, Depends, Query
- from sqlalchemy import and_, func, or_, select
- from sqlalchemy.ext.asyncio import AsyncSession
- from backend.app.api.routes.firmware import FirmwareUpdateInfo, _checks_enabled, _not_checked, _printer_update_info
- from backend.app.api.routes.ha_sensors import _card_sensors, _reading
- from backend.app.api.routes.library import _folders_by_archive
- from backend.app.api.routes.print_queue import _QUEUE_ITEM_LOADS, _enrich_response
- from backend.app.api.routes.printers import _ams_labels, _printer_status, _slot_presets_by_key
- from backend.app.api.routes.smart_plugs import (
- CallerEditsPlugs,
- PrinterCardPlugs,
- _card_script_plugs,
- _pick_main_plug,
- _serialize_plug,
- )
- from backend.app.core.auth import (
- ApiKeyActor,
- RequestPrinterScope,
- RequirePermissionIfAuthEnabled,
- require_ownership_permission,
- )
- from backend.app.core.database import get_db
- from backend.app.core.permissions import Permission
- from backend.app.core.printer_scope import PrinterScope
- from backend.app.models.print_queue import PrintQueueItem
- from backend.app.models.printer import Printer
- from backend.app.models.printer_ha_sensor import PrinterHASensor
- from backend.app.models.slot_preset import SlotPresetMapping
- from backend.app.models.smart_plug import SmartPlug
- from backend.app.models.user import User
- from backend.app.schemas.library import FolderResponse
- from backend.app.schemas.print_queue import PrintQueueItemResponse
- from backend.app.schemas.printer import PrinterStatus
- from backend.app.schemas.printer_ha_sensor import PrinterHASensorReading
- from backend.app.schemas.smart_plug import SmartPlugResponse
- from backend.app.services.firmware_check import get_firmware_service
- from backend.app.utils.id_list import parse_id_list
- router = APIRouter(prefix="/bulk", tags=["bulk"])
- @router.get("/printer-statuses", response_model=list[PrinterStatus])
- async def get_printer_statuses(
- ids: str = Query(..., description="Comma-separated printer ids"),
- _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
- printer_scope: PrinterScope = RequestPrinterScope,
- db: AsyncSession = Depends(get_db),
- ):
- """``GET /printers/{printer_id}/status`` for several printers. Unknown ids are left out."""
- wanted = [i for i in parse_id_list(ids) if printer_scope.allows(i)]
- if not wanted:
- return []
- result = await db.execute(select(Printer).where(Printer.id.in_(wanted)))
- printers = {p.id: p for p in result.scalars().all()}
- return [await _printer_status(db, printers[i]) for i in wanted if i in printers]
- @router.get("/slot-presets")
- async def get_slot_presets_bulk(
- ids: str = Query(..., description="Comma-separated printer ids"),
- _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
- printer_scope: PrinterScope = RequestPrinterScope,
- db: AsyncSession = Depends(get_db),
- ):
- """``GET /printers/{printer_id}/slot-presets`` for several printers, keyed by printer id."""
- wanted = [i for i in parse_id_list(ids) if printer_scope.allows(i)]
- by_printer: dict[int, list] = {i: [] for i in wanted}
- if wanted:
- result = await db.execute(select(SlotPresetMapping).where(SlotPresetMapping.printer_id.in_(wanted)))
- for mapping in result.scalars().all():
- by_printer[mapping.printer_id].append(mapping)
- return {i: _slot_presets_by_key(mappings) for i, mappings in by_printer.items()}
- @router.get("/ams-labels")
- async def get_ams_labels_bulk(
- ids: str = Query(..., description="Comma-separated printer ids"),
- _=RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
- printer_scope: PrinterScope = RequestPrinterScope,
- db: AsyncSession = Depends(get_db),
- ):
- """``GET /printers/{printer_id}/ams-labels`` for several printers, keyed by printer id."""
- wanted = [i for i in parse_id_list(ids) if printer_scope.allows(i)]
- return {i: await _ams_labels(db, i) for i in wanted}
- @router.get("/card-plugs", response_model=None, responses={200: {"model": dict[int, PrinterCardPlugs]}})
- async def get_smart_plugs_by_printers(
- ids: str = Query(..., description="Comma-separated printer ids"),
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.SMART_PLUGS_READ),
- printer_scope: PrinterScope = RequestPrinterScope,
- can_edit: bool = CallerEditsPlugs,
- ):
- """``GET /smart-plugs/by-printer/{printer_id}`` and its ``/scripts``, for several printers.
- The Printers page asked both once per printer card. A printer outside the
- caller's scope is left out, which the client reports as the single routes'
- 404.
- """
- wanted = [i for i in parse_id_list(ids) if printer_scope.allows(i)]
- by_printer: dict[int, list[SmartPlug]] = {i: [] for i in wanted}
- if wanted:
- result = await db.execute(select(SmartPlug).where(SmartPlug.printer_id.in_(wanted)).order_by(SmartPlug.id))
- for plug in result.scalars().all():
- if plug.printer_id is not None:
- by_printer[plug.printer_id].append(plug)
- # Shaped as the single routes shape them: the main plug in the editor's
- # form for a caller who may edit plugs (see _serialize_plug), so no
- # response_model here either.
- cards = {}
- for i, plugs in by_printer.items():
- plug = _pick_main_plug(plugs)
- cards[i] = {
- "plug": _serialize_plug(plug, can_edit=can_edit) if plug else None,
- "scripts": [SmartPlugResponse.model_validate(p) for p in _card_script_plugs(plugs)],
- }
- return cards
- @router.get("/ha-sensor-readings", response_model=dict[int, list[PrinterHASensorReading]])
- async def get_sensor_readings_by_printers(
- ids: str = Query(..., description="Comma-separated printer ids"),
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.SMART_PLUGS_READ),
- printer_scope: PrinterScope = RequestPrinterScope,
- ):
- """``GET /ha-sensors/by-printer/{printer_id}/readings`` for several printers, keyed by printer id.
- The Printers page asked once per card every 15 seconds. A printer outside
- the caller's scope is left out, which the client reports as the single
- route's 404.
- """
- wanted = [i for i in parse_id_list(ids) if printer_scope.allows(i)]
- readings: dict[int, list[PrinterHASensorReading]] = {i: [] for i in wanted}
- if wanted:
- for sensor in await _card_sensors(db, PrinterHASensor.printer_id.in_(wanted)):
- readings[sensor.printer_id].append(_reading(sensor))
- return readings
- @router.get("/firmware-updates", response_model=list[FirmwareUpdateInfo])
- async def check_printers_firmware(
- ids: str = Query(..., description="Comma-separated printer ids"),
- db: AsyncSession = Depends(get_db),
- _: User | None = RequirePermissionIfAuthEnabled(Permission.FIRMWARE_READ),
- printer_scope: PrinterScope = RequestPrinterScope,
- ):
- """``GET /firmware/updates/{printer_id}`` for several printers.
- The Printers page asked once per card. Unknown printers, and printers
- outside the caller's scope, are left out, which the client reports as the
- single route's 404.
- """
- wanted = [i for i in parse_id_list(ids) if printer_scope.allows(i)]
- if not wanted:
- return []
- result = await db.execute(select(Printer).where(Printer.id.in_(wanted)))
- printers = {p.id: p for p in result.scalars().all()}
- ordered = [printers[i] for i in wanted if i in printers]
- if not await _checks_enabled(db):
- return [_not_checked(p) for p in ordered]
- firmware_service = get_firmware_service()
- return [await _printer_update_info(firmware_service, p) for p in ordered]
- @router.get("/printer-queues", response_model=dict[int, list[PrintQueueItemResponse]])
- async def list_queue_by_printers(
- ids: str = Query(..., description="Comma-separated printer ids"),
- status: str | None = Query(None, description="Filter by status; several may be given comma-separated"),
- db: AsyncSession = Depends(get_db),
- auth_result: tuple[User | ApiKeyActor | None, bool] = Depends(
- require_ownership_permission(
- Permission.QUEUE_READ_ALL,
- Permission.QUEUE_READ_OWN,
- )
- ),
- printer_scope: PrinterScope = RequestPrinterScope,
- ):
- """``GET /queue/?printer_id=N&status=...`` for several printers, keyed by printer id.
- Every printer card asked for its own queue, twice (the card and its queue
- widget). Each list is what the single call returns: the printer's own jobs
- plus the "Any <model>" jobs for its model, in dispatch order. A job for a
- model shows up under every printer of that model.
- """
- user, can_read_all = auth_result
- wanted = parse_id_list(ids)
- if not wanted:
- return {}
- models = {
- printer_id: model.lower()
- for printer_id, model in (
- await db.execute(select(Printer.id, Printer.model).where(Printer.id.in_(wanted)))
- ).all()
- if model
- }
- query = (
- select(PrintQueueItem)
- .options(*_QUEUE_ITEM_LOADS)
- .where(
- or_(
- PrintQueueItem.printer_id.in_(wanted),
- and_(
- PrintQueueItem.printer_id.is_(None),
- func.lower(PrintQueueItem.target_model).in_(set(models.values())),
- ),
- )
- )
- # The order the scheduler dispatches in (#3200), as GET /queue/ returns it
- .order_by(PrintQueueItem.position, PrintQueueItem.id)
- )
- if user is not None and not can_read_all:
- query = query.where(PrintQueueItem.created_by_id == user.id)
- # Items bound to printers the caller can't see stay out (#1727)
- if (clause := printer_scope.where(PrintQueueItem.printer_id)) is not None:
- query = query.where(clause)
- if status:
- query = query.where(PrintQueueItem.status.in_([s.strip() for s in status.split(",") if s.strip()]))
- items = (await db.execute(query)).scalars().all()
- responses = {item.id: _enrich_response(item) for item in items}
- def belongs(item: PrintQueueItem, printer_id: int) -> bool:
- if item.printer_id is not None:
- return item.printer_id == printer_id
- model = models.get(printer_id)
- return bool(model and item.target_model and item.target_model.lower() == model)
- return {printer_id: [responses[item.id] for item in items if belongs(item, printer_id)] for printer_id in wanted}
- @router.get("/archive-folders", response_model=dict[int, list[FolderResponse]])
- async def get_folders_by_archives(
- ids: str = Query(..., description="Comma-separated archive ids"),
- db: AsyncSession = Depends(get_db),
- auth_result: tuple[User | ApiKeyActor | None, bool] = Depends(
- require_ownership_permission(
- Permission.LIBRARY_READ_ALL,
- Permission.LIBRARY_READ_OWN,
- )
- ),
- printer_scope: PrinterScope = RequestPrinterScope,
- ):
- """``GET /library/folders/by-archive/{archive_id}`` for several archives, keyed by archive id.
- The Archives page asked once per archive card, each time loading the whole
- folder index to work out what the user may see.
- """
- user, _ = auth_result
- archive_ids = parse_id_list(ids)
- if not archive_ids:
- return {}
- return await _folders_by_archive(db, user, archive_ids, printer_scope)
|