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
fastapi_fastkit/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = 'v1.
|
|
1
|
+
__version__ = 'v1.3.0'
|
|
@@ -23,19 +23,23 @@ import os
|
|
|
23
23
|
import shutil
|
|
24
24
|
import subprocess
|
|
25
25
|
import sys
|
|
26
|
+
import tomllib
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
28
29
|
|
|
29
30
|
import yaml
|
|
30
31
|
|
|
31
32
|
from fastapi_fastkit.backend.main import (
|
|
33
|
+
_parse_setup_dependencies,
|
|
32
34
|
create_venv,
|
|
33
35
|
find_template_core_modules,
|
|
34
36
|
inject_project_metadata,
|
|
35
37
|
install_dependencies_with_manager,
|
|
36
38
|
)
|
|
39
|
+
from fastapi_fastkit.backend.package_managers.poetry_manager import (
|
|
40
|
+
_parse_pip_requirement,
|
|
41
|
+
)
|
|
37
42
|
from fastapi_fastkit.backend.transducer import copy_and_convert_template
|
|
38
|
-
from fastapi_fastkit.core.settings import settings
|
|
39
43
|
from fastapi_fastkit.utils.logging import debug_log, get_logger
|
|
40
44
|
from fastapi_fastkit.utils.main import print_error, print_success, print_warning
|
|
41
45
|
|
|
@@ -127,7 +131,6 @@ class TemplateInspector:
|
|
|
127
131
|
|
|
128
132
|
def _force_cleanup_directory(self, directory_path: str) -> None:
|
|
129
133
|
"""Force cleanup of directory with multiple strategies."""
|
|
130
|
-
import stat
|
|
131
134
|
import time
|
|
132
135
|
|
|
133
136
|
max_retries = 5
|
|
@@ -396,20 +399,39 @@ class TemplateInspector:
|
|
|
396
399
|
return True
|
|
397
400
|
|
|
398
401
|
def _check_file_structure(self) -> bool:
|
|
399
|
-
"""Check the required file and directory structure.
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
402
|
+
"""Check the required file and directory structure.
|
|
403
|
+
|
|
404
|
+
Modern templates may ship ``pyproject.toml-tpl`` as the primary metadata
|
|
405
|
+
file; ``setup.py-tpl`` remains accepted for backward compatibility.
|
|
406
|
+
A template must provide at least one of the two. ``requirements.txt-tpl``
|
|
407
|
+
is no longer strictly required when ``pyproject.toml-tpl`` declares
|
|
408
|
+
``[project].dependencies``.
|
|
409
|
+
"""
|
|
410
|
+
always_required = ["tests", "README.md-tpl"]
|
|
411
|
+
metadata_candidates = ["pyproject.toml-tpl", "setup.py-tpl"]
|
|
412
|
+
|
|
413
|
+
missing_required = [
|
|
414
|
+
path for path in always_required if not (self.template_path / path).exists()
|
|
405
415
|
]
|
|
416
|
+
has_metadata = any(
|
|
417
|
+
(self.template_path / name).exists() for name in metadata_candidates
|
|
418
|
+
)
|
|
406
419
|
|
|
407
|
-
|
|
408
|
-
|
|
420
|
+
if missing_required:
|
|
421
|
+
for path in missing_required:
|
|
409
422
|
error_msg = f"Missing required path: {path}"
|
|
410
423
|
self.errors.append(error_msg)
|
|
411
424
|
debug_log(f"File structure check failed: {error_msg}", "error")
|
|
412
|
-
|
|
425
|
+
if not has_metadata:
|
|
426
|
+
error_msg = (
|
|
427
|
+
"Missing metadata file: expected pyproject.toml-tpl "
|
|
428
|
+
"(preferred) or setup.py-tpl"
|
|
429
|
+
)
|
|
430
|
+
self.errors.append(error_msg)
|
|
431
|
+
debug_log(f"File structure check failed: {error_msg}", "error")
|
|
432
|
+
|
|
433
|
+
if missing_required or not has_metadata:
|
|
434
|
+
return False
|
|
413
435
|
|
|
414
436
|
debug_log("File structure check passed", "info")
|
|
415
437
|
return True
|
|
@@ -427,40 +449,139 @@ class TemplateInspector:
|
|
|
427
449
|
return True
|
|
428
450
|
|
|
429
451
|
def _check_dependencies(self) -> bool:
|
|
430
|
-
"""Check
|
|
452
|
+
"""Check that FastAPI is declared in at least one supported source.
|
|
453
|
+
|
|
454
|
+
All three metadata sources are consulted independently:
|
|
455
|
+
|
|
456
|
+
- ``requirements.txt-tpl``
|
|
457
|
+
- ``pyproject.toml-tpl`` ``[project].dependencies``
|
|
458
|
+
- ``setup.py-tpl`` ``install_requires``
|
|
459
|
+
|
|
460
|
+
The check passes if *any* source declares ``fastapi``, so a template
|
|
461
|
+
with a stale ``requirements.txt-tpl`` still passes when
|
|
462
|
+
``pyproject.toml-tpl`` is authoritative. It fails only when every
|
|
463
|
+
present source is missing ``fastapi`` (or when no source exists at
|
|
464
|
+
all — that also implies no metadata file, which
|
|
465
|
+
``_check_file_structure`` already rejects).
|
|
466
|
+
"""
|
|
431
467
|
req_path = self.template_path / "requirements.txt-tpl"
|
|
468
|
+
pyproject_path = self.template_path / "pyproject.toml-tpl"
|
|
432
469
|
setup_path = self.template_path / "setup.py-tpl"
|
|
433
470
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
471
|
+
sources_checked: List[str] = []
|
|
472
|
+
fastapi_declared = False
|
|
473
|
+
|
|
474
|
+
if req_path.exists():
|
|
475
|
+
sources_checked.append("requirements.txt-tpl")
|
|
476
|
+
try:
|
|
477
|
+
with open(req_path, encoding="utf-8") as f:
|
|
478
|
+
deps = f.read().splitlines()
|
|
479
|
+
package_names = {
|
|
480
|
+
_parse_pip_requirement(dep)[0].lower()
|
|
481
|
+
for dep in deps
|
|
482
|
+
if dep.strip() and not dep.strip().startswith("#")
|
|
483
|
+
}
|
|
484
|
+
debug_log(
|
|
485
|
+
f"requirements.txt-tpl dependencies: {sorted(package_names)}",
|
|
486
|
+
"debug",
|
|
487
|
+
)
|
|
488
|
+
if "fastapi" in package_names:
|
|
489
|
+
fastapi_declared = True
|
|
490
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
491
|
+
error_msg = f"Error reading requirements.txt-tpl: {e}"
|
|
492
|
+
self.errors.append(error_msg)
|
|
493
|
+
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
494
|
+
return False
|
|
495
|
+
|
|
496
|
+
if pyproject_path.exists():
|
|
497
|
+
sources_checked.append("pyproject.toml-tpl")
|
|
498
|
+
package_names, parse_error = self._extract_pyproject_dependency_names(
|
|
499
|
+
pyproject_path
|
|
500
|
+
)
|
|
501
|
+
if parse_error is not None:
|
|
502
|
+
self.errors.append(parse_error)
|
|
503
|
+
debug_log(f"Dependencies check failed: {parse_error}", "error")
|
|
504
|
+
return False
|
|
505
|
+
debug_log(
|
|
506
|
+
f"pyproject.toml-tpl dependencies: {sorted(package_names)}", "debug"
|
|
507
|
+
)
|
|
508
|
+
if "fastapi" in package_names:
|
|
509
|
+
fastapi_declared = True
|
|
510
|
+
|
|
511
|
+
if setup_path.exists():
|
|
512
|
+
sources_checked.append("setup.py-tpl")
|
|
513
|
+
try:
|
|
514
|
+
with open(setup_path, encoding="utf-8") as f:
|
|
515
|
+
content = f.read()
|
|
516
|
+
deps = _parse_setup_dependencies(content)
|
|
517
|
+
package_names = {
|
|
518
|
+
_parse_pip_requirement(dep)[0].lower() for dep in deps if dep
|
|
519
|
+
}
|
|
520
|
+
debug_log(
|
|
521
|
+
f"setup.py-tpl dependencies: {sorted(package_names)}", "debug"
|
|
522
|
+
)
|
|
523
|
+
if "fastapi" in package_names:
|
|
524
|
+
fastapi_declared = True
|
|
525
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
526
|
+
error_msg = f"Error reading setup.py-tpl: {e}"
|
|
527
|
+
self.errors.append(error_msg)
|
|
528
|
+
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
if not sources_checked:
|
|
532
|
+
error_msg = (
|
|
533
|
+
"No dependency source found: expected one of requirements.txt-tpl, "
|
|
534
|
+
"pyproject.toml-tpl, or setup.py-tpl"
|
|
535
|
+
)
|
|
441
536
|
self.errors.append(error_msg)
|
|
442
537
|
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
443
538
|
return False
|
|
444
539
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
error_msg = "FastAPI dependency not found in requirements.txt-tpl"
|
|
452
|
-
self.errors.append(error_msg)
|
|
453
|
-
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
454
|
-
return False
|
|
455
|
-
except (OSError, UnicodeDecodeError) as e:
|
|
456
|
-
error_msg = f"Error reading requirements.txt-tpl: {e}"
|
|
540
|
+
if not fastapi_declared:
|
|
541
|
+
error_msg = (
|
|
542
|
+
"FastAPI dependency not found in any source ("
|
|
543
|
+
+ ", ".join(sources_checked)
|
|
544
|
+
+ ")"
|
|
545
|
+
)
|
|
457
546
|
self.errors.append(error_msg)
|
|
458
547
|
debug_log(f"Dependencies check failed: {error_msg}", "error")
|
|
459
548
|
return False
|
|
460
549
|
|
|
461
|
-
debug_log(
|
|
550
|
+
debug_log(
|
|
551
|
+
f"Dependencies check passed (sources: {', '.join(sources_checked)})",
|
|
552
|
+
"info",
|
|
553
|
+
)
|
|
462
554
|
return True
|
|
463
555
|
|
|
556
|
+
@staticmethod
|
|
557
|
+
def _extract_pyproject_dependency_names(
|
|
558
|
+
pyproject_path: Path,
|
|
559
|
+
) -> Tuple[set, Optional[str]]: # type: ignore
|
|
560
|
+
"""Parse a pyproject.toml-tpl and return (lowercased dep names, error).
|
|
561
|
+
|
|
562
|
+
``error`` is ``None`` on success. Returns an empty set with a descriptive
|
|
563
|
+
error string when the file is unreadable or malformed.
|
|
564
|
+
"""
|
|
565
|
+
try:
|
|
566
|
+
with open(pyproject_path, "rb") as f:
|
|
567
|
+
data = tomllib.load(f)
|
|
568
|
+
except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError) as e:
|
|
569
|
+
return set(), f"Invalid pyproject.toml-tpl: {e}"
|
|
570
|
+
|
|
571
|
+
project = data.get("project", {})
|
|
572
|
+
raw_deps = project.get("dependencies", []) or []
|
|
573
|
+
if not isinstance(raw_deps, list):
|
|
574
|
+
return set(), "pyproject.toml-tpl [project].dependencies must be a list"
|
|
575
|
+
|
|
576
|
+
names: set = set() # type: ignore
|
|
577
|
+
for dep in raw_deps:
|
|
578
|
+
if not isinstance(dep, str) or not dep.strip():
|
|
579
|
+
continue
|
|
580
|
+
name = _parse_pip_requirement(dep)[0]
|
|
581
|
+
if name:
|
|
582
|
+
names.add(name.lower())
|
|
583
|
+
return names, None
|
|
584
|
+
|
|
464
585
|
def _check_fastapi_implementation(self) -> bool:
|
|
465
586
|
"""Check if the template has a proper FastAPI server implementation."""
|
|
466
587
|
try:
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
from .config_builder import InteractiveConfigBuilder
|
|
10
10
|
from .prompts import (
|
|
11
11
|
prompt_additional_features,
|
|
12
|
+
prompt_architecture_preset,
|
|
12
13
|
prompt_authentication_selection,
|
|
13
14
|
prompt_basic_info,
|
|
14
15
|
prompt_caching_selection,
|
|
@@ -31,6 +32,7 @@ from .validators import (
|
|
|
31
32
|
__all__ = [
|
|
32
33
|
"InteractiveConfigBuilder",
|
|
33
34
|
"prompt_basic_info",
|
|
35
|
+
"prompt_architecture_preset",
|
|
34
36
|
"prompt_template_selection",
|
|
35
37
|
"prompt_database_selection",
|
|
36
38
|
"prompt_authentication_selection",
|
|
@@ -12,6 +12,7 @@ from fastapi_fastkit.utils.main import console, print_warning
|
|
|
12
12
|
|
|
13
13
|
from .prompts import (
|
|
14
14
|
prompt_additional_features,
|
|
15
|
+
prompt_architecture_preset,
|
|
15
16
|
prompt_basic_info,
|
|
16
17
|
prompt_template_selection,
|
|
17
18
|
)
|
|
@@ -51,11 +52,15 @@ class InteractiveConfigBuilder:
|
|
|
51
52
|
# Step 1: Basic information
|
|
52
53
|
self._collect_basic_info()
|
|
53
54
|
|
|
54
|
-
# Step 2:
|
|
55
|
+
# Step 2: Architecture preset — chosen early so downstream prompts
|
|
56
|
+
# (and later, preset-specific scaffolding) can branch on the layout.
|
|
57
|
+
self._collect_architecture_preset()
|
|
58
|
+
|
|
59
|
+
# Step 3: Always use Empty template as base for interactive mode
|
|
55
60
|
# (Feature selection will build the project incrementally)
|
|
56
61
|
self.config["base_template"] = None # None = Empty project
|
|
57
62
|
|
|
58
|
-
# Step
|
|
63
|
+
# Step 4: Feature selections
|
|
59
64
|
self._collect_feature_selections()
|
|
60
65
|
|
|
61
66
|
# Step 4: Build final configuration
|
|
@@ -78,6 +83,20 @@ class InteractiveConfigBuilder:
|
|
|
78
83
|
basic_info = prompt_basic_info()
|
|
79
84
|
self.config.update(basic_info)
|
|
80
85
|
|
|
86
|
+
def _collect_architecture_preset(self) -> None:
|
|
87
|
+
"""Collect the user's architecture preset choice.
|
|
88
|
+
|
|
89
|
+
Persists the canonical id on the config under ``architecture_preset``
|
|
90
|
+
(used by downstream code) plus the human-readable description under
|
|
91
|
+
``architecture_preset_description`` (used by the confirmation summary
|
|
92
|
+
so users see what their choice means, not just the raw id).
|
|
93
|
+
"""
|
|
94
|
+
preset = prompt_architecture_preset(self.settings)
|
|
95
|
+
self.config["architecture_preset"] = preset
|
|
96
|
+
self.config["architecture_preset_description"] = (
|
|
97
|
+
self.settings.ARCHITECTURE_PRESETS.get(preset, "")
|
|
98
|
+
)
|
|
99
|
+
|
|
81
100
|
def _collect_template_selection(self) -> None:
|
|
82
101
|
"""Collect template selection."""
|
|
83
102
|
template = prompt_template_selection(self.settings)
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
from typing import Any, Dict, List, Optional, cast
|
|
16
16
|
|
|
17
17
|
import click
|
|
18
|
-
from rich.panel import Panel
|
|
19
18
|
|
|
20
19
|
from fastapi_fastkit.utils.main import console, print_error
|
|
21
20
|
|
|
@@ -64,6 +63,58 @@ def prompt_basic_info() -> Dict[str, str]:
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
|
|
66
|
+
def prompt_architecture_preset(settings: Any) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Prompt for the project's architecture preset.
|
|
69
|
+
|
|
70
|
+
Run early in the interactive flow so downstream prompts (and later,
|
|
71
|
+
preset-specific scaffolding) can branch on the user's choice. Returns
|
|
72
|
+
the canonical preset id (one of the keys in
|
|
73
|
+
``settings.ARCHITECTURE_PRESETS``).
|
|
74
|
+
|
|
75
|
+
The recommended default (``settings.DEFAULT_ARCHITECTURE_PRESET``) is
|
|
76
|
+
annotated as such in the rendered table so users can spot it at a glance
|
|
77
|
+
and accept it with a single Enter press.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
settings: FastkitConfig instance
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Preset id, e.g. ``"domain-starter"``.
|
|
84
|
+
"""
|
|
85
|
+
console.print("\n[bold cyan]🧱 Architecture Preset[/bold cyan]")
|
|
86
|
+
console.print(
|
|
87
|
+
"[dim]Pick a project layout. Generation logic for each preset is "
|
|
88
|
+
"rolled out incrementally — your choice is recorded in the "
|
|
89
|
+
"config either way.[/dim]\n"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
presets: Dict[str, str] = settings.ARCHITECTURE_PRESETS
|
|
93
|
+
default_id: str = settings.DEFAULT_ARCHITECTURE_PRESET
|
|
94
|
+
# Derive a display-only mapping so the recommended default is visibly
|
|
95
|
+
# annotated without mutating the canonical catalog in settings.
|
|
96
|
+
display_options: Dict[str, str] = {
|
|
97
|
+
preset_id: (
|
|
98
|
+
f"{description} [bold green](recommended default)[/bold green]"
|
|
99
|
+
if preset_id == default_id
|
|
100
|
+
else description
|
|
101
|
+
)
|
|
102
|
+
for preset_id, description in presets.items()
|
|
103
|
+
}
|
|
104
|
+
render_selection_table("Architecture Presets", display_options)
|
|
105
|
+
|
|
106
|
+
preset_ids = list(presets.keys())
|
|
107
|
+
default_idx = preset_ids.index(default_id) + 1
|
|
108
|
+
|
|
109
|
+
choice = click.prompt(
|
|
110
|
+
f"\nSelect architecture preset (default: {default_id})",
|
|
111
|
+
type=click.IntRange(1, len(preset_ids)),
|
|
112
|
+
default=default_idx,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return cast(str, preset_ids[choice - 1])
|
|
116
|
+
|
|
117
|
+
|
|
67
118
|
def prompt_template_selection(settings: Any) -> Optional[str]:
|
|
68
119
|
"""
|
|
69
120
|
Prompt for base template selection.
|
|
@@ -133,6 +133,16 @@ def confirm_selections(config: Dict[str, Any]) -> bool:
|
|
|
133
133
|
table.add_row("Email", config.get("author_email", "N/A"))
|
|
134
134
|
table.add_row("Description", config.get("description", "N/A"))
|
|
135
135
|
|
|
136
|
+
# Architecture preset (interactive ``init`` always sets this; older
|
|
137
|
+
# callers may not — guard with a falsy check to stay backward compatible).
|
|
138
|
+
preset = config.get("architecture_preset")
|
|
139
|
+
if preset:
|
|
140
|
+
preset_description = config.get("architecture_preset_description", "")
|
|
141
|
+
table.add_row(
|
|
142
|
+
"Architecture Preset",
|
|
143
|
+
f"{preset} — {preset_description}" if preset_description else preset,
|
|
144
|
+
)
|
|
145
|
+
|
|
136
146
|
# Template
|
|
137
147
|
if config.get("base_template"):
|
|
138
148
|
table.add_row("Base Template", config["base_template"])
|
fastapi_fastkit/backend/main.py
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# --------------------------------------------------------------------------
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
+
import tomllib
|
|
8
9
|
from typing import Dict, List
|
|
9
10
|
|
|
10
11
|
import click
|
|
@@ -18,8 +19,9 @@ from fastapi_fastkit.core.exceptions import BackendExceptions
|
|
|
18
19
|
from fastapi_fastkit.core.settings import settings
|
|
19
20
|
from fastapi_fastkit.utils.logging import debug_log, get_logger
|
|
20
21
|
from fastapi_fastkit.utils.main import (
|
|
22
|
+
FASTKIT_DESCRIPTION_MARKER,
|
|
23
|
+
FASTKIT_TOOL_SECTION,
|
|
21
24
|
handle_exception,
|
|
22
|
-
print_error,
|
|
23
25
|
print_info,
|
|
24
26
|
print_success,
|
|
25
27
|
print_warning,
|
|
@@ -82,12 +84,19 @@ def find_template_core_modules(project_dir: str) -> Dict[str, str]:
|
|
|
82
84
|
|
|
83
85
|
def read_template_stack(template_path: str) -> List[str]:
|
|
84
86
|
"""
|
|
85
|
-
Read dependencies from template
|
|
87
|
+
Read dependencies from a template's metadata files.
|
|
88
|
+
|
|
89
|
+
Lookup order preserves legacy behaviour for templates that still ship
|
|
90
|
+
``requirements.txt-tpl`` while allowing modern pyproject-first templates
|
|
91
|
+
to resolve their stack from ``pyproject.toml-tpl``:
|
|
92
|
+
|
|
93
|
+
1. ``requirements.txt-tpl`` (legacy, highest precedence)
|
|
94
|
+
2. ``pyproject.toml-tpl`` ``[project].dependencies``
|
|
95
|
+
3. ``setup.py-tpl`` ``install_requires``
|
|
86
96
|
|
|
87
97
|
:param template_path: Path to the template directory
|
|
88
|
-
:return: List of
|
|
98
|
+
:return: List of PEP 508 requirement strings
|
|
89
99
|
"""
|
|
90
|
-
# First try requirements.txt-tpl
|
|
91
100
|
req_file = os.path.join(template_path, "requirements.txt-tpl")
|
|
92
101
|
if os.path.exists(req_file):
|
|
93
102
|
try:
|
|
@@ -100,7 +109,12 @@ def read_template_stack(template_path: str) -> List[str]:
|
|
|
100
109
|
)
|
|
101
110
|
return []
|
|
102
111
|
|
|
103
|
-
|
|
112
|
+
pyproject_file = os.path.join(template_path, "pyproject.toml-tpl")
|
|
113
|
+
if os.path.exists(pyproject_file):
|
|
114
|
+
deps = _parse_pyproject_dependencies(pyproject_file)
|
|
115
|
+
if deps:
|
|
116
|
+
return deps
|
|
117
|
+
|
|
104
118
|
setup_file = os.path.join(template_path, "setup.py-tpl")
|
|
105
119
|
if os.path.exists(setup_file):
|
|
106
120
|
try:
|
|
@@ -153,6 +167,28 @@ def _parse_setup_dependencies(content: str) -> List[str]:
|
|
|
153
167
|
return []
|
|
154
168
|
|
|
155
169
|
|
|
170
|
+
def _parse_pyproject_dependencies(pyproject_path: str) -> List[str]:
|
|
171
|
+
"""
|
|
172
|
+
Parse dependencies from a ``pyproject.toml-tpl`` file.
|
|
173
|
+
|
|
174
|
+
Returns the list of PEP 508 requirement strings from
|
|
175
|
+
``[project].dependencies``. Returns an empty list on parse errors so
|
|
176
|
+
callers can fall through to the next metadata source.
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
with open(pyproject_path, "rb") as f:
|
|
180
|
+
data = tomllib.load(f)
|
|
181
|
+
except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError) as e:
|
|
182
|
+
debug_log(f"Error reading pyproject.toml template: {e}", "error")
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
project = data.get("project", {})
|
|
186
|
+
raw_deps = project.get("dependencies", []) or []
|
|
187
|
+
if not isinstance(raw_deps, list):
|
|
188
|
+
return []
|
|
189
|
+
return [dep.strip() for dep in raw_deps if isinstance(dep, str) and dep.strip()]
|
|
190
|
+
|
|
191
|
+
|
|
156
192
|
# ------------------------------------------------------------
|
|
157
193
|
# Project Setup Functions
|
|
158
194
|
# ------------------------------------------------------------
|
|
@@ -185,6 +221,7 @@ def inject_project_metadata(
|
|
|
185
221
|
description,
|
|
186
222
|
)
|
|
187
223
|
_process_config_file(core_modules.get("config", ""), project_name)
|
|
224
|
+
_process_main_file(core_modules.get("main", ""), project_name)
|
|
188
225
|
|
|
189
226
|
print_success("Project metadata injected successfully")
|
|
190
227
|
|
|
@@ -235,6 +272,40 @@ def _process_setup_file(
|
|
|
235
272
|
raise BackendExceptions(f"Failed to process setup.py: {e}")
|
|
236
273
|
|
|
237
274
|
|
|
275
|
+
def _process_main_file(main_py: str, project_name: str) -> None:
|
|
276
|
+
"""
|
|
277
|
+
Replace project name placeholders in the template main.py file.
|
|
278
|
+
|
|
279
|
+
Some templates (e.g. fastapi-single-module) inline the project name directly
|
|
280
|
+
in main.py rather than reading it from a settings module, so the placeholder
|
|
281
|
+
must be substituted here as well.
|
|
282
|
+
|
|
283
|
+
:param main_py: Path to main.py file
|
|
284
|
+
:param project_name: Project name
|
|
285
|
+
"""
|
|
286
|
+
if not main_py or not os.path.exists(main_py):
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
with open(main_py, "r", encoding="utf-8") as f:
|
|
291
|
+
content = f.read()
|
|
292
|
+
|
|
293
|
+
if "<project_name>" not in content:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
content = content.replace("<project_name>", project_name)
|
|
297
|
+
|
|
298
|
+
with open(main_py, "w", encoding="utf-8") as f:
|
|
299
|
+
f.write(content)
|
|
300
|
+
|
|
301
|
+
debug_log("Injected project name into main.py", "info")
|
|
302
|
+
print_info("Injected project name into main.py")
|
|
303
|
+
|
|
304
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
305
|
+
debug_log(f"Error processing main.py: {e}", "error")
|
|
306
|
+
raise BackendExceptions(f"Failed to process main.py: {e}")
|
|
307
|
+
|
|
308
|
+
|
|
238
309
|
def _process_config_file(config_py: str, project_name: str) -> None:
|
|
239
310
|
"""
|
|
240
311
|
Process config file and inject project name.
|
|
@@ -297,6 +368,8 @@ def _process_pyproject_file(
|
|
|
297
368
|
for placeholder, value in replacements.items():
|
|
298
369
|
content = content.replace(placeholder, value)
|
|
299
370
|
|
|
371
|
+
content = _ensure_pyproject_fastkit_markers(content)
|
|
372
|
+
|
|
300
373
|
with open(pyproject_toml, "w", encoding="utf-8") as f:
|
|
301
374
|
f.write(content)
|
|
302
375
|
|
|
@@ -308,6 +381,41 @@ def _process_pyproject_file(
|
|
|
308
381
|
raise BackendExceptions(f"Failed to process pyproject.toml: {e}")
|
|
309
382
|
|
|
310
383
|
|
|
384
|
+
_DESCRIPTION_LINE_PATTERN = re.compile(
|
|
385
|
+
r'^(?P<prefix>description\s*=\s*)"(?P<value>.*?)"(?P<suffix>\s*)$',
|
|
386
|
+
re.MULTILINE,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _ensure_pyproject_fastkit_markers(content: str) -> str:
|
|
391
|
+
"""Idempotently ensure pyproject content carries FastAPI-fastkit markers.
|
|
392
|
+
|
|
393
|
+
Adds the canonical description marker if the ``description`` field does
|
|
394
|
+
not already include it (case-insensitive check), and appends a
|
|
395
|
+
``[tool.fastapi-fastkit]`` section with ``managed = true`` if one is not
|
|
396
|
+
already present. Safe to run on already-marked content — no duplicates
|
|
397
|
+
are introduced.
|
|
398
|
+
"""
|
|
399
|
+
marker = FASTKIT_DESCRIPTION_MARKER
|
|
400
|
+
match = _DESCRIPTION_LINE_PATTERN.search(content)
|
|
401
|
+
if match and marker.lower() not in match.group("value").lower():
|
|
402
|
+
existing = match.group("value")
|
|
403
|
+
new_value = f"{marker} {existing}".strip() if existing else marker
|
|
404
|
+
content = (
|
|
405
|
+
content[: match.start()]
|
|
406
|
+
+ f'{match.group("prefix")}"{new_value}"{match.group("suffix")}'
|
|
407
|
+
+ content[match.end() :]
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
tool_header = f"[tool.{FASTKIT_TOOL_SECTION}]"
|
|
411
|
+
if tool_header not in content:
|
|
412
|
+
if not content.endswith("\n"):
|
|
413
|
+
content += "\n"
|
|
414
|
+
content += f"\n{tool_header}\nmanaged = true\n"
|
|
415
|
+
|
|
416
|
+
return content
|
|
417
|
+
|
|
418
|
+
|
|
311
419
|
def update_setup_py_dependencies(project_dir: str, dependencies: List[str]) -> None:
|
|
312
420
|
"""
|
|
313
421
|
Update setup.py file's install_requires list with new dependencies.
|
|
@@ -519,7 +627,7 @@ def _ensure_project_structure(src_dir: str) -> Dict[str, str]:
|
|
|
519
627
|
# Create __init__.py if it doesn't exist
|
|
520
628
|
init_file = os.path.join(dir_path, "__init__.py")
|
|
521
629
|
if not os.path.exists(init_file):
|
|
522
|
-
with open(init_file, "w")
|
|
630
|
+
with open(init_file, "w"):
|
|
523
631
|
pass
|
|
524
632
|
|
|
525
633
|
return target_dirs
|
|
@@ -136,7 +136,7 @@ class PackageManagerFactory:
|
|
|
136
136
|
:param manager_class: Package manager class
|
|
137
137
|
"""
|
|
138
138
|
if not issubclass(manager_class, BasePackageManager):
|
|
139
|
-
raise ValueError(
|
|
139
|
+
raise ValueError("Manager class must inherit from BasePackageManager")
|
|
140
140
|
|
|
141
141
|
self._managers[name.lower()] = manager_class
|
|
142
142
|
debug_log(f"Registered package manager: {name}", "info")
|
|
@@ -152,11 +152,11 @@ class PdmManager(BasePackageManager):
|
|
|
152
152
|
|
|
153
153
|
# Create basic pyproject.toml content as string
|
|
154
154
|
pyproject_content = f"""[project]
|
|
155
|
-
name = "{project_name or
|
|
155
|
+
name = "{project_name or "fastapi-project"}"
|
|
156
156
|
version = "0.1.0"
|
|
157
|
-
description = "{description or
|
|
157
|
+
description = "{description or "A FastAPI project"}"
|
|
158
158
|
authors = [
|
|
159
|
-
{{name = "{author or
|
|
159
|
+
{{name = "{author or "Author"}", email = "{author_email or "author@example.com"}"}},
|
|
160
160
|
]
|
|
161
161
|
dependencies = {deps_toml}
|
|
162
162
|
requires-python = ">=3.8"
|