This page documents the source code structure and explains how the generator pipeline works end to end.
src/multi_registry_cache/
├── __init__.py # package version constant
├── cli.py # Typer app — command definitions and shell completion scripts
├── generate.py # reads config.yaml, produces compose/ output files
├── setup_wizard.py # interactive wizard — prompts user, writes config.yaml
├── functions.py # shared utilities called by generate.py
└── data/
└── config.sample.yaml # bundled template, loaded by setup_wizard.py
Entry point registered in pyproject.toml:
[project.scripts]
multi-registry-cache = "multi_registry_cache.cli:app"cli.py defines a Typer application with three subcommands: setup, generate, and completion.
Lazy imports — setup_wizard.main and generate.generate are imported inside the command functions, not at module level. This keeps CLI startup fast regardless of which command is invoked.
Static shell completions — rather than using Typer's built-in completion (which requires runtime introspection), completion scripts for zsh, bash, and fish are hardcoded as string constants (_ZSH_COMPLETION, _BASH_COMPLETION, _FISH_COMPLETION). The completion subcommand prints the appropriate script. Auto-detection of the current shell uses the $SHELL environment variable.
The wizard uses ruamel.yaml (instead of PyYAML) to load the bundled config.sample.yaml and write the final config.yaml. ruamel.yaml preserves comments, quote styles, and indentation — this means the generated config.yaml retains the helpful inline comments from the sample file.
Flow:
- Load
data/config.sample.yamlviaimportlib.resources(works correctly when installed as a package or run from Docker). - Clear
config['registries']and prompt the user to define registries in a loop. - Optionally add a
registry-type (private) entry. - Prompt for the Traefik domain pattern and write it to
traefik.perRegistry.router.rule. - Prompt for the storage driver; collect driver-specific fields; write them to
registry.baseConfig.storage. - For
filesystemstorage, optionally append the bind-mount todocker.perRegistry.compose.volumes. - Write the final config to
config_path(or a temp file if the user declines). - Print the next command (
multi-registry-cache generateor the Docker equivalent, detected via$IN_DOCKER).
generate(config_path, output_dir) is the core function. It:
- Loads
config.yamlwithyaml.safe_load. - Extracts the six top-level config sections.
- Creates
output_dir/acme/if needed. - Iterates over
registries[]:- Deep-copies
registry.baseConfigto avoid mutating shared state. - Calls
functions.create_registry_config()to apply type logic, Redis DB, and interpolation. - Writes
output_dir/{name}.yaml. - Strips
passwordfrom the registry dict. - Calls
functions.create_docker_service()andfunctions.create_traefik_router()/functions.create_traefik_service(), merging results into the runningdocker_config/traefik_configdicts. - Increments
count_redis_db.
- Deep-copies
- Writes
compose.yaml,traefik.yaml,redis.conf(databases N). - If
docker-compose.ymlexists in the output dir, asks the user whether to remove it. - Calls
functions.write_http_secret()to writeREGISTRY_HTTP_SECRETto.env.
Recursively walks dict, list, and str values and calls str.format_map(variables) on every string. Non-string leaves are returned unchanged. This is the mechanism behind all {name}, {url}, {ttl} substitutions.
Merges custom (the docker.perRegistry.compose template) into a new dict and runs interpolate_strings with the registry fields as variables. Returns the interpolated service definition.
Same pattern as create_docker_service but for traefik.perRegistry.router.
Same pattern for traefik.perRegistry.service.
Applies type-specific logic before interpolation:
type == 'cache': setsconfig['proxy']['remoteurl'], addsusername/passwordif present, addsttlif present.- any other type: deletes the
proxykey entirely.
Then runs interpolate_strings, and finally sets config['redis']['db'] = int(db) on the already-interpolated dict (the DB number is an integer, not a string template).
Serialises data to YAML using yaml.dump (PyYAML) and writes to filename with UTF-8 encoding.
Writes a plain string to filename. Used for redis.conf.
Checks whether REGISTRY_HTTP_SECRET already exists in output_dir/.env. If not, appends a new 32-byte hex token generated with secrets.token_hex(32). Idempotent — never overwrites an existing secret.
Tests live in tests/ and use pytest.
tests/
├── conftest.py # shared fixtures
├── test_functions.py # unit tests for functions.py
└── test_generate.py # integration tests for generate.py
| Fixture | Description |
|---|---|
sample_config |
Full parsed config.yaml-like dict with two registries |
cache_registry |
Single cache-type registry dict |
private_registry |
Single registry-type dict (no upstream) |
base_registry_config |
Minimal Distribution config dict |
# With uv (recommended)
uv run pytest
# Or with the activated venv
pytest
# Verbose output
pytest -v
# Run a single test file
pytest tests/test_functions.pyruff check src/ tests/git clone https://github.com/obeone/multi-registry-cache
cd multi-registry-cache
# Create venv and install all dependencies including dev extras
uv sync
# Install in editable mode (alternative)
uv pip install -e ".[dev]"
# Run the CLI from the venv
uv run multi-registry-cache --help