Skip to content

Development

pymmich is a small Python package managed with uv and a top-level justfile.

Layout

.
├── justfile            # cross-platform task runner
├── pyproject.toml      # package metadata and dependencies
├── README.md
├── LICENSE.md
├── src/
│   └── pymmich/        # library + CLI source
├── tests/              # pytest suite
├── docs/
│   ├── zensical.yml    # Zensical config
│   └── content/        # Markdown sources (logo, assets, commands/, …)
└── scripts/            # build helpers (pyinstaller, cleanup, ...)

Why just?

The project targets Linux, macOS, and Windows. A shell-driven Makefile doesn't really work on Windows without extra tooling (make.exe from MSYS, workarounds for find/rm/etc.). just is a single prebuilt binary, has identical syntax on every OS, and is shipped as the pip-installable rust-just wheel — so it becomes part of the venv the moment you run uv sync.

Setting up a dev environment

uv sync --all-groups      # creates .venv + installs every dep group,
                          # including `just` itself

After this .venv/bin/just (or .venv\Scripts\just.exe on Windows) is available. You can either prefix every call with uv run:

uv run just <recipe>

or activate the venv and use just directly:

# POSIX
source .venv/bin/activate
just <recipe>

# Windows (PowerShell)
.venv\Scripts\Activate.ps1
just <recipe>

Running the test suite

uv run just test
# or with extra pytest args:
uv run just test -k list_users -v

Tests use respx to mock the Immich HTTP API — no live server is ever contacted, so the suite is hermetic and fast.

The mocked suite reports Immich 2.7.5 by default, so it exercises the v2 API variant; individual tests switch to a 3.x server with the set_server_version fixture. See Server compatibility for what differs.

Running the live tests

The mocked suite pins down which requests pymmich sends. The live suite checks that real Immich servers accept them, and answer the way the client assumes. It runs every test twice — once against Immich 2.7 and once against Immich 3.1 — so a behaviour that differs between the two shows up as a failure instead of a silent divergence.

It needs Docker and is opt-in; uv run just test never touches it.

uv run just docker-test-up       # start both servers and bootstrap them
uv run just docker-test          # run the live tests (implies docker-test-up)
uv run just docker-test-status   # show what is running
uv run just docker-test-down     # stop everything and delete the data

just docker-test forwards extra arguments to pytest, e.g. uv run just docker-test -k shared_album -v.

Two throwaway servers are started from the official ghcr.io/immich-app/immich-server images, each with its own database and cache, published on 127.0.0.1:2287 (2.7) and 127.0.0.1:2317 (3.1). A cold start takes about 20 seconds; the first run pulls roughly 3 GB per version. Nothing is persisted — just docker-test-down removes every trace.

See docker/README.md for how the fixture is put together and why it must not be used as a deployment template.

Disposable fixture servers, not real ones

Both servers are explicitly disposable: they use a well-known admin password, run their database without crash safety, and keep nothing across just docker-test-down. Every container is labelled pymmich.disposable=true to say so, and every port is bound to loopback so they are unreachable from the network — keep it that way.

Building a distribution

uv run just build         # wheel + sdist in ./dist/
uv run just pyinstaller   # standalone one-file binary in ./dist/pyinstaller/

See the Distribution page for the details of each artifact and the per-platform build workflow.

Building / serving the docs

uv run just docs         # static site in ./site/
uv run just docs-serve   # live reload at http://127.0.0.1:8000

Cleaning up

uv run just clean        # remove build, dist, site
uv run just distclean    # clean + caches, __pycache__, editor backups

Adding a new CLI command

The CLI lives in src/pymmich/cli.py and is built on top of Typer. The HTTP client with all the Immich-specific knowledge lives in src/pymmich/client.py. Keep new commands thin — business logic belongs in the client.

Write a test first (TDD is the project rule), then the implementation.

Adding a new just recipe

Edit justfile at the top of the project. The last comment line before a recipe becomes its description in just --list — keep it short and verb-first. Use Python (uv run python scripts/…) for anything that would need different shell commands on Windows vs. POSIX rather than writing two [unix] / [windows] variants.