Metadata-Version: 2.4
Name: zenit
Version: 1.0.7
Summary: Scaffold your next Python project with one command
Author-email: Bojan Konjevic <konjevicbojan1@gmail.com>
License: MIT License
        
        Copyright (c) 2025 Bojan Konjevic
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/BojanKonjevic/zenit
Project-URL: Repository, https://github.com/BojanKonjevic/zenit
Project-URL: Issues, https://github.com/BojanKonjevic/zenit/issues
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.14
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jinja2
Requires-Dist: typer>=0.12
Requires-Dist: pyyaml
Requires-Dist: tomli-w
Requires-Dist: tomlkit
Dynamic: license-file

# 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

1. Asks you to pick a **template** (blank or FastAPI)
2. Asks you to pick **addons** (Docker, Redis, Celery, Sentry, GitHub Actions)
3. Generates the project directory, all files, `pyproject.toml`, and `justfile`
4. Runs `git init` and 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](https://docs.astral.sh/uv/getting-started/installation/)
- **git**
- **just** — [install](https://github.com/casey/just#installation) (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

```bash
# 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):
```bash
brew install direnv
# Add to ~/.zshrc or ~/.bash_profile:
eval "$(direnv hook zsh)"   # or bash
```

---

### Linux (non-NixOS)

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install zenit
```

**direnv**:
```bash
# 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):**

```bash
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:**

```bash
# 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`:
```nix
programs.direnv.enable = true;
```
Or install it in your user environment:
```bash
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.

```powershell
# 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:

1. **Skip it** — instead of auto-activation, run `uv sync` once after scaffolding, then `uv run <command>` (or just use `just`, which routes through `uv run` already).
2. **Use WSL2** — install direnv inside WSL2 as per the Linux instructions above.
3. **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 a `db` (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 `docker` is also selected: appends a `redis:7-alpine` service to `compose.yml`.
- If `docker` is not selected: writes a standalone `compose.redis.yml`.
- For `fastapi`: also patches `settings.py` to add a `redis_url` field.
- 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 `docker` is selected: appends `celery-worker` and `celery-beat` services to `compose.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`: patches `lifecycle.py` to call `init_sentry()` on startup, and patches `settings.py` and `.env` to add `SENTRY_DSN` / `SENTRY_ENVIRONMENT`.
- For `blank`: patches `main.py` to call `init_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:16` service automatically when the `fastapi` template is used.
- Spins up a `redis:7-alpine` service automatically when `redis` is 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.

```bash
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. `docker` on a `fastapi` project).

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

```bash
cd my-project
zenit doctor
```

Runs a series of checks against the current project:

- **Metadata** — `.zenit.toml` is 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.toml` contains 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 in `lifecycle.py`, checking `REDIS_URL` is set in `.env`).
- **Compose** — `compose.yml` is valid YAML, has no duplicate service definitions, and contains all expected services.
- **Environment variables** — all expected keys are present in both `.env` and `.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`                  |

```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:

```bash
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:

1. Define `User` and `RefreshToken` models in `models/` — import them in `models/__init__.py` so Alembic discovers them.
2. Add `core/dependencies.py` with a `get_current_user` dependency using `decode_access_token` from `core/security.py`.
3. Add `api/routes/auth.py` with register, login, and refresh endpoints.
4. Register it in `api/router.py`.
5. Uncomment the auth block in `tests/conftest.py` to enable the authenticated `client` fixture.
6. 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:

```toml
[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

```bash
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:
```bash
nix develop
python main.py my-project
```

---

## License

MIT — see [LICENSE](LICENSE).
