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.
- fastapi_fastkit/__init__.py +1 -1
- fastapi_fastkit/backend/inspector.py +153 -32
- fastapi_fastkit/backend/interactive/__init__.py +2 -0
- fastapi_fastkit/backend/interactive/config_builder.py +21 -2
- fastapi_fastkit/backend/interactive/prompts.py +52 -1
- fastapi_fastkit/backend/interactive/selectors.py +10 -0
- fastapi_fastkit/backend/main.py +79 -6
- fastapi_fastkit/backend/package_managers/factory.py +1 -1
- fastapi_fastkit/backend/package_managers/pdm_manager.py +3 -3
- fastapi_fastkit/backend/package_managers/poetry_manager.py +3 -3
- fastapi_fastkit/backend/package_managers/uv_manager.py +3 -3
- fastapi_fastkit/backend/project_builder/__init__.py +3 -0
- fastapi_fastkit/backend/project_builder/config_generator.py +17 -10
- fastapi_fastkit/backend/project_builder/preset_layout.py +203 -0
- fastapi_fastkit/backend/transducer.py +0 -1
- fastapi_fastkit/cli.py +77 -32
- fastapi_fastkit/core/settings.py +16 -0
- fastapi_fastkit/fastapi_project_template/README.md +72 -23
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.env-tpl +2 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/.gitignore-tpl +31 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl +128 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/pyproject.toml-tpl +70 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/requirements.txt-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/format.sh-tpl +5 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/lint.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/run-server.sh-tpl +8 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/scripts/test.sh-tpl +6 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/api/router.py-tpl +12 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/core/config.py-tpl +49 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/db/memory.py-tpl +48 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/__init__.py-tpl +9 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/models.py-tpl +17 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/repository.py-tpl +48 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/router.py-tpl +59 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/schemas.py-tpl +23 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/service.py-tpl +52 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/main.py-tpl +25 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/conftest.py-tpl +24 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_health.py-tpl +11 -0
- fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/tests/test_items.py-tpl +77 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +2 -2
- fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +4 -1
- fastapi_fastkit/fastapi_project_template/fastapi-single-module/pyproject.toml-tpl +4 -1
- fastapi_fastkit/utils/main.py +73 -6
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/METADATA +8 -5
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/RECORD +63 -32
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/WHEEL +0 -0
- {fastapi_fastkit-1.2.1.dist-info → fastapi_fastkit-1.3.0.dist-info}/entry_points.txt +0 -0
- {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,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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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)
|
|
File without changes
|
|
@@ -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]
|
|
File without changes
|
|
@@ -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
|
fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/__init__.py-tpl
ADDED
|
File without changes
|
fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/models.py-tpl
ADDED
|
@@ -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()
|
fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/router.py-tpl
ADDED
|
@@ -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)
|
fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/schemas.py-tpl
ADDED
|
@@ -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)
|
fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/src/app/domains/items/service.py-tpl
ADDED
|
@@ -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)
|