Skip to content
Open
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
8 changes: 8 additions & 0 deletions .github/workflows/test-redistribute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.10"
# Needed to run tests
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.6"
enable-cache: true
- name: Verify uv is available
run: uv --version
- name: Install build dependencies
run: pip install build
- name: Build source distribution
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.4.15"
version: "0.9.6"
enable-cache: true
cache-dependency-glob: |
requirements**.txt
Expand Down Expand Up @@ -97,7 +97,7 @@ jobs:
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
version: "0.4.15"
version: "0.9.6"
enable-cache: true
cache-dependency-glob: |
requirements**.txt
Expand Down
32 changes: 25 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
# Python-generated files
# Python
__pycache__/
*.py[oc]
*.py[cod]
*$py.class
*.so

# Distribution / packaging
build/
dist/
wheels/
*.egg-info
*.egg-info/
*.egg

# Testing / coverage
htmlcov/
.coverage
.coverage.*
coverage/
.pytest_cache/

# Type checking
.mypy_cache/

# Virtual environments
.venv

# Coverage
htmlcov
.coverage*
# IDE
.vscode/
.idea/

# OS
.DS_Store
Thumbs.db
19 changes: 18 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,24 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dependencies = []
dependencies = [
"typer>=0.12.0",
"rich>=13.0.0",
"typing-extensions>=4.8.0",
"rich-toolkit>=0.15.1",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"coverage[toml]>=7.0.0",
"mypy>=1.8.0",
"ruff>=0.3.0",
]

[project.scripts]
fastapi-new = "fastapi_new.cli:main"

[project.urls]
Homepage = "https://github.com/fastapi/fastapi-new"
Documentation = "https://github.com/fastapi/fastapi-new"
Expand Down
3 changes: 3 additions & 0 deletions src/fastapi_new/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .cli import main

main()
11 changes: 11 additions & 0 deletions src/fastapi_new/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import typer

from fastapi_new.new import new as new_command

app = typer.Typer(rich_markup_mode="rich")

app.command()(new_command)


def main() -> None:
app()
Empty file removed src/fastapi_new/main.py
Empty file.
249 changes: 249 additions & 0 deletions src/fastapi_new/new.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import pathlib
import shutil
import subprocess
from dataclasses import dataclass
from typing import Annotated

import typer
from rich_toolkit import RichToolkit

from .utils.cli import get_rich_toolkit

TEMPLATE_CONTENT = """from fastapi import FastAPI
app = FastAPI()

@app.get("/")
def main():
return {"message": "Hello World"}
"""


@dataclass
class ProjectConfig:
name: str
path: pathlib.Path
python: str | None = None


def _generate_readme(project_name: str) -> str:
return f"""# {project_name}

A project created with FastAPI CLI.

## Quick Start

### Start the development server:

```bash
uv run fastapi dev
```

Visit http://localhost:8000

### Deploy to FastAPI Cloud:

> Reader's note: These commands are not quite ready for prime time yet, but will be soon! Join the waiting list at https://fastapicloud.com!

```bash
uv run fastapi login
uv run fastapi deploy
```

## Project Structure

- `main.py` - Your FastAPI application
- `pyproject.toml` - Project dependencies

## Learn More

- [FastAPI Documentation](https://fastapi.tiangolo.com)
- [FastAPI Cloud](https://fastapicloud.com)
"""


def _exit_with_error(toolkit: RichToolkit, error_msg: str) -> None:
toolkit.print(f"[bold red]Error:[/bold red] {error_msg}", tag="error")
raise typer.Exit(code=1)


def _validate_python_version(python: str | None) -> str | None:
"""
Validate Python version is >= 3.10.
Returns error message if < 3.10, None otherwise.
Let uv handle malformed versions or versions it can't find.
"""
if not python:
return None

try:
parts = python.split(".")
if len(parts) < 2:
return None # Let uv handle malformed version
major, minor = int(parts[0]), int(parts[1])

if major < 3 or (major == 3 and minor < 10):
return f"Python {python} is not supported. FastAPI requires Python 3.10 or higher."
except (ValueError, IndexError):
# Malformed version - let uv handle the error
pass

return None


def _setup(toolkit: RichToolkit, config: ProjectConfig) -> None:
error = _validate_python_version(config.python)
if error:
_exit_with_error(toolkit, error)

msg = "Setting up environment with uv"

if config.python:
msg += f" (Python {config.python})"

toolkit.print(msg, tag="env")

# If config.name is provided, create in subdirectory; otherwise init in current dir
# uv will infer the project name from the directory name
if config.path == pathlib.Path.cwd():
init_cmd = ["uv", "init", "--bare"]
else:
init_cmd = ["uv", "init", "--bare", config.name]

if config.python:
init_cmd.extend(["--python", config.python])

