Skip to content

Latest commit

 

History

History
166 lines (141 loc) · 12.3 KB

File metadata and controls

166 lines (141 loc) · 12.3 KB

CLAUDE.md

Dev notes for beetkeeper — a self-hosted FastAPI web app for managing beets. Source root is src/python (package: src/python/beetkeeper).

Build tooling

  • Pants 2.32 is the build system (pants.toml). Run pants <goal> :: for everything.
  • Pants resolves with pex ([python] resolver = "pex", enable_resolves = true): app deps come from the beetkeeper-resolve lockfile, tools (mypy, bandit, pytest, ...) from tools-resolve — both under 3rdparty/, regenerated with pants generate-lockfiles.
  • uv manages the dev venv and workspace: root pyproject.toml is the workspace root; src/python/pyproject.toml is the member that holds the real distribution [project]. Keep uv.lock in sync (uv lock).

Two pyproject.toml files (intentional — don't merge)

  • Root pyproject.toml: tool config only (ruff, mypy, pytest, bandit), [dependency-groups] dev deps, and [tool.uv.workspace]. No [project] table here.
  • src/python/pyproject.toml: the distribution's [project] (name, deps, scripts, metadata)
    • [build-system]. It lives in the source root so setuptools' PEP 517 backend (run there by Pants) reads it. Version is dynamic from the vMAJOR.MINOR.PATCH git tag via Pants' vcs_version targets (setuptools-scm under the hood), which generate _scm_version.py modules that each package's committed _version.py re-exports (falling back to 0.0.0.dev0 outside Pants, e.g. uv dev builds).
  • The wheel is built with generate_setup=False (see src/python/BUILD), so Pants does NOT inject metadata — runtime deps/scripts are maintained by hand in src/python/pyproject.toml and must stay consistent with what the code imports.

Testing — IMPORTANT

  • Tests run through Pants' built-in pytest: pants test :: runs each python_tests file natively. [python] enable_resolves = true (resolver: pex) supplies 3rd-party deps from the beetkeeper-resolve lockfile (3rdparty/python/beetkeeper-lockfile.json); pytest itself + its plugins install from tools-resolve via [pytest].requirements in pants.toml — any plugin whose CLI options we pass (e.g. pytest-socket) must be listed there.
  • pants test :: also runs the hooks:uv-lockfile-check test_shell_command (defined via the test_cmd macro in build_scripts/pants_macros.py).
  • mypy runs via pants check :: and bandit via pants lint :: (both are Pants backends configured in pants.toml, reading tool config from the root pyproject.toml).
  • uv run --all-groups pytest ... also works for ad-hoc local runs against the uv dev venv.

Common commands

pants test ::                         # pytest (per-file, native Pants) + uv-lockfile check hook
pants lint ::                         # ruff, bandit, shellcheck, shfmt, hadolint, taplo, yamllint, visibility
pants check ::                        # mypy
pants package src/python:beetkeeper-whl   # build the wheel -> dist/
pants package //:beetkeeper-server-image  # build app docker image
pants package ::                      # Packages all pants targets which support the package command.
pants generate-lockfiles              # regenerate the Pants resolve lockfiles under 3rdparty/
uv lock                               # regenerate uv.lock after dep changes
uv lock --check                       # verify uv.lock is current (gated in prek + CI)

prek hooks (.pre-commit-config.yaml) call the same Pants goals (lint fmt, check, test hooks:...) plus actionlint and an MkDocs build check. Install git hooks with prek install.

Docker

  • The Dockerfile has an ffmpeg fetch stage + the final app stage; only app is packaged, as the single //:beetkeeper-server-image docker_image target in root BUILD. The image is named/pushed via the @ghcr registry (ghcr.io/zach-overflow/beetkeeper); see pants.toml [docker.registries.ghcr] and the env("RELEASE_TAG", "dev") tag in BUILD.
  • The app stage runs no uv/resolve — it just COPYs a thin, single-arch PEX (//:beetkeeper-linux-<arch>, one per linux arch via complete_platforms, selected by ARG TARGETARCH). pants package builds only the host arch; CI builds the image per-arch on native runners (no QEMU) and stitches a multi-arch manifest list — see .github/workflows/publish.yml.

Releases

  • Versioning: no committed version. vcs_version targets (src/python/beetkeeper/BUILD, src/beetsplug/beetkeeper_plugin/BUILD) generate _scm_version.py from git via setuptools-scm; each package's committed _version.py re-exports it, falling back to 0.0.0.dev0 outside Pants (uv builds). An exact vMAJOR.MINOR.PATCH tag yields a clean version; anything else is a dev version.
  • Flow (details: docs/contributor_docs/release_management.md): releases are driven by conventional commits via cocogitto (cog.toml; only commits since the latest v tag count). PRs are squash-merged, so the PR title is the conventional commit that lands on main (enforced by .github/workflows/pr-title.yml running cog verify). Run the Release workflow from main (checks commits, validates + builds everything, publishes nothing) → approve the release environment gate, where cog bump --auto computes the next semver and pushes a vX.Y.Z tag onto the validated commit (tag-only bump — no commit or CHANGELOG.md is ever pushed to main, so branch protection can't conflict), and the GitHub release is uploaded with the cog-generated changelog → Release dispatches publish.yml (workflow_dispatch; a GITHUB_TOKEN-created tag can't fire push triggers, and workflow_call would break PyPI trusted publishing), which builds wheels/image/docs in parallel from the tag, then publishes them in parallel (PyPI + Pages keep their environment gates).

CRITICAL: Beets DB ("Library") interactions

Any code which interacts with the beets database (NOT the beetkeeper database) should be done carefully, and must take into consideration the official beets docs on their programming interface for the db here.

Additionally, this beets blog post is helpful for details on the threading and locking model beets enforces for programmatic access to their DB (aka Library).

Beets event flow (beetkeeper-plugin → API)

  • All recorded beets listener events originate exclusively from the beetkeeper beets plugin (src/beetsplug/beetkeeper_plugin), which POSTs them to the /api/events/* endpoints. The API parses those requests and stores them (ListenerEvent + AlbumEvent/TrackEvent rows). The server must never synthesize or "fill in" event records itself — not even for imports it runs. If no POST arrived, no record exists, period. (The server distribution depends on beetkeeper-plugin for exactly this reason.)
  • The import worker registers its own in-process beets listener (core.import_worker._ImportNarrator), but it is narration-only: it feeds the job's human-readable output log and never writes event rows.
  • Consequence: beetkeeper-run imports appear on the events page only when the beets config loads the beetkeeper plugin and its pushes succeed (correct server_url, and api_token when login protection is enabled).
  • The shared event-type vocabulary is beetkeeper.constants.BeetsEventType. It lives at the top level (not under beetkeeper.api) because beetkeeper.api.__init__ pulls in the whole FastAPI app, whose routers import beetkeeper.db.models — the db layer importing it from beetkeeper.api.* would be a circular import. Integration tests (src/integration_tests/events_plugin_integration_tests/) keep it in sync with the plugin's _EVENT_PAYLOAD_KEYS and beets' own beets.plugins.EventType literals.

Relevant public docs

Coding style and conventions

In-code comments

In-code comments (#, /* */, etc., across Python, BUILD, config, CI, and CSS) are discouraged. Write self-explanatory code and let names, types, and docstrings carry the intent. Only add a comment when it explains something genuinely unexpected or unintuitive — a workaround, a subtle gotcha, or a non-obvious constraint a competent reader couldn't infer from the code. Such cases should be rare. When one is warranted, keep it brief (ideally one line) and, where useful, link out (GitHub issue thread, doc, etc.) rather than explaining at length. (Python docstrings are documentation, not comments, and are encouraged.)

Python source code

  1. Write code with the expectation that it may be running within an asynchronous event loop.
    • Use the anyio library instead of the builtin asyncio library.
    • Prefer async coroutine definitions for FastAPI route definitions.
  2. Whenever possible, aim to keep the code modular. Avoid monolithic files in preference of breaking out into functional domains.
    • The source code under src/python/beetkeeper shows a starting point for this structure, but feel free to create or consolidate things if needed.
  3. Type hints are required.
  4. Test code should live under src/python/tests, and not colocated with the source code, as some Pantsbuild examples show.

FastAPI Code Structure

  1. All FastAPI code lives under src/python/beetkeeper/api
  2. The app is created from a factory function in src/python/beetkeeper/api/fastapi_app.py
  3. All custom FastAPI Dependencies should live in src/python/beetkeeper/api/dependencies.py
  4. All public REST API endpoints are defined in FastAPI APIRouter instances created under src/python/beetkeeper/api/api_routes
  5. All ui-related endpoints are defined in FastAPI APIRouter instances created under src/python/beetkeeper/api/ui_routes

Frontend

  1. The frontend should be handled ONLY by the following, both for any static components, as well as for dynamic HTML + event-based DOM manipulation:

    1. A monolithic classless CSS file at src/python/beetkeeper/api/static/css/classless.css
    2. HTMX (vendored in-repo, and baked into the common base HTML template at src/python/beetkeeper/api/static/html_templates/base_template.html.)
    3. Any pure, simple javascript -- only if absolutely needed -- and should be defined in the common shared base HTML template within a `<script> block.
  2. Read the docstring at src/python/beetkeeper/api/ui_routes/__init__.py for details on the frontend code structure expectations.

  3. Do not use ANY javascript framework or any other additional frontend library other than the vendored HTMX (src/python/beetkeeper/api/static/js/htmx.min.js). No CDN scripts, no npm/build step, no CSS frameworks beyond the classless stylesheet above.

Test code

  1. Use pytest. Do not use the unittest builtin library.
  2. Use of pytest.mark.parametrize and pytest.fixture are the preferred ways to generate test cases and reduce test code repetition.
  3. Type hints are required in test code too.
    • This is true even when using pytest's "built in" fixtures, such as tmp_path, or mocker from pytest-mock.

Mocks

  1. Use any mock tooling from pytest-mock. Avoid using features from unittest.mock unless necessary.
  2. Do not use mock decorators (e.g. @patch(...)).
  3. Use @pytest.mark.anyio for async tests. Do not use pytest-asyncio.
  4. Tests should never make real network calls.
    • When testing any FastAPI route functions, use testing tools FastAPI offers. (see here, and the relevant testing pages under the advanced user guide.)