Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 82 additions & 57 deletions src/fastapi_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ def callback(
"--version", help="Show the version and exit.", callback=version_callback
),
] = None,
verbose: bool = typer.Option(False, help="Enable verbose output"),
) -> None:
"""
FastAPI CLI - The [bold]fastapi[/bold] command line app. 😎
Expand All @@ -101,9 +100,7 @@ def callback(
Read more in the docs: [link=https://fastapi.tiangolo.com/fastapi-cli/]https://fastapi.tiangolo.com/fastapi-cli/[/link].
"""

log_level = logging.DEBUG if verbose else logging.INFO

setup_logging(level=log_level)
setup_logging()


def _get_module_tree(module_paths: list[Path]) -> Tree:
Expand Down Expand Up @@ -142,23 +139,29 @@ def _run(
proxy_headers: bool = False,
forwarded_allow_ips: str | None = None,
public_url: str | None = None,
verbose: bool = False,
) -> None:
with get_rich_toolkit() as toolkit:
use_rich = should_use_rich_logs()
# The step-by-step "how the app was found and imported" narration is a
# teaching aid; show it only with --verbose so the default output stays lean
show_details = use_rich and verbose
with get_rich_toolkit(use_rich=use_rich) as toolkit:
server_type = "development" if command == "dev" else "production"

toolkit.print_title(f"Starting {server_type} server 🚀", tag="FastAPI")
toolkit.print_line()
toolkit.print(f"Starting FastAPI in {server_type} mode", emoji="⚡️")

toolkit.print(
"Searching for package file structure from directories with [blue]__init__.py[/blue] files"
)
if show_details:
toolkit.print_line()
toolkit.print(
"Searching for package file structure from directories with [blue]__init__.py[/blue] files",
emoji="🔎",
)

if entrypoint and (path or app):
toolkit.print_line()
toolkit.print(
"[error]Cannot use --entrypoint together with path or --app arguments"
)
toolkit.print_line()
raise typer.Exit(code=1)

try:
Expand All @@ -172,8 +175,6 @@ def _run(
field = ".".join(str(loc) for loc in error["loc"])
toolkit.print(f" [red]•[/red] {field}: {error['msg']}")

toolkit.print_line()

raise typer.Exit(code=1) from None

try:
Expand All @@ -196,46 +197,58 @@ def _run(

module_data = import_data.module_data
import_string = import_data.import_string
is_auto_discovery = import_data.module_config_source == "auto-discovery"

toolkit.print(f"Importing from {module_data.extra_sys_path}")
toolkit.print_line()
if show_details:
toolkit.print_line()
toolkit.print(f"Importing from {module_data.extra_sys_path}", emoji="📂")
toolkit.print_line()

if module_data.module_paths:
root_tree = _get_module_tree(module_data.module_paths)
if module_data.module_paths:
root_tree = _get_module_tree(module_data.module_paths)

toolkit.print(root_tree)

toolkit.print(root_tree, tag="module")
toolkit.print_line()

toolkit.print(
"Importing the FastAPI app object from the module with the following code:",
tag="code",
)
toolkit.print_line()
toolkit.print(
f"[underline]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]"
)
toolkit.print_line()
toolkit.print(
"Importing the FastAPI app object from the module with the following code:",
)
toolkit.print_line()
toolkit.print(
f"[blue]from [bold]{module_data.module_import_str}[/bold] import [bold]{import_data.app_name}[/bold]",
)

# Outside --verbose, fold the resolution source into the import string
# line and point newcomers at --verbose for the full explanation
if is_auto_discovery and not verbose:
app_note = " [dim](auto-discovered, use --verbose to learn more)[/]"
else:
app_note = ""

toolkit.print_line()
toolkit.print(
f"Using import string: [blue]{import_string}[/]",
tag="app",
f"Using import string: [blue]{import_string}[/]{app_note}", emoji="🐍"
)

mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
toolkit.print_line()
toolkit.print("Configuration sources:", tag="info")
if mod_source_desc == app_source_desc:
toolkit.print(f" • Import string: {mod_source_desc}")
else:
toolkit.print(f" • Module: {mod_source_desc}")
toolkit.print(f" • App name: {app_source_desc}")
if show_details:
toolkit.print_line()
mod_source_desc = SOURCE_DESCRIPTIONS[import_data.module_config_source]
app_source_desc = SOURCE_DESCRIPTIONS[import_data.app_name_config_source]
toolkit.print("Configuration sources:", emoji="📋")
if mod_source_desc == app_source_desc:
toolkit.print(f"• Import string: {mod_source_desc}")
else:
toolkit.print(f"• Module: {mod_source_desc}")
toolkit.print(f"• App name: {app_source_desc}")

if import_data.module_config_source == "auto-discovery":
# Nudge to pin the entrypoint whenever it was auto-discovered, so it's
# explicit next time — shown in the default output, not just --verbose
if use_rich and is_auto_discovery:
toolkit.print_line()
toolkit.print(
"You can configure an entrypoint in [blue]pyproject.toml[/] for this app with:",
tag="tip",
emoji="💡",
)
toolkit.print_line()
toolkit.print(
Expand All @@ -246,37 +259,31 @@ def _run(
),
"toml",
theme="ansi_light",
)
),
)

