Skip to main content

A library card catalog system for physical items

Project description

Compendium

A library card catalog system for physical items — books, vinyl records, DVDs, CDs.

Features

  • Catalog — add items by ISBN / UPC / MusicBrainz ID / TMDb ID / title search (Google Books / Open Library, MusicBrainz, TMDb) or manually for obscure items; search and browse works and copies; faceted discovery (media type, decade, availability)
  • Circulation — checkout, checkin, loan renewal with category-aware per-media-type loan policies; lost / damaged / claims-returned states; self-checkout kiosk mode; library hours and holiday calendar so due dates never land on closed days and overdue fines skip closed days
  • Holds — patron reservation queue; immediate promotion when a copy is available; suspend/resume; auto-expiry via maintenance command
  • Fines — configurable per-policy overdue rates with caps and grace periods; lost/damaged fees; threshold-based checkout/hold blocking; pay/waive workflow; per-patron and bulk overdue assessment
  • Notifications — outbox-pattern email delivery (hold-ready, due-soon, overdue) drained by a cron-invoked CLI; admin viewer + retry; per-patron opt-out; configurable retention
  • Reports — checkouts/month, popular works, dormant items (weeding list), current overdues; CSV export; Chart.js trendlines
  • Patrons & cards — patron categories (Adult/Child/Staff/Teacher seeded), card expiry with maintenance auto-deactivation, optional 1:1 patron↔user link for self-service
  • Bulk import/export — round-trippable CSV; MARC21 binary + MARCXML import/export; LibraryThing TSV import; GoodReads CSV import (all with lenient encoding for messy real-world exports)
  • Backup/restore — portable JSONL tarballs; backend-agnostic (SQLite ↔ Postgres); doubles as a DB migration path
  • Labels — Avery-template item labels (spine / pocket) and patron cards (full / sticker) as PDFs. Spine templates: avery-5167-spine (½" narrow face), avery-5160-spine (1" medium face, rotated), avery-5160 (flat wrap-around, centered text), avery-22805/avery-22806 (square classification labels). Live in-page SVG preview updates as you change kind/template/fields — no PDF round-trip needed.
  • Curated lists — librarian-editable named collections of Works ("Staff picks", "Summer reads") with slugs, per-work annotations, public/private and featured toggles; featured lists appear as a shelf on the OPAC landing page; public browse at /ui/lists
  • Auth — five preset roles (ReadOnly, Patron, Librarian, SystemAdmin, Administrator) plus custom roles via the admin UI; JWT for API, cookie-based for web UI
  • Audit log — synchronous trail of administrative mutations (Librarian + system tier); queryable via web UI, CLI, or REST
  • DB-editable settings — most configuration knobs (library name, fines, kiosk timeout, SMTP, retention, configurable nav shortcuts, etc.) editable from the UI / CLI / API; env vars still win as a break-glass
  • Remote phone scanner — pair a phone to the circulation desk via QR code; the phone camera dispatches scans to the desk in real time without installing an app. Supports checkout, checkin, and catalog-lookup modes. Pairing is ephemeral: a short-TTL claim secret lives in the QR; after the phone claims it, the secret rotates to a session cookie; the librarian can unpair from the desk at any time. Requires HTTPS on both the server and the phone browser (secure context for camera). Settings: public_base_url (DB-editable, env COMPENDIUM_PUBLIC_BASE_URL) and scan_session_minutes (DB-editable, env COMPENDIUM_SCAN_SESSION_MINUTES, default 60).
  • Web UI — HTMX + Jinja2 browser interface with catalog search, circulation desk (camera-based barcode scanning), patron self-service, light/dark/auto theme; per-user nav shortcut overrides via browser localStorage
  • REST API — FastAPI; consumed by the web UI and available for integrations
  • CLI — full librarian + sysadmin workflow without running a server, including stdin/stdout (-) for backup, import/export, and labels

Install

# Install the CLI as a standalone tool (recommended for production)
uv tool install compendium-ils           # SQLite only
uv tool install "compendium-ils[postgres]"  # + Postgres support

