A CLI that composes single-file Cookiecutter templates via Git
Project description
tpl – Template Puller CLI
tpl treats single-file Cookiecutter templates like versioned building blocks. Instead of copy/pasting snippets or maintaining giant boilerplate repos, point tpl at any Git tag (GitHub, Bitbucket, or a local directory), drop the rendered file into your project, and record the provenance in .tpl-lock.toml. When a new tag ships, tpl upgrade tells you exactly what changed and keeps your tweaks safe.
Install note: The PyPI package is published as
tpl-cli(the CLI binary is stilltpl). Install it via uv (uv sync --package tpl-cli) or pip (pip install tpl-cli).
Why tpl?
- Composable building blocks – pull one template at a time or describe an entire starter kit in
tpl.toml. Every block is still a normal Cookiecutter repo, with tags for versioning and hooks for customization. - Deterministic upgrades – tpl stores the source URL, tag, entry file, checksum, and context for every managed file. Upgrades re-render using the same context, detect local edits, and produce
.tpl-new/.tpl.diffartifacts (or rungit merge-filewhen--mergeis set). - Works offline – reference
local:/path/to/repoentries to test unreleased templates or run integration tests without hitting the network. - Zero vendor lock-in – tpl orchestrates Git + Cookiecutter. Templates live wherever you keep them; tpl just fetches, renders, and tracks provenance.
# Pull a single template
uv run tpl pull gh:you/tpl-logging@v0.3.0 --file infra/logging.yaml=logging.yaml --set project_slug=my-app
# Compose an entire starter kit
uv run tpl compose gh:you/python-service@v1.2.0 --set project_name=my-app
# Reapply a local tpl.toml (hierarchical blocks + shared context)
uv run tpl apply --config tpl.toml --force
# Detect drift / upgrade safely
uv run tpl status
uv run tpl upgrade --merge
Feature highlights
- Single-file template pulls –
tpl pullgrabs any tagged Cookiecutter repo and writes the requested entry file. Supports overrides via--set key=value. - Project composition –
tpl compose gh:user/repo@tagruns every block defined intpl.toml, sharing context variables across blocks and supporting nestedkind = "project"entries. - Local configs – keep
tpl.tomlinside your repo and runtpl apply --config tpl.tomlso teammates can rehydrate the same files without remembering commands. - Hierarchical layering – include other project templates (Docker layer, CI layer, etc.) to build multi-stage starter kits.
- Upgrade safety – checksum tracking, optional three-way merge (
--merge), and consistent conflict artifacts. - Private repo support – authenticate with GitHub or Bitbucket tokens; tpl passes creds to Git via
GIT_ASKPASSso secrets never hit disk.
Commands at a glance
| Command | Purpose | Example |
|---|---|---|
tpl pull |
Render a single file from a tagged template | uv run tpl pull gh:you/tpl-logging@v0.3.0 --file infra/logging.yaml=logging.yaml |
tpl compose |
Apply every block in a project template | uv run tpl compose gh:you/python-service@v1.2.0 --set project_name=my-app |
tpl apply |
Reapply a local tpl.toml config |
uv run tpl apply --config tpl.toml --set project_name=my-app |
tpl status |
Show managed files and drift | uv run tpl status |
tpl upgrade |
Re-render and merge newer versions | uv run tpl upgrade infra/logging.yaml --to v0.4.0 --merge |
Quick start (uv)
-
Install uv and ensure Python 3.8+ exists (recommend 3.12):
uv python install 3.12. -
Sync dependencies (grab the
devextras for tooling):uv sync --extra dev
-
Run commands through uv to pick up the managed
.venv:uv run tpl --help uv run pytest
To run the test suite across multiple Python versions (if installed), use:
tox
Template authoring
tpl works with standard Cookiecutter repositories—no extra metadata files. A typical repo looks like this:
tpl-logging/
├── cookiecutter.json
└── {{cookiecutter.project_slug}}/
└── logging.yaml
cookiecutter.jsondeclares the variables tpl can override via--set key=value.- Everything inside
{{cookiecutter.project_slug}}/renders normally. tpl copies whichever file you request through--file dest=entryor via the project configuration. - Tag releases using normal Git tags (e.g.,
git tag v0.3.0). tpl records the tag you pulled sotpl upgrade --to v0.4.0knows which version to fetch next. - You can keep additional files in the same repo and reference each one as a separate entry, which tpl will render in a single pass.
- If a local template directory has no Cookiecutter templated folder, tpl treats it as a raw file tree and copies files without rendering.
Example templates in this repo
This repo includes a real, ready-to-use logging template at examples/tpl-logging. It renders two files:
logging_setup.py— loads alogging.tomldictConfig, writes all levels to a daily-rotated file underplatformdirs, and tunes Rich console output.logging.toml— the config (daily rotation, 7-day retention, INFO console default).
Pull it with a local repo spec (any tag label works for local):
uv run tpl pull local:examples/tpl-logging@v0.1.0 \
--file logging/logging_setup.py=logging_setup.py \
--file logging/logging.toml=logging.toml
Note: local: paths now work even if the directory is not a git repo (you can omit the @version in that case). If you want tpl upgrade to discover versions, initialize a git repo and add tags.
Wire it into a Typer app with -v/-q support:
import typer
from logging_setup import setup_logging
app = typer.Typer()
@app.callback()
def main(
verbose: int = typer.Option(0, "-v", count=True),
quiet: int = typer.Option(0, "-q", count=True),
) -> None:
setup_logging("my-app", verbose=verbose, quiet=quiet)
Additional example templates:
examples/tpl-justfile— a pragmaticjustfilewith lint/format/typecheck/test targets.examples/tpl-makefile— the same targets, but as aMakefile.examples/tpl-github-actions— a pull_request workflow that runs tests, optionally viajust.
Pull any of these as building blocks:
uv run tpl pull local:examples/tpl-justfile@v0.1.0 \
--file justfile=justfile \
--set project_slug=my-app \
--set python_package=my_app
uv run tpl pull local:examples/tpl-makefile@v0.1.0 \
--file Makefile=Makefile \
--set project_slug=my-app \
--set python_package=my_app
uv run tpl pull local:examples/tpl-github-actions@v0.1.0 \
--file .github/workflows/tests.yml=.github/workflows/tests.yml \
--set project_slug=my-app \
--set python_version=3.12 \
--set use_just=true \
--set just_target=test
Bring them together for a fresh repo (or augment an existing one) by running multiple pulls:
uv run tpl pull local:examples/tpl-logging@v0.1.0 \
--file logging/logging_setup.py=logging_setup.py \
--file logging/logging.toml=logging.toml
uv run tpl pull local:examples/tpl-justfile@v0.1.0 \
--file justfile=justfile \
--set project_slug=my-app \
--set python_package=my_app
uv run tpl pull local:examples/tpl-github-actions@v0.1.0 \
--file .github/workflows/tests.yml=.github/workflows/tests.yml \
--set project_slug=my-app \
--set python_version=3.12 \
--set use_just=true \
--set just_target=test
You can also use a single project config to do the same. See examples/tpl-starter/tpl.toml and run:
uv run tpl apply --config examples/tpl-starter/tpl.toml \
--set project_name=my-app \
--set python_package=my_app
Daily workflow
A three-step workflow: create → pull → upgrade
1. Author a template once
Build a standard Cookiecutter repo (tpl-logging):
tpl-logging/
├── cookiecutter.json
└── {{cookiecutter.project_slug}}/
└── logging.yaml
cookiecutter.json might look like:
{
"project_slug": "logging",
"environment": "staging"
}
…and {{cookiecutter.project_slug}}/logging.yaml can reference those values:
version: 1
environment: {{ cookiecutter.environment }}
Tag releases with meaningful versions (git tag v0.3.0). tpl records the tag you pull so upgrades know where to fetch the next version.
2. Pull it into another project
uv run tpl pull gh:you/tpl-logging@v0.3.0 \
--file infra/logging.yaml=logging.yaml \
--set environment=prod
tpl clones the tagged repo into ~/.tpl-cache, renders it with the provided context, copies logging.yaml into your project, and stores the source/tag/context in .tpl-lock.toml. Now tpl status will track that file.
3. Upgrade when a new tag ships
uv run tpl upgrade infra/logging.yaml --to v0.4.0
tpl re-renders the template, checks if the file changed locally, and either overwrites it or writes <file>.tpl-new plus a <file>.tpl.diff. tpl automatically attempts a Git-like merge when it detects local edits; pass --no-merge to skip the merge step and jump straight to conflict artifacts.
Pull multiple files from one template
Need more than one output? Repeat --file DEST[=ENTRY] for each extra file:
uv run tpl pull gh:you/tpl-logging@v0.3.0 \
--file infra/logging.yaml=logging.yaml \
--file infra/logging-dev.yaml=logging-dev.yaml \
--set environment=dev
tpl renders the template once, copies both files, and records each entry in .tpl-lock.toml so upgrades work the same way.
Pull an entire template
Omit --file to copy every rendered file into your project root, preserving directories:
uv run tpl pull gh:you/python-starter@v1.0.0 --set project_slug=my-app
Each file is tracked in .tpl-lock.toml using its relative path.
Check managed files
uv run tpl status
Outputs each tracked file plus its checksum status (OK, MODIFIED, MISSING).
Create a starter tpl.toml
Generate a minimal tpl.toml in your project and fill in the blocks you want:
uv run tpl init
Use --force to overwrite an existing file and --path to write elsewhere.
Compose a remote project template
Project templates ship a tpl.toml:
name = "python-service"
version = "0.2.0"
[context]
project_name = "my-service"
[[blocks]]
source = "gh:you/logging-block@v0.1.0"
[[blocks.files]]
path = "infra/logging.yaml"
entry = "logging.yaml"
[blocks.context]
environment = "prod"
[[blocks]]
kind = "project"
source = "gh:you/docker-layer@v0.3.0"
kind = "project" layers another template, letting you stack base scaffolds, Docker bits, CI pipelines, etc.
[[blocks.files]] describes each destination/entry pair you want to copy from the same template—add as many as you need.
If you omit path/files entirely for a block, tpl renders the whole template into your project root (preserving directories).
uv run tpl compose gh:you/python-service@v0.2.0 --set project_name=my-service
Apply a local tpl.toml
Keep the same schema in your own repo to define reusable building blocks:
name = "demo"
version = "0.3.0"
[context]
project_name = "demo"
[[blocks]]
source = "gh:you/logging-block@v0.1.0"
[[blocks.files]]
path = "infra/logging.yaml"
entry = "logging.yaml"
[blocks.context]
environment = "prod"
[[blocks]]
source = "bb:team/docker-layer@v0.4.0"
[[blocks.files]]
path = "infra/docker-compose.yaml"
entry = "docker-compose.yaml"
[[blocks]]
source = "local:../templates/tpl-cache@v0.1.0"
[[blocks.files]]
path = "scripts/bootstrap.py"
entry = "scripts/bootstrap.py"
Add more [[blocks.files]] entries to copy additional files from the same template. tpl renders the template once per block and copies each requested output.
uv run tpl apply --config tpl.toml --set project_name=my-service
Upgrade everything
uv run tpl upgrade
If a file’s checksum changed, tpl attempts a Git-style merge first. If it can’t merge cleanly (or you pass --no-merge), tpl writes <file>.tpl-new and <file>.tpl.diff. Target a single file with uv run tpl upgrade path/to/file --to v0.3.0.
CLI reference
| Command | Description |
|---|---|
tpl pull <repo-spec> --file dest=entry [--file ...] [--set key=value] |
Render a Cookiecutter repo once, copy each requested entry into your project, and record provenance in .tpl-lock.toml. Supports GitHub (gh:user/repo@tag), Bitbucket (bb:), and local:/path/to/repo@tag. |
tpl status |
Show whether every managed file matches its stored checksum (OK, MODIFIED, MISSING). |
tpl upgrade <path> [--to tag] [--no-merge] |
Re-render a single managed file. tpl attempts to merge local edits automatically; add --no-merge to skip merging and always emit <file>.tpl-new / .tpl.diff. |
tpl compose <repo-spec> [--set key=value] [--force] |
Fetch a remote project template (tpl.toml), render every block, and write all configured files. |
tpl apply --config tpl.toml [--set key=value] [--force] |
Apply a local project configuration stored in your repo (same schema as a remote tpl.toml). |
tpl upgrade (no path) |
Reapply whichever project template is recorded in .tpl-lock.toml (remote or local). Honors --no-merge. |
Project configuration (tpl.toml) reference
Minimal structure:
name = "demo"
version = "0.1.0"
[context]
project_name = "demo"
[[blocks]]
source = "gh:you/some-template@v1.0.0"
[[blocks.files]]
path = "src/config.yaml" # Destination path in your project
entry = "config.yaml" # File inside the rendered template (defaults to basename of path)
Key fields:
| Field | Where | Description |
|---|---|---|
name, version |
root | Metadata only; helpful for humans/logging. |
[context] |
root | Default context values shared across every block. Overridden per block context or nested project templates. |
[[blocks]] |
root array | Each block references a Cookiecutter repo (GitHub, Bitbucket, or local). Set kind = "project" to include another tpl.toml. |
source |
block | Repo spec. Examples: gh:you/template@v1.0.0, bb:team/repo@v0.4.0, local:../templates/logging@v0.3.0. |
[[blocks.files]] |
block | Destination/entry pairs copied from that repo. Add multiple entries to copy multiple files. |
path |
block file | Destination inside your project. Directories are created automatically. |
entry |
block file | Relative file inside the rendered template. Defaults to basename(path) if omitted. |
[blocks.context] or inline context = { key = "value" } |
block | Optional overrides merged on top of the root context before rendering the block. Useful for per-file tweaks. |
Repository specs
| Prefix | Example | Notes |
|---|---|---|
gh: / github: |
gh:openai/tpl-example@v0.1.0 |
HTTPS clone via GitHub. Use TPL_GITHUB_TOKEN/GITHUB_TOKEN for private repos. |
bb: / bitbucket: |
bb:team/repo@v1.2.0 |
HTTPS clone via Bitbucket. Use TPL_BITBUCKET_USERNAME + TPL_BITBUCKET_APP_PASSWORD. |
local: / file: |
local:../templates/logging@v0.1.0 |
Absolute or relative path to a local git repo. Great for integration tests or unpublished templates. |
Authentication & state
- GitHub – set
TPL_GITHUB_TOKEN(orGITHUB_TOKEN). tpl authenticates asx-access-tokenautomatically. - Bitbucket – set
TPL_BITBUCKET_USERNAMEandTPL_BITBUCKET_APP_PASSWORD. - Cookiecutter state – tpl writes replay/config data under
${HOME}/.tpl-stateby default (override withTPL_COOKIECUTTER_STATE_DIR). The directory is safe to delete if you need a clean slate.
Credentials feed Git via GIT_ASKPASS, so secrets never appear in lockfiles or logs.
Automation & development
| Command | Description |
|---|---|
just fmt |
Run ruff format. |
just lint / just lint-fix |
Run ruff check (optionally with --fix). |
just typecheck |
Run mypy tpl. |
just test |
Run pytest (unit + integration). |
just check |
Run lint, typecheck, and tests sequentially. |
just precommit |
Run the full pre-commit stack. |
Install hooks once per machine:
uv run pre-commit install
Contributing
See CONTRIBUTING.md for the full workflow (uv setup, testing expectations, release process). CI runs Ruff, mypy, pre-commit, and pytest on every PR, and tagged commits on main auto-publish to PyPI.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tpl_cli-0.2.0.tar.gz.
File metadata
- Download URL: tpl_cli-0.2.0.tar.gz
- Upload date:
- Size: 30.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d32e7d0b09705b7fb34d77f475430d38ccee5e9cc83dbff592cecba4562c24e
|
|
| MD5 |
e2dbf1fac0a3a499457cf8d9bea4ab5d
|
|
| BLAKE2b-256 |
67fbfa876fb5d4a8ca8ed147c0337aa43bfc30cba95fe7885d901d6a9b6eb8ef
|
Provenance
The following attestation bundles were made for tpl_cli-0.2.0.tar.gz:
Publisher:
release.yml on wdonofrio/tpl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tpl_cli-0.2.0.tar.gz -
Subject digest:
5d32e7d0b09705b7fb34d77f475430d38ccee5e9cc83dbff592cecba4562c24e - Sigstore transparency entry: 828247465
- Sigstore integration time:
-
Permalink:
wdonofrio/tpl@e4973052e5fa9c7450bdd3bd91c1755511fa4d38 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/wdonofrio
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e4973052e5fa9c7450bdd3bd91c1755511fa4d38 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tpl_cli-0.2.0-py3-none-any.whl.
File metadata
- Download URL: tpl_cli-0.2.0-py3-none-any.whl
- Upload date:
- Size: 23.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0da8839068b50662e91bea216009b85e145746f7ef5c25db58f439f3a1e3f4cc
|
|
| MD5 |
16af7106e20da846a1a8fdbe3dc4d048
|
|
| BLAKE2b-256 |
d93063f66f2a06cd39fe7ee6e27e4cc5295f6f65b4bdbd7b210b7c1b9ac2b60d
|
Provenance
The following attestation bundles were made for tpl_cli-0.2.0-py3-none-any.whl:
Publisher:
release.yml on wdonofrio/tpl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tpl_cli-0.2.0-py3-none-any.whl -
Subject digest:
0da8839068b50662e91bea216009b85e145746f7ef5c25db58f439f3a1e3f4cc - Sigstore transparency entry: 828247466
- Sigstore integration time:
-
Permalink:
wdonofrio/tpl@e4973052e5fa9c7450bdd3bd91c1755511fa4d38 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/wdonofrio
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e4973052e5fa9c7450bdd3bd91c1755511fa4d38 -
Trigger Event:
push
-
Statement type: