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
@@ -0,0 +1,241 @@
1
+ # --------------------------------------------------------------------------
2
+ # Selection logic and UI rendering for interactive mode
3
+ #
4
+ # Provides:
5
+ # - Table-based option displays using rich
6
+ # - Multi-select prompts
7
+ # - Confirmation dialogs
8
+ #
9
+ # @author bnbong bbbong9@gmail.com
10
+ # --------------------------------------------------------------------------
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from rich.panel import Panel
14
+ from rich.table import Table
15
+
16
+ from fastapi_fastkit.utils.main import console
17
+
18
+
19
+ def render_selection_table(
20
+ title: str, options: Dict[str, str], show_numbers: bool = True
21
+ ) -> None:
22
+ """
23
+ Render a selection table using rich.
24
+
25
+ Args:
26
+ title: Table title
27
+ options: Dictionary of option_key: description
28
+ show_numbers: Whether to show numbered options (for single-select)
29
+ """
30
+ table = Table(title=title, show_header=True, header_style="bold magenta")
31
+
32
+ if show_numbers:
33
+ table.add_column("Option", style="cyan", no_wrap=True)
34
+ table.add_column("Name", style="green")
35
+ table.add_column("Description", style="yellow")
36
+
37
+ for i, (key, description) in enumerate(options.items(), 1):
38
+ if show_numbers:
39
+ table.add_row(str(i), key, description)
40
+ else:
41
+ table.add_row(key, description)
42
+
43
+ console.print(table)
44
+
45
+
46
+ def render_feature_options(
47
+ category: str, options: List[str], descriptions: Dict[str, str]
48
+ ) -> None:
49
+ """
50
+ Render feature options for multi-select.
51
+
52
+ Args:
53
+ category: Feature category name
54
+ options: List of option names
55
+ descriptions: Dictionary of option descriptions
56
+ """
57
+ console.print(f"\n[bold cyan]{category}[/bold cyan]")
58
+ console.print(
59
+ "[dim]Select options by entering numbers (comma-separated, e.g., 1,3,4)[/dim]\n"
60
+ )
61
+
62
+ for i, option in enumerate(options, 1):
63
+ desc = descriptions.get(option, "")
64
+ console.print(f" [cyan]{i}[/cyan]. {option} [dim]{desc}[/dim]")
65
+
66
+
67
+ def multi_select_prompt(
68
+ title: str, options: List[str], descriptions: Optional[Dict[str, str]] = None
69
+ ) -> List[int]:
70
+ """
71
+ Multi-select prompt with comma-separated input.
72
+
73
+ Args:
74
+ title: Prompt title
75
+ options: List of options
76
+ descriptions: Optional descriptions for each option
77
+
78
+ Returns:
79
+ List of selected indices (0-based)
80
+ """
81
+ descriptions = descriptions or {}
82
+
83
+ console.print(f"\n[bold]{title}[/bold]")
84
+ render_feature_options(title, options, descriptions)
85
+
86
+ while True:
87
+ selected = console.input(
88
+ "\n[cyan]Your choice (or press Enter to skip):[/cyan] "
89
+ ).strip()
90
+
91
+ if not selected:
92
+ return []
93
+
94
+ try:
95
+ # Parse comma-separated numbers
96
+ indices = [int(x.strip()) for x in selected.split(",") if x.strip()]
97
+
98
+ # Validate indices
99
+ if all(1 <= idx <= len(options) for idx in indices):
100
+ # Convert to 0-based indices
101
+ return [idx - 1 for idx in indices]
102
+ else:
103
+ console.print(
104
+ f"[red]Invalid selection. Please enter numbers between 1 and {len(options)}[/red]"
105
+ )
106
+ except ValueError:
107
+ console.print(
108
+ "[red]Invalid input. Please enter comma-separated numbers (e.g., 1,3,4)[/red]"
109
+ )
110
+
111
+
112
+ def confirm_selections(config: Dict[str, Any]) -> bool:
113
+ """
114
+ Display summary and confirm all selections.
115
+
116
+ Args:
117
+ config: Complete project configuration
118
+
119
+ Returns:
120
+ True if user confirms, False otherwise
121
+ """
122
+ table = Table(
123
+ title="📋 Project Configuration Summary",
124
+ show_header=True,
125
+ header_style="bold magenta",
126
+ )
127
+ table.add_column("Setting", style="cyan", no_wrap=True)
128
+ table.add_column("Value", style="green")
129
+
130
+ # Basic information
131
+ table.add_row("Project Name", config.get("project_name", "N/A"))
132
+ table.add_row("Author", config.get("author", "N/A"))
133
+ table.add_row("Email", config.get("author_email", "N/A"))
134
+ table.add_row("Description", config.get("description", "N/A"))
135
+
136
+ # Template
137
+ if config.get("base_template"):
138
+ table.add_row("Base Template", config["base_template"])
139
+
140
+ # Database
141
+ db_info = config.get("database", {})
142
+ if db_info.get("type") != "None":
143
+ table.add_row("Database", db_info.get("type", "None"))
144
+
145
+ # Authentication
146
+ auth_type = config.get("authentication", "None")
147
+ if auth_type != "None":
148
+ table.add_row("Authentication", auth_type)
149
+
150
+ # Async tasks
151
+ task_type = config.get("async_tasks", "None")
152
+ if task_type != "None":
153
+ table.add_row("Async Tasks", task_type)
154
+
155
+ # Caching
156
+ cache_type = config.get("caching", "None")
157
+ if cache_type != "None":
158
+ table.add_row("Caching", cache_type)
159
+
160
+ # Monitoring
161
+ monitoring_type = config.get("monitoring", "None")
162
+ if monitoring_type != "None":
163
+ table.add_row("Monitoring", monitoring_type)
164
+
165
+ # Testing
166
+ testing_type = config.get("testing", "None")
167
+ if testing_type != "None":
168
+ table.add_row("Testing", testing_type)
169
+
170
+ # Utilities
171
+ utilities = config.get("utilities", [])
172
+ if utilities:
173
+ table.add_row("Utilities", ", ".join(utilities))
174
+
175
+ # Custom packages
176
+ custom = config.get("custom_packages", [])
177
+ if custom:
178
+ table.add_row("Custom Packages", ", ".join(custom))
179
+
180
+ # Package manager
181
+ table.add_row("Package Manager", config.get("package_manager", "uv"))
182
+
183
+ console.print("\n")
184
+ console.print(table)
185
+ console.print("\n")
186
+
187
+ # Show total dependencies count
188
+ total_deps = len(config.get("all_dependencies", []))
189
+ console.print(f"[bold cyan]Total dependencies to install:[/bold cyan] {total_deps}")
190
+
191
+ # Ask for confirmation
192
+ response = (
193
+ console.input(
194
+ "\n[bold yellow]Proceed with project creation? [Y/n]:[/bold yellow] "
195
+ )
196
+ .strip()
197
+ .lower()
198
+ )
199
+
200
+ return response in ["y", "yes", ""]
201
+
202
+
203
+ def display_feature_catalog(
204
+ catalog: Dict[str, Dict[str, List[str]]], descriptions: Dict[str, str]
205
+ ) -> None:
206
+ """
207
+ Display the complete feature catalog.
208
+
209
+ Args:
210
+ catalog: Package catalog from settings
211
+ descriptions: Feature descriptions from settings
212
+ """
213
+ panel = Panel(
214
+ "[bold cyan]FastAPI-fastkit Feature Catalog[/bold cyan]\n\n"
215
+ "Available features and their associated packages for interactive project creation.",
216
+ title="📚 Feature Catalog",
217
+ border_style="cyan",
218
+ )
219
+ console.print(panel)
220
+
221
+ for category, options in catalog.items():
222
+ if category == "utilities":
223
+ continue # Skip utilities as they may have empty package lists
224
+
225
+ desc = descriptions.get(category, category.title())
226
+ table = Table(title=f"{desc}", show_header=True, header_style="bold yellow")
227
+ table.add_column("Option", style="cyan", no_wrap=True)
228
+ table.add_column("Packages", style="green")
229
+
230
+ for option_name, packages in options.items():
231
+ if option_name == "None":
232
+ continue
233
+ pkg_list = (
234
+ ", ".join(packages)
235
+ if packages
236
+ else "[dim](No additional packages)[/dim]"
237
+ )
238
+ table.add_row(option_name, pkg_list)
239
+
240
+ console.print("\n")
241
+ console.print(table)
@@ -0,0 +1,163 @@
1
+ # --------------------------------------------------------------------------
2
+ # Input validation utilities for interactive mode
3
+ #
4
+ # Provides validation functions for:
5
+ # - Package names
6
+ # - Project names
7
+ # - Feature compatibility checking
8
+ #
9
+ # @author bnbong bbbong9@gmail.com
10
+ # --------------------------------------------------------------------------
11
+ import re
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+ from fastapi_fastkit.utils.main import REGEX as EMAIL_REGEX
15
+
16
+
17
+ def validate_package_name(package: str) -> bool:
18
+ """
19
+ Validate package name format.
20
+
21
+ Checks if the package name follows basic PyPI package naming conventions.
22
+ Does not validate existence on PyPI (that will be added in v1.2.1+).
23
+
24
+ Args:
25
+ package: Package name to validate
26
+
27
+ Returns:
28
+ True if format is valid, False otherwise
29
+ """
30
+ if not package or not isinstance(package, str):
31
+ return False
32
+
33
+ # Remove version specifiers and extras for validation
34
+ package_name = re.split(r"[<>=!\[]", package)[0].strip()
35
+
36
+ # Basic package name pattern: letters, numbers, hyphens, underscores, dots
37
+ # Must start with letter or number
38
+ pattern = r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"
39
+
40
+ return bool(re.match(pattern, package_name))
41
+
42
+
43
+ def validate_project_name(name: str) -> Tuple[bool, Optional[str]]:
44
+ """
45
+ Validate project name.
46
+
47
+ Args:
48
+ name: Project name to validate
49
+
50
+ Returns:
51
+ Tuple of (is_valid, error_message)
52
+ """
53
+ if not name:
54
+ return False, "Project name cannot be empty"
55
+
56
+ if not name.replace("_", "").replace("-", "").isalnum():
57
+ return (
58
+ False,
59
+ "Project name must contain only letters, numbers, hyphens, and underscores",
60
+ )
61
+
62
+ if name[0].isdigit():
63
+ return False, "Project name cannot start with a number"
64
+
65
+ # Check for Python keywords
66
+ import keyword
67
+
68
+ if keyword.iskeyword(name):
69
+ return False, f"Project name '{name}' is a Python keyword"
70
+
71
+ return True, None
72
+
73
+
74
+ def validate_email_format(email: str) -> bool:
75
+ """
76
+ Validate email address format using utils/main.py REGEX pattern.
77
+
78
+ Args:
79
+ email: Email address to validate
80
+
81
+ Returns:
82
+ True if format is valid, False otherwise
83
+ """
84
+ if not email:
85
+ return False
86
+
87
+ return bool(re.match(EMAIL_REGEX, email))
88
+
89
+
90
+ def validate_feature_compatibility(
91
+ config: Dict[str, Any],
92
+ ) -> Tuple[bool, Optional[str]]:
93
+ """
94
+ Check compatibility between selected features.
95
+
96
+ Validates that selected features don't have conflicting requirements.
97
+
98
+ Args:
99
+ config: Project configuration dictionary
100
+
101
+ Returns:
102
+ Tuple of (is_compatible, warning_message)
103
+ """
104
+ warnings = []
105
+
106
+ # Check if FastAPI-Users is selected with database
107
+ if config.get("authentication") == "FastAPI-Users":
108
+ db_type = config.get("database", {}).get("type", "None")
109
+ if db_type == "None":
110
+ warnings.append(
111
+ "FastAPI-Users requires a database. Consider selecting PostgreSQL or MySQL."
112
+ )
113
+
114
+ # Check if Celery is selected with Redis
115
+ if config.get("async_tasks") == "Celery" or config.get("async_tasks") == "Dramatiq":
116
+ # These already include Redis in their dependencies
117
+ pass
118
+
119
+ # Check if caching Redis is selected along with task queue Redis
120
+ cache_type = config.get("caching", "None")
121
+ task_type = config.get("async_tasks", "None")
122
+ if cache_type == "Redis" and task_type in ["Celery", "Dramatiq"]:
123
+ # This is actually compatible, just a note
124
+ warnings.append(
125
+ "Info: Redis will be used for both caching and task queue (shared dependency)."
126
+ )
127
+
128
+ # If there are warnings, return them
129
+ if warnings:
130
+ return True, " | ".join(warnings)
131
+
132
+ return True, None
133
+
134
+
135
+ def sanitize_custom_packages(packages: List[str]) -> List[str]:
136
+ """
137
+ Clean and deduplicate custom package list.
138
+
139
+ Args:
140
+ packages: List of package names (possibly with duplicates or empty strings)
141
+
142
+ Returns:
143
+ Cleaned list of unique package names
144
+ """
145
+ if not packages:
146
+ return []
147
+
148
+ # Remove empty strings and strip whitespace
149
+ cleaned = [pkg.strip() for pkg in packages if pkg and pkg.strip()]
150
+
151
+ # Remove duplicates while preserving order
152
+ seen = set()
153
+ unique_packages = []
154
+ for pkg in cleaned:
155
+ pkg_lower = pkg.lower()
156
+ if pkg_lower not in seen:
157
+ seen.add(pkg_lower)
158
+ unique_packages.append(pkg)
159
+
160
+ # Validate each package name
161
+ valid_packages = [pkg for pkg in unique_packages if validate_package_name(pkg)]
162
+
163
+ return valid_packages
@@ -308,6 +308,67 @@ def _process_pyproject_file(
308
308
  raise BackendExceptions(f"Failed to process pyproject.toml: {e}")
309
309
 
310
310
 
311
+ def update_setup_py_dependencies(project_dir: str, dependencies: List[str]) -> None:
312
+ """
313
+ Update setup.py file's install_requires list with new dependencies.
314
+
315
+ This function finds and updates the install_requires list in setup.py
316
+ to match the dependencies selected during interactive mode.
317
+
318
+ :param project_dir: Path to the project directory
319
+ :param dependencies: List of dependency specifications
320
+ """
321
+ setup_py_path = os.path.join(project_dir, "setup.py")
322
+
323
+ if not os.path.exists(setup_py_path):
324
+ debug_log("setup.py not found, skipping dependency update", "info")
325
+ return
326
+
327
+ try:
328
+ with open(setup_py_path, "r", encoding="utf-8") as f:
329
+ content = f.read()
330
+
331
+ # Build the new install_requires list
332
+ deps_str = ",\n ".join(f'"{dep}"' for dep in dependencies)
333
+
334
+ # Try to replace existing install_requires first (with type annotation)
335
+ pattern = r"install_requires:\s*list\[str\]\s*=\s*\[(.*?)\]"
336
+ if re.search(pattern, content, re.DOTALL):
337
+ content = re.sub(
338
+ pattern,
339
+ f"install_requires: list[str] = [\n {deps_str},\n]",
340
+ content,
341
+ flags=re.DOTALL,
342
+ )
343
+ debug_log(
344
+ "Updated install_requires with type annotation in setup.py", "info"
345
+ )
346
+ else:
347
+ # Fallback: try without type annotation
348
+ pattern_old = r"install_requires\s*=\s*\[(.*?)\]"
349
+ if re.search(pattern_old, content, re.DOTALL):
350
+ content = re.sub(
351
+ pattern_old,
352
+ f"install_requires = [\n {deps_str},\n]",
353
+ content,
354
+ flags=re.DOTALL,
355
+ )
356
+ debug_log("Updated install_requires in setup.py", "info")
357
+ else:
358
+ debug_log("Could not find install_requires in setup.py", "warning")
359
+ return
360
+
361
+ # Write updated content
362
+ with open(setup_py_path, "w", encoding="utf-8") as f:
363
+ f.write(content)
364
+
365
+ print_info(f"Updated setup.py with {len(dependencies)} dependencies")
366
+
367
+ except (OSError, UnicodeDecodeError) as e:
368
+ debug_log(f"Error updating setup.py dependencies: {e}", "error")
369
+ print_warning(f"Could not update setup.py: {e}")
370
+
371
+
311
372
  def create_venv_with_manager(project_dir: str, manager_type: str = "pip") -> str:
312
373
  """
313
374
  Create a virtual environment using the specified package manager.
@@ -405,6 +466,22 @@ def generate_dependency_file_with_manager(
405
466
  package_manager.generate_dependency_file(
406
467
  dependencies, project_name, author, author_email, description
407
468
  )
469
+
470
+ # Also generate requirements.txt for pip compatibility (if not using pip)
471
+ if manager_type != "pip":
472
+ from pathlib import Path
473
+
474
+ requirements_path = Path(project_dir) / "requirements.txt"
475
+ try:
476
+ with open(requirements_path, "w", encoding="utf-8") as f:
477
+ for dep in dependencies:
478
+ f.write(f"{dep}\n")
479
+ debug_log("Generated requirements.txt for pip compatibility", "info")
480
+ except (OSError, UnicodeEncodeError) as e:
481
+ debug_log(
482
+ f"Warning: Could not generate requirements.txt: {e}", "warning"
483
+ )
484
+
408
485
  except Exception as e:
409
486
  debug_log(f"Error generating dependency file with {manager_type}: {e}", "error")
410
487
  raise BackendExceptions(f"Failed to generate dependency file: {str(e)}")
@@ -8,10 +8,14 @@ import subprocess
8
8
  import sys
9
9
  from typing import List
10
10
 
11
- from fastapi_fastkit import console
12
11
  from fastapi_fastkit.core.exceptions import BackendExceptions
13
12
  from fastapi_fastkit.utils.logging import debug_log, get_logger
14
- from fastapi_fastkit.utils.main import handle_exception, print_error, print_success
13
+ from fastapi_fastkit.utils.main import (
14
+ console,
15
+ handle_exception,
16
+ print_error,
17
+ print_success,
18
+ )
15
19
 
16
20
  from .base import BasePackageManager
17
21
 
@@ -8,10 +8,14 @@ import subprocess
8
8
  import sys
9
9
  from typing import List
10
10
 
11
- from fastapi_fastkit import console
12
11
  from fastapi_fastkit.core.exceptions import BackendExceptions
13
12
  from fastapi_fastkit.utils.logging import debug_log, get_logger
14
- from fastapi_fastkit.utils.main import handle_exception, print_error, print_success
13
+ from fastapi_fastkit.utils.main import (
14
+ console,
15
+ handle_exception,
16
+ print_error,
17
+ print_success,
18
+ )
15
19
 
16
20
  from .base import BasePackageManager
17
21
 
@@ -6,10 +6,14 @@
6
6
  import subprocess
7
7
  from typing import List
8
8
 
9
- from fastapi_fastkit import console
10
9
  from fastapi_fastkit.core.exceptions import BackendExceptions
11
10
  from fastapi_fastkit.utils.logging import debug_log, get_logger
12
- from fastapi_fastkit.utils.main import handle_exception, print_error, print_success
11
+ from fastapi_fastkit.utils.main import (
12
+ console,
13
+ handle_exception,
14
+ print_error,
15
+ print_success,
16
+ )
13
17
 
14
18
  from .base import BasePackageManager
15
19
 
@@ -7,10 +7,14 @@ import os
7
7
  import subprocess
8
8
  from typing import List
9
9
 
10
- from fastapi_fastkit import console
11
10
  from fastapi_fastkit.core.exceptions import BackendExceptions
12
11
  from fastapi_fastkit.utils.logging import debug_log, get_logger
13
- from fastapi_fastkit.utils.main import handle_exception, print_error, print_success
12
+ from fastapi_fastkit.utils.main import (
13
+ console,
14
+ handle_exception,
15
+ print_error,
16
+ print_success,
17
+ )
14
18
 
15
19
  from .base import BasePackageManager
16
20
 
@@ -0,0 +1,17 @@
1
+ # --------------------------------------------------------------------------
2
+ # Project Builder module for FastAPI-fastkit
3
+ #
4
+ # Provides project building functionality including:
5
+ # - Dependency collection and management
6
+ # - Dynamic configuration file generation
7
+ # - Template merging
8
+ #
9
+ # @author bnbong bbbong9@gmail.com
10
+ # --------------------------------------------------------------------------
11
+ from .config_generator import DynamicConfigGenerator
12
+ from .dependency_collector import DependencyCollector
13
+
14
+ __all__ = [
15
+ "DependencyCollector",
16
+ "DynamicConfigGenerator",
17
+ ]