# Or with pip
pip install compendium-ils
pip install "compendium-ils[postgres]"

The installed command is compendium.

Quick start

Prerequisites

Python 3.11.4 or newer. (Backup restore uses the tarfile data filter from PEP 706, which was backported to 3.11.4.)

Debian / Ubuntu

sudo apt-get update
sudo apt-get install -y python3 python3-venv python3-dev build-essential
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env

RedHat / CentOS / Fedora

sudo dnf install -y python3 python3-devel gcc
curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env

macOS

brew install python uv

Run

# Install dependencies
uv sync --extra dev

# Initialise the database (creates ./compendium.db with SQLite by default)
uv run compendium db init

# Create an Administrator account
uv run compendium user add --username admin --role Administrator

# Add a book by ISBN (looks up metadata from Google Books when key is set, else Open Library)
uv run compendium item add --isbn 9780441013593

# Add a patron
uv run compendium patron add --name "Alice Example"

# Start the server (web UI at http://localhost:8000/ui/catalog)
uv run compendium serve

Log in at http://localhost:8000/ui/login with the username and password you set above.

CLI reference

Run compendium --help for the full command tree, or compendium <group> --help for the subcommands of a specific group.

Catalog & cataloging

Group Common subcommands
item add (--isbn / --upc / --mbid / --tmdb-id / --title), add-manual, show, list, withdraw, set-loanable; note add/list/delete; declare-lost, mark-damaged, clear-lost, clear-damage
work search, show, new-arrivals, recently-returned
creator list, show, merge
curated-list create, list, show, edit, delete, add-work, remove-work, reorder
branch list, set
import csv, marc (use - for stdin)
export csv, marc (use - for stdout)

Circulation

Group Common subcommands
loan checkout, checkin, renew, active, list (system-wide), history, item-history
claim returned (patron disputes), verify (found), write-off, list
hold place, cancel, list, queue, suspend, resume, list-suspended
fine list, pay, waive, assess, assess-overdue

Patrons & accounts

Group Common subcommands
patron add, list (--include-inactive), set, link-user, unlink-user, deactivate, reactivate
patron-category list, create, update, delete
user add (default --role Administrator), list (--include-inactive), set-role, set-password, deactivate, reactivate
role list, create, update, clone
policy list, create, set (configures fines, category-aware)

Reporting & labels

Group Common subcommands
reports checkouts, popular, dormant, overdues (each supports --format csv)
labels templates, spine, pocket, barcode, patron-card, patron-sticker (output -o - writes PDF to stdout; use --show/--hide for field toggles, e.g. --show barcode on spine)
audit list (filters: --entity, --id, --user-id, --limit)

Operations

Command Description
compendium db init / db upgrade / db history Migrate the database
compendium serve Start the API + web UI server
compendium backup --output <path-or-> Write a portable JSONL tarball backup
compendium restore <path-or-> Restore from a backup tarball (lenient — auto-migrates)
compendium settings list/get/set/reset Inspect & edit DB-backed site settings
compendium maintenance ... Cron-invoked tasks: expire-holds, resume-expired-suspends, assess-overdue-fines, queue-due-soon-notices, queue-overdue-notices, send-queued-notifications, prune-notifications, prune-audit-log, deactivate-expired-patrons, prune-cover-cache, prune-metadata-cache, prune-scan-pairings, refresh-metadata
compendium metadata cache stats Show metadata cache row counts by adapter and TTL status
compendium metadata cache clear Delete all metadata cache rows (audited)

File-argument commands (backup, restore, import, export, labels spine/pocket/barcode/patron-card/patron-sticker) accept - for stdin/stdout. Status messages are routed to stderr in stdio mode so they don't corrupt binary output.

Web UI

Start the server with compendium serve and open your browser to http://localhost:8000/ui/catalog.

"Audience" lists the minimum preset that can access each route. Administrator can access everything. SystemAdmin gets the System-tier rows; slimmed Librarian gets librarian-tier rows but not System-tier.

