fastapi-scaffolder 0.1.1__tar.gz
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.
- fastapi_scaffolder-0.1.1/PKG-INFO +51 -0
- fastapi_scaffolder-0.1.1/README.md +27 -0
- fastapi_scaffolder-0.1.1/pyproject.toml +41 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/__init__.py +1 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/cli.py +87 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/.env.example.j2 +3 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/README.md.j2 +40 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/deps.py +6 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/v1/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/v1/api.py +10 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/v1/routes_item.py +41 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/core/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/core/config.py.j2 +22 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/crud/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/crud/item.py +38 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/base.py +6 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/base_class.py +10 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/init_db.py +11 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/session.py +28 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/main.py +26 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/models/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/models/item.py +17 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/schemas/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/schemas/item.py +27 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/utils/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/requirements.txt +4 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/tests/__init__.py +0 -0
- fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/tests/test_items.py +48 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-scaffolder
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Scaffold a FastAPI + SQLAlchemy backend boilerplate in seconds
|
|
5
|
+
Project-URL: Homepage, https://github.com/prathamdmehta/fastapi-scaffolder
|
|
6
|
+
Project-URL: Repository, https://github.com/prathamdmehta/fastapi-scaffolder
|
|
7
|
+
Author: Pratham
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: backend,boilerplate,cli,fastapi,scaffold,sqlalchemy
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: jinja2>=3.1
|
|
22
|
+
Requires-Dist: typer>=0.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# fastapi-scaffolder
|
|
26
|
+
|
|
27
|
+
A CLI that generates a ready-to-code FastAPI + SQLAlchemy + SQLite
|
|
28
|
+
backend project — folders, DB session wiring, an example CRUD
|
|
29
|
+
resource, and tests — so you can skip straight to writing business
|
|
30
|
+
logic instead of rebuilding the same plumbing every time.
|
|
31
|
+
|
|
32
|
+
## Install & use
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# one-off, no permanent install (like npx)
|
|
36
|
+
uvx fastapi-scaffolder init "My API"
|
|
37
|
+
|
|
38
|
+
# or install it properly
|
|
39
|
+
pip install fastapi-scaffolder
|
|
40
|
+
fastapi-scaffold init "My API"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This creates a `my_api/` folder with a working FastAPI app.
|
|
44
|
+
|
|
45
|
+
## Local development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd fastapi-scaffolder
|
|
49
|
+
pip install -e . --break-system-packages # editable install
|
|
50
|
+
fastapi-scaffold init "Demo Project"
|
|
51
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# fastapi-scaffolder
|
|
2
|
+
|
|
3
|
+
A CLI that generates a ready-to-code FastAPI + SQLAlchemy + SQLite
|
|
4
|
+
backend project — folders, DB session wiring, an example CRUD
|
|
5
|
+
resource, and tests — so you can skip straight to writing business
|
|
6
|
+
logic instead of rebuilding the same plumbing every time.
|
|
7
|
+
|
|
8
|
+
## Install & use
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# one-off, no permanent install (like npx)
|
|
12
|
+
uvx fastapi-scaffolder init "My API"
|
|
13
|
+
|
|
14
|
+
# or install it properly
|
|
15
|
+
pip install fastapi-scaffolder
|
|
16
|
+
fastapi-scaffold init "My API"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This creates a `my_api/` folder with a working FastAPI app.
|
|
20
|
+
|
|
21
|
+
## Local development
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd fastapi-scaffolder
|
|
25
|
+
pip install -e . --break-system-packages # editable install
|
|
26
|
+
fastapi-scaffold init "Demo Project"
|
|
27
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastapi-scaffolder"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Scaffold a FastAPI + SQLAlchemy backend boilerplate in seconds"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Pratham" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["fastapi", "sqlalchemy", "scaffold", "boilerplate", "cli", "backend"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: Software Development :: Code Generators",
|
|
21
|
+
"Topic :: Software Development :: Build Tools",
|
|
22
|
+
"Framework :: FastAPI",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"typer>=0.12",
|
|
26
|
+
"jinja2>=3.1",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/prathamdmehta/fastapi-scaffolder"
|
|
31
|
+
Repository = "https://github.com/prathamdmehta/fastapi-scaffolder"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
fastapi-scaffold = "fastapi_scaffolder.cli:main"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/fastapi_scaffolder"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from jinja2 import Environment, FileSystemLoader
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
help="Scaffold a FastAPI + SQLAlchemy backend so you can skip the boilerplate."
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
TEMPLATES_ROOT = Path(__file__).parent / "templates"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def slugify(name: str) -> str:
|
|
16
|
+
"""Turn 'My Cool API' into 'my_cool_api' for use as a folder name."""
|
|
17
|
+
slug = re.sub(r"[^a-zA-Z0-9]+", "_", name.strip()).strip("_").lower()
|
|
18
|
+
return slug or "backend_project"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render_template(template_dir: Path, target_dir: Path, context: dict) -> None:
|
|
22
|
+
env = Environment(loader=FileSystemLoader(str(template_dir)), keep_trailing_newline=True)
|
|
23
|
+
|
|
24
|
+
for src_path in sorted(template_dir.rglob("*")):
|
|
25
|
+
if src_path.is_dir():
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
rel_path = src_path.relative_to(template_dir)
|
|
29
|
+
rel_str = rel_path.as_posix()
|
|
30
|
+
is_template = rel_str.endswith(".j2")
|
|
31
|
+
out_rel = Path(rel_str[:-3]) if is_template else rel_path
|
|
32
|
+
out_path = target_dir / out_rel
|
|
33
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
if is_template:
|
|
36
|
+
rendered = env.get_template(rel_str).render(**context)
|
|
37
|
+
out_path.write_text(rendered, encoding="utf-8")
|
|
38
|
+
else:
|
|
39
|
+
shutil.copy2(src_path, out_path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command()
|
|
43
|
+
def init(
|
|
44
|
+
project_name: str = typer.Argument(..., help="Human-readable project name, e.g. 'My API'"),
|
|
45
|
+
directory: str = typer.Option(
|
|
46
|
+
None,
|
|
47
|
+
"--dir",
|
|
48
|
+
help="Folder to create the project in. Defaults to a slugified version of the project name.",
|
|
49
|
+
),
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Generate a new FastAPI + SQLAlchemy + SQLite backend project.
|
|
53
|
+
"""
|
|
54
|
+
project_slug = slugify(directory or project_name)
|
|
55
|
+
target_dir = Path.cwd() / project_slug
|
|
56
|
+
|
|
57
|
+
if target_dir.exists() and any(target_dir.iterdir()):
|
|
58
|
+
typer.secho(f"'{project_slug}' already exists and is not empty. Aborting.", fg=typer.colors.RED)
|
|
59
|
+
raise typer.Exit(code=1)
|
|
60
|
+
|
|
61
|
+
template_dir = TEMPLATES_ROOT / "fastapi_sqlite"
|
|
62
|
+
context = {"project_name": project_name, "project_slug": project_slug}
|
|
63
|
+
|
|
64
|
+
render_template(template_dir, target_dir, context)
|
|
65
|
+
|
|
66
|
+
typer.secho(f"\n✅ Created '{project_name}' in ./{project_slug}\n", fg=typer.colors.GREEN, bold=True)
|
|
67
|
+
typer.echo("Next steps:")
|
|
68
|
+
typer.echo(f" cd {project_slug}")
|
|
69
|
+
typer.echo(" pip install -r requirements.txt # or: uv pip install -r requirements.txt")
|
|
70
|
+
typer.echo(" cp .env.example .env")
|
|
71
|
+
typer.echo(" uvicorn app.main:app --reload")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command()
|
|
75
|
+
def version():
|
|
76
|
+
"""Print the fastapi-scaffolder version."""
|
|
77
|
+
from fastapi_scaffolder import __version__
|
|
78
|
+
|
|
79
|
+
typer.echo(__version__)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main():
|
|
83
|
+
app()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# {{ project_name }}
|
|
2
|
+
|
|
3
|
+
A FastAPI + SQLAlchemy + SQLite backend, generated by `fastapi-scaffold`.
|
|
4
|
+
|
|
5
|
+
## Run it
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -r requirements.txt
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
uvicorn app.main:app --reload
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open http://127.0.0.1:8000/docs for interactive Swagger docs.
|
|
14
|
+
|
|
15
|
+
## Run tests
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install pytest httpx
|
|
19
|
+
pytest -v
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What's included
|
|
23
|
+
|
|
24
|
+
- `app/main.py` — FastAPI app instance, lifespan startup (creates SQLite
|
|
25
|
+
tables), mounts the API router.
|
|
26
|
+
- `app/core/config.py` — Settings loaded from `.env` via pydantic-settings.
|
|
27
|
+
- `app/db/` — SQLAlchemy engine/session (`session.py`), declarative base
|
|
28
|
+
(`base_class.py`), model import registry (`base.py`), and a
|
|
29
|
+
`create_all`-based init helper (`init_db.py`).
|
|
30
|
+
- `app/models/item.py` — Example ORM model. Delete/rename once you add
|
|
31
|
+
your real models; keep the pattern.
|
|
32
|
+
- `app/schemas/item.py` — Pydantic request/response schemas.
|
|
33
|
+
- `app/crud/item.py` — Data-access layer, kept separate from routes.
|
|
34
|
+
- `app/api/v1/routes_item.py` — Full CRUD route set for the example model.
|
|
35
|
+
- `app/api/v1/api.py` — Aggregates all v1 routers into one.
|
|
36
|
+
- `tests/test_items.py` — Smoke test covering the full create → read →
|
|
37
|
+
update → delete flow.
|
|
38
|
+
|
|
39
|
+
Swap `DATABASE_URL` in `.env` for Postgres/MySQL whenever you're ready —
|
|
40
|
+
everything else is written to be database-agnostic.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Shared FastAPI dependencies used across route modules.
|
|
2
|
+
# Re-exported here so routes just do `from app.api.deps import get_db`
|
|
3
|
+
# instead of reaching into app.db.session directly. This is also the
|
|
4
|
+
# natural place to add get_current_user() once auth is introduced.
|
|
5
|
+
|
|
6
|
+
from app.db.session import get_db # noqa: F401
|
fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/v1/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from app.api.v1 import routes_item
|
|
4
|
+
|
|
5
|
+
api_router = APIRouter()
|
|
6
|
+
api_router.include_router(routes_item.router)
|
|
7
|
+
|
|
8
|
+
# Add future route modules here, e.g.:
|
|
9
|
+
# from app.api.v1 import routes_user
|
|
10
|
+
# api_router.include_router(routes_user.router)
|
fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/v1/routes_item.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
from sqlalchemy.orm import Session
|
|
3
|
+
|
|
4
|
+
from app.api.deps import get_db
|
|
5
|
+
from app.crud import item as crud_item
|
|
6
|
+
from app.schemas.item import ItemCreate, ItemRead, ItemUpdate
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/items", tags=["items"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.post("/", response_model=ItemRead, status_code=201)
|
|
12
|
+
def create_item(item_in: ItemCreate, db: Session = Depends(get_db)):
|
|
13
|
+
return crud_item.create(db, obj_in=item_in)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get("/", response_model=list[ItemRead])
|
|
17
|
+
def list_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
|
18
|
+
return crud_item.get_multi(db, skip=skip, limit=limit)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/{item_id}", response_model=ItemRead)
|
|
22
|
+
def get_item(item_id: int, db: Session = Depends(get_db)):
|
|
23
|
+
item = crud_item.get(db, item_id=item_id)
|
|
24
|
+
if item is None:
|
|
25
|
+
raise HTTPException(status_code=404, detail="Item not found")
|
|
26
|
+
return item
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.patch("/{item_id}", response_model=ItemRead)
|
|
30
|
+
def update_item(item_id: int, item_in: ItemUpdate, db: Session = Depends(get_db)):
|
|
31
|
+
item = crud_item.get(db, item_id=item_id)
|
|
32
|
+
if item is None:
|
|
33
|
+
raise HTTPException(status_code=404, detail="Item not found")
|
|
34
|
+
return crud_item.update(db, db_obj=item, obj_in=item_in)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.delete("/{item_id}", status_code=204)
|
|
38
|
+
def delete_item(item_id: int, db: Session = Depends(get_db)):
|
|
39
|
+
item = crud_item.remove(db, item_id=item_id)
|
|
40
|
+
if item is None:
|
|
41
|
+
raise HTTPException(status_code=404, detail="Item not found")
|
|
File without changes
|
fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/core/config.py.j2
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Settings(BaseSettings):
|
|
5
|
+
"""
|
|
6
|
+
Central app configuration. Values are read from environment
|
|
7
|
+
variables / a .env file, falling back to the defaults below.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
11
|
+
|
|
12
|
+
PROJECT_NAME: str = "{{ project_name }}"
|
|
13
|
+
API_V1_PREFIX: str = "/api/v1"
|
|
14
|
+
|
|
15
|
+
# SQLite by default so the project runs with zero external setup.
|
|
16
|
+
# Swap this for a Postgres/MySQL URL when you're ready.
|
|
17
|
+
DATABASE_URL: str = "sqlite:///./app.db"
|
|
18
|
+
|
|
19
|
+
DEBUG: bool = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
settings = Settings()
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from sqlalchemy.orm import Session
|
|
2
|
+
|
|
3
|
+
from app.models.item import Item
|
|
4
|
+
from app.schemas.item import ItemCreate, ItemUpdate
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get(db: Session, item_id: int) -> Item | None:
|
|
8
|
+
return db.get(Item, item_id)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_multi(db: Session, skip: int = 0, limit: int = 100) -> list[Item]:
|
|
12
|
+
return db.query(Item).offset(skip).limit(limit).all()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create(db: Session, obj_in: ItemCreate) -> Item:
|
|
16
|
+
db_obj = Item(name=obj_in.name, description=obj_in.description)
|
|
17
|
+
db.add(db_obj)
|
|
18
|
+
db.commit()
|
|
19
|
+
db.refresh(db_obj)
|
|
20
|
+
return db_obj
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def update(db: Session, db_obj: Item, obj_in: ItemUpdate) -> Item:
|
|
24
|
+
update_data = obj_in.model_dump(exclude_unset=True)
|
|
25
|
+
for field, value in update_data.items():
|
|
26
|
+
setattr(db_obj, field, value)
|
|
27
|
+
db.add(db_obj)
|
|
28
|
+
db.commit()
|
|
29
|
+
db.refresh(db_obj)
|
|
30
|
+
return db_obj
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def remove(db: Session, item_id: int) -> Item | None:
|
|
34
|
+
db_obj = db.get(Item, item_id)
|
|
35
|
+
if db_obj is not None:
|
|
36
|
+
db.delete(db_obj)
|
|
37
|
+
db.commit()
|
|
38
|
+
return db_obj
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from app.db.base import Base # noqa: F401 (pulls in all models)
|
|
2
|
+
from app.db.session import engine
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def init_db() -> None:
|
|
6
|
+
"""
|
|
7
|
+
Create all tables that don't exist yet.
|
|
8
|
+
Fine for SQLite/dev. In a real project you'd swap this for
|
|
9
|
+
Alembic migrations once the schema stabilizes.
|
|
10
|
+
"""
|
|
11
|
+
Base.metadata.create_all(bind=engine)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import create_engine
|
|
4
|
+
from sqlalchemy.orm import sessionmaker
|
|
5
|
+
|
|
6
|
+
from app.core.config import settings
|
|
7
|
+
|
|
8
|
+
# check_same_thread is only needed for SQLite; harmless to leave the
|
|
9
|
+
# kwarg conditional so this file works unchanged if you switch DBs.
|
|
10
|
+
connect_args = (
|
|
11
|
+
{"check_same_thread": False} if settings.DATABASE_URL.startswith("sqlite") else {}
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
engine = create_engine(settings.DATABASE_URL, connect_args=connect_args)
|
|
15
|
+
|
|
16
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_db() -> Generator:
|
|
20
|
+
"""
|
|
21
|
+
FastAPI dependency that yields a DB session and guarantees it's
|
|
22
|
+
closed afterwards, even if the request raises.
|
|
23
|
+
"""
|
|
24
|
+
db = SessionLocal()
|
|
25
|
+
try:
|
|
26
|
+
yield db
|
|
27
|
+
finally:
|
|
28
|
+
db.close()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
|
|
5
|
+
from app.api.v1.api import api_router
|
|
6
|
+
from app.core.config import settings
|
|
7
|
+
from app.db.init_db import init_db
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@asynccontextmanager
|
|
11
|
+
async def lifespan(app: FastAPI):
|
|
12
|
+
# Creates SQLite tables on startup. Swap for Alembic migrations
|
|
13
|
+
# once the schema stabilizes / you move to Postgres.
|
|
14
|
+
init_db()
|
|
15
|
+
yield
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
app = FastAPI(title=settings.PROJECT_NAME, lifespan=lifespan)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.get("/", tags=["health"])
|
|
22
|
+
def health_check():
|
|
23
|
+
return {"status": "ok", "project": settings.PROJECT_NAME}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
app.include_router(api_router, prefix=settings.API_V1_PREFIX)
|
fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/models/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from sqlalchemy import Integer, String
|
|
2
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
|
|
4
|
+
from app.db.base_class import Base
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Item(Base):
|
|
8
|
+
"""
|
|
9
|
+
Example model showing the pattern every future model should
|
|
10
|
+
follow. Delete or rename this once you add your real models.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__tablename__ = "items"
|
|
14
|
+
|
|
15
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
|
16
|
+
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
17
|
+
description: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/schemas/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ItemBase(BaseModel):
|
|
5
|
+
name: str
|
|
6
|
+
description: str | None = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ItemCreate(ItemBase):
|
|
10
|
+
"""Fields accepted when creating a new item."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ItemUpdate(BaseModel):
|
|
16
|
+
"""Fields accepted when updating an item — all optional."""
|
|
17
|
+
|
|
18
|
+
name: str | None = None
|
|
19
|
+
description: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ItemRead(ItemBase):
|
|
23
|
+
"""Fields returned to the client."""
|
|
24
|
+
|
|
25
|
+
id: int
|
|
26
|
+
|
|
27
|
+
model_config = ConfigDict(from_attributes=True)
|
fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/utils/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fastapi.testclient import TestClient
|
|
3
|
+
|
|
4
|
+
from app.main import app
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture()
|
|
8
|
+
def client():
|
|
9
|
+
# Using a context manager triggers lifespan startup/shutdown,
|
|
10
|
+
# which is what creates the SQLite tables.
|
|
11
|
+
with TestClient(app) as c:
|
|
12
|
+
yield c
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_health_check(client):
|
|
16
|
+
resp = client.get("/")
|
|
17
|
+
assert resp.status_code == 200
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_item_crud_flow(client):
|
|
21
|
+
# create
|
|
22
|
+
resp = client.post("/api/v1/items/", json={"name": "Widget", "description": "A test widget"})
|
|
23
|
+
assert resp.status_code == 201
|
|
24
|
+
item = resp.json()
|
|
25
|
+
item_id = item["id"]
|
|
26
|
+
assert item["name"] == "Widget"
|
|
27
|
+
|
|
28
|
+
# read one
|
|
29
|
+
resp = client.get(f"/api/v1/items/{item_id}")
|
|
30
|
+
assert resp.status_code == 200
|
|
31
|
+
|
|
32
|
+
# list
|
|
33
|
+
resp = client.get("/api/v1/items/")
|
|
34
|
+
assert resp.status_code == 200
|
|
35
|
+
assert any(i["id"] == item_id for i in resp.json())
|
|
36
|
+
|
|
37
|
+
# update
|
|
38
|
+
resp = client.patch(f"/api/v1/items/{item_id}", json={"description": "Updated"})
|
|
39
|
+
assert resp.status_code == 200
|
|
40
|
+
assert resp.json()["description"] == "Updated"
|
|
41
|
+
|
|
42
|
+
# delete
|
|
43
|
+
resp = client.delete(f"/api/v1/items/{item_id}")
|
|
44
|
+
assert resp.status_code == 204
|
|
45
|
+
|
|
46
|
+
# confirm gone
|
|
47
|
+
resp = client.get(f"/api/v1/items/{item_id}")
|
|
48
|
+
assert resp.status_code == 404
|