Skip to content

Commit 9d46f3d

Browse files
committed
feat: Add linting and testing options to the wizard, adding database configuration.
1 parent cf15325 commit 9d46f3d

File tree

10 files changed

+418
-860
lines changed

10 files changed

+418
-860
lines changed

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ members = [
118118
"iya",
119119
]
120120

121+
[dependency-groups]
122+
dev = [
123+
"coverage>=7.11.2",
124+
]
125+
121126
[tool.pdm.build]
122127
source-includes = [
123128
"tests/",

requirements.txt

-190 Bytes
Binary file not shown.

src/fastapi_new/constants/template.py

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,44 @@
1-
TEMPLATE_MAIN = """
1+
from textwrap import dedent
2+
3+
TEMPLATE_DB_CONNECTION = dedent("""
4+
from dotenv import load_dotenv
5+
from sqlalchemy import create_engine
6+
from sqlalchemy.orm import DeclarativeBase, sessionmaker
7+
import os
8+
9+
# Load environment variables from .env file
10+
load_dotenv()
11+
12+
DATABASE_URL = os.getenv("DATABASE_URL")
13+
14+
engine = create_engine(str(DATABASE_URL), echo=True)
15+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
16+
17+
class Base(DeclarativeBase):
18+
pass
19+
20+
def get_db():
21+
db = SessionLocal()
22+
try:
23+
yield db
24+
finally:
25+
db.close()
26+
""").strip()
27+
28+
TEMPLATE_ENV = """
29+
# Option 1: SQLite (Default - No setup required)
30+
# DATABASE_URL=sqlite:///./{project_name}.db
31+
32+
# Option 2: MySQL (Requires: uv add pymysql)
33+
DATABASE_URL=mysql+pymysql://root:password@localhost:3306/{project_name}
34+
35+
# Option 3: PostgreSQL (Requires: uv add psycopg2-binary)
36+
# DATABASE_URL=postgresql://postgres:password@localhost:5432/{project_name}
37+
"""
38+
39+
TEMPLATE_MAIN = dedent("""
240
from fastapi import FastAPI
3-
from fastapi.staticfiles import StaticFiles
41+
# from fastapi.staticfiles import StaticFiles
442
543
app = FastAPI()
644
@@ -10,9 +48,9 @@
1048
@app.get("/")
1149
def main():
1250
return {"message": "Welcome to your FastAPI project!"}
13-
"""
51+
""").strip()
1452

15-
TEMPLATE_HTML = """
53+
TEMPLATE_HTML = dedent("""
1654
<!DOCTYPE html>
1755
<html lang="en">
1856
<head>
@@ -26,9 +64,9 @@ def main():
2664
<script src="/static/js/main.js"></script>
2765
</body>
2866
</html>
29-
"""
67+
""").strip()
3068

31-
TEMPLATE_CSS = """
69+
TEMPLATE_CSS = dedent("""
3270
body {
3371
font-family: sans-serif;
3472
background-color: #f0fdf4; /* Green-50 */
@@ -39,8 +77,61 @@ def main():
3977
height: 100vh;
4078
margin: 0;
4179
}
42-
"""
80+
""").strip()
4381

44-
TEMPLATE_JS = """
82+
TEMPLATE_JS = dedent("""
4583
console.log("FastAPI Views are active!");
46-
"""
84+
""").strip()
85+
86+
TEMPLATE_GITIGNORE = dedent("""
87+
# Byte-compiled / optimized / DLL files
88+
__pycache__/
89+
*.py[cod]
90+
*$py.class
91+
92+
# Distribution / packaging
93+
build/
94+
dist/
95+
*.egg-info/
96+
*.egg
97+
98+
# Environment variables
99+
.env
100+
101+
# Testing / coverage
102+
htmlcov/
103+
.coverage
104+
.coverage.*
105+
coverage/
106+
.pytest_cache/
107+
tox/
108+
109+
# Type checking
110+
.mypy_cache/
111+
112+
# Virtual environments
113+
.venv
114+
venv/
115+
116+
# IDE
117+
.idea/
118+
119+
# OS
120+
.DS_Store
121+
Thumbs.db
122+
""").strip()
123+
124+
TEMPLATE_RUFF = dedent("""
125+
# .ruff.toml - Standalone configuration
126+
line-length = 88
127+
target-version = "py310"
128+
129+
[lint]
130+
select = ["E", "F", "I"]
131+
ignore = []
132+
""").strip()
133+
134+
TEMPLATE_TESTING = dedent("""
135+
def test_example():
136+
assert True
137+
""").strip()

