fastapi-initializer 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- .gitignore +13 -0
- .python-version +1 -0
- LICENSE +21 -0
- PKG-INFO +225 -0
- README.md +195 -0
- fastapi_initializer-1.0.0.dist-info/METADATA +225 -0
- fastapi_initializer-1.0.0.dist-info/RECORD +13 -0
- fastapi_initializer-1.0.0.dist-info/WHEEL +4 -0
- fastapi_initializer-1.0.0.dist-info/entry_points.txt +2 -0
- fastapi_initializer-1.0.0.dist-info/licenses/LICENSE +21 -0
- main.py +1017 -0
- pyproject.toml +54 -0
- uv.lock +158 -0
main.py
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from textwrap import dedent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from InquirerPy import inquirer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatabaseChoice(str, Enum):
|
|
15
|
+
NONE = "none"
|
|
16
|
+
SQLITE = "sqlite"
|
|
17
|
+
MYSQL = "mysql"
|
|
18
|
+
POSTGRESQL = "postgresql"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ORMChoice(str, Enum):
|
|
22
|
+
NONE = "none"
|
|
23
|
+
SQLALCHEMY = "sqlalchemy"
|
|
24
|
+
SQLMODEL = "sqlmodel"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LinterChoice(str, Enum):
|
|
28
|
+
NONE = "none"
|
|
29
|
+
BLACK = "black"
|
|
30
|
+
RUFF = "ruff"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestChoice(str, Enum):
|
|
34
|
+
NONE = "none"
|
|
35
|
+
PYTEST = "pytest"
|
|
36
|
+
PYTEST_ASYNCIO = "pytest-asyncio"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ProjectConfig:
|
|
41
|
+
name: str
|
|
42
|
+
database: DatabaseChoice
|
|
43
|
+
orm: ORMChoice
|
|
44
|
+
linter: LinterChoice
|
|
45
|
+
test_framework: TestChoice
|
|
46
|
+
docker: bool
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def write_file(path: Path, content: str) -> None:
|
|
50
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
path.write_text(content, encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def render_app_main(config: ProjectConfig) -> str:
|
|
55
|
+
return dedent(
|
|
56
|
+
f"""
|
|
57
|
+
from fastapi import FastAPI
|
|
58
|
+
|
|
59
|
+
from app.api import api_router
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
app = FastAPI(title="{config.name}")
|
|
63
|
+
app.include_router(api_router)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.get("/health")
|
|
67
|
+
async def health_check():
|
|
68
|
+
return {{"status": "ok"}}
|
|
69
|
+
"""
|
|
70
|
+
).lstrip()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def render_app_api_init() -> str:
|
|
74
|
+
return dedent(
|
|
75
|
+
"""
|
|
76
|
+
from fastapi import APIRouter
|
|
77
|
+
|
|
78
|
+
from .v1 import users
|
|
79
|
+
|
|
80
|
+
api_router = APIRouter()
|
|
81
|
+
api_router.include_router(users.router)
|
|
82
|
+
"""
|
|
83
|
+
).lstrip()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def render_app_api_deps() -> str:
|
|
87
|
+
return dedent(
|
|
88
|
+
"""
|
|
89
|
+
from typing import Annotated
|
|
90
|
+
|
|
91
|
+
from fastapi import Depends
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_current_user():
|
|
95
|
+
return {"id": 1, "email": "user@example.com"}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
CurrentUser = Annotated[dict, Depends(get_current_user)]
|
|
99
|
+
"""
|
|
100
|
+
).lstrip()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def render_app_api_v1_users() -> str:
|
|
104
|
+
return dedent(
|
|
105
|
+
"""
|
|
106
|
+
from fastapi import APIRouter
|
|
107
|
+
|
|
108
|
+
from app.schemas.user import User
|
|
109
|
+
from app.services.user_service import get_dummy_users
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
router = APIRouter(prefix="/users", tags=["users"])
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@router.get("/", response_model=list[User])
|
|
116
|
+
async def list_users():
|
|
117
|
+
return get_dummy_users()
|
|
118
|
+
"""
|
|
119
|
+
).lstrip()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def render_core_config() -> str:
|
|
123
|
+
return dedent(
|
|
124
|
+
"""
|
|
125
|
+
from pydantic_settings import BaseSettings
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class Settings(BaseSettings):
|
|
129
|
+
app_name: str = "FastAPI Initializer App"
|
|
130
|
+
debug: bool = True
|
|
131
|
+
|
|
132
|
+
class Config:
|
|
133
|
+
env_file = ".env"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
settings = Settings()
|
|
137
|
+
"""
|
|
138
|
+
).lstrip()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def render_core_security() -> str:
|
|
142
|
+
return dedent(
|
|
143
|
+
"""
|
|
144
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
148
|
+
"""
|
|
149
|
+
).lstrip()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def render_models_user(orm: ORMChoice) -> str:
|
|
153
|
+
if orm == ORMChoice.SQLMODEL:
|
|
154
|
+
return dedent(
|
|
155
|
+
"""
|
|
156
|
+
from sqlmodel import Field, SQLModel
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class User(SQLModel, table=True):
|
|
160
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
161
|
+
email: str
|
|
162
|
+
is_active: bool = True
|
|
163
|
+
"""
|
|
164
|
+
).lstrip()
|
|
165
|
+
else:
|
|
166
|
+
return dedent(
|
|
167
|
+
"""
|
|
168
|
+
from sqlalchemy import Boolean, Column, Integer, String
|
|
169
|
+
|
|
170
|
+
from app.db.base import Base
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class User(Base):
|
|
174
|
+
__tablename__ = "users"
|
|
175
|
+
|
|
176
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
177
|
+
email = Column(String, unique=True, index=True, nullable=False)
|
|
178
|
+
is_active = Column(Boolean, default=True)
|
|
179
|
+
"""
|
|
180
|
+
).lstrip()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def render_schemas_user() -> str:
|
|
184
|
+
return dedent(
|
|
185
|
+
"""
|
|
186
|
+
from pydantic import BaseModel
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class User(BaseModel):
|
|
190
|
+
id: int
|
|
191
|
+
email: str
|
|
192
|
+
is_active: bool = True
|
|
193
|
+
"""
|
|
194
|
+
).lstrip()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def render_services_user_service() -> str:
|
|
198
|
+
return dedent(
|
|
199
|
+
"""
|
|
200
|
+
from app.schemas.user import User
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_dummy_users() -> list[User]:
|
|
204
|
+
return [
|
|
205
|
+
User(id=1, email="user1@example.com", is_active=True),
|
|
206
|
+
User(id=2, email="user2@example.com", is_active=False),
|
|
207
|
+
]
|
|
208
|
+
"""
|
|
209
|
+
).lstrip()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def render_db_session(database: DatabaseChoice, orm: ORMChoice) -> str:
|
|
213
|
+
if database == DatabaseChoice.SQLITE:
|
|
214
|
+
url = "sqlite:///./app.db"
|
|
215
|
+
elif database == DatabaseChoice.MYSQL:
|
|
216
|
+
url = "mysql+pymysql://user:password@localhost:3306/app"
|
|
217
|
+
elif database == DatabaseChoice.POSTGRESQL:
|
|
218
|
+
url = "postgresql+psycopg2://user:password@localhost:5432/app"
|
|
219
|
+
else:
|
|
220
|
+
url = "sqlite:///./app.db"
|
|
221
|
+
|
|
222
|
+
if orm == ORMChoice.SQLMODEL:
|
|
223
|
+
return dedent(
|
|
224
|
+
f"""
|
|
225
|
+
import os
|
|
226
|
+
from collections.abc import Generator
|
|
227
|
+
|
|
228
|
+
from sqlmodel import Session, create_engine
|
|
229
|
+
|
|
230
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "{url}")
|
|
231
|
+
|
|
232
|
+
engine = create_engine(DATABASE_URL, echo=False)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_session() -> Generator[Session, None, None]:
|
|
236
|
+
with Session(engine) as session:
|
|
237
|
+
yield session
|
|
238
|
+
"""
|
|
239
|
+
).lstrip()
|
|
240
|
+
else:
|
|
241
|
+
return dedent(
|
|
242
|
+
f"""
|
|
243
|
+
import os
|
|
244
|
+
from collections.abc import Generator
|
|
245
|
+
|
|
246
|
+
from sqlalchemy import create_engine
|
|
247
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
248
|
+
|
|
249
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "{url}")
|
|
250
|
+
|
|
251
|
+
engine = create_engine(DATABASE_URL, echo=False)
|
|
252
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_session() -> Generator[Session, None, None]:
|
|
256
|
+
db = SessionLocal()
|
|
257
|
+
try:
|
|
258
|
+
yield db
|
|
259
|
+
finally:
|
|
260
|
+
db.close()
|
|
261
|
+
"""
|
|
262
|
+
).lstrip()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def render_db_base(orm: ORMChoice) -> str:
|
|
266
|
+
if orm == ORMChoice.SQLMODEL:
|
|
267
|
+
return dedent(
|
|
268
|
+
"""
|
|
269
|
+
from sqlmodel import SQLModel
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
Base = SQLModel
|
|
273
|
+
"""
|
|
274
|
+
).lstrip()
|
|
275
|
+
else:
|
|
276
|
+
return dedent(
|
|
277
|
+
"""
|
|
278
|
+
from sqlalchemy.orm import declarative_base
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
Base = declarative_base()
|
|
282
|
+
"""
|
|
283
|
+
).lstrip()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def render_tests(test_framework: TestChoice) -> str:
|
|
287
|
+
if test_framework in {TestChoice.PYTEST, TestChoice.PYTEST_ASYNCIO}:
|
|
288
|
+
return dedent(
|
|
289
|
+
"""
|
|
290
|
+
import pytest
|
|
291
|
+
from httpx import AsyncClient
|
|
292
|
+
|
|
293
|
+
from app.main import app
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
async def test_health():
|
|
298
|
+
async with AsyncClient(app=app, base_url="http://test") as client:
|
|
299
|
+
response = await client.get("/health")
|
|
300
|
+
assert response.status_code == 200
|
|
301
|
+
assert response.json()["status"] == "ok"
|
|
302
|
+
"""
|
|
303
|
+
).lstrip()
|
|
304
|
+
return ""
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def render_env(config: ProjectConfig) -> str:
|
|
308
|
+
return dedent(
|
|
309
|
+
f"""
|
|
310
|
+
APP_NAME="{config.name}"
|
|
311
|
+
DEBUG=true
|
|
312
|
+
"""
|
|
313
|
+
).lstrip()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def render_dockerfile() -> str:
|
|
317
|
+
return dedent(
|
|
318
|
+
"""
|
|
319
|
+
FROM python:3.11-slim
|
|
320
|
+
|
|
321
|
+
WORKDIR /app
|
|
322
|
+
|
|
323
|
+
COPY pyproject.toml uv.lock ./
|
|
324
|
+
RUN pip install uv && uv sync
|
|
325
|
+
|
|
326
|
+
COPY . .
|
|
327
|
+
|
|
328
|
+
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
329
|
+
"""
|
|
330
|
+
).lstrip()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def render_docker_compose() -> str:
|
|
334
|
+
return dedent(
|
|
335
|
+
"""
|
|
336
|
+
version: "3.9"
|
|
337
|
+
|
|
338
|
+
services:
|
|
339
|
+
api:
|
|
340
|
+
build: .
|
|
341
|
+
ports:
|
|
342
|
+
- "8000:8000"
|
|
343
|
+
env_file:
|
|
344
|
+
- .env
|
|
345
|
+
"""
|
|
346
|
+
).lstrip()
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def render_dockerignore() -> str:
|
|
350
|
+
return dedent(
|
|
351
|
+
"""
|
|
352
|
+
__pycache__
|
|
353
|
+
.venv
|
|
354
|
+
.env
|
|
355
|
+
uv.lock
|
|
356
|
+
"""
|
|
357
|
+
).lstrip()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def render_gitignore() -> str:
|
|
361
|
+
return dedent(
|
|
362
|
+
"""
|
|
363
|
+
# Python
|
|
364
|
+
__pycache__/
|
|
365
|
+
*.py[oc]
|
|
366
|
+
*.egg-info/
|
|
367
|
+
dist/
|
|
368
|
+
build/
|
|
369
|
+
|
|
370
|
+
# Virtual environments
|
|
371
|
+
.venv/
|
|
372
|
+
|
|
373
|
+
# Environment variables
|
|
374
|
+
.env
|
|
375
|
+
|
|
376
|
+
# IDE
|
|
377
|
+
.vscode/
|
|
378
|
+
.idea/
|
|
379
|
+
"""
|
|
380
|
+
).lstrip()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def render_pyproject(config: ProjectConfig) -> str:
|
|
384
|
+
deps: list[str] = ["fastapi", "uvicorn[standard]", "pydantic", "pydantic-settings", "python-dotenv"]
|
|
385
|
+
|
|
386
|
+
if config.orm in {ORMChoice.SQLALCHEMY, ORMChoice.SQLMODEL}:
|
|
387
|
+
deps.append("sqlalchemy")
|
|
388
|
+
if config.orm == ORMChoice.SQLMODEL:
|
|
389
|
+
deps.append("sqlmodel")
|
|
390
|
+
|
|
391
|
+
if config.test_framework == TestChoice.PYTEST:
|
|
392
|
+
deps.extend(["pytest", "httpx"])
|
|
393
|
+
elif config.test_framework == TestChoice.PYTEST_ASYNCIO:
|
|
394
|
+
deps.extend(["pytest", "pytest-asyncio", "httpx"])
|
|
395
|
+
|
|
396
|
+
if config.linter == LinterChoice.BLACK:
|
|
397
|
+
deps.append("black")
|
|
398
|
+
elif config.linter == LinterChoice.RUFF:
|
|
399
|
+
deps.append("ruff")
|
|
400
|
+
|
|
401
|
+
# Database drivers based on URL scheme
|
|
402
|
+
if config.database == DatabaseChoice.MYSQL:
|
|
403
|
+
deps.append("pymysql")
|
|
404
|
+
elif config.database == DatabaseChoice.POSTGRESQL:
|
|
405
|
+
deps.append("psycopg2-binary")
|
|
406
|
+
|
|
407
|
+
deps_lines = "\n".join(f' "{dep}",' for dep in deps)
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
"[project]\n"
|
|
411
|
+
f'name = "{config.name}"\n'
|
|
412
|
+
'version = "0.1.0"\n'
|
|
413
|
+
'description = "FastAPI project generated by FastAPI Initializer"\n'
|
|
414
|
+
'readme = "README.md"\n'
|
|
415
|
+
'requires-python = ">=3.10"\n'
|
|
416
|
+
"dependencies = [\n"
|
|
417
|
+
f"{deps_lines}\n"
|
|
418
|
+
"]\n"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def render_readme(config: ProjectConfig) -> str:
|
|
423
|
+
# --- build the folder-tree block dynamically ---------------------------
|
|
424
|
+
tree_lines = [
|
|
425
|
+
f"{config.name}/",
|
|
426
|
+
"├── app/",
|
|
427
|
+
"│ ├── api/",
|
|
428
|
+
"│ │ ├── v1/",
|
|
429
|
+
"│ │ │ └── users.py # User endpoints",
|
|
430
|
+
"│ │ ├── __init__.py # Mounts versioned routers",
|
|
431
|
+
"│ │ └── deps.py # Shared dependencies",
|
|
432
|
+
"│ ├── core/",
|
|
433
|
+
"│ │ ├── config.py # App settings (pydantic-settings)",
|
|
434
|
+
"│ │ └── security.py # OAuth2 / auth utilities",
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
if config.orm != ORMChoice.NONE:
|
|
438
|
+
tree_lines += [
|
|
439
|
+
"│ ├── models/",
|
|
440
|
+
"│ │ └── user.py # ORM model",
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
if config.database != DatabaseChoice.NONE or config.orm != ORMChoice.NONE:
|
|
444
|
+
db_files = ["│ ├── db/", "│ │ ├── base.py # ORM Base class"]
|
|
445
|
+
if config.database != DatabaseChoice.NONE:
|
|
446
|
+
db_files.append("│ │ └── session.py # Engine & get_session()")
|
|
447
|
+
tree_lines += db_files
|
|
448
|
+
|
|
449
|
+
tree_lines += [
|
|
450
|
+
"│ ├── schemas/",
|
|
451
|
+
"│ │ └── user.py # Pydantic request / response models",
|
|
452
|
+
"│ ├── services/",
|
|
453
|
+
"│ │ └── user_service.py # Business logic",
|
|
454
|
+
"│ └── main.py # FastAPI app entry-point",
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
if config.test_framework != TestChoice.NONE:
|
|
458
|
+
tree_lines += [
|
|
459
|
+
"├── tests/",
|
|
460
|
+
"│ └── test_users.py # Smoke tests",
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
if config.docker:
|
|
464
|
+
tree_lines += [
|
|
465
|
+
"├── Dockerfile",
|
|
466
|
+
"├── docker-compose.yml",
|
|
467
|
+
"├── .dockerignore",
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
tree_lines += [
|
|
471
|
+
"├── .env # Environment variables",
|
|
472
|
+
"├── .gitignore",
|
|
473
|
+
"├── pyproject.toml",
|
|
474
|
+
"└── README.md",
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
tree_block = "\n".join(tree_lines)
|
|
478
|
+
|
|
479
|
+
# --- optional sections -------------------------------------------------
|
|
480
|
+
db_section = ""
|
|
481
|
+
if config.database != DatabaseChoice.NONE:
|
|
482
|
+
db_label = config.database.value.title()
|
|
483
|
+
db_section = dedent(f"""
|
|
484
|
+
## Database
|
|
485
|
+
|
|
486
|
+
This project is pre-configured for **{db_label}**.
|
|
487
|
+
|
|
488
|
+
- Connection URL is set via the `DATABASE_URL` environment variable (see `.env`).
|
|
489
|
+
- A `get_session()` dependency is provided in `app/db/session.py` — inject it into any route with `Depends(get_session)`.
|
|
490
|
+
""")
|
|
491
|
+
|
|
492
|
+
orm_section = ""
|
|
493
|
+
if config.orm != ORMChoice.NONE:
|
|
494
|
+
orm_label = "SQLModel" if config.orm == ORMChoice.SQLMODEL else "SQLAlchemy"
|
|
495
|
+
orm_section = dedent(f"""
|
|
496
|
+
## ORM
|
|
497
|
+
|
|
498
|
+
Models use **{orm_label}** and inherit from the shared `Base` in `app/db/base.py`.
|
|
499
|
+
|
|
500
|
+
To add a new model:
|
|
501
|
+
1. Create a file in `app/models/` (e.g. `item.py`).
|
|
502
|
+
2. Import `Base` from `app.db.base`.
|
|
503
|
+
3. Import the model in `app/models/__init__.py` so migrations can discover it.
|
|
504
|
+
""")
|
|
505
|
+
|
|
506
|
+
docker_section = ""
|
|
507
|
+
if config.docker:
|
|
508
|
+
docker_section = dedent("""
|
|
509
|
+
## Docker
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
# Build and start
|
|
513
|
+
docker compose up --build
|
|
514
|
+
|
|
515
|
+
# Or run directly
|
|
516
|
+
docker build -t app .
|
|
517
|
+
docker run -p 8000:8000 --env-file .env app
|
|
518
|
+
```
|
|
519
|
+
""")
|
|
520
|
+
|
|
521
|
+
test_section = ""
|
|
522
|
+
if config.test_framework != TestChoice.NONE:
|
|
523
|
+
test_section = dedent("""
|
|
524
|
+
## Testing
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
uv run pytest
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Tests live in the `tests/` directory. Every async test needs the `@pytest.mark.asyncio` decorator.
|
|
531
|
+
""")
|
|
532
|
+
|
|
533
|
+
linter_section = ""
|
|
534
|
+
if config.linter == LinterChoice.RUFF:
|
|
535
|
+
linter_section = dedent("""
|
|
536
|
+
## Linting & Formatting
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
uv run ruff check . # Lint
|
|
540
|
+
uv run ruff format . # Format
|
|
541
|
+
```
|
|
542
|
+
""")
|
|
543
|
+
elif config.linter == LinterChoice.BLACK:
|
|
544
|
+
linter_section = dedent("""
|
|
545
|
+
## Formatting
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
uv run black .
|
|
549
|
+
```
|
|
550
|
+
""")
|
|
551
|
+
|
|
552
|
+
# --- env vars table ----------------------------------------------------
|
|
553
|
+
env_rows = [
|
|
554
|
+
"| Variable | Default | Description |",
|
|
555
|
+
"|----------|---------|-------------|",
|
|
556
|
+
f'| `APP_NAME` | `"{config.name}"` | Display name used in OpenAPI docs. |',
|
|
557
|
+
"| `DEBUG` | `true` | Enable debug mode. |",
|
|
558
|
+
]
|
|
559
|
+
if config.database != DatabaseChoice.NONE:
|
|
560
|
+
env_rows.append("| `DATABASE_URL` | *(see .env)* | Database connection string. |")
|
|
561
|
+
env_table = "\n".join(env_rows)
|
|
562
|
+
|
|
563
|
+
# --- tech stack --------------------------------------------------------
|
|
564
|
+
stack_items = ["- **FastAPI** — async web framework", "- **Pydantic** — data validation"]
|
|
565
|
+
if config.orm == ORMChoice.SQLMODEL:
|
|
566
|
+
stack_items.append("- **SQLModel** — ORM (SQLAlchemy + Pydantic)")
|
|
567
|
+
elif config.orm == ORMChoice.SQLALCHEMY:
|
|
568
|
+
stack_items.append("- **SQLAlchemy** — ORM")
|
|
569
|
+
if config.database != DatabaseChoice.NONE:
|
|
570
|
+
stack_items.append(f"- **{config.database.value.title()}** — database")
|
|
571
|
+
if config.test_framework != TestChoice.NONE:
|
|
572
|
+
stack_items.append("- **pytest** + **httpx** — testing")
|
|
573
|
+
if config.linter == LinterChoice.RUFF:
|
|
574
|
+
stack_items.append("- **Ruff** — linter & formatter")
|
|
575
|
+
elif config.linter == LinterChoice.BLACK:
|
|
576
|
+
stack_items.append("- **Black** — code formatter")
|
|
577
|
+
if config.docker:
|
|
578
|
+
stack_items.append("- **Docker** — containerisation")
|
|
579
|
+
tech_stack = "\n".join(stack_items)
|
|
580
|
+
|
|
581
|
+
# --- assemble ----------------------------------------------------------
|
|
582
|
+
return dedent(
|
|
583
|
+
f"""\
|
|
584
|
+
# {config.name}
|
|
585
|
+
|
|
586
|
+
Generated with **FastAPI Initializer**.
|
|
587
|
+
|
|
588
|
+
## Getting Started
|
|
589
|
+
|
|
590
|
+
```bash
|
|
591
|
+
# Install dependencies
|
|
592
|
+
uv sync
|
|
593
|
+
|
|
594
|
+
# Run the development server
|
|
595
|
+
uv run uvicorn app.main:app --reload
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Then open **http://127.0.0.1:8000/docs** to explore the interactive API documentation.
|
|
599
|
+
|
|
600
|
+
## Project Structure
|
|
601
|
+
|
|
602
|
+
```
|
|
603
|
+
{tree_block}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
| Folder | Purpose |
|
|
607
|
+
|--------|---------|
|
|
608
|
+
| `app/api/` | HTTP route definitions, organised by API version. |
|
|
609
|
+
| `app/core/` | App-wide configuration (`Settings`) and security utilities. |
|
|
610
|
+
| `app/schemas/` | Pydantic models for request / response validation. |
|
|
611
|
+
| `app/services/` | Business-logic layer — keeps route handlers thin. |
|
|
612
|
+
{"| `app/models/` | ORM model classes mapped to database tables. |" if config.orm != ORMChoice.NONE else ""}\
|
|
613
|
+
{"| `app/db/` | Database engine, session management, and ORM base class. |" if (config.database != DatabaseChoice.NONE or config.orm != ORMChoice.NONE) else ""}\
|
|
614
|
+
{"| `tests/` | Automated test suite. |" if config.test_framework != TestChoice.NONE else ""}
|
|
615
|
+
{db_section}\
|
|
616
|
+
{orm_section}\
|
|
617
|
+
{docker_section}\
|
|
618
|
+
{test_section}\
|
|
619
|
+
{linter_section}\
|
|
620
|
+
## Environment Variables
|
|
621
|
+
|
|
622
|
+
{env_table}
|
|
623
|
+
|
|
624
|
+
## Tech Stack
|
|
625
|
+
|
|
626
|
+
{tech_stack}
|
|
627
|
+
"""
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def render_readme_app() -> str:
|
|
632
|
+
return dedent(
|
|
633
|
+
"""
|
|
634
|
+
# `app/` — Application Package
|
|
635
|
+
|
|
636
|
+
This is the root Python package for the FastAPI application.
|
|
637
|
+
|
|
638
|
+
## Files
|
|
639
|
+
|
|
640
|
+
| File | Description |
|
|
641
|
+
|------|-------------|
|
|
642
|
+
| `__init__.py` | Makes `app` a Python package. |
|
|
643
|
+
| `main.py` | Application entry-point — creates the `FastAPI` instance, mounts routers, and defines the `/health` endpoint. |
|
|
644
|
+
|
|
645
|
+
## Sub-packages
|
|
646
|
+
|
|
647
|
+
| Folder | Description |
|
|
648
|
+
|--------|-------------|
|
|
649
|
+
| `api/` | HTTP route definitions organised by version. |
|
|
650
|
+
| `core/` | App-wide configuration and security utilities. |
|
|
651
|
+
| `models/` | ORM / database model classes *(present only when an ORM is selected)*. |
|
|
652
|
+
| `schemas/` | Pydantic models used for request / response validation. |
|
|
653
|
+
| `services/` | Business-logic layer — keeps routes thin. |
|
|
654
|
+
| `db/` | Database engine, session, and base model setup *(present only when a database or ORM is selected)*. |
|
|
655
|
+
"""
|
|
656
|
+
).lstrip()
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def render_readme_api() -> str:
|
|
660
|
+
return dedent(
|
|
661
|
+
"""
|
|
662
|
+
# `app/api/` — API Routes
|
|
663
|
+
|
|
664
|
+
All HTTP endpoint definitions live here, organised by API version.
|
|
665
|
+
|
|
666
|
+
## Files
|
|
667
|
+
|
|
668
|
+
| File | Description |
|
|
669
|
+
|------|-------------|
|
|
670
|
+
| `__init__.py` | Creates the top-level `api_router` and includes versioned sub-routers. |
|
|
671
|
+
| `deps.py` | Shared **dependencies** — e.g. `get_current_user` — injected into route handlers via `Depends()`. |
|
|
672
|
+
|
|
673
|
+
## Sub-packages
|
|
674
|
+
|
|
675
|
+
| Folder | Description |
|
|
676
|
+
|--------|-------------|
|
|
677
|
+
| `v1/` | Version 1 endpoints. Add `v2/`, `v3/`, etc. as needed. |
|
|
678
|
+
"""
|
|
679
|
+
).lstrip()
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def render_readme_api_v1() -> str:
|
|
683
|
+
return dedent(
|
|
684
|
+
"""
|
|
685
|
+
# `app/api/v1/` — Version 1 Endpoints
|
|
686
|
+
|
|
687
|
+
Each file in this folder is a **router module** for a single resource.
|
|
688
|
+
|
|
689
|
+
## Files
|
|
690
|
+
|
|
691
|
+
| File | Description |
|
|
692
|
+
|------|-------------|
|
|
693
|
+
| `__init__.py` | Package marker. |
|
|
694
|
+
| `users.py` | `GET /users/` — returns a list of users. Add more CRUD routes here. |
|
|
695
|
+
|
|
696
|
+
## Adding a new resource
|
|
697
|
+
|
|
698
|
+
1. Create a new file, e.g. `items.py`, with its own `router = APIRouter(...)`.
|
|
699
|
+
2. Import and include it in `app/api/__init__.py`:
|
|
700
|
+
```python
|
|
701
|
+
from .v1 import items
|
|
702
|
+
api_router.include_router(items.router)
|
|
703
|
+
```
|
|
704
|
+
"""
|
|
705
|
+
).lstrip()
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def render_readme_core() -> str:
|
|
709
|
+
return dedent(
|
|
710
|
+
"""
|
|
711
|
+
# `app/core/` — Configuration & Security
|
|
712
|
+
|
|
713
|
+
App-wide settings and security utilities that are used across the entire application.
|
|
714
|
+
|
|
715
|
+
## Files
|
|
716
|
+
|
|
717
|
+
| File | Description |
|
|
718
|
+
|------|-------------|
|
|
719
|
+
| `__init__.py` | Package marker. |
|
|
720
|
+
| `config.py` | `Settings` class (powered by **pydantic-settings**). Reads values from environment variables and `.env`. Import as `from app.core.config import settings`. |
|
|
721
|
+
| `security.py` | OAuth2 scheme definition and any future auth helpers (JWT encoding, password hashing, etc.). |
|
|
722
|
+
"""
|
|
723
|
+
).lstrip()
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def render_readme_models() -> str:
|
|
727
|
+
return dedent(
|
|
728
|
+
"""
|
|
729
|
+
# `app/models/` — Database Models
|
|
730
|
+
|
|
731
|
+
ORM model classes that map directly to database tables.
|
|
732
|
+
|
|
733
|
+
## Files
|
|
734
|
+
|
|
735
|
+
| File | Description |
|
|
736
|
+
|------|-------------|
|
|
737
|
+
| `__init__.py` | Package marker. Import all models here so Alembic can discover them. |
|
|
738
|
+
| `user.py` | `User` model with `id`, `email`, and `is_active` columns. |
|
|
739
|
+
|
|
740
|
+
## Notes
|
|
741
|
+
|
|
742
|
+
- Models import `Base` from `app.db.base` — make sure every new model does the same.
|
|
743
|
+
- To add a new model, create a file (e.g. `item.py`), define the class, then import it in `__init__.py`.
|
|
744
|
+
"""
|
|
745
|
+
).lstrip()
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def render_readme_schemas() -> str:
|
|
749
|
+
return dedent(
|
|
750
|
+
"""
|
|
751
|
+
# `app/schemas/` — Pydantic Schemas
|
|
752
|
+
|
|
753
|
+
Data-validation and serialisation models used in request bodies and responses.
|
|
754
|
+
|
|
755
|
+
## Files
|
|
756
|
+
|
|
757
|
+
| File | Description |
|
|
758
|
+
|------|-------------|
|
|
759
|
+
| `__init__.py` | Package marker. |
|
|
760
|
+
| `user.py` | `User` schema with `id`, `email`, and `is_active` fields. |
|
|
761
|
+
|
|
762
|
+
## Conventions
|
|
763
|
+
|
|
764
|
+
- **One file per resource** (e.g. `user.py`, `item.py`).
|
|
765
|
+
- Use `Create`, `Update`, and `Read` suffixes when you need separate shapes for different operations, e.g. `UserCreate`, `UserRead`.
|
|
766
|
+
"""
|
|
767
|
+
).lstrip()
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def render_readme_services() -> str:
|
|
771
|
+
return dedent(
|
|
772
|
+
"""
|
|
773
|
+
# `app/services/` — Business Logic
|
|
774
|
+
|
|
775
|
+
The service layer keeps route handlers thin by encapsulating business rules and data access.
|
|
776
|
+
|
|
777
|
+
## Files
|
|
778
|
+
|
|
779
|
+
| File | Description |
|
|
780
|
+
|------|-------------|
|
|
781
|
+
| `__init__.py` | Package marker. |
|
|
782
|
+
| `user_service.py` | `get_dummy_users()` — returns sample user data. Replace with real DB queries. |
|
|
783
|
+
|
|
784
|
+
## Guidelines
|
|
785
|
+
|
|
786
|
+
- Each service should focus on a **single resource** or **domain concept**.
|
|
787
|
+
- Services receive a database session (from `app.db.session.get_session`) and return schema objects.
|
|
788
|
+
"""
|
|
789
|
+
).lstrip()
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def render_readme_db() -> str:
|
|
793
|
+
return dedent(
|
|
794
|
+
"""
|
|
795
|
+
# `app/db/` — Database Configuration
|
|
796
|
+
|
|
797
|
+
Everything related to the database connection, session management, and ORM base class.
|
|
798
|
+
|
|
799
|
+
## Files
|
|
800
|
+
|
|
801
|
+
| File | Description |
|
|
802
|
+
|------|-------------|
|
|
803
|
+
| `__init__.py` | Package marker. |
|
|
804
|
+
| `base.py` | Declares the ORM `Base` class that all models inherit from. |
|
|
805
|
+
| `session.py` | Creates the database `engine`, and exposes `get_session()` — a FastAPI dependency that yields a database session. |
|
|
806
|
+
|
|
807
|
+
## Usage
|
|
808
|
+
|
|
809
|
+
Inject a session into any route:
|
|
810
|
+
```python
|
|
811
|
+
from fastapi import Depends
|
|
812
|
+
from app.db.session import get_session
|
|
813
|
+
|
|
814
|
+
@router.get("/")
|
|
815
|
+
def list_items(session = Depends(get_session)):
|
|
816
|
+
...
|
|
817
|
+
```
|
|
818
|
+
"""
|
|
819
|
+
).lstrip()
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def render_readme_tests() -> str:
|
|
823
|
+
return dedent(
|
|
824
|
+
"""
|
|
825
|
+
# `tests/` — Test Suite
|
|
826
|
+
|
|
827
|
+
Automated tests for the application, using **pytest** and **httpx**.
|
|
828
|
+
|
|
829
|
+
## Files
|
|
830
|
+
|
|
831
|
+
| File | Description |
|
|
832
|
+
|------|-------------|
|
|
833
|
+
| `__init__.py` | Package marker. |
|
|
834
|
+
| `test_users.py` | Smoke test — calls `GET /health` and asserts a 200 response. |
|
|
835
|
+
|
|
836
|
+
## Running tests
|
|
837
|
+
|
|
838
|
+
```bash
|
|
839
|
+
uv run pytest
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
## Tips
|
|
843
|
+
|
|
844
|
+
- Name test files `test_<module>.py` so pytest discovers them automatically.
|
|
845
|
+
- Use `@pytest.mark.asyncio` on every `async def test_...` function.
|
|
846
|
+
- Add a `conftest.py` for shared fixtures (app client, test DB, etc.).
|
|
847
|
+
"""
|
|
848
|
+
).lstrip()
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def scaffold_project(config: ProjectConfig, target_dir: Path) -> None:
|
|
852
|
+
if target_dir.exists() and any(target_dir.iterdir()):
|
|
853
|
+
raise SystemExit(f"Target directory '{target_dir}' already exists and is not empty.")
|
|
854
|
+
|
|
855
|
+
# Core app structure
|
|
856
|
+
write_file(target_dir / "app" / "__init__.py", "")
|
|
857
|
+
write_file(target_dir / "app" / "main.py", render_app_main(config))
|
|
858
|
+
write_file(target_dir / "app" / "README.md", render_readme_app())
|
|
859
|
+
|
|
860
|
+
# API
|
|
861
|
+
write_file(target_dir / "app" / "api" / "__init__.py", render_app_api_init())
|
|
862
|
+
write_file(target_dir / "app" / "api" / "deps.py", render_app_api_deps())
|
|
863
|
+
write_file(target_dir / "app" / "api" / "README.md", render_readme_api())
|
|
864
|
+
write_file(target_dir / "app" / "api" / "v1" / "__init__.py", "")
|
|
865
|
+
write_file(target_dir / "app" / "api" / "v1" / "users.py", render_app_api_v1_users())
|
|
866
|
+
write_file(target_dir / "app" / "api" / "v1" / "README.md", render_readme_api_v1())
|
|
867
|
+
|
|
868
|
+
# Core config & security
|
|
869
|
+
write_file(target_dir / "app" / "core" / "__init__.py", "")
|
|
870
|
+
write_file(target_dir / "app" / "core" / "config.py", render_core_config())
|
|
871
|
+
write_file(target_dir / "app" / "core" / "security.py", render_core_security())
|
|
872
|
+
write_file(target_dir / "app" / "core" / "README.md", render_readme_core())
|
|
873
|
+
|
|
874
|
+
# Models
|
|
875
|
+
if config.orm != ORMChoice.NONE:
|
|
876
|
+
write_file(target_dir / "app" / "models" / "__init__.py", "")
|
|
877
|
+
write_file(target_dir / "app" / "models" / "user.py", render_models_user(config.orm))
|
|
878
|
+
write_file(target_dir / "app" / "models" / "README.md", render_readme_models())
|
|
879
|
+
|
|
880
|
+
# Schemas / Services
|
|
881
|
+
write_file(target_dir / "app" / "schemas" / "__init__.py", "")
|
|
882
|
+
write_file(target_dir / "app" / "schemas" / "user.py", render_schemas_user())
|
|
883
|
+
write_file(target_dir / "app" / "schemas" / "README.md", render_readme_schemas())
|
|
884
|
+
write_file(target_dir / "app" / "services" / "__init__.py", "")
|
|
885
|
+
write_file(target_dir / "app" / "services" / "user_service.py", render_services_user_service())
|
|
886
|
+
write_file(target_dir / "app" / "services" / "README.md", render_readme_services())
|
|
887
|
+
|
|
888
|
+
# DB
|
|
889
|
+
if config.database != DatabaseChoice.NONE or config.orm != ORMChoice.NONE:
|
|
890
|
+
write_file(target_dir / "app" / "db" / "__init__.py", "")
|
|
891
|
+
write_file(target_dir / "app" / "db" / "base.py", render_db_base(config.orm))
|
|
892
|
+
write_file(target_dir / "app" / "db" / "README.md", render_readme_db())
|
|
893
|
+
if config.database != DatabaseChoice.NONE:
|
|
894
|
+
write_file(target_dir / "app" / "db" / "session.py", render_db_session(config.database, config.orm))
|
|
895
|
+
|
|
896
|
+
# Tests
|
|
897
|
+
if config.test_framework != TestChoice.NONE:
|
|
898
|
+
write_file(target_dir / "tests" / "__init__.py", "")
|
|
899
|
+
write_file(target_dir / "tests" / "test_users.py", render_tests(config.test_framework))
|
|
900
|
+
write_file(target_dir / "tests" / "README.md", render_readme_tests())
|
|
901
|
+
|
|
902
|
+
# Root-level files
|
|
903
|
+
write_file(target_dir / ".env", render_env(config))
|
|
904
|
+
write_file(target_dir / ".gitignore", render_gitignore())
|
|
905
|
+
if config.docker:
|
|
906
|
+
write_file(target_dir / ".dockerignore", render_dockerignore())
|
|
907
|
+
write_file(target_dir / "Dockerfile", render_dockerfile())
|
|
908
|
+
write_file(target_dir / "docker-compose.yml", render_docker_compose())
|
|
909
|
+
write_file(target_dir / "pyproject.toml", render_pyproject(config))
|
|
910
|
+
write_file(target_dir / "README.md", render_readme(config))
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _choice_prompt(title: str, options: list[str], default_index: int = 0) -> str:
|
|
914
|
+
"""Arrow-key selection using InquirerPy."""
|
|
915
|
+
return inquirer.select(
|
|
916
|
+
message=title,
|
|
917
|
+
choices=options,
|
|
918
|
+
default=options[default_index],
|
|
919
|
+
pointer=">",
|
|
920
|
+
qmark="❯",
|
|
921
|
+
amark="✔",
|
|
922
|
+
).execute()
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def main(
|
|
926
|
+
name: str = typer.Argument(..., help="Project name / folder name"),
|
|
927
|
+
) -> None:
|
|
928
|
+
typer.echo(typer.style("FastAPI Initializer", fg=typer.colors.CYAN, bold=True))
|
|
929
|
+
# Validate project name
|
|
930
|
+
sanitised = name.replace("-", "_")
|
|
931
|
+
if not sanitised.isidentifier():
|
|
932
|
+
raise SystemExit(
|
|
933
|
+
f"Invalid project name '{name}'. "
|
|
934
|
+
"Use only letters, digits, hyphens, and underscores, and start with a letter."
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
typer.echo(f"Creating project: {name}")
|
|
938
|
+
|
|
939
|
+
db_text = _choice_prompt(
|
|
940
|
+
"What kind of database do you want to use?",
|
|
941
|
+
["None", "SQLite", "MySQL", "PostgreSQL"],
|
|
942
|
+
default_index=1,
|
|
943
|
+
)
|
|
944
|
+
orm_text = _choice_prompt(
|
|
945
|
+
"Which ORM do you want to use?",
|
|
946
|
+
["None", "SQLAlchemy", "SQLModel"],
|
|
947
|
+
default_index=1,
|
|
948
|
+
)
|
|
949
|
+
linter_text = _choice_prompt(
|
|
950
|
+
"What linter do you want to use?",
|
|
951
|
+
["None", "Black", "Ruff"],
|
|
952
|
+
default_index=2,
|
|
953
|
+
)
|
|
954
|
+
test_text = _choice_prompt(
|
|
955
|
+
"What testing framework do you like to use?",
|
|
956
|
+
["None", "PyTest", "pytest-async-io"],
|
|
957
|
+
default_index=1,
|
|
958
|
+
)
|
|
959
|
+
docker_text = _choice_prompt(
|
|
960
|
+
"Do you want to create a Docker file for this project?",
|
|
961
|
+
["Yes", "No"],
|
|
962
|
+
default_index=0,
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
db_map = {
|
|
966
|
+
"None": DatabaseChoice.NONE,
|
|
967
|
+
"SQLite": DatabaseChoice.SQLITE,
|
|
968
|
+
"MySQL": DatabaseChoice.MYSQL,
|
|
969
|
+
"PostgreSQL": DatabaseChoice.POSTGRESQL,
|
|
970
|
+
}
|
|
971
|
+
orm_map = {
|
|
972
|
+
"None": ORMChoice.NONE,
|
|
973
|
+
"SQLAlchemy": ORMChoice.SQLALCHEMY,
|
|
974
|
+
"SQLModel": ORMChoice.SQLMODEL,
|
|
975
|
+
}
|
|
976
|
+
linter_map = {
|
|
977
|
+
"None": LinterChoice.NONE,
|
|
978
|
+
"Black": LinterChoice.BLACK,
|
|
979
|
+
"Ruff": LinterChoice.RUFF,
|
|
980
|
+
}
|
|
981
|
+
test_map = {
|
|
982
|
+
"None": TestChoice.NONE,
|
|
983
|
+
"PyTest": TestChoice.PYTEST,
|
|
984
|
+
"pytest-async-io": TestChoice.PYTEST_ASYNCIO,
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
config = ProjectConfig(
|
|
988
|
+
name=name,
|
|
989
|
+
database=db_map[db_text],
|
|
990
|
+
orm=orm_map[orm_text],
|
|
991
|
+
linter=linter_map[linter_text],
|
|
992
|
+
test_framework=test_map[test_text],
|
|
993
|
+
docker=(docker_text == "Yes"),
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
target_dir = Path(name).resolve()
|
|
997
|
+
scaffold_project(config, target_dir)
|
|
998
|
+
|
|
999
|
+
# Success message and next steps
|
|
1000
|
+
typer.echo()
|
|
1001
|
+
typer.echo(typer.style(f"✔ FastAPI project '{name}' created successfully!", fg=typer.colors.GREEN, bold=True))
|
|
1002
|
+
typer.echo()
|
|
1003
|
+
typer.echo(typer.style("Next steps:", fg=typer.colors.CYAN, bold=True))
|
|
1004
|
+
typer.echo(f" 1. cd {name}")
|
|
1005
|
+
typer.echo(" 2. uv sync")
|
|
1006
|
+
typer.echo(" 3. uv run uvicorn app.main:app --reload")
|
|
1007
|
+
typer.echo(" 4. Open http://127.0.0.1:8000 in your browser")
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def cli() -> None:
|
|
1011
|
+
"""Entry point for the fastapi-init console script."""
|
|
1012
|
+
typer.run(main)
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
if __name__ == "__main__":
|
|
1016
|
+
cli()
|
|
1017
|
+
|