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.
Files changed (31) hide show
  1. fastapi_scaffolder-0.1.1/PKG-INFO +51 -0
  2. fastapi_scaffolder-0.1.1/README.md +27 -0
  3. fastapi_scaffolder-0.1.1/pyproject.toml +41 -0
  4. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/__init__.py +1 -0
  5. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/cli.py +87 -0
  6. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/.env.example.j2 +3 -0
  7. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/README.md.j2 +40 -0
  8. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/__init__.py +0 -0
  9. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/__init__.py +0 -0
  10. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/deps.py +6 -0
  11. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/v1/__init__.py +0 -0
  12. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/v1/api.py +10 -0
  13. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/api/v1/routes_item.py +41 -0
  14. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/core/__init__.py +0 -0
  15. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/core/config.py.j2 +22 -0
  16. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/crud/__init__.py +0 -0
  17. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/crud/item.py +38 -0
  18. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/__init__.py +0 -0
  19. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/base.py +6 -0
  20. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/base_class.py +10 -0
  21. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/init_db.py +11 -0
  22. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/db/session.py +28 -0
  23. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/main.py +26 -0
  24. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/models/__init__.py +0 -0
  25. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/models/item.py +17 -0
  26. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/schemas/__init__.py +0 -0
  27. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/schemas/item.py +27 -0
  28. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/app/utils/__init__.py +0 -0
  29. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/requirements.txt +4 -0
  30. fastapi_scaffolder-0.1.1/src/fastapi_scaffolder/templates/fastapi_sqlite/tests/__init__.py +0 -0
  31. 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,3 @@
1
+ PROJECT_NAME="{{ project_name }}"
2
+ DATABASE_URL=sqlite:///./app.db
3
+ DEBUG=True
@@ -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.
@@ -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
@@ -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)
@@ -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")
@@ -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()
@@ -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
@@ -0,0 +1,6 @@
1
+ # Import all models here so that Base.metadata is aware of them.
2
+ # This file is imported by init_db.py (and later by Alembic's env.py)
3
+ # purely for its side effects.
4
+
5
+ from app.db.base_class import Base # noqa
6
+ from app.models.item import Item # noqa
@@ -0,0 +1,10 @@
1
+ from sqlalchemy.orm import DeclarativeBase
2
+
3
+
4
+ class Base(DeclarativeBase):
5
+ """
6
+ Shared declarative base for every ORM model in the project.
7
+ All models should inherit from this class.
8
+ """
9
+
10
+ pass
@@ -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)
@@ -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)
@@ -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)
@@ -0,0 +1,4 @@
1
+ fastapi>=0.111
2
+ uvicorn[standard]>=0.30
3
+ sqlalchemy>=2.0
4
+ pydantic-settings>=2.2
@@ -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