Skip to content

Commit 051bfd7

Browse files
feat: cli setup project
1 parent 057609d commit 051bfd7

File tree

7 files changed

+496
-3
lines changed

7 files changed

+496
-3
lines changed

README.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,28 @@ A production-ready FastAPI template with SQLAlchemy, dependency injection, Sentr
4040

4141
## Quick Start
4242

43-
### 1. Setup Environment
43+
### Option 1: Interactive Setup (Recommended)
44+
45+
```bash
46+
git clone <repository-url>
47+
cd fastapi-template
48+
49+
# Install dependencies including setup tool
50+
uv venv
51+
source .venv/bin/activate
52+
uv sync --all-groups --group setup
53+
54+
# Run interactive setup wizard
55+
uv run setup-project
56+
```
57+
58+
The setup wizard will:
59+
- Configure project name, description, and author
60+
- Set up database connection
61+
- Choose CI/CD platform (GitHub Actions, GitLab CI, or none)
62+
- Update all configuration files automatically
63+
64+
### Option 2: Manual Setup
4465

4566
```bash
4667
git clone <repository-url>
@@ -52,7 +73,7 @@ source .venv/bin/activate
5273
uv sync --all-groups
5374
```
5475

55-
### 2. Configure Database
76+
Configure database:
5677

5778
```bash
5879
cp .env.example .env
@@ -65,7 +86,7 @@ Run database migrations:
6586
uv run alembic upgrade head
6687
```
6788

68-
### 3. Start API Server
89+
### Start API Server
6990

7091
```bash
7192
uv run uvicorn main:app --host 0.0.0.0 --port 8000
@@ -328,6 +349,20 @@ docker build -t fastapi-template .
328349
docker run -p 8000:8000 --env-file .env fastapi-template
329350
```
330351

352+
## Removing Setup Tool
353+
354+
After running the setup wizard, you can remove the setup tool:
355+
356+
```bash
357+
rm -rf setup_project/
358+
```
359+
360+
Then remove from `pyproject.toml`:
361+
- The `setup` group from `[dependency-groups]`
362+
- The `setup-project` line from `[project.scripts]`
363+
364+
Finally, run `uv sync` to update the lock file.
365+
331366
## License
332367

333368
MIT

pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ dev = [
4444
"wemake-python-styleguide (>=1.0.0,<2.0.0)",
4545
"pre-commit>=4.2.0",
4646
]
47+
setup = [
48+
"click>=8.1",
49+
"rich>=13.0",
50+
]
51+
52+
[project.scripts]
53+
setup-project = "setup_project.cli:main"
4754

4855
[project.entry-points."flake8.extension"]
4956
WPS = "wemake_python_styleguide.checker:Checker"
@@ -101,6 +108,11 @@ external = [ "WPS" ]
101108
"S603", # do not require `shell=True`
102109
"S607", # partial executable paths
103110
]
111+
"setup_project/*.py" = [
112+
"S603", # subprocess calls are for setup tool
113+
"S607", # partial executable paths
114+
"FBT001", # click uses boolean positional args
115+
]
104116

