Skip to content

Commit 668137d

Browse files
Initial commit for fastapi new command
1 parent 668a403 commit 668137d

File tree

7 files changed

+1289
-1
lines changed

7 files changed

+1289
-1
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ jobs:
3232
- "3.10"
3333
- "3.11"
3434
- "3.12"
35+
- "3.13"
36+
- "3.14"
3537
fail-fast: false
3638
steps:
3739
- name: Dump GitHub context

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# See https://pre-commit.com for more information
22
# See https://pre-commit.com/hooks.html for more hooks
33
default_language_version:
4-
python: python3.10
4+
python: python3.14
55
repos:
66
- repo: https://github.com/pre-commit/pre-commit-hooks
77
rev: v4.6.0

pyproject.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ dependencies = [
4343
[project.optional-dependencies]
4444
standard = ["uvicorn[standard] >= 0.15.0"]
4545

46+
[project.scripts]
47+
fastapi-cloud = "fastapi_cloud_cli.cli:main"
48+
4649
[project.urls]
4750
Homepage = "https://github.com/fastapilabs/fastapi-cloud-cli"
4851
Documentation = "https://fastapi.tiangolo.com/fastapi-cloud-cli/"
@@ -118,3 +121,17 @@ known-third-party = ["typer", "fastapi"]
118121
[tool.ruff.lint.pyupgrade]
119122
# Preserve types, even if a file imports `from __future__ import annotations`.
120123
keep-runtime-typing = true
124+
125+
[tool.uv.workspace]
126+
members = [
127+
"my_fastapi_project",
128+
"sample_project",
129+
]
130+
131+
[dependency-groups]
132+
dev = [
133+
"coverage>=7.6.1",
134+
"pytest>=8.3.5",
135+
"respx>=0.22.0",
136+
"ruff>=0.14.2",
137+
]

src/fastapi_cloud_cli/cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .commands.env import env_app
55
from .commands.login import login
66
from .commands.logout import logout
7+
from .commands.new import new
78
from .commands.unlink import unlink
89
from .commands.whoami import whoami
910
from .logging import setup_logging
@@ -20,6 +21,9 @@
2021
app.command()(deploy)
2122
app.command()(login)
2223
app.command()(logout)
24+
app.command(
25+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
26+
)(new)
2327
app.command()(whoami)
2428
app.command()(unlink)
2529

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

0 commit comments

Comments
 (0)