Skip to content

Project Scaffolding

pyclif ships a built-in project command that generates Django-inspired project structures so you can skip the boilerplate and start writing business logic immediately.

pyclif project --help
pyclif project add --help

Creating a new project

pyclif project init my-project

This creates a fully wired my-project/ directory with a src/ layout, a test suite, a pyproject.toml, and a .gitignore.

Options

Option Default Description
--package-manager uv Toolchain to target — uv or poetry
--integrations (none) Comma-separated integrations to scaffold in one shot
# Target poetry instead of uv
pyclif project init my-project --package-manager poetry

# Generate a project and immediately scaffold two integrations
pyclif project init my-project --integrations github,slack

Generated structure

my-project/
├── pyproject.toml              # build system, scripts, bumpversion config
├── README.md
├── .gitignore
├── src/my_project/
│   ├── __init__.py
│   ├── cli.py                  # @app_group entry point, wires all app exports
│   ├── core/
│   │   ├── context.py          # MyProjectContext(BaseContext) + pass_cli_context
│   │   ├── constants.py
│   │   ├── options.py
│   │   └── integrations/
│   │       └── __init__.py
│   └── apps/
│       └── __init__.py         # exports = [] — add_app appends or extends here
└── tests/
    ├── __init__.py
    └── conftest.py

The generated cli.py wires app exports dynamically so each new app you add is picked up automatically:

from pyclif import app_group, pass_context
from .core.context import MyProjectContext
from .apps import exports

@app_group(handle_response=True, output_format_default="json")
@pass_context
def app(ctx):
    """MyProject CLI."""
    ctx.obj = MyProjectContext()

for item in exports:
    app.add_command(item)

Adding an app

An app is a self-contained feature area with its own commands, interfaces, models, and tables. By default it creates a Click group, giving you my-project app command. Use --no-group when you want commands to appear directly on the root CLI instead.

Grouped app (default)

# Run from the project root
pyclif project add app users

What gets created

src/my_project/apps/users/
├── __init__.py         # @group() decorator + add_command loop
├── interfaces.py       # UsersInterface + UsersRenderer stubs
├── models.py
├── tables.py
└── commands/
    └── __init__.py     # commands = []

What gets wired

apps/__init__.py is updated automatically:

from .users import users
exports.append(users)

Result: my-project users list, my-project users create, …

Flat app — commands without a group layer

Use --no-group when you want commands to appear directly on the root CLI (my-project status, not my-project health status). The internal structure under apps/health/ is identical — only the __init__.py and the wiring differ.

pyclif project add app health --no-group

Options

Option Default Description
--no-group off Skip the @group wrapper — expose commands directly on the root app

What gets created

src/my_project/apps/health/
├── __init__.py         # imports commands — no @group decorator
├── interfaces.py       # HealthInterface + HealthRenderer stubs
├── models.py
├── tables.py
└── commands/
    └── __init__.py     # commands = []

What gets wired

apps/__init__.py uses extend instead of append:

from .health import commands as health_commands
exports.extend(health_commands)

Result: my-project status, my-project ping, … — no intermediate group level.

Adding commands to a flat app works exactly the same way:

pyclif project add command status --app health
pyclif project add command ping   --app health

Mixing grouped and flat apps

Both styles coexist freely. cli.py calls add_command() on every item in exports, whether it is a Click Group (grouped app) or a Click Command (flat app):

# apps/__init__.py after adding both:
exports = []

from .users import users                              # grouped
exports.append(users)

from .health import commands as health_commands       # flat
exports.extend(health_commands)
my-project users list      # grouped app
my-project users create
my-project status          # flat app
my-project ping

Adding a command

A command belongs to an existing app (grouped or flat). It gets its own file and is immediately reachable on the CLI.

pyclif project add command list --app users

Options

Option Required Description
--app yes App that owns this command

What gets created

src/my_project/apps/users/commands/list.py
from pyclif import command, Response
from ....core.context import pass_cli_context
from ..interfaces import UsersInterface

@command()
@pass_cli_context
def list(ctx) -> Response:
    """List description."""
    return UsersInterface(ctx).respond("list")

What gets wired

apps/users/commands/__init__.py is updated automatically:

from .list import list
commands.append(list)

Adding an integration

An integration wraps an external library or service and is attached to the application context so every command can access it via ctx.

# Single-file integration
pyclif project add integration github

# Package integration (client + helpers + models)
pyclif project add integration github --package

Options

Option Default Description
--package off Generate a package with client.py, helpers.py, and models.py

Single-file layout

src/my_project/core/integrations/github.py
class GithubIntegration:
    """Integration for Github."""

    def __init__(self):
        pass

Package layout

src/my_project/core/integrations/github/
├── __init__.py     # exposes GithubIntegration, wires GithubClient
├── client.py       # GithubClient stub
├── helpers.py
└── models.py

What gets wired

core/context.py is updated in two places — an import is injected after the existing imports, and __init__ gets the instance assigned:

from .integrations.github import GithubIntegration   # ← injected

class MyProjectContext(BaseContext):
    def __init__(self):
        super().__init__()
        self.github = GithubIntegration()             # ← injected

Every command with a typed context can then reach the integration via ctx.github.

Name conventions

All scaffolding commands accept names in either kebab-case or snake_case. pyclif derives the other variants automatically:

Input name_snake name_pascal
my-project my_project MyProject
user_profile user_profile UserProfile
github github Github

Error handling

  • Directory already exists (init): exits with code 2 rather than overwriting.
  • App not found (add command): suggests running add app first.
  • File already exists (any command): exits with code 2; no file is touched.
  • src/ not found (add app, add command, add integration): reports that the current directory is not a pyclif project root.

Typical workflow

Grouped app

# 1. Bootstrap
pyclif project init my-project
cd my-project
uv sync --extra dev

# 2. Add a feature area
pyclif project add app users

# 3. Add commands to it
pyclif project add command list   --app users
pyclif project add command get    --app users
pyclif project add command create --app users

# 4. Wrap an external service
pyclif project add integration github --package

# 5. Run the CLI
uv run my-project --help
uv run my-project users list

Flat app

# 1. Bootstrap
pyclif project init deploy-tool
cd deploy-tool
uv sync --extra dev

# 2. Add a flat app — commands appear directly on the root
pyclif project add app deploy --no-group

# 3. Add commands
pyclif project add command up     --app deploy
pyclif project add command down   --app deploy
pyclif project add command status --app deploy

# 4. Run the CLI — no group level
uv run deploy-tool up
uv run deploy-tool status