Repo-as-Template (RAT) engine with lockfile and incremental upgrades.
Project description
retemplar
Keep many repos in sync with living templates — without trampling local changes. Supports multiple template engines including Cookiecutter, regex replacement, and custom processors.
Quick Start
1. Adopt a Template
Make your repo adopt another repo as a template:
# Adopt a local template with default processing
retemplar adopt --template rat:local:../my-template-repo
# Adopt with specific managed paths
retemplar adopt --template rat:local:../my-template-repo \
--managed "**/*.yml" \
--managed "pyproject.toml" \
--ignore "README.md"
This creates a .retemplar.lock file tracking the template relationship.
2. Plan Template Updates
See what changes would be applied when updating to a new template version:
# Preview upgrade to latest
retemplar plan --to rat:local:../my-template-repo
# Preview upgrade to specific version
retemplar plan --to rat:gh:org/template-repo@v1.1.0
3. Apply Changes
Apply the planned changes with variable substitution:
# Apply changes locally
retemplar apply --to rat:local:../my-template-repo
# Apply with custom variables
retemplar apply --to rat:local:../my-template-repo \
--var project_name=my-service \
--var version=1.0.0
Template Engines
retemplar supports multiple template processing engines that can be mixed and matched. The managed path pattern determines which files are processed by each engine, and only the destination needs to be configured:
1. Null Engine (Default)
Simple file copying without any processing:
# .retemplar.lock
managed_paths:
- path: "static/**"
strategy: enforce
engine: null # Default - just copy files
2. Raw String Replace Engine
Simple literal string replacement for basic templating:
managed_paths:
- path: "configs/**"
strategy: enforce
engine: raw_str_replace
engine_options:
variables:
PROJECT_NAME: my-service
VERSION: "1.0.0"
Template files can contain literal strings that get replaced:
# template/config.yml
service_name: PROJECT_NAME
version: VERSION
3. Regex Replace Engine
Advanced pattern matching with regex support:
managed_paths:
- path: "*.md"
strategy: enforce
engine: regex_replace
engine_options:
rules:
- pattern: "v\\d+\\.\\d+\\.\\d+"
replacement: "v2.0.0"
literal: false # Use regex
- pattern: "old-name"
replacement: "new-name"
literal: true # Literal string
4. Jinja2 Engine
Simple Jinja2 templating for file content and paths:
managed_paths:
- path: "templates/**"
strategy: enforce
engine: jinja
engine_options:
jinja_dst: . # Output to repo root
variables:
project_name: my-service
version: "1.0.0"
Template files can use Jinja2 syntax:
# template/config.yml.j2
service_name: { { project_name } }
version: { { version } }
debug: { { debug | default(false) } }
5. Cookiecutter Engine
Full Cookiecutter template support with hooks and project generation:
managed_paths:
- path: "cc/**" # Processes files from cc/ subdirectory
strategy: enforce
engine: cookiecutter
engine_options:
cookiecutter_dst: . # Output to repo root
The cc/** pattern automatically tells the engine to:
- Process files from the
cc/subdirectory in the template - Strip the
cc/prefix from file paths - Apply cookiecutter processing
- Output files to the root level (due to
cookiecutter_dst: .)
Template structure:
template-repo/
├── cc/
│ ├── cookiecutter.json # Variables configuration
│ ├── hooks/ # Pre/post generation hooks
│ │ ├── pre_gen_project.py
│ │ └── post_gen_project.py
│ └── {{cookiecutter.project_slug}}/
│ ├── pyproject.toml # Jinja2 templates
│ ├── src/
│ └── tests/
└── .retemplar.lock
Configuration
Lockfile (.retemplar.lock)
After running adopt, you'll get a lockfile like:
schema_version: 0.1.0
template_ref: rat:local:../template-repo
managed_paths:
# Static files - no processing
- path: ".github/workflows/**"
strategy: enforce
engine: null
# Configuration with variable substitution
- path: "configs/*.yml"
strategy: enforce
engine: raw_str_replace
engine_options:
variables:
service_name: my-service
version: "1.0.0"
# Advanced pattern replacement
- path: "docs/**/*.md"
strategy: enforce
engine: regex_replace
engine_options:
rules:
- pattern: "template-name"
replacement: "my-service"
literal: true
# Jinja2 templating
- path: "templates/**"
strategy: enforce
engine: jinja
engine_options:
jinja_dst: .
variables:
service_name: my-service
# Full Cookiecutter templating
- path: "cc/**"
strategy: enforce
engine: cookiecutter
engine_options:
cookiecutter_dst: .
ignore_paths:
- "README.md"
- "local-configs/**"
# Global engine (optional)
engine: null # Default engine for unspecified paths
Engine Processing Order
Files are processed in the order they appear in managed_paths. Later patterns override earlier ones for conflicting files:
managed_paths:
- path: "**" # Process everything with null engine
engine: null
- path: "*.yml" # Override: process YAML with variables
engine: raw_str_replace
- path: "app.yml" # Override: process app.yml with Cookiecutter
engine: cookiecutter
Advanced Usage
Multi-Engine Templates
You can combine multiple engines in a single template:
# Template provides both static and dynamic content
managed_paths:
- path: "static/**"
strategy: enforce
engine: null
- path: "configs/**"
strategy: enforce
engine: raw_str_replace
engine_options:
variables:
service_name: "{{cookiecutter.project_name}}"
- path: "cc/**"
strategy: enforce
engine: cookiecutter
engine_options:
cookiecutter_dst: .
Custom Engine Integration
You can create custom engines by pointing to local Python files in your lockfile:
Create a custom engine file (./my_engine.py):
from pydantic import BaseModel, ConfigDict
class MyEngineOptions(BaseModel):
"""Options for my custom engine."""
template_vars: dict[str, str] = {}
uppercase: bool = False
model_config = ConfigDict(extra='forbid')
def process_files(src_files, engine_options):
"""Custom engine that processes files with Jinja2-like templating."""
processed_files = {}
for path, content in src_files.items():
if isinstance(content, str):
# Simple variable substitution
processed_content = content
for var_name, var_value in engine_options.template_vars.items():
processed_content = processed_content.replace(
f"{{{{{var_name}}}}}", var_value
)
if engine_options.uppercase:
processed_content = processed_content.upper()
processed_files[path] = processed_content
else:
# Pass through binary files unchanged
processed_files[path] = content
return processed_files
# Required for options validation
options_class = MyEngineOptions
Use it in your lockfile:
# .retemplar.lock
managed_paths:
- path: "templates/**"
strategy: enforce
engine: "./my_engine.py" # Local file path
engine_options:
template_vars:
project_name: "my-awesome-project"
author: "John Doe"
uppercase: false
- path: "configs/**"
strategy: enforce
engine: "../shared-engines/yaml_processor.py" # Relative path
engine_options:
indent: 2
Engine file requirements:
- Must define
process_files(src_files, engine_options)function - Must define
options_class(Pydantic model for validation) - Function should return
dict[str, str | bytes]mapping paths to content - Engines can transform file paths by changing dictionary keys
Variable Inheritance
Variables cascade from multiple sources:
- Lockfile global variables (lowest priority)
- Engine-specific options
- CLI arguments (highest priority)
# CLI variables override lockfile
retemplar apply --to rat:local:../template \
--var service_name=override-name \
--var debug=true
Real-World Examples
Microservice Template
# Template for microservices with CI/CD
managed_paths:
- path: ".github/**"
strategy: enforce
engine: raw_str_replace
engine_options:
variables:
service_name: USER_SERVICE
- path: "service-template/**"
strategy: enforce
engine: cookiecutter
engine_options:
cookiecutter_dst: .
- path: "pyproject.toml"
strategy: merge # Preserve local dependencies
Documentation Template
# Standardize docs across repos
managed_paths:
- path: "docs/templates/**"
strategy: enforce
engine: regex_replace
engine_options:
rules:
- pattern: "\\{\\{repo_name\\}\\}"
replacement: "my-awesome-repo"
- pattern: "\\{\\{team\\}\\}"
replacement: "platform-team"
Configuration Management
# Manage config files with inheritance
managed_paths:
- path: "configs/base/**"
strategy: enforce
engine: null
- path: "configs/app.yml"
strategy: merge
engine: raw_str_replace
engine_options:
variables:
app_name: MY_APP
environment: production
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 retemplar-0.0.0a5.tar.gz.
File metadata
- Download URL: retemplar-0.0.0a5.tar.gz
- Upload date:
- Size: 103.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: python-httpx/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95c89733c03db151d607f9eee8f40358ea7fbf4936333e63304480046c3b8f0b
|
|
| MD5 |
fcf687af62ef2b88634dc9acf4d6637c
|
|
| BLAKE2b-256 |
2c039a19a3d28b24fb862129286a22cf549a26e339112dc3ba4d5add76b90c15
|
File details
Details for the file retemplar-0.0.0a5-py3-none-any.whl.
File metadata
- Download URL: retemplar-0.0.0a5-py3-none-any.whl
- Upload date:
- Size: 42.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: python-httpx/0.28.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b975f75b9b66ee2f8bf219955c40eb7414df933ba27604870b93a7be19a2fb26
|
|
| MD5 |
58f635f81b5bf6f17fd7a989ec3337a0
|
|
| BLAKE2b-256 |
09693d191c7b939bdba4e8977ca63db4bec5c1d3cf4dd7bc51209a11f3b29b70
|