url = public_url.rstrip("/") if public_url else f"http://{host}:{port}"
url_docs = f"{url}/docs"

toolkit.print_line()
toolkit.print(
f"Server started at [link={url}]{url}[/]",
f"Documentation at [link={url_docs}]{url_docs}[/]",
tag="server",
)

if command == "dev":
if use_rich:
toolkit.print_line()
toolkit.print(
"Running in development mode, for production use: [bold]fastapi run[/]",
tag="tip",
)
toolkit.print(f"Server started at [link={url}]{url}[/]", emoji="🌐")
toolkit.print(f"Documentation at [link={url_docs}]{url_docs}[/]")

if not uvicorn:
raise FastAPICLIException(
"Could not import Uvicorn, try running 'pip install uvicorn'"
) from None

toolkit.print_line()
toolkit.print("Logs:")
toolkit.print_line()
if use_rich:
toolkit.print_line()
toolkit.print("Logs:", bullet=False)
toolkit.print_line()
else:
toolkit.print("")

extra_uvicorn_kwargs: dict[str, Any] = (
{"log_config": get_uvicorn_log_config()} if should_use_rich_logs() else {}
{"log_config": get_uvicorn_log_config()} if use_rich else {}
)

uvicorn.run(
Expand Down Expand Up @@ -363,6 +370,14 @@ def dev(
help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
),
] = None,
verbose: Annotated[
bool,
typer.Option(
"--verbose",
"-v",
help="Show detailed startup output: how the [bold]FastAPI[/bold] app was discovered, imported, and configured.",
),
] = False,
) -> Any:
"""
Run a [bold]FastAPI[/bold] app in [yellow]development[/yellow] mode. 🧪
Expand Down Expand Up @@ -402,6 +417,7 @@ def dev(
proxy_headers=proxy_headers,
forwarded_allow_ips=forwarded_allow_ips,
public_url=os.getenv("FASTAPI_PUBLIC_URL"),
verbose=verbose,
)


Expand Down Expand Up @@ -471,6 +487,14 @@ def run(
help="Comma separated list of IP Addresses to trust with proxy headers. The literal '*' means trust everything."
),
] = None,
verbose: Annotated[
bool,
typer.Option(
"--verbose",
"-v",
help="Show detailed startup output: how the [bold]FastAPI[/bold] app was discovered, imported, and configured.",
),
] = False,
) -> Any:
"""
Run a [bold]FastAPI[/bold] app in [green]production[/green] mode. 🚀
Expand Down Expand Up @@ -510,6 +534,7 @@ def run(
proxy_headers=proxy_headers,
forwarded_allow_ips=forwarded_allow_ips,
public_url=os.getenv("FASTAPI_PUBLIC_URL"),
verbose=verbose,
)


Expand Down
Loading
Loading