FastAPI-fastkit 1.2.1__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 +79 -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 +3 -3
  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 +77 -32
  17. fastapi_fastkit/core/settings.py +16 -0
  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.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/METADATA +8 -5
  60. {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/RECORD +63 -32
  61. {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/WHEEL +0 -0
  62. {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/entry_points.txt +0 -0
  63. {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
  ]
@@ -41,6 +41,9 @@ dev = [
41
41
  "setuptools-scm>=8.1.0",
42
42
  ]
43
43
 
44
+ [tool.fastapi-fastkit]
45
+ managed = true
46
+
44
47
  [build-system]
45
48
  requires = ["hatchling"]
46
49
  build-backend = "hatchling.build"
@@ -0,0 +1,2 @@
1
+ SECRET_KEY=changethis
2
+ ENVIRONMENT=development
@@ -0,0 +1,31 @@
1
+ .idea
2
+ .ipynb_checkpoints
3
+ .mypy_cache
4
+ .vscode
5
+ __pycache__
6
+ .pytest_cache
7
+ htmlcov
8
+ dist
9
+ site
10
+ .coverage*
11
+ coverage.xml
12
+ .netlify
13
+ test.db
14
+ log.txt
15
+ Pipfile.lock
16
+ env3.*
17
+ env
18
+ docs_build
19
+ site_build
20
+ venv
21
+ .venv
22
+ docs.zip
23
+ archive.zip
24
+
25
+ # vim temporary files
26
+ *~
27
+ .*.sw?
28
+ .cache
29
+
30
+ # macOS
31
+ .DS_Store
@@ -0,0 +1,128 @@
1
+ # FastAPI Domain Starter
2
+
3
+ Modern, domain-oriented FastAPI starter — the recommended default for
4
+ medium-sized API projects.
5
+
6
+ > Generated project: **<project_name>** — <description>
7
+
8
+ ## When to choose this starter
9
+
10
+ Pick this template when you want:
11
+
12
+ - A **domain-oriented layout** (one folder per business concept under
13
+ `src/app/domains/`) rather than the layered `routes/ + crud/ + schemas/`
14
+ split. Domains scale better as the API grows.
15
+ - A clear separation between **transport** (`router.py`), **business
16
+ logic** (`service.py`), and **data access** (`repository.py`) so each
17
+ layer can evolve independently.
18
+ - A **`pyproject.toml`-first** project (PEP 621) compatible with `uv`,
19
+ `pdm`, or `poetry` out of the box — no `setup.py` to maintain.
20
+ - Sensible defaults for settings, testing, formatting, and a built-in
21
+ `/health` probe.
22
+
23
+ If you only need a quick CRUD demo, start with `fastapi-default`. If you
24
+ need PostgreSQL, start with `fastapi-psql-orm`. If you want a single-file
25
+ sandbox, use `fastapi-single-module`.
26
+
27
+ ## Project structure
28
+
29
+ ```
30
+ .
31
+ ├── README.md
32
+ ├── pyproject.toml
33
+ ├── .env
34
+ ├── .gitignore
35
+ ├── scripts/
36
+ │ ├── format.sh
37
+ │ ├── lint.sh
38
+ │ ├── run-server.sh
39
+ │ └── test.sh
40
+ ├── src/
41
+ │ └── app/
42
+ │ ├── main.py # FastAPI app entry point
43
+ │ ├── core/
44
+ │ │ └── config.py # pydantic-settings configuration
45
+ │ ├── db/
46
+ │ │ └── memory.py # in-memory store stand-in for a real DB
47
+ │ ├── api/
48
+ │ │ ├── router.py # aggregates health + every domain router
49
+ │ │ └── health.py # GET /health
50
+ │ └── domains/
51
+ │ └── items/ # example domain (CRUD over an item entity)
52
+ │ ├── models.py # entity dataclass
53
+ │ ├── schemas.py # API I/O schemas (pydantic)
54
+ │ ├── repository.py # data access layer
55
+ │ ├── service.py # business logic
56
+ │ └── router.py # FastAPI router for the domain
57
+ └── tests/
58
+ ├── conftest.py
59
+ ├── test_health.py
60
+ └── test_items.py
61
+ ```
62
+
63
+ The recipe for adding a new domain is:
64
+
65
+ 1. Create `src/app/domains/<your_domain>/`.
66
+ 2. Mirror the `items` layout (`models.py`, `schemas.py`, `repository.py`,
67
+ `service.py`, `router.py`).
68
+ 3. Register the router in `src/app/api/router.py`.
69
+ 4. Add tests under `tests/test_<your_domain>.py`.
70
+
71
+ ## Running the app
72
+
73
+ ```bash
74
+ # create a virtualenv and install dependencies
75
+ $ uv sync # or: pip install -e ".[dev]"
76
+
77
+ # launch the dev server
78
+ $ bash scripts/run-server.sh
79
+ # or directly:
80
+ $ uvicorn src.app.main:app --reload
81
+ ```
82
+
83
+ API docs are then served at:
84
+
85
+ - Swagger UI: <http://127.0.0.1:8000/docs>
86
+ - ReDoc: <http://127.0.0.1:8000/redoc>
87
+
88
+ ## API endpoints
89
+
90
+ | Method | Endpoint | Description |
91
+ |--------|---------------------------|------------------------------|
92
+ | GET | `/api/v1/health` | Liveness probe |
93
+ | GET | `/api/v1/items` | List items |
94
+ | GET | `/api/v1/items/{item_id}` | Read a single item |
95
+ | POST | `/api/v1/items` | Create an item |
96
+ | PUT | `/api/v1/items/{item_id}` | Replace an item |
97
+ | DELETE | `/api/v1/items/{item_id}` | Delete an item |
98
+
99
+ ## Running tests
100
+
101
+ ```bash
102
+ $ bash scripts/test.sh
103
+ # or directly:
104
+ $ pytest
105
+ ```
106
+
107
+ ## Configuration
108
+
109
+ `src/app/core/config.py` reads settings from environment variables (or a
110
+ local `.env` file). The provided `.env` only sets a placeholder
111
+ `SECRET_KEY` — replace it before deploying.
112
+
113
+ ## Project Origin
114
+
115
+ This project was created from the **`fastapi-domain-starter`** template
116
+ shipped with [FastAPI-fastkit](https://github.com/bnbong/FastAPI-fastkit).
117
+
118
+ The `FastAPI-fastkit` is an open-source project that helps Python and
119
+ FastAPI beginners quickly set up a FastAPI-based application development
120
+ environment in a framework-like structure.
121
+
122
+ For an end-to-end walkthrough of this template — the generated tree,
123
+ the bundled `items` example, and how to add your next domain — see the
124
+ [**Domain-oriented Project tutorial**](https://bnbong.github.io/FastAPI-fastkit/tutorial/domain-starter/).
125
+
126
+ ### Template Information
127
+ - Template creator: [bnbong](mailto:bbbong9@gmail.com)
128
+ - FastAPI-fastkit project maintainer: [bnbong](mailto:bbbong9@gmail.com)
@@ -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)