src/fastapi_new/core/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
class ProjectConfig:
77
name: str
88
path: Path
9+
linter: str = "none"
910
orm: str = "none"
1011
python: str | None = None
1112
structure: str = "simple"

src/fastapi_new/core/generator.py

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def exit_with_error(toolkit: RichToolkit, error_msg: str) -> None:
3838
Args:
3939
toolkit (RichToolkit): The RichToolkit instance for printing messages.
4040
error_msg (str): The error message to display.
41-
41+
4242
Returns:
4343
None
4444
"""
@@ -52,7 +52,7 @@ def validate_python_version(python: str | None) -> str | None:
5252
5353
Args:
5454
python (str|None): The Python version string.
55-
55+
5656
Returns:
5757
str|None: An error message if the version is unsupported, otherwise None.
5858
"""
@@ -77,7 +77,7 @@ def setup_environment(toolkit: RichToolkit, config: ProjectConfig) -> None:
7777
Args:
7878
toolkit (RichToolkit): The RichToolkit instance for printing messages.
7979
config (ProjectConfig): The project configuration.
80-
80+
8181
Returns:
8282
None
8383
"""
@@ -110,13 +110,14 @@ def install_dependencies(toolkit: RichToolkit, config: ProjectConfig) -> None:
110110
Args:
111111
toolkit (RichToolkit): The RichToolkit instance for printing messages.
112112
config (ProjectConfig): The project configuration.
113-
113+
114114
Returns:
115115
None
116116
"""
117117
toolkit.print("Installing dependencies & generating requirements.txt...", tag="deps")
118118
try:
119-
deps = ["fastapi[standard]"]
119+
deps = ["fastapi[standard]", "python-dotenv"]
120+
120121
if config.orm == "sqlmodel":
121122
deps.append("sqlmodel")
122123
elif config.orm == "sqlalchemy":
@@ -129,18 +130,23 @@ def install_dependencies(toolkit: RichToolkit, config: ProjectConfig) -> None:
129130
deps.append("pytest")
130131
deps.append("httpx")
131132

133+
if config.linter == "ruff":
134+
deps.append("ruff")
135+
elif config.linter == "classic":
136+
deps.extend(["black", "isort", "flake8"])
137+
132138
subprocess.run(["uv", "add"] + deps, check=True, capture_output=True, cwd=config.path)
133139

134140
with open(config.path / "requirements.txt", "w") as req_file:
135141
subprocess.run(
136-
["uv", "export", "--format", "requirements-txt"],
142+
["uv", "export", "--format", "requirements-txt", "--no-hashes", "--no-header", "--no-annotate"],
137143
stdout=req_file,
138144
check=True,
139145
cwd=config.path
140146
)
141147

142148
except subprocess.CalledProcessError as e:
143-
exit_with_error(toolkit, f"Failed to install deps: {e.stderr.decode()}")
149+
exit_with_error(toolkit, f"Failed to install deps: {e.stderr.decode() if e.stderr else str(e)}")
144150

145151

