Scaffold your next Python project with one command
Project description
zenit
A CLI that scaffolds a new Python project from a template with optional addons — sets up the package structure, dev tooling, config files, and an initial git commit, then tells you what to run next.
zenit my-project
Once a project exists, zenit is still useful: add or remove addons, run a health check, and preview what any command would do before committing to it.
What it does
- Asks you to pick a template (blank or FastAPI)
- Asks you to pick addons (Docker, Redis, Celery, Sentry, GitHub Actions)
- Generates the project directory, all files,
pyproject.toml, andjustfile - Runs
git initand makes the first commit
It does not run uv sync, start servers, or do anything network-dependent. You get a directory you can immediately cd into and start working.
Requirements
All platforms need:
- Python 3.14+
- uv 0.4+ — install
- git
- just — install (optional but the generated projects use it heavily)
- direnv — optional, auto-activates the virtualenv on
cd(see per-platform notes below)
The fastapi template additionally needs Docker running locally.
Installation
macOS
# Install uv if you haven't
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install zenit
uv tool install zenit
# Or run without installing
uvx zenit my-project
direnv (recommended):
brew install direnv
# Add to ~/.zshrc or ~/.bash_profile:
eval "$(direnv hook zsh)" # or bash
Linux (non-NixOS)
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install zenit
direnv:
# Ubuntu/Debian
sudo apt install direnv
# Arch
sudo pacman -S direnv
# Fedora
sudo dnf install direnv
# Add to ~/.bashrc or ~/.zshrc:
eval "$(direnv hook bash)" # or zsh
NixOS
NixOS does not allow uv to download or manage its own Python binaries — UV_PYTHON_DOWNLOADS must be set to never and uv must use the system Python. zenit handles this for you when run via the Nix flake, but you need Python 3.14+ available in your environment first.
Option A — run directly from the flake (recommended):
nix run github:BojanKonjevic/zenit -- my-project
This uses the bundled flake which sets UV_PYTHON_DOWNLOADS=never and points uv at the Nix-provided Python 3.14 automatically.
Option B — install and run manually:
# Make sure python3.14 is in your PATH (via nix-shell, home-manager, etc.)
# Then:
UV_PYTHON_DOWNLOADS=never uv tool install zenit
UV_PYTHON_DOWNLOADS=never zenit my-project
Generated projects on NixOS:
When zenit detects it's running on NixOS (by checking /etc/NIXOS), it generates a .envrc that uses use nix shell.nix instead of uv's own environment management, and writes a shell.nix that provides the correct LD_LIBRARY_PATH for compiled wheels like greenlet. So the generated project will also work correctly on NixOS.
direnv on NixOS — add to your configuration.nix:
programs.direnv.enable = true;
Or install it in your user environment:
nix-env -iA nixpkgs.direnv
# Add to ~/.bashrc or ~/.zshrc:
eval "$(direnv hook bash)"
Windows
Windows is supported. The generated justfile uses cmd as the shell, and all just recipes work without WSL.
# Install uv
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# Install zenit
uv tool install zenit
direnv on Windows — direnv does not have an official Windows build. Your options:
- Skip it — instead of auto-activation, run
uv synconce after scaffolding, thenuv run <command>(or just usejust, which routes throughuv runalready). - Use WSL2 — install direnv inside WSL2 as per the Linux instructions above.
- Use
scoop— a community-maintained build exists:scoop install direnv(not officially supported).
zenit will copy .envrc regardless, but will warn you if direnv is not found rather than failing.
Usage
zenit <project-name> scaffold a new project
zenit <project-name> --dry-run preview what would be created (nothing is written)
zenit add [addon] add an addon to the current project
zenit add [addon] --dry-run preview what the addon would change
zenit remove [addon] remove an addon from the current project
zenit remove [addon] --dry-run preview what would be removed
zenit doctor check project health against zenit's expectations
zenit config show config file path and current settings
zenit list-templates show available templates
zenit list-addons show available addons
zenit --version
All interactive prompts use arrow keys and space to select. If stdin is not a tty (e.g. piped input or CI), they fall back to numbered selection.
All commands that modify the filesystem support --dry-run — nothing is written, everything that would happen is printed.
Templates
blank
A minimal Python package with dev tooling. Good for CLIs, scripts, and libraries.
my-project/
src/my_project/
__init__.py
main.py
__main__.py
tests/
test_main.py
pyproject.toml
justfile
.gitignore
.gitattributes
.pre-commit-config.yaml
.envrc
.env
Includes: pytest, ruff, mypy, pytest-cov, ipython.
fastapi
A production-oriented FastAPI setup. Requires the docker addon (selected automatically).
my-project/
src/my_project/
main.py FastAPI app + lifespan
lifecycle.py startup/shutdown logic
settings.py pydantic-settings (reads from .env)
exceptions.py HTTP exception helpers
api/
router.py
routes/
health.py GET /health → {"status": "ok"}
core/
security.py JWT encode/decode, bcrypt hashing
db/
base.py SQLAlchemy DeclarativeBase
session.py async engine, session factory, get_session dep
models/
mixins.py TimestampMixin (created_at, updated_at)
schemas/
common.py PaginationParams, PaginatedResponse
alembic/
env.py async-compatible Alembic env
versions/
tests/
conftest.py session + anon_client + client fixtures
integration/
test_health.py
unit/
fixtures/
scripts/
wait_db.py waits for postgres container to be ready
.env auto-generated with a random SECRET_KEY
.env.example
alembic.ini
Addons
Addons are selected interactively at scaffold time and can also be added or removed later with zenit add and zenit remove. They can be mixed freely with any template, with a few constraints noted below.
docker
Generates a multi-stage Dockerfile (uv-based, minimal final image), compose.yml, and .dockerignore.
- For
blank: compose starts just the app container. - For
fastapi: compose also starts adb(postgres:16) service. - Required by
fastapi(auto-selected).
redis
Adds src/<pkg>/integrations/redis.py — an async connection pool with a get_redis FastAPI dependency and a close_redis shutdown helper.
- If
dockeris also selected: appends aredis:7-alpineservice tocompose.yml. - If
dockeris not selected: writes a standalonecompose.redis.yml. - For
fastapi: also patchessettings.pyto add aredis_urlfield. - Required by
celery.
celery
Adds src/<pkg>/tasks/celery_app.py (Celery app configured against Redis) and tasks/example_tasks.py with a trivial add task.
- Requires
redis. - If
dockeris selected: appendscelery-workerandcelery-beatservices tocompose.yml.
sentry
Adds src/<pkg>/integrations/sentry.py with an init_sentry() function that no-ops when SENTRY_DSN is unset — safe to commit and run locally without a DSN.
- For
fastapi: patcheslifecycle.pyto callinit_sentry()on startup, and patchessettings.pyand.envto addSENTRY_DSN/SENTRY_ENVIRONMENT. - For
blank: patchesmain.pyto callinit_sentry()at startup.
github-actions
Writes .github/workflows/ci.yml that runs lint (ruff check), format check (ruff format --check), type check (mypy), and tests (pytest) on push and pull requests.
- Spins up a
postgres:16service automatically when thefastapitemplate is used. - Spins up a
redis:7-alpineservice automatically whenredisis selected. - Runs migrations before tests when postgres is present.
Adding and removing addons
After a project is scaffolded, you can add or remove addons without re-scaffolding.
cd my-project
# Interactive picker (shows what's installed, what's available, what's blocked)
zenit add
zenit remove
# Direct, non-interactive
zenit add redis
zenit remove sentry
# Preview first
zenit add celery --dry-run
zenit remove docker --dry-run
zenit add will refuse to apply an addon if:
- It's already installed.
- Its required addons aren't installed yet.
- The project layout doesn't match what the addon expects (e.g. no
src/directory).
zenit remove will refuse to remove an addon if:
- It's not installed.
- Another installed addon depends on it.
- The template requires it (e.g.
dockeron afastapiproject).
Both commands update .zenit.toml, pyproject.toml, justfile, compose.yml, and .env/.env.example as needed. They do not run uv sync — do that yourself after.
Health checks
cd my-project
zenit doctor
Runs a series of checks against the current project:
- Metadata —
.zenit.tomlis present, valid, and lists known addons with satisfied dependencies. Warns on version skew between the installed zenit and the one that scaffolded the project. - Dependencies —
pyproject.tomlcontains all runtime and dev deps expected by the template and installed addons. - Generated files — all files that should have been created by the template and addons are still present.
- Extension points — sentinel markers used for addon injection are intact in non-Python files.
- Addon integrity — each installed addon runs its own health check (e.g. verifying
init_sentry()is called inlifecycle.py, checkingREDIS_URLis set in.env). - Compose —
compose.ymlis valid YAML, has no duplicate service definitions, and contains all expected services. - Environment variables — all expected keys are present in both
.envand.env.example.
Exits with code 1 if any errors are found.
Configuration
zenit reads an optional config file for personal defaults:
| Platform | Path |
|---|---|
| Linux / macOS | ~/.config/zenit/zenit.toml (or $XDG_CONFIG_HOME/zenit/zenit.toml) |
| Windows | %APPDATA%\zenit\zenit.toml |
default_template = "fastapi"
default_addons = ["docker", "github-actions"]
Run zenit config to see the resolved path and current settings.
Defaults are applied as pre-selections in the interactive prompt — you can still change them before confirming. The tool always works without a config file.
Generated commands (just)
All generated projects come with a justfile. Run just with no arguments to list what's available.
| Command | What it does |
|---|---|
just test |
run pytest |
just cov |
pytest with coverage report |
just lint |
ruff check |
just fmt |
ruff format |
just fix |
ruff check --fix + ruff format |
just check |
mypy |
just run |
start the app (uvicorn --reload for fastapi, python -m for blank) |
FastAPI only:
| Command | What it does |
|---|---|
just migrate "message" |
generate an Alembic migration |
just upgrade |
apply all pending migrations |
just downgrade |
roll back one step |
just db-create |
start db container, create app + test databases, migrate |
just db-reset |
drop and recreate both databases |
just wait-db |
wait until postgres is accepting connections |
Docker addon:
| Command | What it does |
|---|---|
just docker-up |
docker compose up --build |
just docker-down |
docker compose down |
Redis addon:
| Command | What it does |
|---|---|
just redis-up |
start redis |
just redis-down |
stop redis |
just redis-cli |
open redis-cli |
Celery addon:
| Command | What it does |
|---|---|
just celery-up |
start worker + beat via compose |
just celery-down |
stop worker + beat |
just celery-flower |
open Flower monitoring UI on port 5555 |
just celery-logs |
tail worker logs |
Sentry addon:
| Command | What it does |
|---|---|
just sentry-check |
print sentry-sdk version |
just sentry-test |
print whether SENTRY_DSN is set |
Dry run
Pass --dry-run to any command that modifies the filesystem to preview everything without touching disk:
zenit my-project --dry-run
zenit add redis --dry-run
zenit remove sentry --dry-run
Output shows every file that would be created, modified, or deleted — every dependency change, every just recipe, every git command — then exits without writing anything.
Adding auth to a FastAPI project
The fastapi template lays out the security plumbing but stops short of generating auth routes (everyone's auth is different). When you're ready:
- Define
UserandRefreshTokenmodels inmodels/— import them inmodels/__init__.pyso Alembic discovers them. - Add
core/dependencies.pywith aget_current_userdependency usingdecode_access_tokenfromcore/security.py. - Add
api/routes/auth.pywith register, login, and refresh endpoints. - Register it in
api/router.py. - Uncomment the auth block in
tests/conftest.pyto enable the authenticatedclientfixture. - Run
just db-create && just migrate "add users" && just upgrade.
How zenit tracks your project
Every scaffolded project gets a .zenit.toml at the root:
[project]
template = "fastapi"
addons = ["docker", "redis", "sentry"]
zenit_version = "1.0.6"
This is the source of truth for zenit add, zenit remove, and zenit doctor. It is safe to commit. Do not edit it manually unless you know what you're doing — use zenit add and zenit remove instead.
Running from source
git clone https://github.com/BojanKonjevic/zenit.git
cd zenit
uv sync
uv run python main.py my-project
On NixOS, enter the dev shell first:
nix develop
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.7.tar.gz.
File metadata
- Download URL: zenit-1.0.7.tar.gz
- Upload date:
- Size: 109.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","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 |
65cfe335864bc7ea0b1d81277a9b2e4d3b015f5a47b83e026381fbc57b2110b4
|
|
| MD5 |
c5f60de91e793d60558e7a3929a756b3
|
|
| BLAKE2b-256 |
9b142f262f20b18eb47814fdf7988c81587a6992051341fd59599fc2f27e423f
|
File details
Details for the file zenit-1.0.7-py3-none-any.whl.
File metadata
- Download URL: zenit-1.0.7-py3-none-any.whl
- Upload date:
- Size: 107.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","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 |
906c504d7353d49164c20ad5d7094703027b52ffe418648ac4f77d24d2e4b0a6
|
|
| MD5 |
8cccb987ff49c4eb55c306116773cd41
|
|
| BLAKE2b-256 |
00dc89e3f48bb4efafb8c44cd0d54dc9bd26fc6f20f0cf907d9a488947d14cfd
|