{"owner":"internetarchive","repo":"openlibrary","hasSkills":true,"totalSkillsCount":3,"totalTokensCount":4000,"categories":["plugin-manifest","copilot-instructions","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":["openlibrary/plugins/README.md",".github/copilot-instructions.md","AGENTS.md"],"skills":{"openlibrary/plugins/README.md":"# Plugins\n\nThe plugins directory defines and registers all the Routers responsible for responding to requests to the Open Library website.\n\n## Understanding Routers\nIn Open Library, when a patron navigates to an openlibrary.org url in their browser, the request is typically captured by a Router class which is defined by files within the `plugins/` directory. Any Router class will be responsible for doing 4 things:\n\n1. Defining a `path` -- a regular expression pattern used to decide whether a patron's requests should be handled by this Router\n2. Implementing necessary class methods (e.g. GET and POST) corresponding to the patron's HTTP request type\n3. Making calls to the right data `models`\n4. Prepare data to be returned (which could be e.g. a rendered template or json) -- this may include setting any headers or content-types necessary for this data\n\nThe first step is to figure out where within the `plugins/` directory your new route should live and whether it best lives in an existing file or a new file. An overview of the different plugin types are discussed in [this technical overview video](https://archive.org/embed/openlibrary-tour-2020/technical_overview.mp4?start=1017).\n\n## Understanding Directories\n\nThe following describes what each plugin directory does, by convention, in decreasing order of presumed relevance:\n\n* openlibrary -- most public pages meant to be read/consumed live here\n  * `code.py` defines the `/isbn` endpoint and what python functions our html templates have access to!\n  * `js/` is where our javascript lives\n  * `api.py` is many of our APIs are defined\n  * `lists.py` defines many of the routes for the List feature\n* upstream -- handles incoming stateful data requests (wiki edits, uploads, adding a book, logins, etc)\n  * responds to actions (e.g. borrowing) and account related requests: editing, mybooks pages, account login, settings\n* worksearch -- renders our `/search/*` related pages and all our search querying logic.\n  * `code.py` contains most of the logic and routes for `/search`, the `/barcodescanner`, `/advancedsearch`, `/search/lists`, `/search/subjects`, etc, as well as their json returning counterparts\n* importapi -- defines our book import API endpoints\n  * `code.py` again defines most of the core logic we care about\n* admin -- our admin pages and logic\n* books -- old-style book APIs (likely not relevant)\n\n## Code.py\n\nAlmost every `plugins/` directory defines a central `code.py` which is similar to `__init__.py` in that it contains the logic required to register its directory's plugins with the system. In addition to acting as an entry point for all the other plugins in the directory and performing setup(), it also may define its own Routers. If you're not sure where in a plugin to look for a Router, that's likely a good place to start.\n\n## Tutorial: Implementing a new Route\n\nThere are two ways to implement a new Open Library Route. The first way is to use your discretion to add a new Route to the correct pre-existing plugin file. The second way is to create a new directory and/or file which will require an additional step of registering your plugin with Open Library. We'll only walkthrough the first case here -- for those who need to register a new plugin, please see `openlibrary/plugins/openlibrary/code.py` for an [example](https://github.com/internetarchive/openlibrary/blob/208fe9de10f24d1f54b691f4a40d1dc4b0148745/openlibrary/plugins/openlibrary/code.py#L1105-L1143).\n\nLet's say I wanted to create a page on Open Library called `/search/many` for a search experience that lets patrons search for multiple books in bulk. The first thing I'd do is make a determination of where it should live -- likely in `worksearch/code.py`. We'd then define our Route as follows:\n\n```\nclass search_many(delegate.page):\n    path = '/search/many'\n\n    def GET(self):\n        i = web.input(q='')\n        return render_template('search/many', q=i.q)\n```\n\nEvery Router is a class that extends something like `delegate.page`. The Router will define a `GET`, `POST`, etc, as appropriate to deal with corresponding HTTP requests of that type from the patron. The function parameters defined by the GET and POST of the Router depend on and are linked to the regex pattern specified by the Router's `path`.\n\nThe following example from [plugins/openlibrary/api.py](https://github.com/internetarchive/openlibrary/blob/master/openlibrary/plugins/openlibrary/api.py#L65-L71), The `trending_books_api` Router defines a `path` with a regex `\"/trending(/?.*)\"` that captures everything after `/trending/` and forwards it into the GET function as the parameter `period`. In this example, if a patron goes to `/trending/daily`, the request will match the Router's path regex and the Router will call the GET method with `period=daily` as an argument.\n\n```\nclass trending_books_api(delegate.page):\n    path = \"/trending(/?.*)\"\n    # path = \"/trending/(now|daily|weekly|monthly|yearly|forever)\"\n    encoding = \"json\"\n\n    def GET(self, period=\"/daily\"):\n```\n\nIn our first example `i = web.input(q='')` is used to access the GET query parameters specified by the patron's request from the URL (e.g. if the patron queries for /search/many?q=test then `i.q` will be \"test\".\n\nThe final line of the Router in our example makes a call to `render_template` which will fetch the corresponding template, by name, from the `templates/` directory (specified by the 1st argument -- in this case the file \"templates/search/many.html) and then pass in any of the variables this template needs to render (in this case the value `q`). The values passed in to the template must also be defined in the header of the HTML template within the `$ def with(...)` line. For more information on rendering templates, refer to the [Front-end Guide](https://docs.openlibrary.org/developers/frontend/frontend-guide.html#routing-and-templates) in our docs.\n\nIn order to get this example to work, you will first need to create a new file `templates/search/many.html` with the following body content:\n\n```\n$ def with (q=\"\")\n\nYour query was $(q)!\n```\n\nYou can now restart your server and test that your application is working by going to `/search/many?q=test&debug=true`, as described in our [debugging website errors video](https://archive.org/embed/openlibrary-tour-2020/openlibrary-debugging-webpage-errors.mp4)\n",".github/copilot-instructions.md":"# Copilot Instructions\n\n> **Canonical guides:** [`AGENTS.md`](../AGENTS.md) for stack, commands, and conventions; [`docs/ai-coding-guide.md`](../docs/ai-coding-guide.md) for architecture and data-model. Path-specific review guidance lives in [`instructions/`](instructions/).\n\n### Code Style\n\n- **Python:** Ruff for linting and `ruff format` for formatting, line length 162, double quotes\n- **JS:** oxlint + oxfmt (ESLint for Vue files only), single quotes, no jQuery in new code\n- **CSS:** Stylelint — semantic tokens only, no hex/named colors\n- **Templates:** Jinja2 for new templates; Templetor is legacy\n- **Branches:** `{issue-number}/{type}/{slug}`\n\n### Code Quality Guidelines\n\n**Code Cleanup:** Remove dead/commented code, unused variables, and redundant comments that restate the code.\n\n**Naming Conventions:** Use descriptive names consistent with project style (snake_case for Python, UpperCamelCase for Vue components). Avoid vague names like `data`, `p`, `item`.\n\n**Idiomatic Code:** Use language idioms (e.g., comprehensions in Python, modern JS features). Simplify verbose patterns like `if len(items) > 0:` → `if items:`.\n\n**DRY:** Abstract repeated code blocks into shared helpers.\n\n**Dependencies:** When updating a dependency, ensure it's updated across all dependency locations: requirements*.txt files, package.json files, and .pre-commit-config.yaml.\n\n**i18n:** Wrap all user-facing strings in translation functions (`$_(...)` or `$:_('...')`). Do not use string concatenation to construct user-visible strings; prefer a single parameterized translation string instead. Use named placeholders (`_('Hello, %(name)s')`) rather than positional placeholders such as `%s` so translators have more context. Use `$:_()` for HTML content to prevent double-escaping.\n\n**FastAPI:** When touching FastAPI code, follow [FastAPI best practices](https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) and ensure compliance with their recommendations. When in doubt, reference other parts of our FastAPI codebase for how we do things, as we generally follow established best practices.\n\n**web.py Migration:** When modifying a web.py endpoint that is marked as deprecated, update the corresponding FastAPI endpoint instead. FastAPI is the preferred framework for new features and migrations. Ensure backward compatibility by checking that the same inputs are accepted. Validation errors and status codes may differ slightly, which is acceptable.\n","AGENTS.md":"# AGENTS.md\n\n> **Canonical guide:** [`docs/ai-coding-guide.md`](/docs/ai-coding-guide.md) — read that file for full architecture, templates, data-model, and file-location details.\n\n## Quick Reference\n\n**Stack:** Python 3.14 / web.py (Infogami) + FastAPI · Templetor (legacy) / **Jinja2 (preferred for new code)** templates · jQuery, Vue 3, Lit · Vite · Solr 10\n\n> 📖 **Guides:** [`docs/i18n/i18n.md`](/docs/i18n/i18n.md) — i18n best practices for Templetor, Jinja, and client-side strings. [`docs/frontend/design.md`](/docs/frontend/design.md) — read before touching `static/css/`, templates, macros, or `openlibrary/components/`; [`docs/frontend/web-components.md`](/docs/frontend/web-components.md) — also read before touching `openlibrary/components/lit/`. [`docs/ai-coding-guide.md`](/docs/ai-coding-guide.md) — full architecture and data-model. [`docs/core/database.md`](/docs/core/database.md) — read before writing SQL or touching the DB models. [`docs/wiki/developers/frontend/jinja.md`](/developers/frontend/jinja.md) — Jinja template conventions and Templetor→Jinja conversion.\n\n> 🏗️ **FastAPI:** When working on FastAPI endpoints, always load the [FastAPI skill](https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) and follow the existing patterns in the codebase. Don't invent new architectural patterns — match what's already there.\n>\n> 📄 **Jinja:** When touching Jinja files (`*.html.jinja`) — creating, editing, or converting from Templetor — read [`docs/wiki/developers/frontend/jinja.md`](/developers/frontend/jinja.md) first for syntax, conventions, and known conversion pitfalls. `docs/wiki/` is a separate repo (not a submodule), cloned automatically on container startup by `docker/ol-home-start.sh`. If it's missing (e.g. working outside Docker), clone it:\n> ```bash\n> git clone https://github.com/internetarchive/openlibrary.wiki.git docs/wiki\n> ```\n> (Inside the Docker container the same command runs with `/openlibrary/docs/wiki` — see `docker/ol-home-start.sh`.)\n>\n> 🗄️ **Databases:** New DB code goes through the async psycopg3 pool in `openlibrary/core/async_db.py`. Single-statement helpers (`execute`, `fetch_all`, `fetch_one`, `fetch_val`) run one query each; `async with connection()` wraps multi-statement transactions. Legacy sync code keeps `openlibrary/core/db.py`; sync web.py callers reach async model methods via `async_bridge.run` (`_sync` suffix). Reference migration: `openlibrary/core/yearly_reading_goals.py` + `openlibrary/plugins/upstream/yearly_reading_goals.py`. Full guide: [`docs/core/database.md`](/docs/core/database.md).\n\n**Dev setup:** `make git && docker compose up` → http://localhost:8080\n\n### Key Commands\n\n**Before committing**, run pre-commit on your changed files (requires Python 3.14 on host — `brew install python@3.14`):\n\n```bash\npre-commit run --files <file1> <file2> ...\n```\n\nEvery hook passes on the host, `mypy` and `generate-pot` included — pre-commit builds an isolated environment per hook, so none of them need Docker. Run `make git` first, though. Both of those hooks read `infogami`, a symlink into the `vendor/infogami` submodule, so while that submodule is unchecked out they fail with `Cannot read file 'infogami'` and `ModuleNotFoundError: No module named 'infogami'`. Everything must pass. Common auto-fixes that pre-commit applies and you should do yourself first:\n\n- **Double quotes** — use `\"string\"` not `'string'` in all new Python code (the Ruff formatter enforces this)\n- **Import order** — imports must be sorted: stdlib → third-party (alphabetical within each group) → local (ruff isort enforces this)\n- **No trailing whitespace** on any line\n- **Single newline at EOF** — no blank lines at end of file\n- **Walrus operator** — prefer `if x := expr:` over `x = expr` / `if x:` (auto-walrus enforces this)\n- **Line length** — max 162 chars\n\nThe following commands should be run inside docker (with `docker compose run --rm home <command>`). The exception is `test-py-uv`, which runs faster outside Docker using `uv`:\n\n```bash\nmake test-py-uv             # Python tests (preferred — runs outside Docker with uv)\nmake test-py                # Python tests\n\n# Run a subset of Python tests by specifying a path:\nmake test-py-uv PYTEST_ARGS=\"openlibrary/tests/fastapi/\"\n# Or directly: uv run --with-requirements requirements_test.txt pytest openlibrary/tests/fastapi/\n\nnpm run test:js             # JS tests\nnpm run test:js:browser     # JS component tests in real Chromium (browser mode)\nmake lint                   # Python lint (ruff)\nnpm run lint                # JS + CSS lint\nnpm run lint-fix            # Auto-fix JS/CSS\nnpm run watch               # Dev mode with hot reload\n```\n\n### Code Style\n\n- **Python:** Ruff for linting and `ruff format` for formatting, line length 162, double quotes\n- **JS:** oxlint + oxfmt (ESLint for Vue files only), single quotes, no jQuery in new code\n- **CSS:** Stylelint — no hex/named colors, use variables\n- **i18n (Internationalization):** Do not split sentences into separate translatable strings/fragments with HTML links. Instead, use single, unified translatable strings with Python formatting placeholders (e.g. `%(link_start)s` / `%(link_end)s`) so translators can position links according to the target language's grammatical structure.\n- **Branches:** `{issue-number}/{type}/{slug}`\n\n### Entry Points\n\n| What | Where |\n|---|---|\n| App entry | `openlibrary/code.py` |\n| FastAPI | `openlibrary/asgi_app.py` |\n| Route handlers | `openlibrary/plugins/*/code.py` (legacy web.py) · **FastAPI routers** (preferred): `openlibrary/fastapi/*.py` |\n| Templates | `openlibrary/templates/` |\n| JS source | `openlibrary/plugins/openlibrary/js/` |\n| CSS source | `static/css/` |\n| DB access | **Async pool (new code):** `openlibrary/core/async_db.py` · Legacy sync: `openlibrary/core/db.py` |\n\n### Testing Authenticated Endpoints with curl\n\nThe dev environment has a pre-configured test user:\n\n| Property | Value |\n|----------|-------|\n| Username | `openlibrary` |\n| Password | `openlibrary` |\n| Key | `/people/openlibrary` |\n\n1. **Login to get a session cookie:**\n```bash\ncurl -s -c /tmp/cookies.txt -X POST \"http://localhost:8080/account/login.json\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"username\":\"openlibrary\",\"password\":\"openlibrary\"}'\n\n# View the cookie file\ncat /tmp/cookies.txt\n# Example output:\n# # Netscape HTTP Cookie File\n# localhost  FALSE   /       FALSE   0       session /people/openlibrary,2026-01-18T17:25:46,7897f\\$841a3bd2f8e9a5ca46f505fa557d57bd\n```\n\n2. **Use the session cookie in subsequent requests:**\n```bash\n# Just use -b to send the cookie automatically (no manual extraction needed)\ncurl -X POST \"http://localhost:8080/people/openlibrary/lists/OL1L/delete.json\" -b /tmp/cookies.txt\ncurl \"http://localhost:8080/people/openlibrary/lists/OL1L.json\" -b /tmp/cookies.txt\n```\n\n**Note:** Sessions expire — always login fresh before testing. In local dev, FastAPI serves on port 8080 and proxies unmatched requests to web.py; both share the same auth system.\n\n### FastAPI and web.py Interaction\n\nOpen Library runs two web servers:\n- **FastAPI** — primary entry point in local dev (port 8080); unmatched requests proxy to web.py via `openlibrary/fastapi/proxy.py`. All new endpoints go here.\n- **web.py** — **Legacy** (web.py / Infogami) — no new endpoints here, use FastAPI. In local dev it is reached through the FastAPI fallback proxy; in production/staging it is still the front door on port 8080 (FastAPI on 18080).\n\nWhen testing:\n- Both servers share the same database\n- Session cookies work on both (same auth system)\n- FastAPI uses ContextVars (`site.get()`) instead of `web.ctx.site`\n\nKey files for context management:\n- `openlibrary/utils/request_context.py` — Contains `site` ContextVar and other request context.\n- `openlibrary/plugins/openlibrary/code.py` — Sets up context vars in request processor\n"},"files":{"openlibrary/plugins/README.md":"# Plugins\n\nThe plugins directory defines and registers all the Routers responsible for responding to requests to the Open Library website.\n\n## Understanding Routers\nIn Open Library, when a patron navigates to an openlibrary.org url in their browser, the request is typically captured by a Router class which is defined by files within the `plugins/` directory. Any Router class will be responsible for doing 4 things:\n\n1. Defining a `path` -- a regular expression pattern used to decide whether a patron's requests should be handled by this Router\n2. Implementing necessary class methods (e.g. GET and POST) corresponding to the patron's HTTP request type\n3. Making calls to the right data `models`\n4. Prepare data to be returned (which could be e.g. a rendered template or json) -- this may include setting any headers or content-types necessary for this data\n\nThe first step is to figure out where within the `plugins/` directory your new route should live and whether it best lives in an existing file or a new file. An overview of the different plugin types are discussed in [this technical overview video](https://archive.org/embed/openlibrary-tour-2020/technical_overview.mp4?start=1017).\n\n## Understanding Directories\n\nThe following describes what each plugin directory does, by convention, in decreasing order of presumed relevance:\n\n* openlibrary -- most public pages meant to be read/consumed live here\n  * `code.py` defines the `/isbn` endpoint and what python functions our html templates have access to!\n  * `js/` is where our javascript lives\n  * `api.py` is many of our APIs are defined\n  * `lists.py` defines many of the routes for the List feature\n* upstream -- handles incoming stateful data requests (wiki edits, uploads, adding a book, logins, etc)\n  * responds to actions (e.g. borrowing) and account related requests: editing, mybooks pages, account login, settings\n* worksearch -- renders our `/search/*` related pages and all our search querying logic.\n  * `code.py` contains most of the logic and routes for `/search`, the `/barcodescanner`, `/advancedsearch`, `/search/lists`, `/search/subjects`, etc, as well as their json returning counterparts\n* importapi -- defines our book import API endpoints\n  * `code.py` again defines most of the core logic we care about\n* admin -- our admin pages and logic\n* books -- old-style book APIs (likely not relevant)\n\n## Code.py\n\nAlmost every `plugins/` directory defines a central `code.py` which is similar to `__init__.py` in that it contains the logic required to register its directory's plugins with the system. In addition to acting as an entry point for all the other plugins in the directory and performing setup(), it also may define its own Routers. If you're not sure where in a plugin to look for a Router, that's likely a good place to start.\n\n## Tutorial: Implementing a new Route\n\nThere are two ways to implement a new Open Library Route. The first way is to use your discretion to add a new Route to the correct pre-existing plugin file. The second way is to create a new directory and/or file which will require an additional step of registering your plugin with Open Library. We'll only walkthrough the first case here -- for those who need to register a new plugin, please see `openlibrary/plugins/openlibrary/code.py` for an [example](https://github.com/internetarchive/openlibrary/blob/208fe9de10f24d1f54b691f4a40d1dc4b0148745/openlibrary/plugins/openlibrary/code.py#L1105-L1143).\n\nLet's say I wanted to create a page on Open Library called `/search/many` for a search experience that lets patrons search for multiple books in bulk. The first thing I'd do is make a determination of where it should live -- likely in `worksearch/code.py`. We'd then define our Route as follows:\n\n```\nclass search_many(delegate.page):\n    path = '/search/many'\n\n    def GET(self):\n        i = web.input(q='')\n        return render_template('search/many', q=i.q)\n```\n\nEvery Router is a class that extends something like `delegate.page`. The Router will define a `GET`, `POST`, etc, as appropriate to deal with corresponding HTTP requests of that type from the patron. The function parameters defined by the GET and POST of the Router depend on and are linked to the regex pattern specified by the Router's `path`.\n\nThe following example from [plugins/openlibrary/api.py](https://github.com/internetarchive/openlibrary/blob/master/openlibrary/plugins/openlibrary/api.py#L65-L71), The `trending_books_api` Router defines a `path` with a regex `\"/trending(/?.*)\"` that captures everything after `/trending/` and forwards it into the GET function as the parameter `period`. In this example, if a patron goes to `/trending/daily`, the request will match the Router's path regex and the Router will call the GET method with `period=daily` as an argument.\n\n```\nclass trending_books_api(delegate.page):\n    path = \"/trending(/?.*)\"\n    # path = \"/trending/(now|daily|weekly|monthly|yearly|forever)\"\n    encoding = \"json\"\n\n    def GET(self, period=\"/daily\"):\n```\n\nIn our first example `i = web.input(q='')` is used to access the GET query parameters specified by the patron's request from the URL (e.g. if the patron queries for /search/many?q=test then `i.q` will be \"test\".\n\nThe final line of the Router in our example makes a call to `render_template` which will fetch the corresponding template, by name, from the `templates/` directory (specified by the 1st argument -- in this case the file \"templates/search/many.html) and then pass in any of the variables this template needs to render (in this case the value `q`). The values passed in to the template must also be defined in the header of the HTML template within the `$ def with(...)` line. For more information on rendering templates, refer to the [Front-end Guide](https://docs.openlibrary.org/developers/frontend/frontend-guide.html#routing-and-templates) in our docs.\n\nIn order to get this example to work, you will first need to create a new file `templates/search/many.html` with the following body content:\n\n```\n$ def with (q=\"\")\n\nYour query was $(q)!\n```\n\nYou can now restart your server and test that your application is working by going to `/search/many?q=test&debug=true`, as described in our [debugging website errors video](https://archive.org/embed/openlibrary-tour-2020/openlibrary-debugging-webpage-errors.mp4)\n",".github/copilot-instructions.md":"# Copilot Instructions\n\n> **Canonical guides:** [`AGENTS.md`](../AGENTS.md) for stack, commands, and conventions; [`docs/ai-coding-guide.md`](../docs/ai-coding-guide.md) for architecture and data-model. Path-specific review guidance lives in [`instructions/`](instructions/).\n\n### Code Style\n\n- **Python:** Ruff for linting and `ruff format` for formatting, line length 162, double quotes\n- **JS:** oxlint + oxfmt (ESLint for Vue files only), single quotes, no jQuery in new code\n- **CSS:** Stylelint — semantic tokens only, no hex/named colors\n- **Templates:** Jinja2 for new templates; Templetor is legacy\n- **Branches:** `{issue-number}/{type}/{slug}`\n\n### Code Quality Guidelines\n\n**Code Cleanup:** Remove dead/commented code, unused variables, and redundant comments that restate the code.\n\n**Naming Conventions:** Use descriptive names consistent with project style (snake_case for Python, UpperCamelCase for Vue components). Avoid vague names like `data`, `p`, `item`.\n\n**Idiomatic Code:** Use language idioms (e.g., comprehensions in Python, modern JS features). Simplify verbose patterns like `if len(items) > 0:` → `if items:`.\n\n**DRY:** Abstract repeated code blocks into shared helpers.\n\n**Dependencies:** When updating a dependency, ensure it's updated across all dependency locations: requirements*.txt files, package.json files, and .pre-commit-config.yaml.\n\n**i18n:** Wrap all user-facing strings in translation functions (`$_(...)` or `$:_('...')`). Do not use string concatenation to construct user-visible strings; prefer a single parameterized translation string instead. Use named placeholders (`_('Hello, %(name)s')`) rather than positional placeholders such as `%s` so translators have more context. Use `$:_()` for HTML content to prevent double-escaping.\n\n**FastAPI:** When touching FastAPI code, follow [FastAPI best practices](https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) and ensure compliance with their recommendations. When in doubt, reference other parts of our FastAPI codebase for how we do things, as we generally follow established best practices.\n\n**web.py Migration:** When modifying a web.py endpoint that is marked as deprecated, update the corresponding FastAPI endpoint instead. FastAPI is the preferred framework for new features and migrations. Ensure backward compatibility by checking that the same inputs are accepted. Validation errors and status codes may differ slightly, which is acceptable.\n","AGENTS.md":"# AGENTS.md\n\n> **Canonical guide:** [`docs/ai-coding-guide.md`](/docs/ai-coding-guide.md) — read that file for full architecture, templates, data-model, and file-location details.\n\n## Quick Reference\n\n**Stack:** Python 3.14 / web.py (Infogami) + FastAPI · Templetor (legacy) / **Jinja2 (preferred for new code)** templates · jQuery, Vue 3, Lit · Vite · Solr 10\n\n> 📖 **Guides:** [`docs/i18n/i18n.md`](/docs/i18n/i18n.md) — i18n best practices for Templetor, Jinja, and client-side strings. [`docs/frontend/design.md`](/docs/frontend/design.md) — read before touching `static/css/`, templates, macros, or `openlibrary/components/`; [`docs/frontend/web-components.md`](/docs/frontend/web-components.md) — also read before touching `openlibrary/components/lit/`. [`docs/ai-coding-guide.md`](/docs/ai-coding-guide.md) — full architecture and data-model. [`docs/core/database.md`](/docs/core/database.md) — read before writing SQL or touching the DB models. [`docs/wiki/developers/frontend/jinja.md`](/developers/frontend/jinja.md) — Jinja template conventions and Templetor→Jinja conversion.\n\n> 🏗️ **FastAPI:** When working on FastAPI endpoints, always load the [FastAPI skill](https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) and follow the existing patterns in the codebase. Don't invent new architectural patterns — match what's already there.\n>\n> 📄 **Jinja:** When touching Jinja files (`*.html.jinja`) — creating, editing, or converting from Templetor — read [`docs/wiki/developers/frontend/jinja.md`](/developers/frontend/jinja.md) first for syntax, conventions, and known conversion pitfalls. `docs/wiki/` is a separate repo (not a submodule), cloned automatically on container startup by `docker/ol-home-start.sh`. If it's missing (e.g. working outside Docker), clone it:\n> ```bash\n> git clone https://github.com/internetarchive/openlibrary.wiki.git docs/wiki\n> ```\n> (Inside the Docker container the same command runs with `/openlibrary/docs/wiki` — see `docker/ol-home-start.sh`.)\n>\n> 🗄️ **Databases:** New DB code goes through the async psycopg3 pool in `openlibrary/core/async_db.py`. Single-statement helpers (`execute`, `fetch_all`, `fetch_one`, `fetch_val`) run one query each; `async with connection()` wraps multi-statement transactions. Legacy sync code keeps `openlibrary/core/db.py`; sync web.py callers reach async model methods via `async_bridge.run` (`_sync` suffix). Reference migration: `openlibrary/core/yearly_reading_goals.py` + `openlibrary/plugins/upstream/yearly_reading_goals.py`. Full guide: [`docs/core/database.md`](/docs/core/database.md).\n\n**Dev setup:** `make git && docker compose up` → http://localhost:8080\n\n### Key Commands\n\n**Before committing**, run pre-commit on your changed files (requires Python 3.14 on host — `brew install python@3.14`):\n\n```bash\npre-commit run --files <file1> <file2> ...\n```\n\nEvery hook passes on the host, `mypy` and `generate-pot` included — pre-commit builds an isolated environment per hook, so none of them need Docker. Run `make git` first, though. Both of those hooks read `infogami`, a symlink into the `vendor/infogami` submodule, so while that submodule is unchecked out they fail with `Cannot read file 'infogami'` and `ModuleNotFoundError: No module named 'infogami'`. Everything must pass. Common auto-fixes that pre-commit applies and you should do yourself first:\n\n- **Double quotes** — use `\"string\"` not `'string'` in all new Python code (the Ruff formatter enforces this)\n- **Import order** — imports must be sorted: stdlib → third-party (alphabetical within each group) → local (ruff isort enforces this)\n- **No trailing whitespace** on any line\n- **Single newline at EOF** — no blank lines at end of file\n- **Walrus operator** — prefer `if x := expr:` over `x = expr` / `if x:` (auto-walrus enforces this)\n- **Line length** — max 162 chars\n\nThe following commands should be run inside docker (with `docker compose run --rm home <command>`). The exception is `test-py-uv`, which runs faster outside Docker using `uv`:\n\n```bash\nmake test-py-uv             # Python tests (preferred — runs outside Docker with uv)\nmake test-py                # Python tests\n\n# Run a subset of Python tests by specifying a path:\nmake test-py-uv PYTEST_ARGS=\"openlibrary/tests/fastapi/\"\n# Or directly: uv run --with-requirements requirements_test.txt pytest openlibrary/tests/fastapi/\n\nnpm run test:js             # JS tests\nnpm run test:js:browser     # JS component tests in real Chromium (browser mode)\nmake lint                   # Python lint (ruff)\nnpm run lint                # JS + CSS lint\nnpm run lint-fix            # Auto-fix JS/CSS\nnpm run watch               # Dev mode with hot reload\n```\n\n### Code Style\n\n- **Python:** Ruff for linting and `ruff format` for formatting, line length 162, double quotes\n- **JS:** oxlint + oxfmt (ESLint for Vue files only), single quotes, no jQuery in new code\n- **CSS:** Stylelint — no hex/named colors, use variables\n- **i18n (Internationalization):** Do not split sentences into separate translatable strings/fragments with HTML links. Instead, use single, unified translatable strings with Python formatting placeholders (e.g. `%(link_start)s` / `%(link_end)s`) so translators can position links according to the target language's grammatical structure.\n- **Branches:** `{issue-number}/{type}/{slug}`\n\n### Entry Points\n\n| What | Where |\n|---|---|\n| App entry | `openlibrary/code.py` |\n| FastAPI | `openlibrary/asgi_app.py` |\n| Route handlers | `openlibrary/plugins/*/code.py` (legacy web.py) · **FastAPI routers** (preferred): `openlibrary/fastapi/*.py` |\n| Templates | `openlibrary/templates/` |\n| JS source | `openlibrary/plugins/openlibrary/js/` |\n| CSS source | `static/css/` |\n| DB access | **Async pool (new code):** `openlibrary/core/async_db.py` · Legacy sync: `openlibrary/core/db.py` |\n\n### Testing Authenticated Endpoints with curl\n\nThe dev environment has a pre-configured test user:\n\n| Property | Value |\n|----------|-------|\n| Username | `openlibrary` |\n| Password | `openlibrary` |\n| Key | `/people/openlibrary` |\n\n1. **Login to get a session cookie:**\n```bash\ncurl -s -c /tmp/cookies.txt -X POST \"http://localhost:8080/account/login.json\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"username\":\"openlibrary\",\"password\":\"openlibrary\"}'\n\n# View the cookie file\ncat /tmp/cookies.txt\n# Example output:\n# # Netscape HTTP Cookie File\n# localhost  FALSE   /       FALSE   0       session /people/openlibrary,2026-01-18T17:25:46,7897f\\$841a3bd2f8e9a5ca46f505fa557d57bd\n```\n\n2. **Use the session cookie in subsequent requests:**\n```bash\n# Just use -b to send the cookie automatically (no manual extraction needed)\ncurl -X POST \"http://localhost:8080/people/openlibrary/lists/OL1L/delete.json\" -b /tmp/cookies.txt\ncurl \"http://localhost:8080/people/openlibrary/lists/OL1L.json\" -b /tmp/cookies.txt\n```\n\n**Note:** Sessions expire — always login fresh before testing. In local dev, FastAPI serves on port 8080 and proxies unmatched requests to web.py; both share the same auth system.\n\n### FastAPI and web.py Interaction\n\nOpen Library runs two web servers:\n- **FastAPI** — primary entry point in local dev (port 8080); unmatched requests proxy to web.py via `openlibrary/fastapi/proxy.py`. All new endpoints go here.\n- **web.py** — **Legacy** (web.py / Infogami) — no new endpoints here, use FastAPI. In local dev it is reached through the FastAPI fallback proxy; in production/staging it is still the front door on port 8080 (FastAPI on 18080).\n\nWhen testing:\n- Both servers share the same database\n- Session cookies work on both (same auth system)\n- FastAPI uses ContextVars (`site.get()`) instead of `web.ctx.site`\n\nKey files for context management:\n- `openlibrary/utils/request_context.py` — Contains `site` ContextVar and other request context.\n- `openlibrary/plugins/openlibrary/code.py` — Sets up context vars in request processor\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/internetarchive/openlibrary/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\n> **Canonical guide:** [`docs/ai-coding-guide.md`](/docs/ai-coding-guide.md) — read that file for full architecture, templates, data-model, and file-location details.\n\n## Quick Reference\n\n**Stack:** Python 3.14 / web.py (Infogami) + FastAPI · Templetor (legacy) / **Jinja2 (preferred for new code)** templates · jQuery, Vue 3, Lit · Vite · Solr 10\n\n> 📖 **Guides:** [`docs/i18n/i18n.md`](/docs/i18n/i18n.md) — i18n best practices for Templetor, Jinja, and client-side strings. [`docs/frontend/design.md`](/docs/frontend/design.md) — read before touching `static/css/`, templates, macros, or `openlibrary/components/`; [`docs/frontend/web-components.md`](/docs/frontend/web-components.md) — also read before touching `openlibrary/components/lit/`. [`docs/ai-coding-guide.md`](/docs/ai-coding-guide.md) — full architecture and data-model. [`docs/core/database.md`](/docs/core/database.md) — read before writing SQL or touching the DB models. [`docs/wiki/developers/frontend/jinja.md`](/developers/frontend/jinja.md) — Jinja template conventions and Templetor→Jinja conversion.\n\n> 🏗️ **FastAPI:** When working on FastAPI endpoints, always load the [FastAPI skill](https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) and follow the existing patterns in the codebase. Don't invent new architectural patterns — match what's already there.\n>\n> 📄 **Jinja:** When touching Jinja files (`*.html.jinja`) — creating, editing, or converting from Templetor — read [`docs/wiki/developers/frontend/jinja.md`](/developers/frontend/jinja.md) first for syntax, conventions, and known conversion pitfalls. `docs/wiki/` is a separate repo (not a submodule), cloned automatically on container startup by `docker/ol-home-start.sh`. If it's missing (e.g. working outside Docker), clone it:\n> ```bash\n> git clone https://github.com/internetarchive/openlibrary.wiki.git docs/wiki\n> ```\n> (Inside the Docker container the same command runs with `/openlibrary/docs/wiki` — see `docker/ol-home-start.sh`.)\n>\n> 🗄️ **Databases:** New DB code goes through the async psycopg3 pool in `openlibrary/core/async_db.py`. Single-statement helpers (`execute`, `fetch_all`, `fetch_one`, `fetch_val`) run one query each; `async with connection()` wraps multi-statement transactions. Legacy sync code keeps `openlibrary/core/db.py`; sync web.py callers reach async model methods via `async_bridge.run` (`_sync` suffix). Reference migration: `openlibrary/core/yearly_reading_goals.py` + `openlibrary/plugins/upstream/yearly_reading_goals.py`. Full guide: [`docs/core/database.md`](/docs/core/database.md).\n\n**Dev setup:** `make git && docker compose up` → http://localhost:8080\n\n### Key Commands\n\n**Before committing**, run pre-commit on your changed files (requires Python 3.14 on host — `brew install python@3.14`):\n\n```bash\npre-commit run --files <file1> <file2> ...\n```\n\nEvery hook passes on the host, `mypy` and `generate-pot` included — pre-commit builds an isolated environment per hook, so none of them need Docker. Run `make git` first, though. Both of those hooks read `infogami`, a symlink into the `vendor/infogami` submodule, so while that submodule is unchecked out they fail with `Cannot read file 'infogami'` and `ModuleNotFoundError: No module named 'infogami'`. Everything must pass. Common auto-fixes that pre-commit applies and you should do yourself first:\n\n- **Double quotes** — use `\"string\"` not `'string'` in all new Python code (the Ruff formatter enforces this)\n- **Import order** — imports must be sorted: stdlib → third-party (alphabetical within each group) → local (ruff isort enforces this)\n- **No trailing whitespace** on any line\n- **Single newline at EOF** — no blank lines at end of file\n- **Walrus operator** — prefer `if x := expr:` over `x = expr` / `if x:` (auto-walrus enforces this)\n- **Line length** — max 162 chars\n\nThe following commands should be run inside docker (with `docker compose run --rm home <command>`). The exception is `test-py-uv`, which runs faster outside Docker using `uv`:\n\n```bash\nmake test-py-uv             # Python tests (preferred — runs outside Docker with uv)\nmake test-py                # Python tests\n\n# Run a subset of Python tests by specifying a path:\nmake test-py-uv PYTEST_ARGS=\"openlibrary/tests/fastapi/\"\n# Or directly: uv run --with-requirements requirements_test.txt pytest openlibrary/tests/fastapi/\n\nnpm run test:js             # JS tests\nnpm run test:js:browser     # JS component tests in real Chromium (browser mode)\nmake lint                   # Python lint (ruff)\nnpm run lint                # JS + CSS lint\nnpm run lint-fix            # Auto-fix JS/CSS\nnpm run watch               # Dev mode with hot reload\n```\n\n### Code Style\n\n- **Python:** Ruff for linting and `ruff format` for formatting, line length 162, double quotes\n- **JS:** oxlint + oxfmt (ESLint for Vue files only), single quotes, no jQuery in new code\n- **CSS:** Stylelint — no hex/named colors, use variables\n- **i18n (Internationalization):** Do not split sentences into separate translatable strings/fragments with HTML links. Instead, use single, unified translatable strings with Python formatting placeholders (e.g. `%(link_start)s` / `%(link_end)s`) so translators can position links according to the target language's grammatical structure.\n- **Branches:** `{issue-number}/{type}/{slug}`\n\n### Entry Points\n\n| What | Where |\n|---|---|\n| App entry | `openlibrary/code.py` |\n| FastAPI | `openlibrary/asgi_app.py` |\n| Route handlers | `openlibrary/plugins/*/code.py` (legacy web.py) · **FastAPI routers** (preferred): `openlibrary/fastapi/*.py` |\n| Templates | `openlibrary/templates/` |\n| JS source | `openlibrary/plugins/openlibrary/js/` |\n| CSS source | `static/css/` |\n| DB access | **Async pool (new code):** `openlibrary/core/async_db.py` · Legacy sync: `openlibrary/core/db.py` |\n\n### Testing Authenticated Endpoints with curl\n\nThe dev environment has a pre-configured test user:\n\n| Property | Value |\n|----------|-------|\n| Username | `openlibrary` |\n| Password | `openlibrary` |\n| Key | `/people/openlibrary` |\n\n1. **Login to get a session cookie:**\n```bash\ncurl -s -c /tmp/cookies.txt -X POST \"http://localhost:8080/account/login.json\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"username\":\"openlibrary\",\"password\":\"openlibrary\"}'\n\n# View the cookie file\ncat /tmp/cookies.txt\n# Example output:\n# # Netscape HTTP Cookie File\n# localhost  FALSE   /       FALSE   0       session /people/openlibrary,2026-01-18T17:25:46,7897f\\$841a3bd2f8e9a5ca46f505fa557d57bd\n```\n\n2. **Use the session cookie in subsequent requests:**\n```bash\n# Just use -b to send the cookie automatically (no manual extraction needed)\ncurl -X POST \"http://localhost:8080/people/openlibrary/lists/OL1L/delete.json\" -b /tmp/cookies.txt\ncurl \"http://localhost:8080/people/openlibrary/lists/OL1L.json\" -b /tmp/cookies.txt\n```\n\n**Note:** Sessions expire — always login fresh before testing. In local dev, FastAPI serves on port 8080 and proxies unmatched requests to web.py; both share the same auth system.\n\n### FastAPI and web.py Interaction\n\nOpen Library runs two web servers:\n- **FastAPI** — primary entry point in local dev (port 8080); unmatched requests proxy to web.py via `openlibrary/fastapi/proxy.py`. All new endpoints go here.\n- **web.py** — **Legacy** (web.py / Infogami) — no new endpoints here, use FastAPI. In local dev it is reached through the FastAPI fallback proxy; in production/staging it is still the front door on port 8080 (FastAPI on 18080).\n\nWhen testing:\n- Both servers share the same database\n- Session cookies work on both (same auth system)\n- FastAPI uses ContextVars (`site.get()`) instead of `web.ctx.site`\n\nKey files for context management:\n- `openlibrary/utils/request_context.py` — Contains `site` ContextVar and other request context.\n- `openlibrary/plugins/openlibrary/code.py` — Sets up context vars in request processor\n","isInternal":false,"tokens":2012,"sizeBytes":8002},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/internetarchive/openlibrary/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# Copilot Instructions\n\n> **Canonical guides:** [`AGENTS.md`](../AGENTS.md) for stack, commands, and conventions; [`docs/ai-coding-guide.md`](../docs/ai-coding-guide.md) for architecture and data-model. Path-specific review guidance lives in [`instructions/`](instructions/).\n\n### Code Style\n\n- **Python:** Ruff for linting and `ruff format` for formatting, line length 162, double quotes\n- **JS:** oxlint + oxfmt (ESLint for Vue files only), single quotes, no jQuery in new code\n- **CSS:** Stylelint — semantic tokens only, no hex/named colors\n- **Templates:** Jinja2 for new templates; Templetor is legacy\n- **Branches:** `{issue-number}/{type}/{slug}`\n\n### Code Quality Guidelines\n\n**Code Cleanup:** Remove dead/commented code, unused variables, and redundant comments that restate the code.\n\n**Naming Conventions:** Use descriptive names consistent with project style (snake_case for Python, UpperCamelCase for Vue components). Avoid vague names like `data`, `p`, `item`.\n\n**Idiomatic Code:** Use language idioms (e.g., comprehensions in Python, modern JS features). Simplify verbose patterns like `if len(items) > 0:` → `if items:`.\n\n**DRY:** Abstract repeated code blocks into shared helpers.\n\n**Dependencies:** When updating a dependency, ensure it's updated across all dependency locations: requirements*.txt files, package.json files, and .pre-commit-config.yaml.\n\n**i18n:** Wrap all user-facing strings in translation functions (`$_(...)` or `$:_('...')`). Do not use string concatenation to construct user-visible strings; prefer a single parameterized translation string instead. Use named placeholders (`_('Hello, %(name)s')`) rather than positional placeholders such as `%s` so translators have more context. Use `$:_()` for HTML content to prevent double-escaping.\n\n**FastAPI:** When touching FastAPI code, follow [FastAPI best practices](https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) and ensure compliance with their recommendations. When in doubt, reference other parts of our FastAPI codebase for how we do things, as we generally follow established best practices.\n\n**web.py Migration:** When modifying a web.py endpoint that is marked as deprecated, update the corresponding FastAPI endpoint instead. FastAPI is the preferred framework for new features and migrations. Ensure backward compatibility by checking that the same inputs are accepted. Validation errors and status codes may differ slightly, which is acceptable.\n","isInternal":false,"tokens":549,"sizeBytes":2510},{"name":"README.md","path":"openlibrary/plugins/README.md","rawUrl":"https://raw.githubusercontent.com/internetarchive/openlibrary/HEAD/openlibrary/plugins/README.md","title":"plugins Documentation","category":"plugin-manifest","format":"markdown","content":"# Plugins\n\nThe plugins directory defines and registers all the Routers responsible for responding to requests to the Open Library website.\n\n## Understanding Routers\nIn Open Library, when a patron navigates to an openlibrary.org url in their browser, the request is typically captured by a Router class which is defined by files within the `plugins/` directory. Any Router class will be responsible for doing 4 things:\n\n1. Defining a `path` -- a regular expression pattern used to decide whether a patron's requests should be handled by this Router\n2. Implementing necessary class methods (e.g. GET and POST) corresponding to the patron's HTTP request type\n3. Making calls to the right data `models`\n4. Prepare data to be returned (which could be e.g. a rendered template or json) -- this may include setting any headers or content-types necessary for this data\n\nThe first step is to figure out where within the `plugins/` directory your new route should live and whether it best lives in an existing file or a new file. An overview of the different plugin types are discussed in [this technical overview video](https://archive.org/embed/openlibrary-tour-2020/technical_overview.mp4?start=1017).\n\n## Understanding Directories\n\nThe following describes what each plugin directory does, by convention, in decreasing order of presumed relevance:\n\n* openlibrary -- most public pages meant to be read/consumed live here\n  * `code.py` defines the `/isbn` endpoint and what python functions our html templates have access to!\n  * `js/` is where our javascript lives\n  * `api.py` is many of our APIs are defined\n  * `lists.py` defines many of the routes for the List feature\n* upstream -- handles incoming stateful data requests (wiki edits, uploads, adding a book, logins, etc)\n  * responds to actions (e.g. borrowing) and account related requests: editing, mybooks pages, account login, settings\n* worksearch -- renders our `/search/*` related pages and all our search querying logic.\n  * `code.py` contains most of the logic and routes for `/search`, the `/barcodescanner`, `/advancedsearch`, `/search/lists`, `/search/subjects`, etc, as well as their json returning counterparts\n* importapi -- defines our book import API endpoints\n  * `code.py` again defines most of the core logic we care about\n* admin -- our admin pages and logic\n* books -- old-style book APIs (likely not relevant)\n\n## Code.py\n\nAlmost every `plugins/` directory defines a central `code.py` which is similar to `__init__.py` in that it contains the logic required to register its directory's plugins with the system. In addition to acting as an entry point for all the other plugins in the directory and performing setup(), it also may define its own Routers. If you're not sure where in a plugin to look for a Router, that's likely a good place to start.\n\n## Tutorial: Implementing a new Route\n\nThere are two ways to implement a new Open Library Route. The first way is to use your discretion to add a new Route to the correct pre-existing plugin file. The second way is to create a new directory and/or file which will require an additional step of registering your plugin with Open Library. We'll only walkthrough the first case here -- for those who need to register a new plugin, please see `openlibrary/plugins/openlibrary/code.py` for an [example](https://github.com/internetarchive/openlibrary/blob/208fe9de10f24d1f54b691f4a40d1dc4b0148745/openlibrary/plugins/openlibrary/code.py#L1105-L1143).\n\nLet's say I wanted to create a page on Open Library called `/search/many` for a search experience that lets patrons search for multiple books in bulk. The first thing I'd do is make a determination of where it should live -- likely in `worksearch/code.py`. We'd then define our Route as follows:\n\n```\nclass search_many(delegate.page):\n    path = '/search/many'\n\n    def GET(self):\n        i = web.input(q='')\n        return render_template('search/many', q=i.q)\n```\n\nEvery Router is a class that extends something like `delegate.page`. The Router will define a `GET`, `POST`, etc, as appropriate to deal with corresponding HTTP requests of that type from the patron. The function parameters defined by the GET and POST of the Router depend on and are linked to the regex pattern specified by the Router's `path`.\n\nThe following example from [plugins/openlibrary/api.py](https://github.com/internetarchive/openlibrary/blob/master/openlibrary/plugins/openlibrary/api.py#L65-L71), The `trending_books_api` Router defines a `path` with a regex `\"/trending(/?.*)\"` that captures everything after `/trending/` and forwards it into the GET function as the parameter `period`. In this example, if a patron goes to `/trending/daily`, the request will match the Router's path regex and the Router will call the GET method with `period=daily` as an argument.\n\n```\nclass trending_books_api(delegate.page):\n    path = \"/trending(/?.*)\"\n    # path = \"/trending/(now|daily|weekly|monthly|yearly|forever)\"\n    encoding = \"json\"\n\n    def GET(self, period=\"/daily\"):\n```\n\nIn our first example `i = web.input(q='')` is used to access the GET query parameters specified by the patron's request from the URL (e.g. if the patron queries for /search/many?q=test then `i.q` will be \"test\".\n\nThe final line of the Router in our example makes a call to `render_template` which will fetch the corresponding template, by name, from the `templates/` directory (specified by the 1st argument -- in this case the file \"templates/search/many.html) and then pass in any of the variables this template needs to render (in this case the value `q`). The values passed in to the template must also be defined in the header of the HTML template within the `$ def with(...)` line. For more information on rendering templates, refer to the [Front-end Guide](https://docs.openlibrary.org/developers/frontend/frontend-guide.html#routing-and-templates) in our docs.\n\nIn order to get this example to work, you will first need to create a new file `templates/search/many.html` with the following body content:\n\n```\n$ def with (q=\"\")\n\nYour query was $(q)!\n```\n\nYou can now restart your server and test that your application is working by going to `/search/many?q=test&debug=true`, as described in our [debugging website errors video](https://archive.org/embed/openlibrary-tour-2020/openlibrary-debugging-webpage-errors.mp4)\n","isInternal":false,"tokens":1439,"sizeBytes":6346}],"systemPromptSnippet":"<yakaai_skills repo=\"internetarchive/openlibrary\">\n<!-- File: AGENTS.md (Tokens: ~2012 | Category: root-instruction) -->\n# AGENTS.md\n\n> **Canonical guide:** [`docs/ai-coding-guide.md`](/docs/ai-coding-guide.md) — read that file for full architecture, templates, data-model, and file-location details.\n\n## Quick Reference\n\n**Stack:** Python 3.14 / web.py (Infogami) + FastAPI · Templetor (legacy) / **Jinja2 (preferred for new code)** templates · jQuery, Vue 3, Lit · Vite · Solr 10\n\n> 📖 **Guides:** [`docs/i18n/i18n.md`](/docs/i18n/i18n.md) — i18n best practices for Templetor, Jinja, and client-side strings. [`docs/frontend/design.md`](/docs/frontend/design.md) — read before touching `static/css/`, templates, macros, or `openlibrary/components/`; [`docs/frontend/web-components.md`](/docs/frontend/web-components.md) — also read before touching `openlibrary/components/lit/`. [`docs/ai-coding-guide.md`](/docs/ai-coding-guide.md) — full architecture and data-model. [`docs/core/database.md`](/docs/core/database.md) — read before writing SQL or touching the DB models. [`docs/wiki/developers/frontend/jinja.md`](/developers/frontend/jinja.md) — Jinja template conventions and Templetor→Jinja conversion.\n\n> 🏗️ **FastAPI:** When working on FastAPI endpoints, always load the [FastAPI skill](https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) and follow the existing patterns in the codebase. Don't invent new architectural patterns — match what's already there.\n>\n> 📄 **Jinja:** When touching Jinja files (`*.html.jinja`) — creating, editing, or converting from Templetor — read [`docs/wiki/developers/frontend/jinja.md`](/developers/frontend/jinja.md) first for syntax, conventions, and known conversion pitfalls. `docs/wiki/` is a separate repo (not a submodule), cloned automatically on container startup by `docker/ol-home-start.sh`. If it's missing (e.g. working outside Docker), clone it:\n> ```bash\n> git clone https://github.com/internetarchive/openlibrary.wiki.git docs/wiki\n> ```\n> (Inside the Docker container the same command runs with `/openlibrary/docs/wiki` — see `docker/ol-home-start.sh`.)\n>\n> 🗄️ **Databases:** New DB code goes through the async psycopg3 pool in `openlibrary/core/async_db.py`. Single-statement helpers (`execute`, `fetch_all`, `fetch_one`, `fetch_val`) run one query each; `async with connection()` wraps multi-statement transactions. Legacy sync code keeps `openlibrary/core/db.py`; sync web.py callers reach async model methods via `async_bridge.run` (`_sync` suffix). Reference migration: `openlibrary/core/yearly_reading_goals.py` + `openlibrary/plugins/upstream/yearly_reading_goals.py`. Full guide: [`docs/core/database.md`](/docs/core/database.md).\n\n**Dev setup:** `make git && docker compose up` → http://localhost:8080\n\n### Key Commands\n\n**Before committing**, run pre-commit on your changed files (requires Python 3.14 on host — `brew install python@3.14`):\n\n```bash\npre-commit run --files <file1> <file2> ...\n```\n\nEvery hook passes on the host, `mypy` and `generate-pot` included — pre-commit builds an isolated environment per hook, so none of them need Docker. Run `make git` first, though. Both of those hooks read `infogami`, a symlink into the `vendor/infogami` submodule, so while that submodule is unchecked out they fail with `Cannot read file 'infogami'` and `ModuleNotFoundError: No module named 'infogami'`. Everything must pass. Common auto-fixes that pre-commit applies and you should do yourself first:\n\n- **Double quotes** — use `\"string\"` not `'string'` in all new Python code (the Ruff formatter enforces this)\n- **Import order** — imports must be sorted: stdlib → third-party (alphabetical within each group) → local (ruff isort enforces this)\n- **No trailing whitespace** on any line\n- **Single newline at EOF** — no blank lines at end of file\n- **Walrus operator** — prefer `if x := expr:` over `x = expr` / `if x:` (auto-walrus enforces this)\n- **Line length** — max 162 chars\n\nThe following commands should be run inside docker (with `docker compose run --rm home <command>`). The exception is `test-py-uv`, which runs faster outside Docker using `uv`:\n\n```bash\nmake test-py-uv             # Python tests (preferred — runs outside Docker with uv)\nmake test-py                # Python tests\n\n# Run a subset of Python tests by specifying a path:\nmake test-py-uv PYTEST_ARGS=\"openlibrary/tests/fastapi/\"\n# Or directly: uv run --with-requirements requirements_test.txt pytest openlibrary/tests/fastapi/\n\nnpm run test:js             # JS tests\nnpm run test:js:browser     # JS component tests in real Chromium (browser mode)\nmake lint                   # Python lint (ruff)\nnpm run lint                # JS + CSS lint\nnpm run lint-fix            # Auto-fix JS/CSS\nnpm run watch               # Dev mode with hot reload\n```\n\n### Code Style\n\n- **Python:** Ruff for linting and `ruff format` for formatting, line length 162, double quotes\n- **JS:** oxlint + oxfmt (ESLint for Vue files only), single quotes, no jQuery in new code\n- **CSS:** Stylelint — no hex/named colors, use variables\n- **i18n (Internationalization):** Do not split sentences into separate translatable strings/fragments with HTML links. Instead, use single, unified translatable strings with Python formatting placeholders (e.g. `%(link_start)s` / `%(link_end)s`) so translators can position links according to the target language's grammatical structure.\n- **Branches:** `{issue-number}/{type}/{slug}`\n\n### Entry Points\n\n| What | Where |\n|---|---|\n| App entry | `openlibrary/code.py` |\n| FastAPI | `openlibrary/asgi_app.py` |\n| Route handlers | `openlibrary/plugins/*/code.py` (legacy web.py) · **FastAPI routers** (preferred): `openlibrary/fastapi/*.py` |\n| Templates | `openlibrary/templates/` |\n| JS source | `openlibrary/plugins/openlibrary/js/` |\n| CSS source | `static/css/` |\n| DB access | **Async pool (new code):** `openlibrary/core/async_db.py` · Legacy sync: `openlibrary/core/db.py` |\n\n### Testing Authenticated Endpoints with curl\n\nThe dev environment has a pre-configured test user:\n\n| Property | Value |\n|----------|-------|\n| Username | `openlibrary` |\n| Password | `openlibrary` |\n| Key | `/people/openlibrary` |\n\n1. **Login to get a session cookie:**\n```bash\ncurl -s -c /tmp/cookies.txt -X POST \"http://localhost:8080/account/login.json\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"username\":\"openlibrary\",\"password\":\"openlibrary\"}'\n\n# View the cookie file\ncat /tmp/cookies.txt\n# Example output:\n# # Netscape HTTP Cookie File\n# localhost  FALSE   /       FALSE   0       session /people/openlibrary,2026-01-18T17:25:46,7897f\\$841a3bd2f8e9a5ca46f505fa557d57bd\n```\n\n2. **Use the session cookie in subsequent requests:**\n```bash\n# Just use -b to send the cookie automatically (no manual extraction needed)\ncurl -X POST \"http://localhost:8080/people/openlibrary/lists/OL1L/delete.json\" -b /tmp/cookies.txt\ncurl \"http://localhost:8080/people/openlibrary/lists/OL1L.json\" -b /tmp/cookies.txt\n```\n\n**Note:** Sessions expire — always login fresh before testing. In local dev, FastAPI serves on port 8080 and proxies unmatched requests to web.py; both share the same auth system.\n\n### FastAPI and web.py Interaction\n\nOpen Library runs two web servers:\n- **FastAPI** — primary entry point in local dev (port 8080); unmatched requests proxy to web.py via `openlibrary/fastapi/proxy.py`. All new endpoints go here.\n- **web.py** — **Legacy** (web.py / Infogami) — no new endpoints here, use FastAPI. In local dev it is reached through the FastAPI fallback proxy; in production/staging it is still the front door on port 8080 (FastAPI on 18080).\n\nWhen testing:\n- Both servers share the same database\n- Session cookies work on both (same auth system)\n- FastAPI uses ContextVars (`site.get()`) instead of `web.ctx.site`\n\nKey files for context management:\n- `openlibrary/utils/request_context.py` — Contains `site` ContextVar and other request context.\n- `openlibrary/plugins/openlibrary/code.py` — Sets up context vars in request processor\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~549 | Category: copilot-instructions) -->\n# Copilot Instructions\n\n> **Canonical guides:** [`AGENTS.md`](../AGENTS.md) for stack, commands, and conventions; [`docs/ai-coding-guide.md`](../docs/ai-coding-guide.md) for architecture and data-model. Path-specific review guidance lives in [`instructions/`](instructions/).\n\n### Code Style\n\n- **Python:** Ruff for linting and `ruff format` for formatting, line length 162, double quotes\n- **JS:** oxlint + oxfmt (ESLint for Vue files only), single quotes, no jQuery in new code\n- **CSS:** Stylelint — semantic tokens only, no hex/named colors\n- **Templates:** Jinja2 for new templates; Templetor is legacy\n- **Branches:** `{issue-number}/{type}/{slug}`\n\n### Code Quality Guidelines\n\n**Code Cleanup:** Remove dead/commented code, unused variables, and redundant comments that restate the code.\n\n**Naming Conventions:** Use descriptive names consistent with project style (snake_case for Python, UpperCamelCase for Vue components). Avoid vague names like `data`, `p`, `item`.\n\n**Idiomatic Code:** Use language idioms (e.g., comprehensions in Python, modern JS features). Simplify verbose patterns like `if len(items) > 0:` → `if items:`.\n\n**DRY:** Abstract repeated code blocks into shared helpers.\n\n**Dependencies:** When updating a dependency, ensure it's updated across all dependency locations: requirements*.txt files, package.json files, and .pre-commit-config.yaml.\n\n**i18n:** Wrap all user-facing strings in translation functions (`$_(...)` or `$:_('...')`). Do not use string concatenation to construct user-visible strings; prefer a single parameterized translation string instead. Use named placeholders (`_('Hello, %(name)s')`) rather than positional placeholders such as `%s` so translators have more context. Use `$:_()` for HTML content to prevent double-escaping.\n\n**FastAPI:** When touching FastAPI code, follow [FastAPI best practices](https://raw.githubusercontent.com/fastapi/fastapi/refs/heads/master/fastapi/.agents/skills/fastapi/SKILL.md) and ensure compliance with their recommendations. When in doubt, reference other parts of our FastAPI codebase for how we do things, as we generally follow established best practices.\n\n**web.py Migration:** When modifying a web.py endpoint that is marked as deprecated, update the corresponding FastAPI endpoint instead. FastAPI is the preferred framework for new features and migrations. Ensure backward compatibility by checking that the same inputs are accepted. Validation errors and status codes may differ slightly, which is acceptable.\n\n<!-- File: openlibrary/plugins/README.md (Tokens: ~1439 | Category: plugin-manifest) -->\n# Plugins\n\nThe plugins directory defines and registers all the Routers responsible for responding to requests to the Open Library website.\n\n## Understanding Routers\nIn Open Library, when a patron navigates to an openlibrary.org url in their browser, the request is typically captured by a Router class which is defined by files within the `plugins/` directory. Any Router class will be responsible for doing 4 things:\n\n1. Defining a `path` -- a regular expression pattern used to decide whether a patron's requests should be handled by this Router\n2. Implementing necessary class methods (e.g. GET and POST) corresponding to the patron's HTTP request type\n3. Making calls to the right data `models`\n4. Prepare data to be returned (which could be e.g. a rendered template or json) -- this may include setting any headers or content-types necessary for this data\n\nThe first step is to figure out where within the `plugins/` directory your new route should live and whether it best lives in an existing file or a new file. An overview of the different plugin types are discussed in [this technical overview video](https://archive.org/embed/openlibrary-tour-2020/technical_overview.mp4?start=1017).\n\n## Understanding Directories\n\nThe following describes what each plugin directory does, by convention, in decreasing order of presumed relevance:\n\n* openlibrary -- most public pages meant to be read/consumed live here\n  * `code.py` defines the `/isbn` endpoint and what python functions our html templates have access to!\n  * `js/` is where our javascript lives\n  * `api.py` is many of our APIs are defined\n  * `lists.py` defines many of the routes for the List feature\n* upstream -- handles incoming stateful data requests (wiki edits, uploads, adding a book, logins, etc)\n  * responds to actions (e.g. borrowing) and account related requests: editing, mybooks pages, account login, settings\n* worksearch -- renders our `/search/*` related pages and all our search querying logic.\n  * `code.py` contains most of the logic and routes for `/search`, the `/barcodescanner`, `/advancedsearch`, `/search/lists`, `/search/subjects`, etc, as well as their json returning counterparts\n* importapi -- defines our book import API endpoints\n  * `code.py` again defines most of the core logic we care about\n* admin -- our admin pages and logic\n* books -- old-style book APIs (likely not relevant)\n\n## Code.py\n\nAlmost every `plugins/` directory defines a central `code.py` which is similar to `__init__.py` in that it contains the logic required to register its directory's plugins with the system. In addition to acting as an entry point for all the other plugins in the directory and performing setup(), it also may define its own Routers. If you're not sure where in a plugin to look for a Router, that's likely a good place to start.\n\n## Tutorial: Implementing a new Route\n\nThere are two ways to implement a new Open Library Route. The first way is to use your discretion to add a new Route to the correct pre-existing plugin file. The second way is to create a new directory and/or file which will require an additional step of registering your plugin with Open Library. We'll only walkthrough the first case here -- for those who need to register a new plugin, please see `openlibrary/plugins/openlibrary/code.py` for an [example](https://github.com/internetarchive/openlibrary/blob/208fe9de10f24d1f54b691f4a40d1dc4b0148745/openlibrary/plugins/openlibrary/code.py#L1105-L1143).\n\nLet's say I wanted to create a page on Open Library called `/search/many` for a search experience that lets patrons search for multiple books in bulk. The first thing I'd do is make a determination of where it should live -- likely in `worksearch/code.py`. We'd then define our Route as follows:\n\n```\nclass search_many(delegate.page):\n    path = '/search/many'\n\n    def GET(self):\n        i = web.input(q='')\n        return render_template('search/many', q=i.q)\n```\n\nEvery Router is a class that extends something like `delegate.page`. The Router will define a `GET`, `POST`, etc, as appropriate to deal with corresponding HTTP requests of that type from the patron. The function parameters defined by the GET and POST of the Router depend on and are linked to the regex pattern specified by the Router's `path`.\n\nThe following example from [plugins/openlibrary/api.py](https://github.com/internetarchive/openlibrary/blob/master/openlibrary/plugins/openlibrary/api.py#L65-L71), The `trending_books_api` Router defines a `path` with a regex `\"/trending(/?.*)\"` that captures everything after `/trending/` and forwards it into the GET function as the parameter `period`. In this example, if a patron goes to `/trending/daily`, the request will match the Router's path regex and the Router will call the GET method with `period=daily` as an argument.\n\n```\nclass trending_books_api(delegate.page):\n    path = \"/trending(/?.*)\"\n    # path = \"/trending/(now|daily|weekly|monthly|yearly|forever)\"\n    encoding = \"json\"\n\n    def GET(self, period=\"/daily\"):\n```\n\nIn our first example `i = web.input(q='')` is used to access the GET query parameters specified by the patron's request from the URL (e.g. if the patron queries for /search/many?q=test then `i.q` will be \"test\".\n\nThe final line of the Router in our example makes a call to `render_template` which will fetch the corresponding template, by name, from the `templates/` directory (specified by the 1st argument -- in this case the file \"templates/search/many.html) and then pass in any of the variables this template needs to render (in this case the value `q`). The values passed in to the template must also be defined in the header of the HTML template within the `$ def with(...)` line. For more information on rendering templates, refer to the [Front-end Guide](https://docs.openlibrary.org/developers/frontend/frontend-guide.html#routing-and-templates) in our docs.\n\nIn order to get this example to work, you will first need to create a new file `templates/search/many.html` with the following body content:\n\n```\n$ def with (q=\"\")\n\nYour query was $(q)!\n```\n\nYou can now restart your server and test that your application is working by going to `/search/many?q=test&debug=true`, as described in our [debugging website errors video](https://archive.org/embed/openlibrary-tour-2020/openlibrary-debugging-webpage-errors.mp4)\n</yakaai_skills>"}