105117
[tool.ruff.lint.flake8-bugbear]
106118
extend-immutable-calls = [

setup_project/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""FastAPI Template Setup CLI."""

setup_project/cli.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Setup CLI for FastAPI template project."""
2+
3+
import subprocess
4+
from pathlib import Path
5+
from typing import Literal
6+
7+
import click
8+
from rich.console import Console
9+
from rich.panel import Panel
10+
from rich.prompt import Confirm, IntPrompt, Prompt
11+
12+
from setup_project.config import ProjectConfig
13+
from setup_project.updaters import apply_all_updates
14+
15+
console = Console()
16+
17+
18+
def get_git_config(key: str) -> str | None:
19+
"""Get git config value."""
20+
try:
21+
result = subprocess.run(
22+
["git", "config", "--get", key],
23+
capture_output=True,
24+
text=True,
25+
check=False,
26+
)
27+
if result.returncode == 0:
28+
return result.stdout.strip()
29+
except FileNotFoundError:
30+
pass
31+
return None
32+
33+
34+
def get_default_project_name() -> str:
35+
"""Get default project name from current directory."""
36+
return Path.cwd().name
37+
38+
39+
def prompt_project_config(
40+
name: str | None,
41+
description: str | None,
42+
) -> tuple[str, str, str, str]:
43+
"""Prompt for project configuration."""
44+
console.print("[bold]Project Configuration[/bold]")
45+
46+
project_name = (
47+
name
48+
if name
49+
else Prompt.ask(
50+
" Project name (kebab-case)",
51+
default=get_default_project_name(),
52+
)
53+
)
54+
55+
project_description = (
56+
description
57+
if description
58+
else Prompt.ask(
59+
" Description",
60+
default="Production-ready FastAPI application",
61+
)
62+
)
63+
64+
author_name = Prompt.ask(
65+
" Author name",
66+
default=get_git_config("user.name") or "Your Name",
67+
)
68+
69+
author_email = Prompt.ask(
70+
" Author email",
71+
default=get_git_config("user.email") or "you@example.com",
72+
)
73+
74+
console.print()
75+
return project_name, project_description, author_name, author_email
76+
77+
78+
def prompt_db_config(project_name: str) -> tuple[str, str, str, str, int]:
79+
"""Prompt for database configuration."""
80+
console.print("[bold]Database Configuration[/bold]")
81+
82+
default_db_name = ProjectConfig.derive_db_name(project_name)
83+
84+
db_name = Prompt.ask(" Database name", default=default_db_name)
85+
db_user = Prompt.ask(" Username", default="postgres")
86+
db_password = Prompt.ask(" Password", password=True, default="postgres")
87+
db_host = Prompt.ask(" Host", default="localhost")
88+
db_port = IntPrompt.ask(" Port", default=5432)
89+
90+
console.print()
91+
return db_name, db_user, db_password, db_host, db_port
92+
93+
94+
def prompt_ci_config(skip_ci: bool) -> tuple[Literal["github", "gitlab", "none"], str | None]:
95+
"""Prompt for CI/CD configuration."""
96+
ci_platform: Literal["github", "gitlab", "none"] = "github"
97+
github_repo: str | None = None
98+
99+
if skip_ci:
100+
return ci_platform, github_repo
101+
102+
console.print("[bold]CI/CD Platform[/bold]")
103+
console.print(" [1] GitHub Actions")
104+
console.print(" [2] GitLab CI")
105+
console.print(" [3] None")
106+
107+
ci_choice = Prompt.ask(" Select", choices=["1", "2", "3"], default="1")
108+
ci_map: dict[str, Literal["github", "gitlab", "none"]] = {
109+
"1": "github",
110+
"2": "gitlab",
111+
"3": "none",
112+
}
113+
ci_platform = ci_map[ci_choice]
114+
115+
if ci_platform == "github":
116+
github_repo = Prompt.ask(" GitHub repo URL (for badges)", default="")
117+
if not github_repo:
118+
github_repo = None
119+
120+
console.print()
121+
return ci_platform, github_repo
122+
123+
124+
def print_summary(updated_files: list[str], db_name: str) -> None:
125+
"""Print summary of changes."""
126+
for file in updated_files:
127+
if file.startswith("Removed"):
128+
console.print(f" [yellow]{file}[/yellow]")
129+
else:
130+
console.print(f" [green]{file}[/green]")
131+
132+
console.print()
133+
console.print("[bold green]Done![/bold green] Next steps:")
134+
console.print(" 1. [cyan]uv sync[/cyan]")
135+
console.print(f" 2. Create database: [cyan]{db_name}[/cyan]")
136+
console.print(" 3. [cyan]make upgrade-db[/cyan]")
137+
console.print(" 4. [cyan]make[/cyan]")
138+
console.print()
139+
140+
141+
@click.command()
142+
@click.option("--name", help="Project name (kebab-case)")
143+
@click.option("--description", help="Project description")
144+
@click.option("--skip-ci", is_flag=True, help="Skip CI/CD configuration prompts")
145+
def main(name: str | None, description: str | None, skip_ci: bool) -> None:
146+
"""Setup your FastAPI project from template."""
147+
console.print()
148+
console.print(Panel("[bold blue]FastAPI Template Setup[/bold blue]", expand=False))
149+
console.print()
150+
151+
project_name, project_description, author_name, author_email = prompt_project_config(name, description)
152+
db_name, db_user, db_password, db_host, db_port = prompt_db_config(project_name)
153+
ci_platform, github_repo = prompt_ci_config(skip_ci)
154+
155+
if not Confirm.ask("Apply changes?", default=True):
156+
console.print("[yellow]Aborted.[/yellow]")
157+
raise SystemExit(0)
158+
159+
console.print()
160+
161+
config = ProjectConfig(
162+
name=project_name,
163+
package_name=ProjectConfig.derive_package_name(project_name),
164+
description=project_description,
165+
author_name=author_name,
166+
author_email=author_email,
167+
db_name=db_name,
168+
db_user=db_user,
169+
db_password=db_password,
170+
db_host=db_host,
171+
db_port=db_port,
172+
ci_platform=ci_platform,
173+
github_repo=github_repo,
174+
)
175+
176+
console.print("[bold]Applying changes...[/bold]")
177+
updated_files = apply_all_updates(config)
178+
print_summary(updated_files, db_name)
179+
180+
181+
if __name__ == "__main__":
182+
main()

setup_project/config.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Project configuration dataclass."""
2+
3+
from dataclasses import dataclass
4+
from typing import Literal
5+
6+
7+
@dataclass
8+
class ProjectConfig:
9+
"""Configuration collected from user prompts."""
10+
11+
name: str # kebab-case project name
12+
package_name: str # snake_case (derived from name)
13+
description: str
14+
author_name: str
15+
author_email: str
16+
db_name: str
17+
db_user: str
18+
db_password: str
19+
db_host: str
20+
db_port: int
21+
ci_platform: Literal["github", "gitlab", "none"]
22+
github_repo: str | None = None
23+
24+
@classmethod
25+
def derive_package_name(cls, name: str) -> str:
26+
"""Convert kebab-case name to snake_case package name."""
27+
return name.replace("-", "_")
28+
29+
@classmethod
30+
def derive_db_name(cls, name: str) -> str:
31+
"""Convert kebab-case name to snake_case database name."""
32+
return name.replace("-", "_")

0 commit comments

Comments
 (0)