URL Audience Description
/ui/catalog Anyone Search catalog with facets (media type, decade, availability); featured curated lists shelf
/ui/catalog/{work_id} Anyone Work detail, items, place-hold button
/ui/lists Anyone Public OPAC browse of public curated lists
/ui/lists/{slug} Anyone Public curated list detail with annotated works
/ui/login Anyone Login form
/ui/me/loans Patron Active loans with inline renew + "I returned this" claim
/ui/me/holds Patron Active holds with inline cancel + suspend/resume
/ui/me/fines Patron Outstanding and historical fines
/ui/me/preferences Patron Notification opt-out
/ui/me/password Patron Self-service password change
/ui/circ Librarian Circulation desk — checkout / checkin / renew
/ui/kiosk Librarian Self-checkout kiosk landing/session (patron card-number-only auth)
/ui/items/new Librarian Add item by ISBN / UPC / MBID / TMDb ID / title search (with barcode scanner)
/ui/items/new/manual Librarian Manually add an item not found in external sources
/ui/items/{barcode} Librarian Item detail, loan history, withdraw, set-loanable, lost/damaged/claims actions
/ui/patrons Librarian Patron list
/ui/patrons/new Librarian Create patron (with category + expiry)
/ui/patrons/{card} Librarian Patron detail with active loans, holds, link/unlink user, deactivate, reactivate
/ui/patrons/{card}/loans Librarian Patron loan history (active / returned / all)
/ui/patrons/{card}/fines Librarian Patron fines with pay/waive
/ui/admin/loans Librarian All active loans (system-wide) with overdue/due-soon filters
/ui/admin/fines Librarian All outstanding fines (system-wide) with running total
/ui/admin/holds Librarian All active holds with status/branch/work filters
/ui/admin/claims Librarian Outstanding claims-returned investigations
/ui/admin/notifications Librarian Notification log + manual retry
/ui/admin/import Librarian Bulk CSV / GoodReads CSV / LibraryThing TSV / MARC import
/ui/admin/export Librarian Bulk CSV/MARC export
/ui/admin/patron-categories Librarian Manage patron categories
/ui/curated-lists Librarian Curated list admin CRUD (curatedlist.manage)
/ui/admin/settings/general Librarian Library name, default theme, guest search
/ui/admin/settings/circulation Librarian Currency, fine thresholds, hold/overdue/due-soon defaults
/ui/admin/settings/kiosk Librarian Kiosk idle timeout
/ui/scan/pair Librarian Generate a QR code to pair a phone as a remote barcode scanner
/ui/scan/desk Librarian Desk-side view: shows live scan count and unpair button for an active phone session
/ui/scan/phone/* Phone browser Phone-side claim, mode selector, and continuous scan loop (camera; requires HTTPS / secure context)
/ui/policies Librarian Loan policy list with inline edit
/ui/policies/new Librarian Create loan policy (per media type + patron category)
/ui/branches Librarian Branch list with classification scheme
/ui/branches/{id}/edit Librarian Edit a branch's default classification scheme
/ui/reports Librarian Reports landing — checkouts, popular, dormant, overdues
/ui/labels Librarian Generate item-label and patron-card PDFs (Avery templates)
/ui/audit Librarian Audit log viewer
/ui/users SystemAdmin User list
/ui/users/new SystemAdmin Create user
/ui/users/{username} SystemAdmin User detail — change role, deactivate, reactivate, reset password
/ui/roles SystemAdmin Role list
/ui/roles/new SystemAdmin Create custom role
/ui/roles/{id} SystemAdmin Role detail — edit permissions, clone
/ui/admin/system/smtp SystemAdmin SMTP host/port/from settings
/ui/admin/system/secrets SystemAdmin Encrypted secrets (SMTP password, TMDb / Google Books API keys) — requires COMPENDIUM_SECRET_KEY
/ui/admin/system/retention SystemAdmin Notification + audit retention, batch sizes

Guest catalog search is enabled by default (toggle via /ui/admin/settings/general or COMPENDIUM_GUEST_SEARCH_ENABLED=false).

REST API endpoints

The API is mounted at the root. Authenticate with POST /auth/login and pass the result as Authorization: Bearer <token>. Interactive docs live at http://localhost:8000/docs when the server is running.

Below is a high-level inventory grouped by concern; the OpenAPI document is the source of truth for parameters and bodies.

Group Common endpoints Min permission
Auth POST /auth/login none
Catalog GET /works/search, /works/new-arrivals, /works/recently-returned; GET /items/{barcode}; POST /items/{barcode}/{withdraw,loanable,lost,damaged,clear-lost,clear-damage} guest / item.view / item.delete / fine.manage
Patrons GET/POST /patrons, PATCH /patrons/{card}, POST /patrons/{card}/{deactivate,reactivate}, POST /patrons/{card}/account, GET/POST /patron-categories, PATCH/DELETE /patron-categories/{id} patron.manage / patron.account.manage for account endpoints
Loans POST /loans/checkout, /loans/{id}/{checkin,renew}; GET /loans (system-wide), /loans/patron/{card}, /loans/item/{barcode} loan.* (see below)
Claims GET /claims; POST /claims/{barcode}/{returned,verify,write-off} loan.checkin
Holds GET /holds, /holds/queue/{work_id}; POST/DELETE /holds, /holds/{id}; POST /holds/{id}/{suspend,resume} hold.*
Fines GET /fines, GET/POST /patrons/{card}/fines, POST /fines/{id}/{pay,waive}, POST /patrons/{card}/fines/assess-overdue fine.manage / fine.view.self
Self-service GET /me/loans, /me/holds; POST /me/holds, /me/holds/{id}/{suspend,resume}; DELETE /me/holds/{id}; POST /me/loans/{id}/{renew,claim-returned} *.self permissions
Notifications GET /notifications, POST /notifications/{id}/retry notification.manage
Reports GET /reports/{checkouts,popular,dormant,overdues} report.view
Bulk import/export POST /import/{csv,goodreads,librarything,marc} (multipart), GET /export/{csv,marc} (streaming) catalog.import / item.view
Labels GET /labels/items, /labels/patrons (PDF) labels.generate
Policies GET/POST /policies item.view / policy.edit
Users POST /users, POST /users/{username}/{deactivate,reactivate}, POST/DELETE /users/{username}/patron user.manage
Curated lists GET/POST /curated-lists, PATCH/DELETE /curated-lists/{slug}, POST/DELETE /curated-lists/{slug}/works guest / curatedlist.manage
Settings GET /settings/, GET/PATCH/DELETE /settings/{key} patron.manage (librarian-tier) / system.manage (system-tier)
Calendar GET /library-hours/, PATCH /library-hours/{weekday}; GET/POST /closed-dates/, PATCH/DELETE /closed-dates/{id} calendar.manage
Audit GET /audit/ audit.view

The "min permission" column lists the lowest preset role that's allowed. Administrator (wildcard) covers everything. Slimmed Librarian covers librarian-tier endpoints; SystemAdmin covers user/role/system-tier endpoints.

Configuration

Compendium settings come from three layers, in order of precedence:

  1. Environment variable (COMPENDIUM_<KEY>) — break-glass override, wins over everything.
  2. site_setting table — DB-backed, editable from /ui/admin/settings/*, the CLI (compendium settings ...), or the API (PATCH /settings/{key}). Changes take effect on the next page render — no restart.
  3. Registry default — fallback hard-coded in services/settings_registry.py.

Most settings (library name, theme, fine thresholds, hold/overdue defaults, kiosk timeout, SMTP host/port/from, retention) are DB-editable. A handful stay env-only because they're either secrets or required before the DB is reachable:

Env var Why env-only
COMPENDIUM_DATABASE_URL Bootstrap — needed before any DB read
COMPENDIUM_JWT_SECRET_KEY Secret — required; server refuses to start with the built-in default. Generate with python -c "import secrets; print(secrets.token_urlsafe(48))". Set COMPENDIUM_ALLOW_INSECURE_JWT=1 to bypass for first-run/dev only.
COMPENDIUM_JWT_ALGORITHM / COMPENDIUM_JWT_EXPIRE_MINUTES Auth deployment knobs
COMPENDIUM_SSL_CERTFILE / COMPENDIUM_SSL_KEYFILE OS-level paths
COMPENDIUM_SECURE_COOKIES Default true; set false only for plain-HTTP LAN deployments (browsers won't send Secure cookies over non-HTTPS, except localhost).
COMPENDIUM_SECRET_KEY Encryption key for secrets stored in the DB (SMTP password, API keys). Optional — set to enable the /ui/admin/system/secrets page. Generate with compendium keygen --secret.
COMPENDIUM_TMDB_API_KEY TMDb API key. Override — also settable via Admin → System → Secrets when COMPENDIUM_SECRET_KEY is configured.
COMPENDIUM_GOOGLE_BOOKS_API_KEY Google Books API key (primary book metadata source when set). Override — also settable via Admin → System → Secrets when COMPENDIUM_SECRET_KEY is configured.
COMPENDIUM_BOOK_METADATA_SOURCE_PREFERENCE Primary book metadata source: googlebooks (default; uses GB when key is set, else OL) or openlibrary. DB-editable at Admin → System → Metadata Sources.
COMPENDIUM_BOOK_METADATA_FALLBACK_ENABLED When true (default), a miss from the primary book adapter automatically tries the secondary (GB→OL and OL→GB symmetric). Set false to disable. DB-editable at Admin → System → Metadata Sources.
COMPENDIUM_METADATA_CACHE_TTL_DAYS Positive-hit TTL for the metadata cache in days (default 30). Negative (not-found) entries always expire after 24 hours. DB-editable at Admin → System → Metadata Sources.
COMPENDIUM_SMTP_PASSWORD SMTP password. Override — also settable via Admin → System → Secrets when COMPENDIUM_SECRET_KEY is configured.
COMPENDIUM_MAX_UPLOAD_BYTES Hard cap on bulk-import upload size (default 100 MB / 104857600). Env-only so a compromised admin token can't raise it.
COMPENDIUM_LOGIN_MAX_FAILURES Max consecutive failed logins before the identity is throttled (default 10). DB-editable at System → Security.
COMPENDIUM_LOGIN_FAILURE_WINDOW_SECONDS Sliding window for counting failures in seconds (default 300 = 5 min). DB-editable at System → Security.
COMPENDIUM_PUBLIC_BASE_URL External HTTPS origin used to build QR codes for phone pairing (e.g. https://library.example.org). The QR base URL is normally derived from the request, honoring X-Forwarded-Proto — the bundled nginx sets this header, so the shipped Docker stack works without this setting. Set it when your proxy does not set that header, or to pin a specific hostname. A non-HTTPS value is refused at QR-render time. DB-editable at Admin → Settings → General; env wins.
COMPENDIUM_SCAN_SESSION_MINUTES How long a paired phone session stays active in minutes (default 60). DB-editable at Admin → Settings → General; env wins.

For everything else, run compendium settings list to see the current value, source (env vs db/default), and per-key help text. The web admin UI shows the same with an "⚠ Overridden by env var" indicator on rows where an env var is currently masking the DB value.

See docs/deployment.md for full deployment guidance, or docker/README.md for the bundled Docker Compose setup (app + Postgres + nginx with auto-generated self-signed TLS).

Docker quick start

A pre-built multi-arch image (linux/amd64 + linux/arm64) is published to GitHub Container Registry on every release:

cd docker
cp .env.example .env && $EDITOR .env   # set passwords and JWT secret
docker compose pull
docker compose up -d

Image: ghcr.io/statyk/compendium — tags latest, X.Y.Z, X.Y are published for each release. Pin a version in .env with COMPENDIUM_IMAGE=ghcr.io/statyk/compendium:1.0.2.

PostgreSQL

SQLite is the default and fine for home or classroom use (up to ~10k items). For larger collections or multiple concurrent writers, use PostgreSQL:

uv sync --extra postgres

Then set:

COMPENDIUM_DATABASE_URL=postgresql+psycopg://compendium:<password>@localhost:5432/compendium

and run compendium db init to apply migrations. Full setup (creating the role and database, TLS, backups) is in docs/deployment.md.

Scheduled maintenance

Several maintenance commands need to run on a cadence — most importantly the email outbox drain (send-queued-notifications), without which queued notifications never go out. Install the bundled crontab via:

scripts/install-cron.sh

By default this writes the project path into the crontab and points logs at $HOME/.local/state/compendium/maintenance.log. Override either with flags:

scripts/install-cron.sh --project-dir /opt/compendium --log-file journal
scripts/install-cron.sh --log-file /var/log/compendium/maintenance.log

--log-file journal routes output to the systemd journal (view with journalctl -t compendium-maintenance -f). For paths the installer can't create unprivileged (e.g. /var/log/...), it prints the one-time sudo command and exits without touching the crontab.

See docs/crontab.sample for the full schedule and docs/compendium.service.sample for running the daemon under systemd.

Running tests

uv run pytest -q

Tests are split into tests/unit/ (no DB, mock repos) and tests/integration/ (SQLite in-memory).

Browser tests (E2E)

Browser tests run against a real compendium serve subprocess in Chromium. They are excluded from the default pytest run and require a one-time install:

uv sync --extra e2e
playwright install chromium

Then:

uv run pytest -m e2e

Expected wall time: 30–60 seconds. Tests live in tests/e2e/. The test_csp_no_console_errors.py test is the keystone: it navigates to every major page and asserts no console errors, which catches CSP/HTMX loading regressions that unit tests miss.

Layout

src/compendium/
├── domain/        # models, enums, permissions, errors
├── repositories/  # base protocols + SQLAlchemy implementations
├── services/      # business logic (catalog, circulation, holds, patrons, policies, auth, audit)
├── api/           # FastAPI routes + Pydantic schemas + JWT auth
├── web/           # HTMX + Jinja2 web UI + CSRF protection
├── cli/           # Typer CLI commands
├── config/        # settings, seed data
└── db/            # engine factory, session lifecycle

Changelog

Release-by-release changes are recorded in CHANGELOG.md.

License

MIT (to be finalised before first release).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

compendium_ils-1.2.1.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

compendium_ils-1.2.1-py3-none-any.whl (1.0 MB view details)

Uploaded Python 3

File details

Details for the file compendium_ils-1.2.1.tar.gz.

File metadata

  • Download URL: compendium_ils-1.2.1.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for compendium_ils-1.2.1.tar.gz
Algorithm Hash digest
SHA256 fad8d39a1848b71221501d6048047f8aa6ad029d81c016c0a1a78e05df853329
MD5 d4fffc10ba74e940ba9a698865e0c2db
BLAKE2b-256 947f0109c1c6707642e9c5e4d4c48b8227d8d9b0a78855e0c6a9fc2a8464afaa

See more details on using hashes here.

Provenance

The following attestation bundles were made for compendium_ils-1.2.1.tar.gz:

Publisher: release.yml on statyk/compendium

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file compendium_ils-1.2.1-py3-none-any.whl.

File metadata

  • Download URL: compendium_ils-1.2.1-py3-none-any.whl
  • Upload date:
  • Size: 1.0 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for compendium_ils-1.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 933cd4c9aa794a29897997ff9ecc8f2dd950b8595d40ac1d67c03249c0eb2e01
MD5 d446009e57d117c90be65c82afe51002
BLAKE2b-256 5afd473525da33a00e9b64693042392a0cacfaf8e97c96138390fec3c768f015

See more details on using hashes here.

Provenance

The following attestation bundles were made for compendium_ils-1.2.1-py3-none-any.whl:

Publisher: release.yml on statyk/compendium

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page