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.
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
+