FastAPI-fastkit 1.2.0__py3-none-any.whl → 1.3.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.
Files changed (63) hide show
  1. fastapi_fastkit/__init__.py +1 -1
  2. fastapi_fastkit/backend/inspector.py +153 -32
  3. fastapi_fastkit/backend/interactive/__init__.py +2 -0
  4. fastapi_fastkit/backend/interactive/config_builder.py +21 -2
  5. fastapi_fastkit/backend/interactive/prompts.py +52 -1
  6. fastapi_fastkit/backend/interactive/selectors.py +10 -0
  7. fastapi_fastkit/backend/main.py +114 -6
  8. fastapi_fastkit/backend/package_managers/factory.py +1 -1
  9. fastapi_fastkit/backend/package_managers/pdm_manager.py +3 -3
  10. fastapi_fastkit/backend/package_managers/poetry_manager.py +71 -10
  11. fastapi_fastkit/backend/package_managers/uv_manager.py +3 -3
  12. fastapi_fastkit/backend/project_builder/__init__.py +3 -0
  13. fastapi_fastkit/backend/project_builder/config_generator.py +17 -10
  14. fastapi_fastkit/backend/project_builder/preset_layout.py +203 -0
  15. fastapi_fastkit/backend/transducer.py +0 -1
  16. fastapi_fastkit/cli.py +109 -44
  17. fastapi_fastkit/core/settings.py +17 -1
  18. fastapi_fastkit/fastapi_project_template/README.md +72 -23
  19. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl +4 -1
  20. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl +4 -1
  21. fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl +4 -1
  22. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl +4 -1
  23. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.env-tpl +2 -0
  24. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.gitignore-tpl +31 -0
  25. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl +128 -0
  26. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/pyproject.toml-tpl +70 -0
  27. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/requirements.txt-tpl +11 -0
  28. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/format.sh-tpl +5 -0
  29. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/lint.sh-tpl +6 -0
  30. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/run-server.sh-tpl +8 -0
  31. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/test.sh-tpl +6 -0
  32. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/__init__.py-tpl +0 -0
  33. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/__init__.py-tpl +0 -0
  34. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/__init__.py-tpl +0 -0
  35. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/health.py-tpl +11 -0
  36. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/router.py-tpl +12 -0
  37. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/__init__.py-tpl +0 -0
  38. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/config.py-tpl +49 -0
  39. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/__init__.py-tpl +0 -0
  40. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/memory.py-tpl +48 -0
  41. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/__init__.py-tpl +0 -0
  42. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/__init__.py-tpl +9 -0
  43. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/models.py-tpl +17 -0
  44. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/repository.py-tpl +48 -0
  45. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/router.py-tpl +59 -0
  46. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/schemas.py-tpl +23 -0
  47. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/service.py-tpl +52 -0
  48. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/main.py-tpl +25 -0
  49. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/__init__.py-tpl +0 -0
  50. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/conftest.py-tpl +24 -0
  51. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_health.py-tpl +11 -0
  52. fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_items.py-tpl +77 -0
  53. fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +2 -2
  54. fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +4 -1
  55. fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +4 -1
  56. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +4 -1
  57. fastapi_fastkit/fastapi_project_template/fastapi-single-module/pyproject.toml-tpl +4 -1
  58. fastapi_fastkit/utils/main.py +73 -6
  59. {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/METADATA +33 -3
  60. {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/RECORD +63 -32
  61. {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/WHEEL +1 -1
  62. {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/entry_points.txt +0 -0
  63. {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "<project_name>"
3
+ version = "0.1.0"
4
+ description = "[FastAPI-fastkit templated] <description>"
5
+ authors = [
6
+ {name = "<author>", email = "<author_email>"},
7
+ ]
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "fastapi>=0.115.8",
13
+ "uvicorn[standard]>=0.34.0",
14
+ "pydantic>=2.10.6",
15
+ "pydantic-settings>=2.7.1",
16
+ "python-dotenv>=1.0.1",
17
+ "fastapi-fastkit>=1.1.5",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "pytest>=8.3.4",
23
+ "httpx>=0.28.1",
24
+ "black>=25.1.0",
25
+ "isort>=6.0.0",
26
+ "mypy>=1.15.0",
27
+ ]
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=8.3.4",
32
+ "httpx>=0.28.1",
33
+ "black>=25.1.0",
34
+ "isort>=6.0.0",
35
+ "mypy>=1.15.0",
36
+ ]
37
+
38
+ [tool.fastapi-fastkit]
39
+ managed = true
40
+ template = "fastapi-domain-starter"
41
+
42
+ [build-system]
43
+ requires = ["hatchling"]
44
+ build-backend = "hatchling.build"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src"]
48
+
49
+ [tool.black]
50
+ line-length = 88
51
+ target-version = ["py312"]
52
+
53
+ [tool.isort]
54
+ profile = "black"
55
+ line_length = 88
56
+ known_first_party = ["src"]
57
+
58
+ [tool.mypy]
59
+ python_version = "3.12"
60
+ strict = true
61
+ warn_return_any = true
62
+ warn_unused_configs = true
63
+ disallow_untyped_defs = true
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ python_files = ["test_*.py"]
68
+ python_classes = ["Test*"]
69
+ python_functions = ["test_*"]
70
+ addopts = "-v --tb=short"
@@ -0,0 +1,11 @@
1
+ fastapi==0.115.8
2
+ uvicorn[standard]==0.34.0
3
+ pydantic==2.10.6
4
+ pydantic-settings==2.7.1
5
+ python-dotenv==1.0.1
6
+ pytest==8.3.4
7
+ httpx==0.28.1
8
+ black==25.1.0
9
+ isort==6.0.0
10
+ mypy==1.15.0
11
+ FastAPI-fastkit==1.1.5
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -x
3
+
4
+ black .
5
+ isort .
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -x
3
+
4
+ black . --check
5
+ isort . --check-only
6
+ mypy src
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+ set -x
5
+
6
+ source .venv/bin/activate
7
+
8
+ uvicorn src.app.main:app --reload
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+ set -x
5
+
6
+ pytest
@@ -0,0 +1,11 @@
1
+ # --------------------------------------------------------------------------
2
+ # Health endpoint — small, side-effect-free probe for liveness checks.
3
+ # --------------------------------------------------------------------------
4
+ from fastapi import APIRouter
5
+
6
+ router = APIRouter(tags=["health"])
7
+
8
+
9
+ @router.get("/health")
10
+ def health() -> dict[str, str]:
11
+ return {"status": "ok"}
@@ -0,0 +1,12 @@
1
+ # --------------------------------------------------------------------------
2
+ # Top-level API router — aggregates the health probe with every domain
3
+ # module under ``src/app/domains/``.
4
+ # --------------------------------------------------------------------------
5
+ from fastapi import APIRouter
6
+
7
+ from src.app.api import health
8
+ from src.app.domains.items import router as items_router
9
+
10
+ api_router = APIRouter()
11
+ api_router.include_router(health.router)
12
+ api_router.include_router(items_router.router)
@@ -0,0 +1,49 @@
1
+ # --------------------------------------------------------------------------
2
+ # Application settings (pydantic-settings).
3
+ # --------------------------------------------------------------------------
4
+ import secrets
5
+ from typing import Annotated, Any, List, Literal, Union
6
+
7
+ from pydantic import AnyUrl, BeforeValidator, computed_field
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+
10
+
11
+ def _parse_cors(v: Any) -> Union[List[str], str]:
12
+ if isinstance(v, str) and not v.startswith("["):
13
+ return [item.strip() for item in v.split(",") if item.strip()]
14
+ if isinstance(v, (list, str)):
15
+ return v
16
+ raise ValueError(v)
17
+
18
+
19
+ class Settings(BaseSettings):
20
+ """Centralized configuration for the application.
21
+
22
+ Values are populated from environment variables (or a local ``.env``
23
+ file) so the same image / build can run in development, staging, and
24
+ production without code changes.
25
+ """
26
+
27
+ model_config = SettingsConfigDict(
28
+ env_file=".env",
29
+ env_ignore_empty=True,
30
+ extra="ignore",
31
+ )
32
+
33
+ PROJECT_NAME: str = "<project_name>"
34
+ ENVIRONMENT: Literal["development", "staging", "production"] = "development"
35
+ SECRET_KEY: str = secrets.token_urlsafe(32)
36
+
37
+ API_V1_PREFIX: str = "/api/v1"
38
+
39
+ BACKEND_CORS_ORIGINS: Annotated[
40
+ Union[List[AnyUrl], str], BeforeValidator(_parse_cors)
41
+ ] = []
42
+
43
+ @computed_field # type: ignore[prop-decorator]
44
+ @property
45
+ def all_cors_origins(self) -> List[str]:
46
+ return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS]
47
+
48
+
49
+ settings = Settings() # type: ignore[call-arg]
@@ -0,0 +1,48 @@
1
+ # --------------------------------------------------------------------------
2
+ # Process-local in-memory store used as a placeholder for a real database.
3
+ #
4
+ # Domain repositories sit on top of this store, so swapping the store for
5
+ # SQLAlchemy / async drivers later is a contained change — only the
6
+ # repositories need to be rewritten.
7
+ # --------------------------------------------------------------------------
8
+ from typing import Dict, Generic, Iterable, Optional, TypeVar
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class InMemoryStore(Generic[T]):
14
+ """Tiny ID-keyed collection backed by a dict.
15
+
16
+ Not thread-safe and not suitable for production — but lets the rest of
17
+ the application code stay realistic (repository / service / router) while
18
+ keeping the starter self-contained.
19
+ """
20
+
21
+ def __init__(self) -> None:
22
+ self._data: Dict[int, T] = {}
23
+ self._next_id: int = 1
24
+
25
+ def list(self) -> Iterable[T]:
26
+ return list(self._data.values())
27
+
28
+ def get(self, id_: int) -> Optional[T]:
29
+ return self._data.get(id_)
30
+
31
+ def add(self, item: T) -> int:
32
+ new_id = self._next_id
33
+ self._data[new_id] = item
34
+ self._next_id += 1
35
+ return new_id
36
+
37
+ def replace(self, id_: int, item: T) -> bool:
38
+ if id_ not in self._data:
39
+ return False
40
+ self._data[id_] = item
41
+ return True
42
+
43
+ def delete(self, id_: int) -> bool:
44
+ return self._data.pop(id_, None) is not None
45
+
46
+ def clear(self) -> None:
47
+ self._data.clear()
48
+ self._next_id = 1
@@ -0,0 +1,9 @@
1
+ # Re-exports for the items domain so callers can write:
2
+ # from src.app.domains.items import service
3
+ from src.app.domains.items import ( # noqa: F401
4
+ models,
5
+ repository,
6
+ router,
7
+ schemas,
8
+ service,
9
+ )
@@ -0,0 +1,17 @@
1
+ # --------------------------------------------------------------------------
2
+ # Items domain — internal entity model.
3
+ #
4
+ # Kept separate from the API schemas in ``schemas.py`` so the wire format
5
+ # can evolve without touching domain logic, and vice versa.
6
+ # --------------------------------------------------------------------------
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass
11
+ class Item:
12
+ """Domain entity: an item tracked by the application."""
13
+
14
+ id: int
15
+ name: str
16
+ price: float
17
+ in_stock: bool = True
@@ -0,0 +1,48 @@
1
+ # --------------------------------------------------------------------------
2
+ # Items domain — repository (data access boundary).
3
+ #
4
+ # Wraps the in-memory store with item-typed methods so the service layer
5
+ # stays decoupled from how items are persisted.
6
+ # --------------------------------------------------------------------------
7
+ from typing import List, Optional
8
+
9
+ from src.app.db.memory import InMemoryStore
10
+ from src.app.domains.items.models import Item
11
+
12
+ _store: InMemoryStore[Item] = InMemoryStore()
13
+
14
+
15
+ class ItemRepository:
16
+ """Data access for ``Item`` entities."""
17
+
18
+ def __init__(self, store: Optional[InMemoryStore[Item]] = None) -> None:
19
+ self._store = store if store is not None else _store
20
+
21
+ def list_all(self) -> List[Item]:
22
+ return list(self._store.list())
23
+
24
+ def get(self, item_id: int) -> Optional[Item]:
25
+ return self._store.get(item_id)
26
+
27
+ def add(self, name: str, price: float, in_stock: bool = True) -> Item:
28
+ # Reserve the next id from the store, then back-fill it on the
29
+ # entity so the repository is the single source of identity.
30
+ item = Item(id=0, name=name, price=price, in_stock=in_stock)
31
+ new_id = self._store.add(item)
32
+ item.id = new_id
33
+ return item
34
+
35
+ def replace(self, item_id: int, name: str, price: float, in_stock: bool) -> Optional[Item]:
36
+ existing = self._store.get(item_id)
37
+ if existing is None:
38
+ return None
39
+ updated = Item(id=item_id, name=name, price=price, in_stock=in_stock)
40
+ self._store.replace(item_id, updated)
41
+ return updated
42
+
43
+ def delete(self, item_id: int) -> bool:
44
+ return self._store.delete(item_id)
45
+
46
+ def reset(self) -> None:
47
+ # Convenience for tests / local development; not exposed via the API.
48
+ self._store.clear()
@@ -0,0 +1,59 @@
1
+ # --------------------------------------------------------------------------
2
+ # Items domain — HTTP transport (FastAPI router).
3
+ # --------------------------------------------------------------------------
4
+ from typing import List
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Response, status
7
+
8
+ from src.app.domains.items.schemas import ItemCreate, ItemRead
9
+ from src.app.domains.items.service import ItemNotFoundError, ItemService
10
+
11
+ router = APIRouter(prefix="/items", tags=["items"])
12
+
13
+
14
+ def get_item_service() -> ItemService:
15
+ """Default dependency — overridable in tests."""
16
+ return ItemService()
17
+
18
+
19
+ @router.get("", response_model=List[ItemRead])
20
+ def list_items(service: ItemService = Depends(get_item_service)) -> List[ItemRead]:
21
+ return [ItemRead.model_validate(item) for item in service.list_items()]
22
+
23
+
24
+ @router.get("/{item_id}", response_model=ItemRead)
25
+ def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> ItemRead:
26
+ try:
27
+ return ItemRead.model_validate(service.get_item(item_id))
28
+ except ItemNotFoundError as exc:
29
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
30
+
31
+
32
+ @router.post("", response_model=ItemRead, status_code=status.HTTP_201_CREATED)
33
+ def create_item(
34
+ payload: ItemCreate, service: ItemService = Depends(get_item_service)
35
+ ) -> ItemRead:
36
+ return ItemRead.model_validate(service.create_item(payload))
37
+
38
+
39
+ @router.put("/{item_id}", response_model=ItemRead)
40
+ def replace_item(
41
+ item_id: int,
42
+ payload: ItemCreate,
43
+ service: ItemService = Depends(get_item_service),
44
+ ) -> ItemRead:
45
+ try:
46
+ return ItemRead.model_validate(service.replace_item(item_id, payload))
47
+ except ItemNotFoundError as exc:
48
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
49
+
50
+
51
+ @router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
52
+ def delete_item(
53
+ item_id: int, service: ItemService = Depends(get_item_service)
54
+ ) -> Response:
55
+ try:
56
+ service.delete_item(item_id)
57
+ except ItemNotFoundError as exc:
58
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
59
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
@@ -0,0 +1,23 @@
1
+ # --------------------------------------------------------------------------
2
+ # Items domain — API I/O schemas (pydantic).
3
+ # --------------------------------------------------------------------------
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+
7
+ class ItemCreate(BaseModel):
8
+ """Payload accepted on POST /items."""
9
+
10
+ name: str = Field(min_length=1, max_length=120)
11
+ price: float = Field(ge=0)
12
+ in_stock: bool = True
13
+
14
+
15
+ class ItemRead(BaseModel):
16
+ """Outgoing representation of an item."""
17
+
18
+ id: int
19
+ name: str
20
+ price: float
21
+ in_stock: bool
22
+
23
+ model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,52 @@
1
+ # --------------------------------------------------------------------------
2
+ # Items domain — service layer (business logic).
3
+ #
4
+ # Routers should call into the service rather than the repository directly,
5
+ # so policy and validation rules can grow here without touching transport
6
+ # code or persistence.
7
+ # --------------------------------------------------------------------------
8
+ from typing import List, Optional
9
+
10
+ from src.app.domains.items.models import Item
11
+ from src.app.domains.items.repository import ItemRepository
12
+ from src.app.domains.items.schemas import ItemCreate
13
+
14
+
15
+ class ItemNotFoundError(Exception):
16
+ """Raised when an item lookup misses."""
17
+
18
+
19
+ class ItemService:
20
+ def __init__(self, repository: Optional[ItemRepository] = None) -> None:
21
+ self._repository = repository if repository is not None else ItemRepository()
22
+
23
+ def list_items(self) -> List[Item]:
24
+ return self._repository.list_all()
25
+
26
+ def get_item(self, item_id: int) -> Item:
27
+ item = self._repository.get(item_id)
28
+ if item is None:
29
+ raise ItemNotFoundError(f"Item {item_id} does not exist")
30
+ return item
31
+
32
+ def create_item(self, payload: ItemCreate) -> Item:
33
+ return self._repository.add(
34
+ name=payload.name,
35
+ price=payload.price,
36
+ in_stock=payload.in_stock,
37
+ )
38
+
39
+ def replace_item(self, item_id: int, payload: ItemCreate) -> Item:
40
+ updated = self._repository.replace(
41
+ item_id,
42
+ name=payload.name,
43
+ price=payload.price,
44
+ in_stock=payload.in_stock,
45
+ )
46
+ if updated is None:
47
+ raise ItemNotFoundError(f"Item {item_id} does not exist")
48
+ return updated
49
+
50
+ def delete_item(self, item_id: int) -> None:
51
+ if not self._repository.delete(item_id):
52
+ raise ItemNotFoundError(f"Item {item_id} does not exist")
@@ -0,0 +1,25 @@
1
+ # --------------------------------------------------------------------------
2
+ # FastAPI application entry point for <project_name>.
3
+ # --------------------------------------------------------------------------
4
+ from fastapi import FastAPI
5
+ from starlette.middleware.cors import CORSMiddleware
6
+
7
+ from src.app.api.router import api_router
8
+ from src.app.core.config import settings
9
+
10
+ app = FastAPI(
11
+ title=settings.PROJECT_NAME,
12
+ description="<description>",
13
+ version="0.1.0",
14
+ )
15
+
16
+ if settings.all_cors_origins:
17
+ app.add_middleware(
18
+ CORSMiddleware,
19
+ allow_origins=settings.all_cors_origins,
20
+ allow_credentials=True,
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ app.include_router(api_router, prefix=settings.API_V1_PREFIX)
@@ -0,0 +1,24 @@
1
+ # --------------------------------------------------------------------------
2
+ # pytest fixtures for <project_name>.
3
+ # --------------------------------------------------------------------------
4
+ from collections.abc import Generator
5
+
6
+ import pytest
7
+ from fastapi.testclient import TestClient
8
+
9
+ from src.app.domains.items.repository import ItemRepository
10
+ from src.app.main import app
11
+
12
+
13
+ @pytest.fixture(autouse=True)
14
+ def reset_items_store() -> Generator[None, None, None]:
15
+ """Each test starts with an empty items store."""
16
+ ItemRepository().reset()
17
+ yield
18
+ ItemRepository().reset()
19
+
20
+
21
+ @pytest.fixture
22
+ def client() -> Generator[TestClient, None, None]:
23
+ with TestClient(app) as c:
24
+ yield c
@@ -0,0 +1,11 @@
1
+ # --------------------------------------------------------------------------
2
+ # Health endpoint smoke test.
3
+ # --------------------------------------------------------------------------
4
+ from src.app.core.config import settings
5
+
6
+
7
+ def test_health_endpoint_returns_ok(client):
8
+ response = client.get(f"{settings.API_V1_PREFIX}/health")
9
+
10
+ assert response.status_code == 200
11
+ assert response.json() == {"status": "ok"}
@@ -0,0 +1,77 @@
1
+ # --------------------------------------------------------------------------
2
+ # Items domain — end-to-end coverage of the CRUD endpoints.
3
+ # --------------------------------------------------------------------------
4
+ from src.app.core.config import settings
5
+
6
+ ITEMS_URL = f"{settings.API_V1_PREFIX}/items"
7
+
8
+
9
+ def test_list_items_starts_empty(client):
10
+ response = client.get(ITEMS_URL)
11
+
12
+ assert response.status_code == 200
13
+ assert response.json() == []
14
+
15
+
16
+ def test_create_then_read_item(client):
17
+ create_response = client.post(
18
+ ITEMS_URL,
19
+ json={"name": "Sticker", "price": 2.5, "in_stock": True},
20
+ )
21
+
22
+ assert create_response.status_code == 201
23
+ created = create_response.json()
24
+ assert created["id"] == 1
25
+ assert created["name"] == "Sticker"
26
+
27
+ read_response = client.get(f"{ITEMS_URL}/{created['id']}")
28
+
29
+ assert read_response.status_code == 200
30
+ assert read_response.json() == created
31
+
32
+
33
+ def test_get_unknown_item_returns_404(client):
34
+ response = client.get(f"{ITEMS_URL}/999")
35
+
36
+ assert response.status_code == 404
37
+
38
+
39
+ def test_replace_item(client):
40
+ created = client.post(
41
+ ITEMS_URL,
42
+ json={"name": "Mug", "price": 9.0, "in_stock": True},
43
+ ).json()
44
+
45
+ response = client.put(
46
+ f"{ITEMS_URL}/{created['id']}",
47
+ json={"name": "Mug v2", "price": 11.0, "in_stock": False},
48
+ )
49
+
50
+ assert response.status_code == 200
51
+ body = response.json()
52
+ assert body["id"] == created["id"]
53
+ assert body["name"] == "Mug v2"
54
+ assert body["price"] == 11.0
55
+ assert body["in_stock"] is False
56
+
57
+
58
+ def test_delete_item(client):
59
+ created = client.post(
60
+ ITEMS_URL,
61
+ json={"name": "Keychain", "price": 4.0, "in_stock": True},
62
+ ).json()
63
+
64
+ delete_response = client.delete(f"{ITEMS_URL}/{created['id']}")
65
+ assert delete_response.status_code == 204
66
+
67
+ follow_up = client.get(f"{ITEMS_URL}/{created['id']}")
68
+ assert follow_up.status_code == 404
69
+
70
+
71
+ def test_create_item_validates_payload(client):
72
+ response = client.post(
73
+ ITEMS_URL,
74
+ json={"name": "", "price": -1.0},
75
+ )
76
+
77
+ assert response.status_code == 422
@@ -1,6 +1,6 @@
1
- # <project_name>
1
+ # Minimal FastAPI Template
2
2
 
3
- > Minimal FastAPI template for rapid prototyping
3
+ > Generated project: **<project_name>** — minimal FastAPI scaffold for rapid prototyping
4
4
 
5
5
  A minimal FastAPI template that provides the basic structure for building FastAPI applications. Perfect for getting started quickly or as a foundation for more complex projects.
6
6
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "<project_name>"
3
3
  version = "0.1.0"
4
- description = "<description>"
4
+ description = "[FastAPI-fastkit templated] <description>"
5
5
  authors = [
6
6
  {name = "<author>", email = "<author_email>"},
7
7
  ]
@@ -34,6 +34,9 @@ dev = [
34
34
  "mypy>=1.15.0",
35
35
  ]
36
36
 
37
+ [tool.fastapi-fastkit]
38
+ managed = true
39
+
37
40
  [build-system]
38
41
  requires = ["hatchling"]
39
42
  build-backend = "hatchling.build"
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "<project_name>"
3
3
  version = "0.1.0"
4
- description = "<description>"
4
+ description = "[FastAPI-fastkit templated] <description>"
5
5
  authors = [
6
6
  {name = "<author>", email = "<author_email>"},
7
7
  ]
@@ -45,6 +45,9 @@ dev = [
45
45
  "PyYAML>=6.0.2",
46
46
  ]
47
47
 
48
+ [tool.fastapi-fastkit]
49
+ managed = true
50
+
48
51
  [build-system]
49
52
  requires = ["hatchling"]
50
53
  build-backend = "hatchling.build"