Unfazed Command¶
Unfazed provides a CLI framework built on top of Click. It ships with several built-in commands (project scaffolding, shell, database migrations, etc.) and automatically discovers custom commands placed in each app's commands/ directory. Both sync and async command handlers are supported.
Quick Start¶
1. Create a command file¶
Add a Python file in your app's commands/ directory. The file name becomes the CLI command name (underscores are replaced with hyphens).
2. Write the command¶
Every command file must define a Command class that extends BaseCommand:
# myapp/commands/greet.py
import typing as t
from unfazed.command import BaseCommand
class Command(BaseCommand):
help_text = "Print a greeting message"
async def handle(self, **options: t.Any) -> None:
print("Hello from Unfazed!")
3. Run it¶
The command is discovered automatically — no extra registration is needed beyond having the app in INSTALLED_APPS.
Creating Custom Commands¶
The basics¶
A command is a Command class inside a file under myapp/commands/. Unfazed scans this directory at startup and registers every .py file whose name does not start with _.
myapp/commands/
├── __init__.py
├── import_data.py # registered as "import-data"
├── send_report.py # registered as "send-report"
└── _helpers.py # ignored (starts with _)
Adding arguments¶
Override add_arguments() to return a list of Click Option objects:
# myapp/commands/import_data.py
import typing as t
from click import Option
from unfazed.command import BaseCommand
class Command(BaseCommand):
help_text = "Import data from a CSV file"
def add_arguments(self) -> t.List[Option]:
return [
Option(
["--file", "-f"],
type=str,
help="Path to the CSV file",
required=True,
),
Option(
["--dry-run"],
is_flag=True,
default=False,
help="Preview without writing to the database",
),
]
async def handle(self, **options: t.Any) -> None:
file_path = options["file"]
dry_run = options["dry_run"]
if dry_run:
print(f"[DRY RUN] Would import from {file_path}")
return
# read and import data ...
print(f"Imported data from {file_path}")
Sync vs async handlers¶
The handle() method can be either async or sync. Unfazed detects which one you wrote and runs it accordingly:
# Async handler
class Command(BaseCommand):
async def handle(self, **options: t.Any) -> None:
await some_async_work()
# Sync handler
class Command(BaseCommand):
def handle(self, **options: t.Any) -> None:
some_sync_work()
Accessing the Unfazed instance¶
Every command has access to self.unfazed (the application instance) and self.app_label (the label of the app the command belongs to):
class Command(BaseCommand):
async def handle(self, **options: t.Any) -> None:
print(f"Running in app: {self.app_label}")
print(f"Debug mode: {self.unfazed.settings.DEBUG}")
Built-in Commands¶
startproject — Create a new project¶
Available without a project context (runs via unfazed-cli anywhere).
| Flag | Description |
|---|---|
-n, --project_name |
Name of the project. |
-l, --location |
Parent directory (defaults to current directory). |
startapp — Create a new app¶
Run from inside a project directory:
| Flag | Description |
|---|---|
-n, --app_name |
Name of the app (lowercase letters, numbers, underscores only). |
-l, --location |
Parent directory (defaults to current directory). |
-t, --template |
Template type: simple (default) or standard. |
The simple template creates flat files (models.py, endpoints.py, etc.). The standard template creates sub-packages (models/, endpoints/, serializers/, etc.).
shell — Interactive IPython shell¶
Launches an IPython session with the unfazed application instance pre-loaded:
Requires ipython to be installed. The shell supports await directly.
create-superuser — Create an admin superuser¶
| Flag | Description |
|---|---|
-e, --email |
Email address for the superuser. |
Generates a random password and prints it to stdout. Requires unfazed.contrib.auth to be configured.
export-openapi — Export OpenAPI schema¶
| Flag | Description |
|---|---|
-l, --location |
Output directory (defaults to current directory). |
Writes openapi.yaml to the specified directory. Requires the pyyaml package.
Tortoise ORM commands¶
When using Tortoise ORM, additional database migration commands are available. See the Tortoise ORM documentation for details.
Examples¶
A data export command with multiple options¶
# myapp/commands/export_users.py
import typing as t
from pathlib import Path
from click import Choice, Option
from unfazed.command import BaseCommand
class Command(BaseCommand):
help_text = "Export users to a file"
def add_arguments(self) -> t.List[Option]:
return [
Option(
["--output", "-o"],
type=str,
required=True,
help="Output file path",
),
Option(
["--format", "-f"],
type=Choice(["csv", "json"]),
default="csv",
show_choices=True,
help="Output format",
),
Option(
["--limit"],
type=int,
default=100,
help="Maximum number of records",
),
]
async def handle(self, **options: t.Any) -> None:
output = Path(options["output"])
fmt = options["format"]
limit = options["limit"]
# query users from database ...
print(f"Exported {limit} users to {output} ({fmt})")
A simple sync utility command¶
# myapp/commands/check_config.py
import typing as t
from unfazed.command import BaseCommand
class Command(BaseCommand):
help_text = "Validate the current project configuration"
def handle(self, **options: t.Any) -> None:
settings = self.unfazed.settings
print(f"Project: {settings.PROJECT_NAME}")
print(f"Debug: {settings.DEBUG}")
print(f"Apps: {len(settings.INSTALLED_APPS)} installed")
print("Configuration OK")
API Reference¶
BaseCommand¶
class BaseCommand(click.Command, ABC):
def __init__(self, unfazed: Unfazed, name: str, app_label: str, ...) -> None
Abstract base class for all commands. Extends Click's Command.
Attributes:
help_text: str— Default help text displayed for--help.unfazed: Unfazed— The application instance.app_label: str— Label of the app this command belongs to.
Methods:
add_arguments() -> List[click.Option]: Override to declare CLI options. Returns[]by default.handle(**options: Any) -> Any: Abstract. The command logic. Can beasync defordef.
CommandCenter¶
class CommandCenter(click.Group):
def __init__(self, unfazed: Unfazed, app_center: AppCenter, name: str) -> None
Manages all project-level commands. Loads internal commands (startapp, shell, create-superuser, export-openapi) and commands from every installed app.
Methods:
async setup() -> None: Discovers and loads all commands.load_command(command: Command) -> None: Loads a single command into the group. RaisesTypeErrorif the class is not aBaseCommandsubclass.list_internal_command() -> List[Command]: Returns internal framework commands (excludesstartproject).
CliCommandCenter¶
Lightweight command group used outside of a project context. Only loads the startproject command.
Methods:
setup() -> None: Loads CLI-only commands.list_cli_command() -> List[Command]: Returns the list of CLI commands.