Skip to content

Commit 031b857

Browse files
✨ Add implementation of fastapi-new CLI, and base for fastapi new command (#5)
1 parent 206e333 commit 031b857

File tree

13 files changed

+974
-10
lines changed

13 files changed

+974
-10
lines changed

.github/workflows/test-redistribute.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ jobs:
2525
uses: actions/setup-python@v6
2626
with:
2727
python-version: "3.10"
28+
# Needed to run tests
29+
- name: Setup uv
30+
uses: astral-sh/setup-uv@v7
31+
with:
32+
version: "0.9.6"
33+
enable-cache: true
34+
- name: Verify uv is available
35+
run: uv --version
2836
- name: Install build dependencies
2937
run: pip install build
3038
- name: Build source distribution

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
- name: Setup uv
5454
uses: astral-sh/setup-uv@v7
5555
with:
56-
version: "0.4.15"
56+
version: "0.9.6"
5757
enable-cache: true
5858
cache-dependency-glob: |
5959
requirements**.txt
@@ -93,7 +93,7 @@ jobs:
9393
- name: Setup uv
9494
uses: astral-sh/setup-uv@v7
9595
with:
96-
version: "0.4.15"
96+
version: "0.9.6"
9797
enable-cache: true
9898
cache-dependency-glob: |
9999
requirements**.txt

.gitignore

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1-
# Python-generated files
1+
# Python
22
__pycache__/
3-
*.py[oc]
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
7+
# Distribution / packaging
48
build/
59
dist/
6-
wheels/
7-
*.egg-info
10+
*.egg-info/
11+
*.egg
12+
13+
# Testing / coverage
14+
htmlcov/
15+
.coverage
16+
.coverage.*
17+
coverage/
18+
.pytest_cache/
19+
20+
# Type checking
21+
.mypy_cache/
822

923
# Virtual environments
1024
.venv
1125

12-
# Coverage
13-
htmlcov
14-
.coverage*
26+
# IDE
27+
.vscode/
28+
.idea/
29+
30+
# OS
31+
.DS_Store
32+
Thumbs.db

pyproject.toml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,24 @@ classifiers = [
3030
"Programming Language :: Python :: 3.13",
3131
"Programming Language :: Python :: 3.14",
3232
]
33-
dependencies = []
33+
dependencies = [
34+
"typer>=0.12.0",
35+
"rich>=13.0.0",
36+
"typing-extensions>=4.8.0",
37+
"rich-toolkit>=0.15.1",
38+
]
39+
40+
[project.optional-dependencies]
41+
dev = [
42+
"pytest>=8.0.0",
43+
"coverage[toml]>=7.0.0",
44+
"mypy>=1.8.0",
45+
"ruff>=0.3.0",
46+
]
47+
48+
[project.scripts]
49+
fastapi-new = "fastapi_new.cli:main"
50+
3451
[project.urls]
3552
Homepage = "https://github.com/fastapi/fastapi-new"
3653
Documentation = "https://github.com/fastapi/fastapi-new"

src/fastapi_new/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .cli import main
2+
3+
main()

src/fastapi_new/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import typer
2+
3+
from fastapi_new.new import new as new_command
4+
5+
app = typer.Typer(rich_markup_mode="rich")
6+
7+
app.command()(new_command)
8+
9+
10+
def main() -> None:
11+
app()

src/fastapi_new/main.py

Whitespace-only changes.

src/fastapi_new/new.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import pathlib
2+
import shutil
3+
import subprocess
4+
from dataclasses import dataclass
5+
from typing import Annotated
6+
7+
import typer
8+
from rich_toolkit import RichToolkit
9+
10+
from .utils.cli import get_rich_toolkit
11+
12+
TEMPLATE_CONTENT = """from fastapi import FastAPI
13+
app = FastAPI()
14+
15+
@app.get("/")
16+
def main():
17+
return {"message": "Hello World"}
18+
"""
19+
20+
21+
@dataclass
22+
class ProjectConfig:
23+
name: str
24+
path: pathlib.Path
25+
python: str | None = None
26+
27+
28+
def _generate_readme(project_name: str) -> str:
29+
return f"""# {project_name}
30+
31+
A project created with FastAPI CLI.
32+
33+
## Quick Start
34+
35+
### Start the development server:
36+
37+
```bash
38+
uv run fastapi dev
39+
```
40+
41+
Visit http://localhost:8000
42+
43+
### Deploy to FastAPI Cloud:
44+
45+
> 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!
46+
47+
```bash
48+
uv run fastapi login
49+
uv run fastapi deploy
50+
```
51+
52+
## Project Structure
53+
54+
- `main.py` - Your FastAPI application
55+
- `pyproject.toml` - Project dependencies
56+
57+
## Learn More
58+
59+
- [FastAPI Documentation](https://fastapi.tiangolo.com)
60+
- [FastAPI Cloud](https://fastapicloud.com)
61+
"""
62+
63+
64+
def _exit_with_error(toolkit: RichToolkit, error_msg: str) -> None:
65+
toolkit.print(f"[bold red]Error:[/bold red] {error_msg}", tag="error")
66+
raise typer.Exit(code=1)
67+
68+
69+
def _validate_python_version(python: str | None) -> str | None:
70+
"""
71+
Validate Python version is >= 3.10.
72+
Returns error message if < 3.10, None otherwise.
73+
Let uv handle malformed versions or versions it can't find.
74+
"""
75+
if not python:
76+
return None
77+
78+
try:
79+
parts = python.split(".")
80+
if len(parts) < 2:
81+
return None # Let uv handle malformed version
82+
major, minor = int(parts[0]), int(parts[1])
83+
84+
if major < 3 or (major == 3 and minor < 10):
85+
return f"Python {python} is not supported. FastAPI requires Python 3.10 or higher."
86+
except (ValueError, IndexError):
87+
# Malformed version - let uv handle the error
88+
pass
89+
90+
return None
91+
92+
93+
def _setup(toolkit: RichToolkit, config: ProjectConfig) -> None:
94+
error = _validate_python_version(config.python)
95+
if error:
96+
_exit_with_error(toolkit, error)
97+
98+
msg = "Setting up environment with uv"
99+
100+
if config.python:
101+
msg += f" (Python {config.python})"
102+
103+
toolkit.print(msg, tag="env")
104+
105+
# If config.name is provided, create in subdirectory; otherwise init in current dir
106+
# uv will infer the project name from the directory name
107+
if config.path == pathlib.Path.cwd():
108+
init_cmd = ["uv", "init", "--bare"]
109+
else:
110+
init_cmd = ["uv", "init", "--bare", config.name]
111+
112+
if config.python:
113+
init_cmd.extend(["--python", config.python])
114+
115+
try:
116+
subprocess.run(init_cmd, check=True, capture_output=True)
117+
except subprocess.CalledProcessError as e:
118+
stderr = e.stderr.decode() if e.stderr else "No details available"
119+
_exit_with_error(toolkit, f"Failed to initialize project with uv. {stderr}")
120+
121+
122+
def _install_dependencies(toolkit: RichToolkit, config: ProjectConfig) -> None:
123+
toolkit.print("Installing dependencies...", tag="deps")
124+
125+
try:
126+
subprocess.run(
127+
["uv", "add", "fastapi[standard]"],
128+
check=True,
129+
capture_output=True,
130+
cwd=config.path,
131+
)
132+
except subprocess.CalledProcessError as e:
133+
stderr = e.stderr.decode() if e.stderr else "No details available"
134+
_exit_with_error(toolkit, f"Failed to install dependencies. {stderr}")
135+
136+
137+
def _write_template_files(toolkit: RichToolkit, config: ProjectConfig) -> None:
138+
toolkit.print("Writing template files...", tag="template")
139+
readme_content = _generate_readme(config.name)
140+
141+
try:
142+
(config.path / "main.py").write_text(TEMPLATE_CONTENT)
143+
(config.path / "README.md").write_text(readme_content)
144+
except Exception as e:
145+
_exit_with_error(toolkit, f"Failed to write template files. {str(e)}")
146+
147+
148+
def new(
149+
ctx: typer.Context,
150+
project_name: Annotated[
151+
str | None,
152+
typer.Argument(
153+
help="The name of the new FastAPI project. If not provided, initializes in the current directory.",
154+
),
155+
] = None,
156+
python: Annotated[
157+
str | None,
158+
typer.Option(
159+
"--python",
160+
"-p",
161+
help="Specify the Python version for the new project (e.g., 3.14). Must be 3.10 or higher.",
162+
),
163+
] = None,
164+
) -> None:
165+
if project_name:
166+
name = project_name
167+
path = pathlib.Path.cwd() / project_name
168+
else:
169+
name = pathlib.Path.cwd().name
170+
path = pathlib.Path.cwd()
171+
172+
config = ProjectConfig(
173+
name=name,
174+
path=path,
175+
python=python,
176+
)
177+
178+
with get_rich_toolkit() as toolkit:
179+
toolkit.print_title("Creating a new project 🚀", tag="FastAPI")
180+
181+
toolkit.print_line()
182+
183+
if not project_name:
184+
toolkit.print(
185+
f"[yellow]⚠️ No project name provided. Initializing in current directory: {path}[/yellow]",
186+
tag="warning",
187+
)
188+
toolkit.print_line()
189+
190+
# Check if project directory already exists (only for new subdirectory)
191+
if project_name and config.path.exists():
192+
_exit_with_error(toolkit, f"Directory '{project_name}' already exists.")
193+
194+
if shutil.which("uv") is None:
195+
_exit_with_error(
196+
toolkit,
197+
"uv is required to create new projects. Install it from https://docs.astral.sh/uv/getting-started/installation/",
198+
)
199+
200+
_setup(toolkit, config)
201+
202+
toolkit.print_line()
203+
204+
_install_dependencies(toolkit, config)
205+
206+
toolkit.print_line()
207+
208+
_write_template_files(toolkit, config)
209+
210+
toolkit.print_line()
211+
212+
# Print success message
213+
if project_name:
214+
toolkit.print(
215+
f"[bold green]✨ Success![/bold green] Created FastAPI project: [cyan]{project_name}[/cyan]",
216+
tag="success",
217+
)
218+
219+
toolkit.print_line()
220+
221+
toolkit.print("[bold]Next steps:[/bold]")
222+
toolkit.print(f" [dim]$[/dim] cd {project_name}")
223+
toolkit.print(" [dim]$[/dim] uv run fastapi dev")
224+
else:
225+
toolkit.print(
226+
"[bold green]✨ Success![/bold green] Initialized FastAPI project in current directory",
227+
tag="success",
228+
)
229+
230+
toolkit.print_line()
231+
232+
toolkit.print("[bold]Next steps:[/bold]")
233+
toolkit.print(" [dim]$[/dim] uv run fastapi dev")
234+
235+
toolkit.print_line()
236+
237+
toolkit.print("Visit [blue]http://localhost:8000[/blue]")
238+
239+
toolkit.print_line()
240+
241+
toolkit.print("[bold]Deploy to FastAPI Cloud:[/bold]")
242+
toolkit.print(" [dim]$[/dim] uv run fastapi login")
243+
toolkit.print(" [dim]$[/dim] uv run fastapi deploy")
244+
245+
toolkit.print_line()
246+
247+
toolkit.print(
248+
"[dim]💡 Tip: Use 'uv run' to automatically use the project's environment[/dim]"
249+
)

src/fastapi_new/utils/cli.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import logging
2+
3+
from rich_toolkit import RichToolkit, RichToolkitTheme
4+
from rich_toolkit.styles import MinimalStyle, TaggedStyle
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
class FastAPIStyle(TaggedStyle):
10+
def __init__(self, tag_width: int = 11):
11+
super().__init__(tag_width=tag_width)
12+
13+
14+
def get_rich_toolkit(minimal: bool = False) -> RichToolkit:
15+
style = MinimalStyle() if minimal else FastAPIStyle(tag_width=11)
16+
17+
theme = RichToolkitTheme(
18+
style=style,
19+
theme={
20+
"tag.title": "white on #009485",
21+
"tag": "white on #007166",
22+
"placeholder": "grey85",
23+
"text": "white",
24+
"selected": "#007166",
25+
"result": "grey85",
26+
"progress": "on #007166",
27+
"error": "red",
28+
},
29+
)
30+
31+
return RichToolkit(theme=theme)

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Tests for fastapi-new

0 commit comments

Comments
 (0)