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
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
#
|
|
11
11
|
# @author bnbong bbbong9@gmail.com
|
|
12
12
|
# --------------------------------------------------------------------------
|
|
13
|
-
import os
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
from typing import Any, Dict, List, Optional
|
|
16
15
|
|
|
@@ -179,11 +178,11 @@ class DynamicConfigGenerator:
|
|
|
179
178
|
# App initialization
|
|
180
179
|
project_name = self.config.get("project_name", "FastAPI App")
|
|
181
180
|
description = self.config.get("description", "")
|
|
182
|
-
content_parts.append(
|
|
181
|
+
content_parts.append("app = FastAPI(")
|
|
183
182
|
content_parts.append(f' title="{project_name}",')
|
|
184
183
|
content_parts.append(f' description="{description}",')
|
|
185
|
-
content_parts.append(
|
|
186
|
-
content_parts.append(
|
|
184
|
+
content_parts.append(' version="0.1.0",')
|
|
185
|
+
content_parts.append(")")
|
|
187
186
|
content_parts.append("")
|
|
188
187
|
|
|
189
188
|
# App configuration
|
|
@@ -448,12 +447,20 @@ class DynamicConfigGenerator:
|
|
|
448
447
|
|
|
449
448
|
return "\n".join(content)
|
|
450
449
|
|
|
451
|
-
def generate_docker_files(self) -> None:
|
|
452
|
-
"""Generate Dockerfile and docker-compose.yml.
|
|
450
|
+
def generate_docker_files(self, app_module: str = "src.main:app") -> None:
|
|
451
|
+
"""Generate Dockerfile and docker-compose.yml.
|
|
452
|
+
|
|
453
|
+
The ``app_module`` is the ``module:attr`` string baked into the
|
|
454
|
+
Dockerfile's ``CMD`` (and into docker-compose's `command` if the
|
|
455
|
+
compose generator ever needs it). Architecture presets that put
|
|
456
|
+
the FastAPI app at a non-default location (e.g. ``domain-starter``
|
|
457
|
+
ships ``src/app/main.py``) must pass the matching dotted path so
|
|
458
|
+
the generated container actually starts.
|
|
459
|
+
"""
|
|
453
460
|
deployment = self.config.get("deployment", [])
|
|
454
461
|
|
|
455
462
|
if "Docker" in deployment:
|
|
456
|
-
dockerfile_content = self._generate_dockerfile()
|
|
463
|
+
dockerfile_content = self._generate_dockerfile(app_module=app_module)
|
|
457
464
|
dockerfile_path = self.project_dir / "Dockerfile"
|
|
458
465
|
with open(dockerfile_path, "w") as f:
|
|
459
466
|
f.write(dockerfile_content)
|
|
@@ -464,8 +471,8 @@ class DynamicConfigGenerator:
|
|
|
464
471
|
with open(compose_path, "w") as f:
|
|
465
472
|
f.write(compose_content)
|
|
466
473
|
|
|
467
|
-
def _generate_dockerfile(self) -> str:
|
|
468
|
-
"""Generate Dockerfile content."""
|
|
474
|
+
def _generate_dockerfile(self, app_module: str = "src.main:app") -> str:
|
|
475
|
+
"""Generate Dockerfile content with a layout-aware uvicorn target."""
|
|
469
476
|
content = []
|
|
470
477
|
content.append(
|
|
471
478
|
"# --------------------------------------------------------------------------"
|
|
@@ -488,7 +495,7 @@ class DynamicConfigGenerator:
|
|
|
488
495
|
content.append("")
|
|
489
496
|
content.append("# Run application")
|
|
490
497
|
content.append(
|
|
491
|
-
'CMD ["uvicorn", "
|
|
498
|
+
f'CMD ["uvicorn", "{app_module}", "--host", "0.0.0.0", "--port", "8000"]'
|
|
492
499
|
)
|
|
493
500
|
content.append("")
|
|
494
501
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# Architecture-preset layout strategy for interactive project generation.
|
|
3
|
+
#
|
|
4
|
+
# Maps each architecture preset (issue #44) to the actual generation
|
|
5
|
+
# decisions interactive ``init`` makes:
|
|
6
|
+
#
|
|
7
|
+
# - which template ships as the base scaffold,
|
|
8
|
+
# - whether the dynamic ``main.py`` overlay should overwrite the shipped one,
|
|
9
|
+
# - where database / auth config files should land so they sit next to the
|
|
10
|
+
# template's existing structure rather than in a parallel ``src/config``,
|
|
11
|
+
# - and which feature combinations need a "you must wire this up manually"
|
|
12
|
+
# warning because the dynamic ``main.py`` overlay isn't applied.
|
|
13
|
+
#
|
|
14
|
+
# Keeping every preset's layout knowledge in one place lets the CLI flow stay
|
|
15
|
+
# linear ("ask the strategist where to write the file") instead of growing a
|
|
16
|
+
# branching maze of preset-specific if/else blocks.
|
|
17
|
+
#
|
|
18
|
+
# @author bnbong bbbong9@gmail.com
|
|
19
|
+
# --------------------------------------------------------------------------
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Tuple
|
|
25
|
+
|
|
26
|
+
# Canonical preset id used when a caller doesn't supply one. Picked to
|
|
27
|
+
# preserve pre-#45 behaviour: interactive ``init`` historically deployed
|
|
28
|
+
# ``fastapi-empty`` and regenerated ``src/main.py`` from feature flags.
|
|
29
|
+
_FALLBACK_PRESET_ID: str = "minimal"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class PresetProfile:
|
|
34
|
+
"""Per-preset generation decisions. Treat as a value object."""
|
|
35
|
+
|
|
36
|
+
preset_id: str
|
|
37
|
+
base_template: str
|
|
38
|
+
regenerate_main: bool
|
|
39
|
+
main_py_relpath: str
|
|
40
|
+
db_config_relpath: str
|
|
41
|
+
auth_config_relpath: str
|
|
42
|
+
# Hint shown when ``regenerate_main`` is False and the user picked a
|
|
43
|
+
# feature whose dynamic main.py overlay won't run. Empty string means
|
|
44
|
+
# "no special note for this preset".
|
|
45
|
+
manual_wiring_note: str = ""
|
|
46
|
+
extra_warning_targets: Tuple[str, ...] = field(default_factory=tuple)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_PRESET_PROFILES: Dict[str, PresetProfile] = {
|
|
50
|
+
"minimal": PresetProfile(
|
|
51
|
+
preset_id="minimal",
|
|
52
|
+
base_template="fastapi-empty",
|
|
53
|
+
regenerate_main=True,
|
|
54
|
+
main_py_relpath="src/main.py",
|
|
55
|
+
db_config_relpath="src/config/database.py",
|
|
56
|
+
auth_config_relpath="src/config/auth.py",
|
|
57
|
+
),
|
|
58
|
+
"single-module": PresetProfile(
|
|
59
|
+
preset_id="single-module",
|
|
60
|
+
base_template="fastapi-single-module",
|
|
61
|
+
regenerate_main=True,
|
|
62
|
+
main_py_relpath="src/main.py",
|
|
63
|
+
db_config_relpath="src/config/database.py",
|
|
64
|
+
auth_config_relpath="src/config/auth.py",
|
|
65
|
+
),
|
|
66
|
+
"classic-layered": PresetProfile(
|
|
67
|
+
preset_id="classic-layered",
|
|
68
|
+
base_template="fastapi-default",
|
|
69
|
+
regenerate_main=False,
|
|
70
|
+
main_py_relpath="src/main.py",
|
|
71
|
+
db_config_relpath="src/core/database.py",
|
|
72
|
+
auth_config_relpath="src/core/auth.py",
|
|
73
|
+
# CORS is intentionally NOT in this list: fastapi-default's shipped
|
|
74
|
+
# main.py already imports CORSMiddleware and adds it conditionally
|
|
75
|
+
# on settings.all_cors_origins, so the user only has to populate
|
|
76
|
+
# BACKEND_CORS_ORIGINS in .env — no code edits needed.
|
|
77
|
+
manual_wiring_note=(
|
|
78
|
+
"fastapi-default's shipped src/main.py is preserved. The "
|
|
79
|
+
"selections below need manual wiring there (CORS is already "
|
|
80
|
+
"wired — set BACKEND_CORS_ORIGINS in .env to activate it)."
|
|
81
|
+
),
|
|
82
|
+
extra_warning_targets=("Rate-Limiting", "Prometheus"),
|
|
83
|
+
),
|
|
84
|
+
"domain-starter": PresetProfile(
|
|
85
|
+
preset_id="domain-starter",
|
|
86
|
+
base_template="fastapi-domain-starter",
|
|
87
|
+
regenerate_main=False,
|
|
88
|
+
main_py_relpath="src/app/main.py",
|
|
89
|
+
db_config_relpath="src/app/core/database.py",
|
|
90
|
+
auth_config_relpath="src/app/core/auth.py",
|
|
91
|
+
manual_wiring_note=(
|
|
92
|
+
"fastapi-domain-starter's shipped src/app/main.py is preserved. "
|
|
93
|
+
"The selections below need manual wiring there (CORS is already "
|
|
94
|
+
"wired — set BACKEND_CORS_ORIGINS in .env to activate it)."
|
|
95
|
+
),
|
|
96
|
+
extra_warning_targets=("Rate-Limiting", "Prometheus"),
|
|
97
|
+
),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PresetLayoutStrategist:
|
|
102
|
+
"""Single source of truth for preset → generation-layout decisions."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, preset_id: str | None) -> None:
|
|
105
|
+
# Empty / None / unknown ids fall back to ``minimal`` so older callers
|
|
106
|
+
# that pre-date the architecture-preset prompt keep working.
|
|
107
|
+
canonical = (preset_id or _FALLBACK_PRESET_ID).strip()
|
|
108
|
+
self.profile: PresetProfile = _PRESET_PROFILES.get(
|
|
109
|
+
canonical, _PRESET_PROFILES[_FALLBACK_PRESET_ID]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def supported_presets(cls) -> List[str]:
|
|
114
|
+
"""Return the ordered list of preset ids the strategist understands."""
|
|
115
|
+
return list(_PRESET_PROFILES.keys())
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def preset_id(self) -> str:
|
|
119
|
+
return self.profile.preset_id
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def base_template(self) -> str:
|
|
123
|
+
return self.profile.base_template
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def should_regenerate_main(self) -> bool:
|
|
127
|
+
return self.profile.regenerate_main
|
|
128
|
+
|
|
129
|
+
def main_py_target(self, project_dir: str) -> Path:
|
|
130
|
+
"""Absolute path where the dynamic main.py overlay should land."""
|
|
131
|
+
return Path(project_dir) / self.profile.main_py_relpath
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def app_module(self) -> str:
|
|
135
|
+
"""Return the ``module:attr`` string uvicorn / Docker should target.
|
|
136
|
+
|
|
137
|
+
Derived from ``main_py_relpath`` so docker generation, runserver,
|
|
138
|
+
and any future container-orchestration code all agree on the
|
|
139
|
+
entrypoint a given preset produces.
|
|
140
|
+
"""
|
|
141
|
+
# Strip the trailing ``.py`` and convert path separators to dots.
|
|
142
|
+
relpath = self.profile.main_py_relpath
|
|
143
|
+
if relpath.endswith(".py"):
|
|
144
|
+
relpath = relpath[: -len(".py")]
|
|
145
|
+
module_part = relpath.replace("/", ".").replace("\\", ".")
|
|
146
|
+
return f"{module_part}:app"
|
|
147
|
+
|
|
148
|
+
def db_config_target(self, project_dir: str) -> Path:
|
|
149
|
+
"""Absolute path for the generated database config module."""
|
|
150
|
+
return Path(project_dir) / self.profile.db_config_relpath
|
|
151
|
+
|
|
152
|
+
def auth_config_target(self, project_dir: str) -> Path:
|
|
153
|
+
"""Absolute path for the generated authentication config module."""
|
|
154
|
+
return Path(project_dir) / self.profile.auth_config_relpath
|
|
155
|
+
|
|
156
|
+
def compatibility_warnings(self, config: Dict[str, Any]) -> List[str]:
|
|
157
|
+
"""Return user-facing warnings for unsupported preset/feature mixes.
|
|
158
|
+
|
|
159
|
+
The dynamic ``main.py`` overlay (CORS middleware wiring, Prometheus
|
|
160
|
+
instrumentation, rate-limit hookup) only runs for presets that
|
|
161
|
+
regenerate ``main.py``. For the other presets we keep the
|
|
162
|
+
template-shipped ``main.py`` intact and surface a single warning
|
|
163
|
+
listing the affected features so users know to wire them up
|
|
164
|
+
themselves rather than assuming the package install was enough.
|
|
165
|
+
"""
|
|
166
|
+
if self.profile.regenerate_main:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
affected = self._affected_overlay_targets(config)
|
|
170
|
+
if not affected:
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
warnings: List[str] = []
|
|
174
|
+
if self.profile.manual_wiring_note:
|
|
175
|
+
warnings.append(self.profile.manual_wiring_note)
|
|
176
|
+
warnings.append(
|
|
177
|
+
"Affected selections (packages installed, but no dynamic main.py "
|
|
178
|
+
"edits applied for the '"
|
|
179
|
+
+ self.profile.preset_id
|
|
180
|
+
+ "' preset): "
|
|
181
|
+
+ ", ".join(affected)
|
|
182
|
+
)
|
|
183
|
+
return warnings
|
|
184
|
+
|
|
185
|
+
def _affected_overlay_targets(self, config: Dict[str, Any]) -> List[str]:
|
|
186
|
+
"""Detect which of the user's selections rely on main.py overlay."""
|
|
187
|
+
triggered: List[str] = []
|
|
188
|
+
utilities = set(config.get("utilities") or [])
|
|
189
|
+
|
|
190
|
+
for target in self.profile.extra_warning_targets:
|
|
191
|
+
if target in {"CORS", "Rate-Limiting"}:
|
|
192
|
+
if target in utilities:
|
|
193
|
+
triggered.append(target)
|
|
194
|
+
elif target == "Prometheus":
|
|
195
|
+
if config.get("monitoring") == "Prometheus":
|
|
196
|
+
triggered.append(target)
|
|
197
|
+
return triggered
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__all__ = [
|
|
201
|
+
"PresetLayoutStrategist",
|
|
202
|
+
"PresetProfile",
|
|
203
|
+
]
|
fastapi_fastkit/cli.py
CHANGED
|
@@ -267,7 +267,7 @@ def startdemo(
|
|
|
267
267
|
if template_deps:
|
|
268
268
|
deps_table = create_info_table(
|
|
269
269
|
"Template Dependencies",
|
|
270
|
-
{f"Dependency {i+1}": dep for i, dep in enumerate(template_deps)},
|
|
270
|
+
{f"Dependency {i + 1}": dep for i, dep in enumerate(template_deps)},
|
|
271
271
|
)
|
|
272
272
|
console.print("\n")
|
|
273
273
|
console.print(deps_table)
|
|
@@ -334,7 +334,11 @@ def startdemo(
|
|
|
334
334
|
"--interactive",
|
|
335
335
|
is_flag=True,
|
|
336
336
|
default=False,
|
|
337
|
-
help=
|
|
337
|
+
help=(
|
|
338
|
+
"Enable interactive mode for guided project setup. Walks through an "
|
|
339
|
+
"architecture preset (minimal / single-module / classic-layered / "
|
|
340
|
+
"domain-starter, default: domain-starter), then feature selection."
|
|
341
|
+
),
|
|
338
342
|
)
|
|
339
343
|
@click.option(
|
|
340
344
|
"--project-name",
|
|
@@ -372,7 +376,11 @@ def init(
|
|
|
372
376
|
"""
|
|
373
377
|
Start a FastAPI project setup.
|
|
374
378
|
|
|
375
|
-
Use --interactive for guided setup
|
|
379
|
+
Use --interactive for guided setup. Interactive mode prompts for an
|
|
380
|
+
architecture preset (``minimal`` / ``single-module`` / ``classic-layered``
|
|
381
|
+
/ ``domain-starter`` — default: ``domain-starter``) and then walks
|
|
382
|
+
through feature selection (database, auth, testing, deployment, ...).
|
|
383
|
+
|
|
376
384
|
Without --interactive, creates an empty project with predefined stacks.
|
|
377
385
|
|
|
378
386
|
This command will automatically create a new FastAPI project directory
|
|
@@ -414,8 +422,17 @@ def init(
|
|
|
414
422
|
try:
|
|
415
423
|
user_local = settings.USER_WORKSPACE
|
|
416
424
|
|
|
417
|
-
#
|
|
418
|
-
|
|
425
|
+
# Pick the base template from the architecture preset chosen
|
|
426
|
+
# earlier in the interactive flow. Older callers without a
|
|
427
|
+
# preset fall back to ``minimal`` (= fastapi-empty), preserving
|
|
428
|
+
# pre-#45 behaviour.
|
|
429
|
+
from fastapi_fastkit.backend.project_builder import (
|
|
430
|
+
PresetLayoutStrategist,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
preset_id = config.get("architecture_preset")
|
|
434
|
+
strategist = PresetLayoutStrategist(preset_id)
|
|
435
|
+
template = strategist.base_template
|
|
419
436
|
template_dir = settings.FASTKIT_TEMPLATE_ROOT
|
|
420
437
|
target_template = os.path.join(template_dir, template)
|
|
421
438
|
|
|
@@ -464,38 +481,44 @@ def init(
|
|
|
464
481
|
|
|
465
482
|
generator = DynamicConfigGenerator(config, project_dir)
|
|
466
483
|
|
|
467
|
-
#
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
484
|
+
# main.py overlay — only regenerated for presets that ship a
|
|
485
|
+
# placeholder app (minimal, single-module). For richer presets
|
|
486
|
+
# (classic-layered, domain-starter) we keep the template's
|
|
487
|
+
# router-aware main.py intact.
|
|
488
|
+
#
|
|
489
|
+
# The strategist's ``main_py_target`` is always ``src/main.py``
|
|
490
|
+
# for both regenerate-main presets, and both fastapi-empty and
|
|
491
|
+
# fastapi-single-module ship that file, so we can write
|
|
492
|
+
# straight to the strategist's path without a flat-``main.py``
|
|
493
|
+
# fallback branch.
|
|
494
|
+
if strategist.should_regenerate_main:
|
|
495
|
+
main_py_path = strategist.main_py_target(project_dir)
|
|
496
|
+
main_py_path.parent.mkdir(parents=True, exist_ok=True)
|
|
497
|
+
main_py_path.write_text(generator.generate_main_py())
|
|
498
|
+
else:
|
|
499
|
+
print_info(
|
|
500
|
+
f"Preserving template-shipped main.py for preset "
|
|
501
|
+
f"'{strategist.preset_id}'."
|
|
502
|
+
)
|
|
475
503
|
|
|
476
|
-
# Generate database configuration if selected
|
|
504
|
+
# Generate database configuration if selected — preset chooses
|
|
505
|
+
# where the file lives so it sits next to the existing structure.
|
|
477
506
|
db_info = config.get("database", {})
|
|
478
507
|
if isinstance(db_info, dict) and db_info.get("type") != "None":
|
|
479
508
|
db_config_content = generator.generate_database_config()
|
|
480
509
|
if db_config_content:
|
|
481
|
-
db_config_path =
|
|
482
|
-
|
|
483
|
-
)
|
|
484
|
-
os.makedirs(os.path.dirname(db_config_path), exist_ok=True)
|
|
485
|
-
with open(db_config_path, "w") as f:
|
|
486
|
-
f.write(db_config_content)
|
|
510
|
+
db_config_path = strategist.db_config_target(project_dir)
|
|
511
|
+
db_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
512
|
+
db_config_path.write_text(db_config_content)
|
|
487
513
|
|
|
488
514
|
# Generate auth configuration if selected
|
|
489
515
|
auth_type = config.get("authentication", "None")
|
|
490
516
|
if auth_type != "None":
|
|
491
517
|
auth_config_content = generator.generate_auth_config()
|
|
492
518
|
if auth_config_content:
|
|
493
|
-
auth_config_path =
|
|
494
|
-
|
|
495
|
-
)
|
|
496
|
-
os.makedirs(os.path.dirname(auth_config_path), exist_ok=True)
|
|
497
|
-
with open(auth_config_path, "w") as f:
|
|
498
|
-
f.write(auth_config_content)
|
|
519
|
+
auth_config_path = strategist.auth_config_target(project_dir)
|
|
520
|
+
auth_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
521
|
+
auth_config_path.write_text(auth_config_content)
|
|
499
522
|
|
|
500
523
|
# Generate test configuration if testing selected
|
|
501
524
|
testing_type = config.get("testing", "None")
|
|
@@ -509,9 +532,20 @@ def init(
|
|
|
509
532
|
# Generate Docker files if deployment selected
|
|
510
533
|
deployment = config.get("deployment", [])
|
|
511
534
|
if deployment and deployment != ["None"]:
|
|
512
|
-
|
|
535
|
+
# Thread the preset-aware app module so the generated
|
|
536
|
+
# Dockerfile's ``CMD ["uvicorn", "<module>:app", ...]``
|
|
537
|
+
# matches the layout the user actually generated. Default
|
|
538
|
+
# ``src.main:app`` only works for minimal / single-module /
|
|
539
|
+
# classic-layered; domain-starter needs ``src.app.main:app``.
|
|
540
|
+
generator.generate_docker_files(app_module=strategist.app_module)
|
|
513
541
|
print_success("Generated Docker deployment files")
|
|
514
542
|
|
|
543
|
+
# Surface preset-specific warnings (e.g. "you picked a preset
|
|
544
|
+
# whose shipped main.py we kept; CORS/Prometheus must be wired
|
|
545
|
+
# manually").
|
|
546
|
+
for warning in strategist.compatibility_warnings(config):
|
|
547
|
+
print_warning(warning, title="Preset compatibility")
|
|
548
|
+
|
|
515
549
|
print_success("Generated configuration files for selected stack")
|
|
516
550
|
|
|
517
551
|
# Create virtual environment and install dependencies
|
|
@@ -579,7 +613,7 @@ def init(
|
|
|
579
613
|
for stack_name, deps in settings.PROJECT_STACKS.items():
|
|
580
614
|
table = create_info_table(
|
|
581
615
|
f"{stack_name.upper()} Stack",
|
|
582
|
-
{f"Dependency {i+1}": dep for i, dep in enumerate(deps)},
|
|
616
|
+
{f"Dependency {i + 1}": dep for i, dep in enumerate(deps)},
|
|
583
617
|
)
|
|
584
618
|
console.print(table)
|
|
585
619
|
console.print("\n")
|
|
@@ -819,6 +853,20 @@ def deleteproject(ctx: Context, project_name: str) -> None:
|
|
|
819
853
|
print_error(f"Error during project deletion: {e}")
|
|
820
854
|
|
|
821
855
|
|
|
856
|
+
def _derive_app_module(project_dir: str, main_path: str) -> str:
|
|
857
|
+
"""Convert a discovered ``main.py`` path into a uvicorn ``module:attr``.
|
|
858
|
+
|
|
859
|
+
Templates can place ``main.py`` anywhere under the project (``main.py``,
|
|
860
|
+
``src/main.py``, ``src/app/main.py``, ...). The previous ``"src/"`` /
|
|
861
|
+
``""`` heuristic mis-mapped the domain-starter layout (``src/app/main.py``
|
|
862
|
+
→ wrongly produced ``src.main:app``); deriving the dotted path from the
|
|
863
|
+
actual relative location avoids that drift for any future layout too.
|
|
864
|
+
"""
|
|
865
|
+
rel_path = os.path.relpath(main_path, project_dir)
|
|
866
|
+
module_part = os.path.splitext(rel_path)[0].replace(os.sep, ".")
|
|
867
|
+
return f"{module_part}:app"
|
|
868
|
+
|
|
869
|
+
|
|
822
870
|
@fastkit_cli.command()
|
|
823
871
|
@click.option(
|
|
824
872
|
"--host",
|
|
@@ -893,10 +941,7 @@ def runserver(
|
|
|
893
941
|
return
|
|
894
942
|
|
|
895
943
|
main_path = core_modules["main"]
|
|
896
|
-
|
|
897
|
-
app_module = "src.main:app"
|
|
898
|
-
else:
|
|
899
|
-
app_module = "main:app"
|
|
944
|
+
app_module = _derive_app_module(project_dir, main_path)
|
|
900
945
|
|
|
901
946
|
if venv_python:
|
|
902
947
|
print_info(f"Using Python from virtual environment: {venv_python}")
|
fastapi_fastkit/core/settings.py
CHANGED
|
@@ -24,6 +24,7 @@ class FastkitConfig:
|
|
|
24
24
|
TEMPLATE_PATHS: dict[str, list[str] | dict[str, list[str]]] = {
|
|
25
25
|
"main": [
|
|
26
26
|
"src/main.py",
|
|
27
|
+
"src/app/main.py",
|
|
27
28
|
"main.py",
|
|
28
29
|
],
|
|
29
30
|
"setup": [
|
|
@@ -37,12 +38,27 @@ class FastkitConfig:
|
|
|
37
38
|
"files": ["settings.py", "config.py"],
|
|
38
39
|
"paths": [
|
|
39
40
|
"src/core",
|
|
41
|
+
"src/app/core",
|
|
40
42
|
"src",
|
|
41
43
|
"",
|
|
42
44
|
],
|
|
43
45
|
},
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
# Architecture Presets (interactive ``init`` wizard)
|
|
49
|
+
#
|
|
50
|
+
# The preset shapes how the generated project is laid out (single file vs.
|
|
51
|
+
# layered vs. domain-oriented). Preset-specific generation logic lives in
|
|
52
|
+
# later issues — this catalog is the user-facing menu and the canonical
|
|
53
|
+
# set of preset ids persisted in the interactive config.
|
|
54
|
+
ARCHITECTURE_PRESETS: dict[str, str] = {
|
|
55
|
+
"minimal": "Smallest viable FastAPI app — a single app + a couple of files.",
|
|
56
|
+
"single-module": "Everything in one module; ideal for tiny scripts and prototypes.",
|
|
57
|
+
"classic-layered": "Layered split: api/routes, crud, schemas, core (a la fastapi-default).",
|
|
58
|
+
"domain-starter": "Domain-oriented: src/app/domains/<concept>/ with router/service/repository (recommended).",
|
|
59
|
+
}
|
|
60
|
+
DEFAULT_ARCHITECTURE_PRESET: str = "domain-starter"
|
|
61
|
+
|
|
46
62
|
# Startproject Options
|
|
47
63
|
PROJECT_STACKS: dict[str, list[str]] = {
|
|
48
64
|
"minimal": ["fastapi", "uvicorn", "pydantic", "pydantic-settings"],
|
|
@@ -18,37 +18,86 @@ template-name/
|
|
|
18
18
|
│ ├── models/
|
|
19
19
|
│ ├── routes/
|
|
20
20
|
│ └── utils/
|
|
21
|
-
├── tests/
|
|
21
|
+
├── tests/ # required
|
|
22
22
|
├── scripts/
|
|
23
|
-
├──
|
|
24
|
-
├── setup.py-tpl
|
|
25
|
-
|
|
23
|
+
├── pyproject.toml-tpl # preferred primary metadata file (PEP 621)
|
|
24
|
+
├── setup.py-tpl # legacy alternative, still accepted
|
|
25
|
+
├── requirements.txt-tpl # optional when pyproject.toml-tpl declares deps
|
|
26
|
+
└── README.md-tpl # required
|
|
26
27
|
```
|
|
27
28
|
|
|
29
|
+
The minimum required files for a modern template are `tests/`, `README.md-tpl`,
|
|
30
|
+
and at least one metadata file (`pyproject.toml-tpl` or `setup.py-tpl`).
|
|
31
|
+
`requirements.txt-tpl` is optional when the template's dependencies are
|
|
32
|
+
declared under `[project].dependencies` in `pyproject.toml-tpl`.
|
|
33
|
+
|
|
34
|
+
Modern templates **SHOULD** ship `pyproject.toml-tpl` as the primary metadata
|
|
35
|
+
file. `setup.py-tpl` remains supported for backward compatibility.
|
|
36
|
+
|
|
28
37
|
### Key Requirements:
|
|
29
38
|
|
|
30
39
|
1. All source files must use `.py-tpl` extension
|
|
31
|
-
2.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
40
|
-
```
|
|
41
|
-
3. `setup.py` must include `install_requires` section, it must include essential dependencies for the template project. Also, note that install_requires list must be type annotated.
|
|
42
|
-
for example:
|
|
40
|
+
2. The template must declare `fastapi` as a dependency in at least one of:
|
|
41
|
+
- `pyproject.toml-tpl` under `[project].dependencies` (preferred)
|
|
42
|
+
- `requirements.txt-tpl`
|
|
43
|
+
- `setup.py-tpl` under `install_requires`
|
|
44
|
+
3. `pyproject.toml-tpl` (preferred) should use PEP 621 metadata and carry the
|
|
45
|
+
FastAPI-fastkit identity markers so that `is_fastkit_project()` can tell
|
|
46
|
+
generated projects apart from unrelated FastAPI projects in the user's
|
|
47
|
+
workspace:
|
|
43
48
|
```
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
]
|
|
49
|
+
[project]
|
|
50
|
+
name = "<project_name>"
|
|
51
|
+
version = "0.1.0"
|
|
52
|
+
description = "[FastAPI-fastkit templated] <description>"
|
|
53
|
+
dependencies = [
|
|
54
|
+
"fastapi>=0.115.0",
|
|
55
|
+
...
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.fastapi-fastkit]
|
|
59
|
+
managed = true
|
|
48
60
|
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
The `[FastAPI-fastkit templated]` marker in `description` and the
|
|
62
|
+
`[tool.fastapi-fastkit]` table are both recognized by detection (any one
|
|
63
|
+
suffices; matching is case-insensitive). Metadata injection will also add
|
|
64
|
+
these markers at project-generation time if a template forgets them, but
|
|
65
|
+
authors should include them explicitly.
|
|
66
|
+
4. Legacy templates using `setup.py-tpl` should:
|
|
67
|
+
- declare dependencies via a type-annotated `install_requires` list, e.g.
|
|
68
|
+
```
|
|
69
|
+
install_requires: list[str] = [
|
|
70
|
+
...
|
|
71
|
+
]
|
|
72
|
+
```
|
|
73
|
+
- include the `[FastAPI-fastkit templated]` marker in the project
|
|
74
|
+
description. Detection falls back to a case-insensitive scan for
|
|
75
|
+
`fastapi-fastkit` in `setup.py`, so this marker keeps legacy projects
|
|
76
|
+
identifiable:
|
|
77
|
+
```
|
|
78
|
+
setup(
|
|
79
|
+
...
|
|
80
|
+
description = "[FastAPI-fastkit templated] <description>",
|
|
81
|
+
...
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
5. Basic CRUD operations example
|
|
85
|
+
6. Unit tests implementation
|
|
86
|
+
7. API documentation (OpenAPI/Swagger)
|
|
87
|
+
|
|
88
|
+
## Available templates
|
|
89
|
+
|
|
90
|
+
| Template | When to choose |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `fastapi-default` | Quick CRUD demo with the classic layered layout (`api/routes`, `crud`, `schemas`). Good first stop. |
|
|
93
|
+
| `fastapi-empty` | Minimal scaffold for users who want to add their own structure on top. |
|
|
94
|
+
| `fastapi-single-module` | Single-file sandbox for tiny prototypes / scripts. |
|
|
95
|
+
| `fastapi-async-crud` | Async-flavoured equivalent of `fastapi-default`. |
|
|
96
|
+
| `fastapi-custom-response` | Demonstrates custom response formatting / envelope patterns. |
|
|
97
|
+
| `fastapi-dockerized` | Adds a production-ready Dockerfile to the default layout. |
|
|
98
|
+
| `fastapi-psql-orm` | PostgreSQL + SQLAlchemy + Alembic; pick this when you need a real database. |
|
|
99
|
+
| `fastapi-mcp` | Model Context Protocol integration. |
|
|
100
|
+
| `fastapi-domain-starter` | **Recommended modern default for medium-sized APIs.** Pyproject-first, domain-oriented layout (`src/app/domains/<concept>/`) with a clean transport / service / repository split, plus a built-in `/health` probe. |
|
|
52
101
|
|
|
53
102
|
## Base structure of modules template
|
|
54
103
|
|
|
@@ -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
|
"PyYAML>=6.0.2",
|
|
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"
|
|
@@ -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
|
"PyYAML>=6.0.2",
|
|
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"
|
|
@@ -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
|
]
|
|
@@ -38,6 +38,9 @@ dev = [
|
|
|
38
38
|
"PyYAML>=6.0.2",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
|
+
[tool.fastapi-fastkit]
|
|
42
|
+
managed = true
|
|
43
|
+
|
|
41
44
|
[build-system]
|
|
42
45
|
requires = ["hatchling"]
|
|
43
46
|
build-backend = "hatchling.build"
|