Files
openapi-first/openapi_first/cli.py
2026-06-16 04:14:14 +05:30

201 lines
5.7 KiB
Python

"""
Command-line interface for FastAPI OpenAPI-first scaffolding utilities.
---
## Summary
This CLI bootstraps OpenAPI-first FastAPI applications from versioned,
bundled templates packaged with the library.
"""
import argparse
import shutil
from pathlib import Path
from importlib import resources
DEFAULT_TEMPLATE = "health_app"
def available_templates() -> list[str]:
"""
Return a list of available application templates.
Returns:
list[str]:
Sorted list of template names found in the internal templates directory.
"""
root = resources.files("openapi_first.templates")
return sorted(
item.name
for item in root.iterdir()
if item.is_dir() and not item.name.startswith("_")
)
def copy_template(template: str, target_dir: Path) -> None:
"""
Copy a bundled OpenAPI-first application template into a directory.
Args:
template (str):
Name of the template to copy.
target_dir (Path):
Filesystem path where the template should be copied.
Raises:
FileNotFoundError:
If the requested template does not exist.
"""
target_dir = target_dir.resolve()
target_dir.mkdir(parents=True, exist_ok=True)
root = resources.files("openapi_first.templates")
src = root / template
if not src.exists():
raise FileNotFoundError(
f"Template '{template}' not found. "
f"Available templates: {', '.join(available_templates())}"
)
with resources.as_file(src) as path:
shutil.copytree(path, target_dir, dirs_exist_ok=True)
def main() -> None:
parser = argparse.ArgumentParser(
description="FastAPI OpenAPI-first developer tools"
)
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
# Scaffold command
scaffold_parser = subparsers.add_parser(
"scaffold", help="Scaffold a new OpenAPI-first FastAPI application"
)
scaffold_parser.add_argument(
"template",
nargs="?",
default=DEFAULT_TEMPLATE,
help=f"Template name (default: {DEFAULT_TEMPLATE})",
)
scaffold_parser.add_argument(
"path",
nargs="?",
default=None,
help="Target directory (defaults to template name)",
)
scaffold_parser.add_argument(
"--list",
action="store_true",
help="List available templates and exit",
)
# Models command
models_parser = subparsers.add_parser(
"models", help="Generate Pydantic models from an OpenAPI specification"
)
models_parser.add_argument(
"spec",
help="Path to the OpenAPI specification file",
)
models_parser.add_argument(
"-o",
"--output",
required=True,
help="Path to the output Python file",
)
# Routes command
routes_parser = subparsers.add_parser(
"routes",
help="Generate route handler stubs from an OpenAPI specification",
)
routes_parser.add_argument(
"spec",
help="Path to the OpenAPI specification file",
)
routes_parser.add_argument(
"-o",
"--output-dir",
required=True,
help="Directory where route files will be created",
)
routes_parser.add_argument(
"--use-models",
action="store_true",
default=False,
help="Import Pydantic models for request bodies",
)
routes_parser.add_argument(
"--models-module",
default="models",
help="Python module path for models (default: models)",
)
args = parser.parse_args()
if args.command == "models":
from .codegen import generate_models
try:
generate_models(Path(args.spec), Path(args.output))
print(f"Models generated successfully at {args.output}")
except Exception as exc:
raise SystemExit(str(exc)) from exc
return
if args.command == "routes":
from .codegen import generate_routes
try:
files = generate_routes(
spec_path=Path(args.spec),
output_dir=Path(args.output_dir),
use_models=args.use_models,
models_module=args.models_module,
)
print(f"Generated {len(files)} route file(s):")
for f in files:
print(f" {f}")
except Exception as exc:
raise SystemExit(str(exc)) from exc
return
# Default to scaffold if no command or scaffold command
if args.command == "scaffold" or args.command is None:
# Handle the case where someone uses the old CLI style (openapi-first template path)
# argparse with subparsers might not automatically handle this if positional args are passed
# but let's assume we want to encourage the new style.
# If no command was provided but positional args were, they might be for scaffolding
# This is a bit tricky with argparse subparsers.
# For simplicity, let's just support the new explicit commands.
if args.command is None and not any(vars(args).values()):
parser.print_help()
return
if getattr(args, "list", False):
for name in available_templates():
print(name)
return
template = getattr(args, "template", DEFAULT_TEMPLATE)
path_arg = getattr(args, "path", None)
target = Path(path_arg or template.replace("_", "-"))
try:
copy_template(template, target)
print(f"Template '{template}' created at {target}")
except Exception as exc:
raise SystemExit(str(exc)) from exc
return
parser.print_help()
if __name__ == "__main__":
main()