Scaffold your next Python project with one command
Project description
zenit
Scaffold a new Python project with one command.
zenit my-project
Picks a template, applies addons, writes all files, generates pyproject.toml and justfile, runs git init, and tells you what to do next. Nothing network-dependent — you get a directory you can immediately work in.
Requirements
- Python 3.14+
- uv 0.4+ — install
- git
- just — optional, but generated projects use it heavily
- direnv — optional, auto-activates the virtualenv on
cd
The fastapi template additionally requires Docker running locally.
Installation
uv tool install zenit
# or run without installing
uvx zenit my-project
NixOS: UV_PYTHON_DOWNLOADS=never must be set. Generated projects detect NixOS automatically and write a shell.nix + .envrc that works with the system Python.
Usage
zenit create <name> scaffold a new project
zenit add [addon] add an addon to the current project
zenit remove [addon] remove an addon from the current project
zenit doctor check project health
zenit doctor --thorough full fingerprint integrity check
zenit list-templates
zenit list-addons
zenit config show config file path and current settings
zenit --version
All commands that modify files support --dry-run. Interactive prompts use arrow keys; fall back to numbered input in CI.
Templates
blank — minimal Python package with pytest, ruff, mypy, and a justfile.
fastapi — production-oriented FastAPI setup with SQLAlchemy (async), Alembic, asyncpg, pydantic-settings, a health endpoint, and test fixtures. Requires the docker addon.
Addons
Addons can be selected at scaffold time or added/removed later with zenit add and zenit remove.
| Addon | Description |
|---|---|
docker |
Dockerfile, compose.yml, .dockerignore |
redis |
Async Redis connection helper + compose service |
celery |
Celery worker + beat, backed by Redis |
sentry |
Sentry SDK initialisation (no-ops when DSN is unset) |
github-actions |
CI workflow: lint, type-check, test on push/PR |
auth-manual |
JWT auth: register, login, refresh, logout (fastapi only) |
Dependencies are enforced (celery requires redis) and tracked in .zenit.toml.
Adding and removing addons
cd my-project
zenit add # interactive picker
zenit add redis # direct
zenit add celery --dry-run
zenit remove sentry
Both commands update .zenit.toml, pyproject.toml, justfile, compose.yml, and .env as needed. Run uv sync after to install/uninstall packages.
Health checks
zenit doctor
Checks that all expected files exist, manifest blocks are intact, dependencies match the installed addons, compose services are present, and env vars are defined. Exits with code 1 if errors are found.
zenit doctor --thorough
Adds a full fingerprint integrity pass for all Python blocks recorded in the manifest — useful after running a formatter or making manual edits.
Configuration
Optional config file for personal defaults:
| Platform | Path |
|---|---|
| Linux / macOS | ~/.config/zenit/zenit.toml |
| Windows | %APPDATA%\zenit\zenit.toml |
default_template = "fastapi"
default_addons = ["docker", "github-actions"]
Defaults appear as pre-selections in the interactive prompt — you can still change them before confirming.
How zenit tracks your project
Every scaffolded project gets a .zenit.toml at the root with two sections.
[project] — lockfile
[project]
template = "fastapi"
addons = ["docker", "redis"]
zenit_version = "1.0.8"
schema_version = 2
This is the source of truth for zenit add, zenit remove, and zenit doctor. Commit it. Don't edit it manually — use the CLI instead.
[manifest] — injection registry
The manifest is zenit's record of everything it has injected into your project. It is written at scaffold time and updated on every add / remove. Each entry tracks what was injected, where, and a fingerprint so the code can be located reliably even after formatting.
[[manifest.python_blocks]]
addon = "redis"
point = "settings_fields"
file = "src/my_project/config.py"
lines = "14-14"
fingerprint = "sha256:abc123..."
fingerprint_normalised = "sha256:def456..."
[manifest.python_blocks.locator]
name = "after_last_class_attribute"
args = {class_name = "Settings"}
[[manifest.env]]
key = "REDIS_URL"
source = "addon"
addon = "redis"
[[manifest.dependencies]]
package = "redis"
spec = "redis>=5"
source = "addon"
addon = "redis"
dev = false
[[manifest.just_recipes]]
name = "redis-up"
source = "addon"
addon = "redis"
Do not edit the [manifest] section manually. Run zenit doctor after making any structural changes to confirm everything is consistent.
How code injection works
When an addon adds code to an existing Python file, zenit uses a CST + structural locator pipeline instead of sentinel comments.
Locators
Each template declares named injection points. A locator is a pure function that receives a parsed libcst.Module and returns the body index at which new statements should be inserted. The following locators are available:
| Locator | Description |
|---|---|
after_last_import |
After the last import statement at module level |
after_last_class_attribute |
After the last field/annotation in a named class |
after_statement_matching |
After the first top-level statement matching a regex |
before_yield_in_function |
Before the yield in an async generator (lifespan functions) |
before_return_in_function |
Before the first return in a named function |
in_function_body |
Before or after an anchor statement inside a named function |
at_module_end |
Append at the end of the module body |
at_file_end |
Append at the end of a non-Python file |
File handlers
Injection and removal are dispatched through typed file handlers, one per file type. Each handler knows how to apply a snippet and how to undo it cleanly.
| Handler | Matches | Strategy |
|---|---|---|
PythonHandler |
*.py |
libcst parse → locator → line-level splice |
TomlHandler |
*.toml |
tomlkit round-trip; skips if top-level key already exists |
YamlHandler |
*.yml, *.yaml |
Append; skips if first content key already present |
JustfileHandler |
justfile |
Append; skips if any recipe name already exists |
EnvHandler |
.env* |
Append; skips individual keys that are already defined |
Removal
When an addon is removed, zenit uses the manifest entry to locate the injected block. It tries three stages in order:
- Exact fingerprint — SHA-256 of the canonical libcst output matches the recorded value.
- Normalised fingerprint — SHA-256 after stripping trailing whitespace and collapsing blank lines; resilient to formatter runs.
- Fuzzy match — SequenceMatcher similarity ≥ 85 % within a 20-line window around the recorded position; prints a warning when used.
If none of the three stages succeed, zenit prints an error with manual recovery instructions rather than silently corrupting the file.
Running from source
git clone https://github.com/BojanKonjevic/zenit.git
cd zenit
uv sync
uv run python main.py my-project
License
MIT — see LICENSE.
Project details
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 zenit-1.0.9.tar.gz.
File metadata
- Download URL: zenit-1.0.9.tar.gz
- Upload date:
- Size: 127.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"NixOS","version":"26.05","id":"yarara","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
833af086474919ac980498187072e943e97be7a7deab082965d0d0716c06c19a
|
|
| MD5 |
d0f75a015ecd2e064bbf7d23c00e4bc9
|
|
| BLAKE2b-256 |
c4ee5264d697b2424cfcb4ce3bec21ade57e5539ffde0bced085e010658e8d41
|
File details
Details for the file zenit-1.0.9-py3-none-any.whl.
File metadata
- Download URL: zenit-1.0.9-py3-none-any.whl
- Upload date:
- Size: 160.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"NixOS","version":"26.05","id":"yarara","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
409c25bef887fb3bb0fcddd8677248045d0f9273cf09ba79ca6b8a342e226cde
|
|
| MD5 |
4a2877d818ac2e9fc52bbb5f85f82313
|
|
| BLAKE2b-256 |
90b0926709606f10adeaf3d3ff7fd9d366df856b10ce46377374e7a85b016172
|