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.
- 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 +114 -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 +71 -10
- 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 +109 -44
- fastapi_fastkit/core/settings.py +17 -1
- 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.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/METADATA +33 -3
- {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/RECORD +63 -32
- {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/WHEEL +1 -1
- {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/entry_points.txt +0 -0
- {fastapi_fastkit-1.2.0.dist-info → fastapi_fastkit-1.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# @author bnbong bbbong9@gmail.com
|
|
5
5
|
# --------------------------------------------------------------------------
|
|
6
6
|
import subprocess
|
|
7
|
-
from typing import List
|
|
7
|
+
from typing import List, Tuple
|
|
8
8
|
|
|
9
9
|
from fastapi_fastkit.core.exceptions import BackendExceptions
|
|
10
10
|
from fastapi_fastkit.utils.logging import debug_log, get_logger
|
|
@@ -19,6 +19,47 @@ from .base import BasePackageManager
|
|
|
19
19
|
|
|
20
20
|
logger = get_logger(__name__)
|
|
21
21
|
|
|
22
|
+
# PEP 440 version specifier operators, longest-match first so "===" beats "==".
|
|
23
|
+
_PEP440_OPERATORS = ("===", "==", "!=", "<=", ">=", "~=", "<", ">")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parse_pip_requirement(
|
|
27
|
+
requirement: str,
|
|
28
|
+
) -> Tuple[str, List[str], str, str]:
|
|
29
|
+
"""Parse a pip/PEP 508 requirement string.
|
|
30
|
+
|
|
31
|
+
Returns ``(name, extras, version_spec, marker)`` where ``version_spec``
|
|
32
|
+
includes the operator (e.g. ``">=1.2.3"``) or is empty when unspecified,
|
|
33
|
+
and ``marker`` is the environment marker without the leading semicolon.
|
|
34
|
+
"""
|
|
35
|
+
req = requirement.strip()
|
|
36
|
+
|
|
37
|
+
marker = ""
|
|
38
|
+
if ";" in req:
|
|
39
|
+
req, marker = req.split(";", 1)
|
|
40
|
+
req = req.strip()
|
|
41
|
+
marker = marker.strip()
|
|
42
|
+
|
|
43
|
+
version_spec = ""
|
|
44
|
+
split_at = len(req)
|
|
45
|
+
for op in _PEP440_OPERATORS:
|
|
46
|
+
idx = req.find(op)
|
|
47
|
+
if idx != -1 and idx < split_at:
|
|
48
|
+
split_at = idx
|
|
49
|
+
if split_at < len(req):
|
|
50
|
+
version_spec = req[split_at:].strip()
|
|
51
|
+
req = req[:split_at].strip()
|
|
52
|
+
|
|
53
|
+
extras: List[str] = []
|
|
54
|
+
if "[" in req and req.endswith("]"):
|
|
55
|
+
bare_name, _, extras_part = req.partition("[")
|
|
56
|
+
extras = [
|
|
57
|
+
extra.strip() for extra in extras_part[:-1].split(",") if extra.strip()
|
|
58
|
+
]
|
|
59
|
+
req = bare_name.strip()
|
|
60
|
+
|
|
61
|
+
return req, extras, version_spec, marker
|
|
62
|
+
|
|
22
63
|
|
|
23
64
|
class PoetryManager(BasePackageManager):
|
|
24
65
|
"""Poetry package manager implementation."""
|
|
@@ -182,22 +223,42 @@ build-backend = "poetry.core.masonry.api"
|
|
|
182
223
|
pyproject_path = self.get_dependency_file_path()
|
|
183
224
|
|
|
184
225
|
try:
|
|
185
|
-
# Create dependencies section for Poetry
|
|
226
|
+
# Create dependencies section for Poetry. Parse each requirement as
|
|
227
|
+
# PEP 508 so non-``==`` specifiers (``>=``, ``~=``, ...), extras, and
|
|
228
|
+
# environment markers all round-trip to valid TOML.
|
|
186
229
|
deps_section = ""
|
|
187
230
|
for dep in dependencies:
|
|
188
|
-
|
|
189
|
-
if
|
|
190
|
-
|
|
191
|
-
|
|
231
|
+
name_part, extras, version_spec, marker = _parse_pip_requirement(dep)
|
|
232
|
+
if not name_part:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if not version_spec:
|
|
236
|
+
version_str = "*"
|
|
237
|
+
elif version_spec.startswith("=="):
|
|
238
|
+
# Poetry treats a bare version as a pin; keep the old
|
|
239
|
+
# formatting to avoid churn in generated files.
|
|
240
|
+
version_str = version_spec[2:].strip()
|
|
241
|
+
else:
|
|
242
|
+
version_str = version_spec
|
|
243
|
+
|
|
244
|
+
if extras or marker:
|
|
245
|
+
parts = [f'version = "{version_str}"']
|
|
246
|
+
if extras:
|
|
247
|
+
extras_str = ", ".join(f'"{extra}"' for extra in extras)
|
|
248
|
+
parts.append(f"extras = [{extras_str}]")
|
|
249
|
+
if marker:
|
|
250
|
+
escaped_marker = marker.replace('"', '\\"')
|
|
251
|
+
parts.append(f'markers = "{escaped_marker}"')
|
|
252
|
+
deps_section += f"{name_part} = {{{', '.join(parts)}}}\n"
|
|
192
253
|
else:
|
|
193
|
-
deps_section += f'{
|
|
254
|
+
deps_section += f'{name_part} = "{version_str}"\n'
|
|
194
255
|
|
|
195
256
|
# Create basic pyproject.toml content for Poetry
|
|
196
257
|
pyproject_content = f"""[tool.poetry]
|
|
197
|
-
name = "{project_name or
|
|
258
|
+
name = "{project_name or "fastapi-project"}"
|
|
198
259
|
version = "0.1.0"
|
|
199
|
-
description = "{description or
|
|
200
|
-
authors = ["{author or
|
|
260
|
+
description = "{description or "A FastAPI project"}"
|
|
261
|
+
authors = ["{author or "Author"} <{author_email or "author@example.com"}>"]
|
|
201
262
|
readme = "README.md"
|
|
202
263
|
license = "MIT"
|
|
203
264
|
packages = [{{include = "src"}}]
|
|
@@ -160,11 +160,11 @@ class UvManager(BasePackageManager):
|
|
|
160
160
|
|
|
161
161
|
# Create basic pyproject.toml content for UV
|
|
162
162
|
pyproject_content = f"""[project]
|
|
163
|
-
name = "{project_name or
|
|
163
|
+
name = "{project_name or "fastapi-project"}"
|
|
164
164
|
version = "0.1.0"
|
|
165
|
-
description = "{description or
|
|
165
|
+
description = "{description or "A FastAPI project"}"
|
|
166
166
|
authors = [
|
|
167
|
-
{{name = "{author or
|
|
167
|
+
{{name = "{author or "Author"}", email = "{author_email or "author@example.com"}"}},
|
|
168
168
|
]
|
|
169
169
|
dependencies = {deps_toml}
|
|
170
170
|
requires-python = ">=3.8"
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
# --------------------------------------------------------------------------
|
|
11
11
|
from .config_generator import DynamicConfigGenerator
|
|
12
12
|
from .dependency_collector import DependencyCollector
|
|
13
|
+
from .preset_layout import PresetLayoutStrategist, PresetProfile
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"DependencyCollector",
|
|
16
17
|
"DynamicConfigGenerator",
|
|
18
|
+
"PresetLayoutStrategist",
|
|
19
|
+
"PresetProfile",
|
|
17
20
|
]
|
|
@@ -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
|
-
|
|
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
|
+
]
|