Skip to content

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).

myapp/
├── app.py
└── commands/
    ├── __init__.py
    └── greet.py

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

unfazed-cli greet
# Hello from Unfazed!

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}")
unfazed-cli import-data --file data.csv --dry-run
# [DRY RUN] Would import from data.csv

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).

unfazed-cli startproject -n myproject
unfazed-cli startproject -n myproject -l /path/to/parent
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:

unfazed-cli startapp -n blog
unfazed-cli startapp -n blog -t standard
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:

unfazed-cli shell

Requires ipython to be installed. The shell supports await directly.

create-superuser — Create an admin superuser

unfazed-cli create-superuser --email admin@example.com
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

unfazed-cli export-openapi -l ./docs
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})")
unfazed-cli export-users -o users.json -f json --limit 500

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")
unfazed-cli check-config
# Project: myproject
# Debug:   True
# Apps:    3 installed
# 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 be async def or def.

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. Raises TypeError if the class is not a BaseCommand subclass.
  • list_internal_command() -> List[Command]: Returns internal framework commands (excludes startproject).

CliCommandCenter

class CliCommandCenter(click.Group):
    def __init__(self, unfazed: Unfazed) -> None

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.