try:
subprocess.run(init_cmd, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
stderr = e.stderr.decode() if e.stderr else "No details available"
_exit_with_error(toolkit, f"Failed to initialize project with uv. {stderr}")


def _install_dependencies(toolkit: RichToolkit, config: ProjectConfig) -> None:
toolkit.print("Installing dependencies...", tag="deps")

try:
subprocess.run(
["uv", "add", "fastapi[standard]"],
check=True,
capture_output=True,
cwd=config.path,
)
except subprocess.CalledProcessError as e:
stderr = e.stderr.decode() if e.stderr else "No details available"
_exit_with_error(toolkit, f"Failed to install dependencies. {stderr}")


def _write_template_files(toolkit: RichToolkit, config: ProjectConfig) -> None:
toolkit.print("Writing template files...", tag="template")
readme_content = _generate_readme(config.name)

try:
(config.path / "main.py").write_text(TEMPLATE_CONTENT)
(config.path / "README.md").write_text(readme_content)
except Exception as e:
_exit_with_error(toolkit, f"Failed to write template files. {str(e)}")


def new(
ctx: typer.Context,
project_name: Annotated[
str | None,
typer.Argument(
help="The name of the new FastAPI project. If not provided, initializes in the current directory.",
),
] = None,
python: Annotated[
str | None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing we can require Python 3.10+ for running fastapi-new, so we can finally use the new union vertical bar [tears of joy 🥹].

typer.Option(
"--python",
"-p",
help="Specify the Python version for the new project (e.g., 3.14). Must be 3.10 or higher.",
),
] = None,
) -> None:
if project_name:
name = project_name
path = pathlib.Path.cwd() / project_name
else:
name = pathlib.Path.cwd().name
path = pathlib.Path.cwd()

config = ProjectConfig(
name=name,
path=path,
python=python,
)

with get_rich_toolkit() as toolkit:
toolkit.print_title("Creating a new project 🚀", tag="FastAPI")

toolkit.print_line()

if not project_name:
toolkit.print(
f"[yellow]⚠️ No project name provided. Initializing in current directory: {path}[/yellow]",
tag="warning",
)
toolkit.print_line()

# Check if project directory already exists (only for new subdirectory)
if project_name and config.path.exists():
_exit_with_error(toolkit, f"Directory '{project_name}' already exists.")

if shutil.which("uv") is None:
_exit_with_error(
toolkit,
"uv is required to create new projects. Install it from https://docs.astral.sh/uv/getting-started/installation/",
)

_setup(toolkit, config)

toolkit.print_line()

_install_dependencies(toolkit, config)

toolkit.print_line()

_write_template_files(toolkit, config)

toolkit.print_line()

# Print success message
if project_name:
toolkit.print(
f"[bold green]✨ Success![/bold green] Created FastAPI project: [cyan]{project_name}[/cyan]",
tag="success",
)

toolkit.print_line()

toolkit.print("[bold]Next steps:[/bold]")
toolkit.print(f" [dim]$[/dim] cd {project_name}")
toolkit.print(" [dim]$[/dim] uv run fastapi dev")
else:
toolkit.print(
"[bold green]✨ Success![/bold green] Initialized FastAPI project in current directory",
tag="success",
)

toolkit.print_line()

toolkit.print("[bold]Next steps:[/bold]")
toolkit.print(" [dim]$[/dim] uv run fastapi dev")

toolkit.print_line()

toolkit.print("Visit [blue]http://localhost:8000[/blue]")

toolkit.print_line()

toolkit.print("[bold]Deploy to FastAPI Cloud:[/bold]")
toolkit.print(" [dim]$[/dim] uv run fastapi login")
toolkit.print(" [dim]$[/dim] uv run fastapi deploy")

toolkit.print_line()

toolkit.print(
"[dim]💡 Tip: Use 'uv run' to automatically use the project's environment[/dim]"
)
31 changes: 31 additions & 0 deletions src/fastapi_new/utils/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging

from rich_toolkit import RichToolkit, RichToolkitTheme
from rich_toolkit.styles import MinimalStyle, TaggedStyle

logger = logging.getLogger(__name__)


class FastAPIStyle(TaggedStyle):
def __init__(self, tag_width: int = 11):
super().__init__(tag_width=tag_width)


def get_rich_toolkit(minimal: bool = False) -> RichToolkit:
style = MinimalStyle() if minimal else FastAPIStyle(tag_width=11)

theme = RichToolkitTheme(
style=style,
theme={
"tag.title": "white on #009485",
"tag": "white on #007166",
"placeholder": "grey85",
"text": "white",
"selected": "#007166",
"result": "grey85",
"progress": "on #007166",
"error": "red",
},
)

return RichToolkit(theme=theme)
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Tests for fastapi-new
Loading