FastAPI-fastkit 1.1.4__py3-none-any.whl → 1.2.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 (39) hide show
  1. fastapi_fastkit/__init__.py +1 -1
  2. fastapi_fastkit/backend/inspector.py +170 -12
  3. fastapi_fastkit/backend/interactive/__init__.py +51 -0
  4. fastapi_fastkit/backend/interactive/config_builder.py +186 -0
  5. fastapi_fastkit/backend/interactive/prompts.py +528 -0
  6. fastapi_fastkit/backend/interactive/selectors.py +241 -0
  7. fastapi_fastkit/backend/interactive/validators.py +163 -0
  8. fastapi_fastkit/backend/main.py +77 -0
  9. fastapi_fastkit/backend/package_managers/pdm_manager.py +6 -2
  10. fastapi_fastkit/backend/package_managers/pip_manager.py +6 -2
  11. fastapi_fastkit/backend/package_managers/poetry_manager.py +6 -2
  12. fastapi_fastkit/backend/package_managers/uv_manager.py +6 -2
  13. fastapi_fastkit/backend/project_builder/__init__.py +17 -0
  14. fastapi_fastkit/backend/project_builder/config_generator.py +594 -0
  15. fastapi_fastkit/backend/project_builder/dependency_collector.py +210 -0
  16. fastapi_fastkit/cli.py +218 -20
  17. fastapi_fastkit/core/settings.py +74 -0
  18. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl +2 -1
  19. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +1 -0
  20. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl +2 -1
  21. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/requirements.txt-tpl +1 -0
  22. fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl +2 -1
  23. fastapi_fastkit/fastapi_project_template/fastapi-default/requirements.txt-tpl +1 -0
  24. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl +2 -1
  25. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/requirements.txt-tpl +1 -0
  26. fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +2 -1
  27. fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +1 -0
  28. fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +2 -1
  29. fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +1 -0
  30. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +2 -1
  31. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +1 -0
  32. fastapi_fastkit/fastapi_project_template/fastapi-single-module/pyproject.toml-tpl +2 -1
  33. fastapi_fastkit/fastapi_project_template/fastapi-single-module/requirements.txt-tpl +1 -0
  34. fastapi_fastkit/utils/main.py +121 -10
  35. {fastapi_fastkit-1.1.4.dist-info → fastapi_fastkit-1.2.0.dist-info}/METADATA +9 -6
  36. {fastapi_fastkit-1.1.4.dist-info → fastapi_fastkit-1.2.0.dist-info}/RECORD +39 -31
  37. {fastapi_fastkit-1.1.4.dist-info → fastapi_fastkit-1.2.0.dist-info}/WHEEL +1 -1
  38. {fastapi_fastkit-1.1.4.dist-info → fastapi_fastkit-1.2.0.dist-info}/entry_points.txt +0 -0
  39. {fastapi_fastkit-1.1.4.dist-info → fastapi_fastkit-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1 +1 @@
1
- __version__ = 'v1.1.4'
1
+ __version__ = 'v1.2.0'
@@ -26,13 +26,12 @@ import sys
26
26
  from pathlib import Path
27
27
  from typing import Any, Callable, Dict, List, Optional, Tuple
28
28
 
29
- import yaml # type: ignore
29
+ import yaml
30
30
 
31
31
  from fastapi_fastkit.backend.main import (
32
32
  create_venv,
33
33
  find_template_core_modules,
34
34
  inject_project_metadata,
35
- install_dependencies,
36
35
  install_dependencies_with_manager,
37
36
  )
38
37
  from fastapi_fastkit.backend.transducer import copy_and_convert_template
@@ -50,17 +49,38 @@ class TemplateInspector:
50
49
  Uses context manager protocol for proper resource cleanup.
51
50
  """
52
51
 
53
- def __init__(self, template_path: str):
52
+ def __init__(self, template_path: str, temp_base_dir: Optional[str] = None):
54
53
  self.template_path = Path(template_path)
55
54
  self.errors: List[str] = []
56
55
  self.warnings: List[str] = []
57
- self.temp_dir = os.path.join(os.path.dirname(__file__), "temp")
56
+ template_name = Path(template_path).name
57
+
58
+ # use temp_base_dir or fall back to backend directory (temp)
59
+ if temp_base_dir:
60
+ self.temp_dir = os.path.join(temp_base_dir, f"temp_{template_name}")
61
+ else:
62
+ self.temp_dir = os.path.join(
63
+ os.path.dirname(__file__), f"temp_{template_name}"
64
+ )
65
+
58
66
  self._cleanup_needed = False
59
67
  self.template_config: Optional[Dict[str, Any]] = None
60
68
 
61
69
  def __enter__(self) -> "TemplateInspector":
62
70
  """Enter context manager - create temp directory and copy template."""
63
71
  try:
72
+ # Clean up any existing temp directory for this template
73
+ if os.path.exists(self.temp_dir):
74
+ debug_log(
75
+ f"Cleaning up existing temp directory: {self.temp_dir}", "info"
76
+ )
77
+ try:
78
+ shutil.rmtree(self.temp_dir)
79
+ except OSError as e:
80
+ debug_log(
81
+ f"Failed to cleanup existing temp directory: {e}", "warning"
82
+ )
83
+
64
84
  os.makedirs(self.temp_dir, exist_ok=True)
65
85
  copy_and_convert_template(str(self.template_path), self.temp_dir)
66
86
 
@@ -81,18 +101,142 @@ class TemplateInspector:
81
101
  self._cleanup()
82
102
 
83
103
  def _cleanup(self) -> None:
84
- """Cleanup temp directory if it exists and cleanup is needed."""
104
+ """Cleanup temp directory."""
85
105
  if self._cleanup_needed and os.path.exists(self.temp_dir):
106
+ temp_dir_path = self.temp_dir
86
107
  try:
87
- shutil.rmtree(self.temp_dir)
88
- debug_log(f"Cleaned up temp directory {self.temp_dir}", "debug")
89
- except OSError as e:
108
+ self._cleanup_docker_services()
109
+
110
+ import time
111
+
112
+ time.sleep(3)
113
+
114
+ self._force_cleanup_directory(temp_dir_path)
115
+
116
+ except Exception as e:
90
117
  debug_log(
91
- f"Failed to cleanup temp directory {self.temp_dir}: {e}", "warning"
118
+ f"Warning: Unexpected error during cleanup of {temp_dir_path}: {e}",
119
+ "warning",
92
120
  )
121
+ try:
122
+ self._force_cleanup_directory(temp_dir_path)
123
+ except Exception:
124
+ pass
93
125
  finally:
94
126
  self._cleanup_needed = False
95
127
 
128
+ def _force_cleanup_directory(self, directory_path: str) -> None:
129
+ """Force cleanup of directory with multiple strategies."""
130
+ import stat
131
+ import time
132
+
133
+ max_retries = 5
134
+ for attempt in range(max_retries):
135
+ try:
136
+ shutil.rmtree(directory_path)
137
+ debug_log(
138
+ f"Successfully cleaned up temp directory: {directory_path}", "info"
139
+ )
140
+ return
141
+ except (OSError, PermissionError) as e:
142
+ if attempt < max_retries - 1:
143
+ debug_log(
144
+ f"Attempt {attempt + 1} failed to cleanup {directory_path}: {e}",
145
+ "warning",
146
+ )
147
+
148
+ try:
149
+ self._fix_directory_permissions(directory_path)
150
+ shutil.rmtree(directory_path)
151
+ debug_log(
152
+ f"Successfully cleaned up temp directory after permission fix: {directory_path}",
153
+ "info",
154
+ )
155
+ return
156
+ except Exception:
157
+ pass
158
+
159
+ try:
160
+ self._remove_directory_contents(directory_path)
161
+ os.rmdir(directory_path)
162
+ debug_log(
163
+ f"Successfully cleaned up temp directory by removing contents: {directory_path}",
164
+ "info",
165
+ )
166
+ return
167
+ except Exception:
168
+ pass
169
+
170
+ time.sleep(2)
171
+ else:
172
+ debug_log(
173
+ f"Failed to cleanup temp directory after {max_retries} attempts: {directory_path}",
174
+ "warning",
175
+ )
176
+ try:
177
+ import subprocess
178
+
179
+ if os.name == "nt": # Windows
180
+ subprocess.run(
181
+ ["rmdir", "/s", "/q", directory_path],
182
+ check=False,
183
+ shell=True,
184
+ )
185
+ else: # Unix-like
186
+ subprocess.run(["rm", "-rf", directory_path], check=False)
187
+ debug_log(
188
+ f"Force cleanup completed for: {directory_path}", "info"
189
+ )
190
+ except Exception:
191
+ debug_log(
192
+ f"All cleanup attempts failed for: {directory_path}",
193
+ "warning",
194
+ )
195
+
196
+ def _fix_directory_permissions(self, directory_path: str) -> None:
197
+ """Fix directory permissions to allow removal."""
198
+ import os
199
+ import stat
200
+
201
+ def fix_permissions(path: str) -> None:
202
+ try:
203
+ os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
204
+ except Exception:
205
+ pass
206
+
207
+ # Fix permissions for directory and all contents
208
+ fix_permissions(directory_path)
209
+ for root, dirs, files in os.walk(directory_path):
210
+ fix_permissions(root)
211
+ for d in dirs:
212
+ fix_permissions(os.path.join(root, d))
213
+ for f in files:
214
+ fix_permissions(os.path.join(root, f))
215
+
216
+ def _remove_directory_contents(self, directory_path: str) -> None:
217
+ """Remove directory contents file by file."""
218
+ import os
219
+ import stat
220
+
221
+ for root, dirs, files in os.walk(directory_path, topdown=False):
222
+ # Remove files first
223
+ for file in files:
224
+ file_path = os.path.join(root, file)
225
+ try:
226
+ os.chmod(file_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
227
+ os.remove(file_path)
228
+ except Exception:
229
+ pass
230
+
231
+ # Remove directories
232
+ for dir_name in dirs:
233
+ dir_path = os.path.join(root, dir_name)
234
+ try:
235
+ os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
236
+ os.rmdir(dir_path)
237
+ except Exception:
238
+ pass
239
+
96
240
  def _load_template_config(self) -> Optional[Dict[str, Any]]:
97
241
  """Load template configuration from template-config.yml if available."""
98
242
  config_file = os.path.join(self.temp_dir, "template-config.yml")
@@ -1036,12 +1180,23 @@ class TemplateInspector:
1036
1180
  try:
1037
1181
  debug_log("Cleaning up Docker services", "info")
1038
1182
  subprocess.run(
1039
- ["docker-compose", "down", "-v"],
1183
+ ["docker-compose", "down", "-v", "--remove-orphans"],
1040
1184
  cwd=self.temp_dir,
1041
1185
  capture_output=True,
1042
1186
  text=True,
1043
1187
  timeout=60,
1044
1188
  )
1189
+
1190
+ try:
1191
+ subprocess.run(
1192
+ ["docker", "system", "prune", "-f"],
1193
+ capture_output=True,
1194
+ text=True,
1195
+ timeout=30,
1196
+ )
1197
+ except Exception:
1198
+ pass
1199
+
1045
1200
  except Exception as e:
1046
1201
  debug_log(f"Failed to cleanup Docker services: {e}", "warning")
1047
1202
 
@@ -1086,11 +1241,14 @@ class TemplateInspector:
1086
1241
  }
1087
1242
 
1088
1243
 
1089
- def inspect_fastapi_template(template_path: str) -> Dict[str, Any]:
1244
+ def inspect_fastapi_template(
1245
+ template_path: str, temp_base_dir: Optional[str] = None
1246
+ ) -> Dict[str, Any]:
1090
1247
  """
1091
1248
  Convenience function to inspect a FastAPI template.
1092
1249
 
1093
1250
  :param template_path: Path to the template to inspect
1251
+ :param temp_base_dir: Base directory for temporary files (defaults to backend directory)
1094
1252
  :return: Inspection report dictionary
1095
1253
  """
1096
1254
  template_name = Path(template_path).name
@@ -1098,7 +1256,7 @@ def inspect_fastapi_template(template_path: str) -> Dict[str, Any]:
1098
1256
  f"Starting template inspection for {template_name} at {template_path}", "info"
1099
1257
  )
1100
1258
 
1101
- with TemplateInspector(template_path) as inspector:
1259
+ with TemplateInspector(template_path, temp_base_dir) as inspector:
1102
1260
  is_valid = inspector.inspect_template()
1103
1261
  report = inspector.get_report()
1104
1262
 
@@ -0,0 +1,51 @@
1
+ # --------------------------------------------------------------------------
2
+ # Interactive module for FastAPI-fastkit
3
+ #
4
+ # Provides interactive prompts and configuration building for dynamic
5
+ # project creation.
6
+ #
7
+ # @author bnbong bbbong9@gmail.com
8
+ # --------------------------------------------------------------------------
9
+ from .config_builder import InteractiveConfigBuilder
10
+ from .prompts import (
11
+ prompt_additional_features,
12
+ prompt_authentication_selection,
13
+ prompt_basic_info,
14
+ prompt_caching_selection,
15
+ prompt_custom_packages,
16
+ prompt_database_selection,
17
+ prompt_deployment_options,
18
+ prompt_monitoring_selection,
19
+ prompt_package_manager_selection,
20
+ prompt_template_selection,
21
+ prompt_testing_selection,
22
+ prompt_utilities_selection,
23
+ )
24
+ from .selectors import confirm_selections, multi_select_prompt, render_selection_table
25
+ from .validators import (
26
+ sanitize_custom_packages,
27
+ validate_feature_compatibility,
28
+ validate_package_name,
29
+ )
30
+
31
+ __all__ = [
32
+ "InteractiveConfigBuilder",
33
+ "prompt_basic_info",
34
+ "prompt_template_selection",
35
+ "prompt_database_selection",
36
+ "prompt_authentication_selection",
37
+ "prompt_additional_features",
38
+ "prompt_testing_selection",
39
+ "prompt_deployment_options",
40
+ "prompt_custom_packages",
41
+ "prompt_caching_selection",
42
+ "prompt_monitoring_selection",
43
+ "prompt_utilities_selection",
44
+ "prompt_package_manager_selection",
45
+ "render_selection_table",
46
+ "multi_select_prompt",
47
+ "confirm_selections",
48
+ "validate_package_name",
49
+ "validate_feature_compatibility",
50
+ "sanitize_custom_packages",
51
+ ]
@@ -0,0 +1,186 @@
1
+ # --------------------------------------------------------------------------
2
+ # Build comprehensive project configuration from user selections
3
+ #
4
+ # Aggregates all user choices into a structured configuration dict
5
+ # that can be consumed by the project builder.
6
+ #
7
+ # @author bnbong bbbong9@gmail.com
8
+ # --------------------------------------------------------------------------
9
+ from typing import Any, Dict, List
10
+
11
+ from fastapi_fastkit.utils.main import console, print_warning
12
+
13
+ from .prompts import (
14
+ prompt_additional_features,
15
+ prompt_basic_info,
16
+ prompt_template_selection,
17
+ )
18
+ from .selectors import confirm_selections
19
+ from .validators import sanitize_custom_packages, validate_feature_compatibility
20
+
21
+
22
+ class InteractiveConfigBuilder:
23
+ """
24
+ Builds project configuration from interactive prompts.
25
+
26
+ Orchestrates the entire interactive flow and aggregates all
27
+ user selections into a cohesive configuration dictionary.
28
+ """
29
+
30
+ def __init__(self, settings: Any) -> None:
31
+ """
32
+ Initialize the config builder.
33
+
34
+ Args:
35
+ settings: FastkitConfig instance
36
+ """
37
+ self.settings = settings
38
+ self.config: Dict[str, Any] = {}
39
+
40
+ def run_interactive_flow(self) -> Dict[str, Any]:
41
+ """
42
+ Execute full interactive flow and return config.
43
+
44
+ Returns:
45
+ Complete project configuration dictionary
46
+ """
47
+ console.print(
48
+ "\n[bold magenta]⚡ FastAPI-fastkit Interactive Project Setup ⚡[/bold magenta]\n"
49
+ )
50
+
51
+ # Step 1: Basic information
52
+ self._collect_basic_info()
53
+
54
+ # Step 2: Always use Empty template as base for interactive mode
55
+ # (Feature selection will build the project incrementally)
56
+ self.config["base_template"] = None # None = Empty project
57
+
58
+ # Step 3: Feature selections
59
+ self._collect_feature_selections()
60
+
61
+ # Step 4: Build final configuration
62
+ final_config = self._build_final_config()
63
+
64
+ # Step 5: Validate compatibility
65
+ is_valid, warning = validate_feature_compatibility(final_config)
66
+ if warning:
67
+ print_warning(warning, title="Feature Compatibility")
68
+
69
+ # Step 6: Confirm selections
70
+ if confirm_selections(final_config):
71
+ return final_config
72
+ else:
73
+ print_warning("Project creation cancelled by user.")
74
+ return {}
75
+
76
+ def _collect_basic_info(self) -> None:
77
+ """Collect basic project information."""
78
+ basic_info = prompt_basic_info()
79
+ self.config.update(basic_info)
80
+
81
+ def _collect_template_selection(self) -> None:
82
+ """Collect template selection."""
83
+ template = prompt_template_selection(self.settings)
84
+ self.config["base_template"] = template
85
+
86
+ def _collect_feature_selections(self) -> None:
87
+ """Collect all feature selections."""
88
+ features = prompt_additional_features(self.settings)
89
+ self.config.update(features)
90
+
91
+ def _build_final_config(self) -> Dict[str, Any]:
92
+ """
93
+ Build and validate final configuration.
94
+
95
+ Returns:
96
+ Complete configuration dictionary with collected dependencies
97
+ """
98
+ # Collect all dependencies
99
+ all_deps = self._collect_all_dependencies()
100
+
101
+ # Add to config
102
+ self.config["all_dependencies"] = all_deps
103
+
104
+ return self.config
105
+
106
+ def _collect_all_dependencies(self) -> List[str]:
107
+ """
108
+ Collect all dependencies from selected features.
109
+
110
+ Returns:
111
+ Deduplicated list of all package dependencies
112
+ """
113
+ dependencies = set()
114
+
115
+ # Always add base FastAPI dependencies
116
+ dependencies.update(["fastapi", "uvicorn", "pydantic", "pydantic-settings"])
117
+
118
+ # Database dependencies
119
+ db_info = self.config.get("database", {})
120
+ if isinstance(db_info, dict) and db_info.get("packages"):
121
+ dependencies.update(db_info["packages"])
122
+
123
+ # Authentication dependencies
124
+ auth_type = self.config.get("authentication", "None")
125
+ if auth_type != "None":
126
+ auth_packages = self.settings.PACKAGE_CATALOG["authentication"].get(
127
+ auth_type, []
128
+ )
129
+ dependencies.update(auth_packages)
130
+
131
+ # Async tasks dependencies
132
+ tasks_type = self.config.get("async_tasks", "None")
133
+ if tasks_type != "None":
134
+ task_packages = self.settings.PACKAGE_CATALOG["async_tasks"].get(
135
+ tasks_type, []
136
+ )
137
+ dependencies.update(task_packages)
138
+
139
+ # Caching dependencies
140
+ cache_type = self.config.get("caching", "None")
141
+ if cache_type != "None":
142
+ cache_packages = self.settings.PACKAGE_CATALOG["caching"].get(
143
+ cache_type, []
144
+ )
145
+ dependencies.update(cache_packages)
146
+
147
+ # Monitoring dependencies
148
+ monitoring_type = self.config.get("monitoring", "None")
149
+ if monitoring_type != "None":
150
+ monitoring_packages = self.settings.PACKAGE_CATALOG["monitoring"].get(
151
+ monitoring_type, []
152
+ )
153
+ dependencies.update(monitoring_packages)
154
+
155
+ # Testing dependencies
156
+ testing_type = self.config.get("testing", "None")
157
+ if testing_type != "None":
158
+ testing_packages = self.settings.PACKAGE_CATALOG["testing"].get(
159
+ testing_type, []
160
+ )
161
+ dependencies.update(testing_packages)
162
+
163
+ # Utilities dependencies
164
+ utilities = self.config.get("utilities", [])
165
+ for util in utilities:
166
+ if util in self.settings.PACKAGE_CATALOG["utilities"]:
167
+ util_packages = self.settings.PACKAGE_CATALOG["utilities"][util]
168
+ dependencies.update(util_packages)
169
+
170
+ # Custom packages
171
+ custom_packages = self.config.get("custom_packages", [])
172
+ if custom_packages:
173
+ sanitized = sanitize_custom_packages(custom_packages)
174
+ dependencies.update(sanitized)
175
+
176
+ # Convert to sorted list
177
+ return sorted(list(dependencies))
178
+
179
+ def get_config(self) -> Dict[str, Any]:
180
+ """
181
+ Get the current configuration.
182
+
183
+ Returns:
184
+ Current configuration dictionary
185
+ """
186
+ return self.config