146152
def create_file(path: pathlib.Path, content: str = "") -> None:
@@ -150,51 +156,101 @@ def create_file(path: pathlib.Path, content: str = "") -> None:
150156
Args:
151157
path (pathlib.Path): The file path to create.
152158
content (str): The content to write to the file.
153-
159+
154160
Returns:
155161
None
156162
"""
157163
path.parent.mkdir(parents=True, exist_ok=True)
158-
path.write_text(content)
164+
path.write_text(content, encoding="utf-8")
159165

160166

161167
def write_project_files(toolkit: RichToolkit, config: ProjectConfig) -> None:
162168
"""
163169
Write the project files based on the configuration.
170+
Refactored for readability using helper functions.
164171
165172
Args:
166173
toolkit (RichToolkit): The RichToolkit instance for printing messages.
167174
config (ProjectConfig): The project configuration.
168-
175+
169176
Returns:
170177
None
171178
"""
172179
toolkit.print("Scaffolding project structure...", tag="template")
173180
try:
174-
create_file(config.path / "main.py", TEMPLATE_MAIN)
175-
create_file(config.path / "README.md", generate_readme(config.name))
181+
_create_base_files(config)
182+
_setup_git(config)
176183

177184
if config.views:
178-
create_file(config.path / "views" / "html" /"index.html", TEMPLATE_HTML)
179-
create_file(config.path / "views" / "css" /"style.css", TEMPLATE_CSS)
180-
create_file(config.path / "views" / "js" / "main.js", TEMPLATE_JS)
181-
create_file(config.path / "views" / "assets" / ".gitkeep", "")
185+
_create_view_files(config)
186+
187+
if config.orm != "none":
188+
_configure_database(config)
182189

183190
if config.structure == "advanced":
184-
create_file(config.path / "app" / "__init__.py")
185-
create_file(config.path / "app" / "controllers" / "__init__.py")
186-
create_file(config.path / "app" / "models" / "__init__.py")
187-
create_file(config.path / "app" / "schemas" / "__init__.py")
191+
_setup_advanced_structure(config)
188192

189-
create_file(config.path / "database" / "migrations" / ".gitkeep")
190-
create_file(config.path / "database" / "seeders" / ".gitkeep")
193+
hello_file = config.path / "hello.py"
194+
if hello_file.exists():
195+
hello_file.unlink()
191196

192-
if config.tests:
193-
create_file(config.path / "tests" / "__init__.py")
194-
create_file(config.path / "tests" / "test_main.py", "def test_example(): assert True")
197+
except Exception as e:
198+
exit_with_error(toolkit, f"Failed to write files: {str(e)}")
195199

196-
if (config.path / "hello.py").exists():
197-
(config.path / "hello.py").unlink()
198200

199-
except Exception as e:
200-
exit_with_error(toolkit, f"Failed to write files: {str(e)}")
201+
def _create_base_files(config: ProjectConfig) -> None:
202+
"""Create the fundamental files for the project."""
203+
create_file(config.path / "main.py", TEMPLATE_MAIN)
204+
create_file(config.path / "README.md", generate_readme(config.name))
205+
206+
207+
def _setup_git(config: ProjectConfig) -> None:
208+
"""Create or update .gitignore."""
209+
gitignore_path = config.path / ".gitignore"
210+
211+
if gitignore_path.exists():
212+
with open(gitignore_path, "a") as f:
213+
f.write("\n" + TEMPLATE_GITIGNORE)
214+
else:
215+
create_file(gitignore_path, TEMPLATE_GITIGNORE)
216+
217+
218+
def _create_view_files(config: ProjectConfig) -> None:
219+
"""Create HTML, CSS, and JS files."""
220+
base_view_path = config.path / "views"
221+
create_file(base_view_path / "html" / "index.html", TEMPLATE_HTML)
222+
create_file(base_view_path / "css" / "style.css", TEMPLATE_CSS)
223+
create_file(base_view_path / "js" / "main.js", TEMPLATE_JS)
224+
create_file(base_view_path / "assets" / ".gitkeep", "")
225+
226+
227+
def _configure_database(config: ProjectConfig) -> None:
228+
"""Setup database connection, env file, and gitignore."""
229+
# Create database config
230+
create_file(config.path / "config" / "database.py", TEMPLATE_DB_CONNECTION)
231+
232+
# Create .env
233+
env_content = TEMPLATE_ENV.format(project_name=config.name)
234+
create_file(config.path / ".env", env_content.strip())
235+
236+
237+
def _setup_advanced_structure(config: ProjectConfig) -> None:
238+
"""Setup MVC folders, Migrations, Tests, and Linter config."""
239+
# 1. App Structure (MVC)
240+
mvc_paths = ["app", "app/controllers", "app/models", "app/schemas"]
241+
242+
for p in mvc_paths:
243+
create_file(config.path / p / "__init__.py")
244+
245+
# 2. Database Migrations
246+
create_file(config.path / "database" / "migrations" / ".gitkeep")
247+
create_file(config.path / "database" / "seeders" / ".gitkeep")
248+
249+
# 3. Testing
250+
if config.tests:
251+
create_file(config.path / "tests" / "__init__.py")
252+
create_file(config.path / "tests" / "test_main.py", TEMPLATE_TESTING)
253+
254+
# 4. Linter
255+
if config.linter == "ruff":
256+
create_file(config.path / ".ruff.toml", TEMPLATE_RUFF)

0 commit comments

Comments
 (0)