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.
Creating a new 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)¶
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:
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.
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:
Result: my-project status, my-project ping, … — no intermediate group level.
Adding commands to a flat app works exactly the same way:
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.
Options
| Option | Required | Description |
|---|---|---|
--app |
yes | App that owns this command |
What gets created
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:
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
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 runningadd appfirst. - 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