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
@@ -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(f"app = FastAPI(")
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(f' version="0.1.0",')
186
- content_parts.append(f")")
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", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]'
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
+ ]
@@ -10,7 +10,6 @@ import os
10
10
  import shutil
11
11
  from typing import Dict, Optional
12
12
 
13
- from fastapi_fastkit.core.settings import settings
14
13
  from fastapi_fastkit.utils.logging import debug_log, get_logger
15
14
 
16
15
  logger = get_logger(__name__)
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="Enable interactive mode for guided project setup with feature selection.",
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 with dynamic feature selection.
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
- # Use fastapi-empty template as base
418
- template = "fastapi-empty"
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
- # Generate main.py with selected features
468
- main_py_content = generator.generate_main_py()
469
- main_py_path = os.path.join(project_dir, "src", "main.py")
470
- if not os.path.exists(main_py_path):
471
- main_py_path = os.path.join(project_dir, "main.py")
472
-
473
- with open(main_py_path, "w") as f:
474
- f.write(main_py_content)
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 = os.path.join(
482
- project_dir, "src", "config", "database.py"
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 = os.path.join(
494
- project_dir, "src", "config", "auth.py"
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
- generator.generate_docker_files()
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
- if "src/" in main_path:
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}")
@@ -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
- ├── requirements.txt-tpl
24
- ├── setup.py-tpl
25
- └── README.md-tpl
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. `setup.py` must include `fastapi-fastkit` string in project description
32
- for example:
33
- ```
34
- ...
35
- setup(
36
- ...
37
- description = "[fastapi-fastkit templated] <description>",
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
- install_requires: list[str] = [
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
- 4. Basic CRUD operations example
50
- 5. Unit tests implementation
51
- 6. API documentation (OpenAPI/Swagger)
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"