Guidance for AI coding agents working in this repository.
- Respect
.gitignoreand.aiignorefiles in the repo. - Respect existing AI conventions documented in the repo — read and follow
CLAUDE.md(behavioral guidelines: think before coding, simplicity first, surgical changes, goal-driven execution) in addition to this file
Single Python package qr/ exposing the same QR generation core through two entry points:
- CLI (
qr/cli.py,qr/__main__.py):python -m qr generate '<json>'andpython -m qr server. Uses Click; thegeneratecommand parses a JSON string OR a file path, validates via the same PydanticCreateQrRequestused by the API, and writes to./output/output.<format>. - HTTP API (
qr/api.py): FastAPI app, single business endpointPOST /v1/qr. Versioned prefixv1is hardcoded (version_prefix). Mounts the Zensical-built docs site at/v1/readme(built into/app/sitebytask zensical:build).
Both paths converge on qr.generate.generate_qr_image(...), which dispatches via the MODULE_DRAWERS dict (qr/generate.py) to either:
- SVG path →
create_dynamic_svg_image_class(qr/svg.py) returning an image class (factory). - PIL path →
create_pil_drawer_instance_factory(qr/pil.py) returning a drawer instance. Eye customization swaps factory toStyledPilImageEyeDrawer+ConfigurableEyeDrawer. Color masks come fromqr/mask.py.
The discriminator pattern is central: CreateQrRequest.module_drawer is a Union[...] discriminated by type (the QrModuleDrawer enum value, e.g. "svg_circle", "rounded_module"). Mismatches between drawer family and output.format are rejected by validate_module_drawer_for_format (SVG drawer + PNG output → 422). Logos require error_correction == "H" (validate_logo_error_correction).
- Schemas (
qr/schemas.py) are the source of truth for validation, OpenAPI examples, and CLI input. When adding a drawer/mask/option, add an enum value, a*OptionsPydantic class withtype: Literal[QrModuleDrawer.X] = QrModuleDrawer.X, register it inMODULE_DRAWERS, and add it to theUnioninCreateQrRequest.module_drawer. - Colors: always
RGBColor(Pydantic) externally; convert viaqr.helper.to_rgb_tuplebefore passing topython-qrcode. - Logging: use
qr.log.logger.get_logger(__name__, CustomJSONFormatter("%(asctime)s")). The API middleware injectsrequest.state.trace_id(uuid4) and logs structured JSON viaextra={"extra_info": get_extra_info(...)}as aBackgroundTaskafter the response. - Errors in API:
ValueError→ 422 JSON{"error": ...}; other exceptions → 500. Don't raiseHTTPExceptionfor validation; let Pydantic do it (returns 422 automatically). - Content-Type guard:
POST /v1/qrrequiresapplication/json(enforced byrequire_jsondependency → 415).
All commands run through Docker via Taskfile.yml — do NOT assume a local venv.
task up/task down— run dev server at http://localhost:8000/v1/docs (compose mounts./into/appwith--reload).task dev:cli -- '{"data":"hi"}'— exercise CLI in dev container; output written to./output/.task pytest— runspytest --snapshot-update(uses syrupy; snapshots intests/__snapshots__/test_api.ambr). Tests hit FastAPI viaTestClient; binary QR output is snapshot-compared byte-for-byte, so regenerating drawers will require updating snapshots.task contribute— runsblack:fix,ruff:fix,mypy,pytest(the pre-merge gate).task openapi— requirestask upfirst; curls/v1/openapi.jsonintodocs/version/v1/openapi.json(committed;scripts/check-version-docs.shvalidates this in CI).task zensical:build/task zensical:live— builds versioned docs site needed by/v1/readmemount. Uses Zensical (zensical/zensicalDocker image, config inzensical.toml). The Taskfile runsenvsubstonzensical.tomlto expand$MAJOR_VERSION(auto-detected as the highestv*dir underdocs/version/) into a temp.zensical.runtime.tomlbefore invokingzensical build/zensical serve -f ....
APP_ROOTenv var (default/app) locates the built docs atsite/version/v1; falls back to repo root for source checkouts.- App version resolved via
importlib.metadata.version("qr-code-api"), falling back toqr.__version__. - Static assets (
favicon.ico,thumbnail.png) shipped inside the package viaimportlib.resources.files("qr").joinpath("static").
The version is defined in one place: qr/__init__.py (__version__ = "X.Y.Z").
setup.py parses it via regex; qr/api.py reads it through importlib.metadata (with qr.__version__ as a fallback), so the value flows automatically into the installed package metadata, the FastAPI /v1/openapi.json info.version field, and anywhere else the version is consumed.
Before creating a new git tag, you MUST bump qr/__init__.py to match the tag you intend to push. The git tag and __version__ are expected to be identical (e.g. tag 1.2.4 ↔ __version__ = "1.2.4"). The build.yml workflow does not auto-derive the version from git, so a mismatch will publish a Docker image whose internal version disagrees with its tag.
Recommended release flow:
- Edit
qr/__init__.py→ set__version__ = "X.Y.Z". - Commit (
chore: bump version to X.Y.Z). - Tag:
git tag X.Y.Z && git push origin main --tags. - The
Build and Push Docker imageworkflow handles the rest (publishes:X.Y.Z,:X.Y,:X, and — if it's the highest stable tag —:latest).
Quick sanity check after bumping (run from the dev container):
task shell:dev -- python3 -c "from qr import __version__; print(__version__)"