forgeapi 0.1.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 (57) hide show
  1. fastapi_forge/__init__.py +15 -0
  2. fastapi_forge/cli.py +125 -0
  3. fastapi_forge/commands/__init__.py +5 -0
  4. fastapi_forge/commands/create.py +107 -0
  5. fastapi_forge/generator.py +300 -0
  6. fastapi_forge/models.py +187 -0
  7. fastapi_forge/prompts.py +364 -0
  8. fastapi_forge/templates/base/.dockerignore.jinja +20 -0
  9. fastapi_forge/templates/base/.github/workflows/ci.yml.jinja +155 -0
  10. fastapi_forge/templates/base/.gitignore.jinja +68 -0
  11. fastapi_forge/templates/base/Dockerfile.jinja +69 -0
  12. fastapi_forge/templates/base/README.md.jinja +141 -0
  13. fastapi_forge/templates/base/alembic/README.md.jinja +43 -0
  14. fastapi_forge/templates/base/alembic/env.py.jinja +93 -0
  15. fastapi_forge/templates/base/alembic/script.py.mako.jinja +26 -0
  16. fastapi_forge/templates/base/alembic/versions/.gitkeep.jinja +1 -0
  17. fastapi_forge/templates/base/alembic.ini.jinja +70 -0
  18. fastapi_forge/templates/base/app/__init__.py.jinja +5 -0
  19. fastapi_forge/templates/base/app/api/__init__.py.jinja +3 -0
  20. fastapi_forge/templates/base/app/api/auth_api.py.jinja +72 -0
  21. fastapi_forge/templates/base/app/api/health_api.py.jinja +41 -0
  22. fastapi_forge/templates/base/app/config/__init__.py.jinja +31 -0
  23. fastapi_forge/templates/base/app/config/base.py.jinja +52 -0
  24. fastapi_forge/templates/base/app/config/env.py.jinja +75 -0
  25. fastapi_forge/templates/base/app/core/__init__.py.jinja +3 -0
  26. fastapi_forge/templates/base/app/core/auth.py.jinja +96 -0
  27. fastapi_forge/templates/base/app/core/config.py.jinja +56 -0
  28. fastapi_forge/templates/base/app/core/database.py.jinja +68 -0
  29. fastapi_forge/templates/base/app/core/deps.py.jinja +55 -0
  30. fastapi_forge/templates/base/app/core/redis.py.jinja +41 -0
  31. fastapi_forge/templates/base/app/daos/__init__.py.jinja +3 -0
  32. fastapi_forge/templates/base/app/exceptions/__init__.py.jinja +7 -0
  33. fastapi_forge/templates/base/app/exceptions/exception.py.jinja +34 -0
  34. fastapi_forge/templates/base/app/exceptions/handler.py.jinja +56 -0
  35. fastapi_forge/templates/base/app/main.py.jinja +24 -0
  36. fastapi_forge/templates/base/app/models/__init__.py.jinja +7 -0
  37. fastapi_forge/templates/base/app/models/base.py.jinja +13 -0
  38. fastapi_forge/templates/base/app/schemas/__init__.py.jinja +3 -0
  39. fastapi_forge/templates/base/app/schemas/api_schema.py.jinja +20 -0
  40. fastapi_forge/templates/base/app/schemas/auth_schema.py.jinja +21 -0
  41. fastapi_forge/templates/base/app/server.py.jinja +99 -0
  42. fastapi_forge/templates/base/app/services/__init__.py.jinja +3 -0
  43. fastapi_forge/templates/base/app/utils/__init__.py.jinja +3 -0
  44. fastapi_forge/templates/base/app/utils/log.py.jinja +21 -0
  45. fastapi_forge/templates/base/config.yaml.jinja +71 -0
  46. fastapi_forge/templates/base/docker-compose.yml.jinja +117 -0
  47. fastapi_forge/templates/base/pyproject.toml.jinja +86 -0
  48. fastapi_forge/templates/base/pytest.ini.jinja +10 -0
  49. fastapi_forge/templates/base/ruff.toml.jinja +33 -0
  50. fastapi_forge/templates/base/tests/__init__.py.jinja +3 -0
  51. fastapi_forge/templates/base/tests/conftest.py.jinja +31 -0
  52. fastapi_forge/templates/base/tests/test_health.py.jinja +24 -0
  53. forgeapi-0.1.0.dist-info/METADATA +182 -0
  54. forgeapi-0.1.0.dist-info/RECORD +57 -0
  55. forgeapi-0.1.0.dist-info/WHEEL +4 -0
  56. forgeapi-0.1.0.dist-info/entry_points.txt +2 -0
  57. forgeapi-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,15 @@
1
+ """
2
+ FastAPI-Forge: A highly modular FastAPI project scaffolding CLI tool.
3
+
4
+ This package provides a CLI tool for generating FastAPI projects with
5
+ best practices including Clean Architecture, Docker support, database
6
+ integration, and authentication.
7
+ """
8
+
9
+ __version__ = "0.1.0"
10
+ __author__ = "Zachary"
11
+ __email__ = "zachary@example.com"
12
+
13
+ from fastapi_forge.cli import app
14
+
15
+ __all__ = ["app", "__version__"]
fastapi_forge/cli.py ADDED
@@ -0,0 +1,125 @@
1
+ """
2
+ ForgeAPI CLI - Command Line Interface
3
+
4
+ This module provides the main CLI application using Typer.
5
+ """
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+
11
+ from fastapi_forge import __version__
12
+
13
+ # Initialize Typer app
14
+ app = typer.Typer(
15
+ name="forge",
16
+ help="🚀 ForgeAPI - A highly modular FastAPI project scaffolding CLI tool",
17
+ add_completion=False,
18
+ rich_markup_mode="rich",
19
+ )
20
+
21
+ # Rich console for beautiful output
22
+ console = Console()
23
+
24
+
25
+ def version_callback(value: bool) -> None:
26
+ """Display version information and exit."""
27
+ if value:
28
+ console.print(
29
+ Panel(
30
+ f"[bold cyan]ForgeAPI[/bold cyan] version [green]{__version__}[/green]\n\n"
31
+ f"🚀 A highly modular FastAPI project scaffolding CLI tool\n"
32
+ f"📚 Documentation: https://github.com/zachary/fastapi-forge",
33
+ title="Version Info",
34
+ border_style="cyan",
35
+ )
36
+ )
37
+ raise typer.Exit()
38
+
39
+
40
+ @app.callback()
41
+ def main(
42
+ version: bool = typer.Option(
43
+ None,
44
+ "--version",
45
+ "-v",
46
+ help="Show version information and exit.",
47
+ callback=version_callback,
48
+ is_eager=True,
49
+ ),
50
+ ) -> None:
51
+ """
52
+ 🚀 ForgeAPI - Create FastAPI projects with best practices.
53
+
54
+ A highly modular FastAPI project scaffolding CLI tool for the AI application era.
55
+ Generate projects with Clean Architecture, Docker, Authentication, and more.
56
+ """
57
+ pass
58
+
59
+
60
+ @app.command()
61
+ def create(
62
+ project_name: str = typer.Argument(
63
+ ...,
64
+ help="Name of the project to create",
65
+ ),
66
+ package_manager: str = typer.Option(
67
+ None,
68
+ "--package-manager",
69
+ "-pm",
70
+ help="Package manager to use (uv, poetry, pip)",
71
+ ),
72
+ database: str = typer.Option(
73
+ None,
74
+ "--database",
75
+ "-db",
76
+ help="Database to use (postgres, mysql, sqlite, none)",
77
+ ),
78
+ auth: bool = typer.Option(
79
+ None,
80
+ "--auth/--no-auth",
81
+ help="Include JWT authentication module",
82
+ ),
83
+ docker: bool = typer.Option(
84
+ None,
85
+ "--docker/--no-docker",
86
+ help="Generate Docker configuration",
87
+ ),
88
+ no_interactive: bool = typer.Option(
89
+ False,
90
+ "--no-interactive",
91
+ "-y",
92
+ help="Skip interactive prompts and use defaults",
93
+ ),
94
+ ) -> None:
95
+ """
96
+ Create a new FastAPI project with the specified configuration.
97
+
98
+ Examples:
99
+
100
+ $ forge create my-api
101
+
102
+ $ forge create my-api --package-manager uv --database postgres --auth --docker
103
+
104
+ $ forge create my-api --no-interactive
105
+ """
106
+ from fastapi_forge.commands.create import create_project
107
+
108
+ create_project(
109
+ project_name=project_name,
110
+ package_manager=package_manager,
111
+ database=database,
112
+ auth=auth,
113
+ docker=docker,
114
+ no_interactive=no_interactive,
115
+ )
116
+
117
+
118
+ @app.command()
119
+ def version() -> None:
120
+ """Show version information."""
121
+ version_callback(True)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ app()
@@ -0,0 +1,5 @@
1
+ """
2
+ FastAPI-Forge Commands Package
3
+
4
+ This package contains all CLI command implementations.
5
+ """
@@ -0,0 +1,107 @@
1
+ """
2
+ Create Command - Project Creation Logic
3
+
4
+ This module handles the 'forge create' command implementation.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ from fastapi_forge.generator import ProjectGenerator
13
+ from fastapi_forge.models import (
14
+ DatabaseType,
15
+ PackageManager,
16
+ ProjectConfig,
17
+ )
18
+ from fastapi_forge.prompts import (
19
+ collect_project_config,
20
+ confirm_config,
21
+ )
22
+
23
+ console = Console()
24
+
25
+
26
+ def create_project(
27
+ project_name: str,
28
+ package_manager: str | None = None,
29
+ database: str | None = None,
30
+ auth: bool | None = None,
31
+ docker: bool | None = None,
32
+ no_interactive: bool = False,
33
+ ) -> None:
34
+ """
35
+ Create a new FastAPI project.
36
+
37
+ Args:
38
+ project_name: Name of the project to create
39
+ package_manager: Package manager to use (uv, poetry, pip)
40
+ database: Database to use (postgres, mysql, sqlite, none)
41
+ auth: Whether to include JWT authentication
42
+ docker: Whether to generate Docker configuration
43
+ no_interactive: Skip interactive prompts
44
+ """
45
+ try:
46
+ if no_interactive:
47
+ # Non-interactive mode: use defaults or provided values
48
+ config = ProjectConfig(
49
+ project_name=project_name,
50
+ package_manager=PackageManager(package_manager or "uv"),
51
+ database=DatabaseType(database or "postgres"),
52
+ use_auth=auth if auth is not None else True,
53
+ use_docker=docker if docker is not None else True,
54
+ use_docker_compose=docker if docker is not None else True,
55
+ )
56
+ else:
57
+ # Interactive mode: collect configuration through prompts
58
+ try:
59
+ config = collect_project_config(project_name)
60
+
61
+ # Ask for confirmation
62
+ if not confirm_config(config):
63
+ console.print("\n[yellow]Project creation cancelled.[/yellow]")
64
+ return
65
+ except KeyboardInterrupt:
66
+ return
67
+
68
+ # Display confirmation
69
+ console.print(
70
+ Panel(
71
+ f"[bold green]✅ Configuration ready![/bold green]\n\n"
72
+ f"Project: [cyan]{config.project_name}[/cyan]\n"
73
+ f"Slug: [cyan]{config.project_slug}[/cyan]\n"
74
+ f"Package Manager: [cyan]{config.package_manager.value}[/cyan]\n"
75
+ f"Database: [cyan]{config.database.value}[/cyan]",
76
+ title="Ready to Generate",
77
+ border_style="green",
78
+ )
79
+ )
80
+
81
+ # Generate the project
82
+ output_dir = Path.cwd() / config.project_slug
83
+ generator = ProjectGenerator(config, output_dir)
84
+
85
+ with console.status("[bold blue]Generating project...[/bold blue]"):
86
+ generated_files = generator.generate()
87
+
88
+ # Display success message
89
+ console.print(
90
+ Panel(
91
+ f"[bold green]🚀 Project created successfully![/bold green]\n\n"
92
+ f"Location: [cyan]{output_dir}[/cyan]\n"
93
+ f"Files generated: [cyan]{len(generated_files)}[/cyan]\n\n"
94
+ f"[bold]Next steps:[/bold]\n"
95
+ f" cd {config.project_slug}\n"
96
+ + (" uv sync\n" if config.package_manager == PackageManager.UV else "")
97
+ + (" poetry install\n" if config.package_manager == PackageManager.POETRY else "")
98
+ + (" pip install -r requirements.txt\n" if config.package_manager == PackageManager.PIP else "")
99
+ + f" {'uv run ' if config.package_manager == PackageManager.UV else 'poetry run ' if config.package_manager == PackageManager.POETRY else ''}uvicorn app.main:app --reload",
100
+ title="Success",
101
+ border_style="green",
102
+ )
103
+ )
104
+
105
+ except ValueError as e:
106
+ console.print(f"\n[red]❌ Error: {e}[/red]")
107
+ raise SystemExit(1) from e
@@ -0,0 +1,300 @@
1
+ """
2
+ Project Generator Module
3
+
4
+ This module handles the generation of FastAPI projects from templates using Jinja2.
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ from pathlib import Path
10
+
11
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
12
+ from rich.console import Console
13
+ from rich.progress import Progress, SpinnerColumn, TextColumn
14
+
15
+ from fastapi_forge.models import ProjectConfig
16
+
17
+ console = Console()
18
+
19
+ # Get the templates directory path
20
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
21
+
22
+
23
+ class ProjectGenerator:
24
+ """
25
+ Project generator using Jinja2 templates.
26
+
27
+ This class handles loading templates, rendering them with project configuration,
28
+ and writing the generated files to the target directory.
29
+ """
30
+
31
+ def __init__(self, config: ProjectConfig, output_dir: Path | None = None):
32
+ """
33
+ Initialize the project generator.
34
+
35
+ Args:
36
+ config: Project configuration
37
+ output_dir: Output directory for the generated project (default: current dir)
38
+ """
39
+ self.config = config
40
+ self.output_dir = output_dir or Path.cwd()
41
+ # output_dir is the project directory itself
42
+ self.project_dir = self.output_dir
43
+
44
+ # Initialize Jinja2 environment
45
+ self.env = Environment(
46
+ loader=FileSystemLoader(TEMPLATES_DIR),
47
+ autoescape=select_autoescape(default=False),
48
+ keep_trailing_newline=True,
49
+ trim_blocks=True,
50
+ lstrip_blocks=True,
51
+ )
52
+
53
+ # Add custom filters
54
+ self.env.filters["snake_case"] = self._to_snake_case
55
+
56
+ @staticmethod
57
+ def _to_snake_case(value: str) -> str:
58
+ """Convert a string to snake_case."""
59
+ result = value.lower()
60
+ result = result.replace("-", "_")
61
+ result = result.replace(" ", "_")
62
+ return "".join(c for c in result if c.isalnum() or c == "_")
63
+
64
+ def _get_template_context(self) -> dict:
65
+ """
66
+ Get the template context from project configuration.
67
+
68
+ Returns:
69
+ Dictionary with all template variables
70
+ """
71
+ return {
72
+ # Basic info
73
+ "project_name": self.config.project_name,
74
+ "project_slug": self.config.project_slug,
75
+ "description": self.config.project_description,
76
+ "project_description": self.config.project_description,
77
+ "author_name": self.config.author_name,
78
+ "author_email": self.config.author_email,
79
+ "python_version": self.config.python_version,
80
+ # Package manager
81
+ "package_manager": self.config.package_manager.value,
82
+ "use_uv": self.config.package_manager.value == "uv",
83
+ "use_poetry": self.config.package_manager.value == "poetry",
84
+ "use_pip": self.config.package_manager.value == "pip",
85
+ # Database
86
+ "database": self.config.database.value,
87
+ "database_type": self.config.database.value,
88
+ "use_postgres": self.config.database.value == "postgres",
89
+ "use_mysql": self.config.database.value == "mysql",
90
+ "use_sqlite": self.config.database.value == "sqlite",
91
+ "use_database": self.config.database.value != "none",
92
+ "use_alembic": self.config.use_alembic,
93
+ # Features
94
+ "use_auth": self.config.use_auth,
95
+ "use_redis": self.config.use_redis,
96
+ # Docker
97
+ "use_docker": self.config.use_docker,
98
+ "use_docker_compose": self.config.use_docker_compose,
99
+ "docker_services": self.config.docker_services,
100
+ # Testing & Quality
101
+ "use_pytest": self.config.use_pytest,
102
+ "use_ruff": self.config.use_ruff,
103
+ # CI/CD & Editor
104
+ "use_github_actions": self.config.use_github_actions,
105
+ "use_vscode": self.config.use_vscode,
106
+ }
107
+
108
+ def _render_template(self, template_path: str, context: dict) -> str:
109
+ """
110
+ Render a template with the given context.
111
+
112
+ Args:
113
+ template_path: Path to the template file (relative to templates dir)
114
+ context: Template context dictionary
115
+
116
+ Returns:
117
+ Rendered template content
118
+ """
119
+ template = self.env.get_template(template_path)
120
+ return template.render(**context)
121
+
122
+ def _should_include_template(self, template_path: str, context: dict) -> bool:
123
+ """
124
+ Check if a template should be included based on configuration.
125
+
126
+ Args:
127
+ template_path: Path to the template file
128
+ context: Template context dictionary
129
+
130
+ Returns:
131
+ True if template should be included
132
+ """
133
+ # Check for conditional directories/files
134
+ path_lower = template_path.lower()
135
+
136
+ # Database related
137
+ if "alembic" in path_lower and not context["use_alembic"]:
138
+ return False
139
+ if "database" in path_lower and not context["use_database"]:
140
+ return False
141
+
142
+ # Auth related
143
+ if "auth" in path_lower and not context["use_auth"]:
144
+ return False
145
+
146
+ # Redis related
147
+ if "redis" in path_lower and not context["use_redis"]:
148
+ return False
149
+
150
+ # Docker related
151
+ if "docker" in path_lower:
152
+ if "compose" in path_lower and not context["use_docker_compose"]:
153
+ return False
154
+ if not context["use_docker"]:
155
+ return False
156
+
157
+ # CI/CD related
158
+ if ".github" in path_lower and not context["use_github_actions"]:
159
+ return False
160
+
161
+ # VS Code related
162
+ if ".vscode" in path_lower and not context["use_vscode"]:
163
+ return False
164
+
165
+ # Pytest related
166
+ if "pytest" in path_lower and not context["use_pytest"]:
167
+ return False
168
+ if "conftest" in path_lower and not context["use_pytest"]:
169
+ return False
170
+
171
+ # Ruff related
172
+ if "ruff" in path_lower and not context["use_ruff"]:
173
+ return False
174
+
175
+ return True
176
+
177
+ def _get_output_path(self, template_path: str) -> Path:
178
+ """
179
+ Get the output path for a template file.
180
+
181
+ Args:
182
+ template_path: Path to the template file
183
+
184
+ Returns:
185
+ Output path for the rendered file
186
+ """
187
+ # Remove .jinja extension
188
+ output_path = template_path
189
+ if output_path.endswith(".jinja"):
190
+ output_path = output_path[:-6]
191
+
192
+ # Handle special directory mappings
193
+ # templates/base/app -> app
194
+ if output_path.startswith("base/"):
195
+ output_path = output_path[5:]
196
+
197
+ return self.project_dir / output_path
198
+
199
+ def _collect_templates(self) -> list[str]:
200
+ """
201
+ Collect all template files to be processed.
202
+
203
+ Returns:
204
+ List of template paths relative to templates directory
205
+ """
206
+ templates = []
207
+
208
+ for root, _dirs, files in os.walk(TEMPLATES_DIR):
209
+ for file in files:
210
+ if file.endswith(".jinja"):
211
+ full_path = Path(root) / file
212
+ rel_path = full_path.relative_to(TEMPLATES_DIR)
213
+ templates.append(str(rel_path))
214
+
215
+ return templates
216
+
217
+ def generate(self) -> list[Path]:
218
+ """
219
+ Generate the project from templates.
220
+
221
+ Returns:
222
+ List of paths to generated files
223
+ """
224
+ # Check if project directory already exists
225
+ if self.project_dir.exists():
226
+ raise FileExistsError(
227
+ f"Directory '{self.project_dir}' already exists. "
228
+ "Please choose a different project name or remove the existing directory."
229
+ )
230
+
231
+ context = self._get_template_context()
232
+ templates = self._collect_templates()
233
+ generated_files: list[Path] = []
234
+
235
+ with Progress(
236
+ SpinnerColumn(),
237
+ TextColumn("[progress.description]{task.description}"),
238
+ console=console,
239
+ ) as progress:
240
+ task = progress.add_task(
241
+ "[cyan]Generating project...", total=len(templates)
242
+ )
243
+
244
+ # Create project directory
245
+ self.project_dir.mkdir(parents=True, exist_ok=True)
246
+
247
+ for template_path in templates:
248
+ # Check if template should be included
249
+ if not self._should_include_template(template_path, context):
250
+ progress.advance(task)
251
+ continue
252
+
253
+ # Get output path
254
+ output_path = self._get_output_path(template_path)
255
+
256
+ # Create parent directories
257
+ output_path.parent.mkdir(parents=True, exist_ok=True)
258
+
259
+ # Render and write template
260
+ try:
261
+ content = self._render_template(template_path, context)
262
+ output_path.write_text(content)
263
+ generated_files.append(output_path)
264
+ except Exception as e:
265
+ console.print(f"[yellow]Warning: Failed to render {template_path}: {e}[/yellow]")
266
+
267
+ progress.advance(task)
268
+
269
+ return generated_files
270
+
271
+ def cleanup(self) -> None:
272
+ """Remove the generated project directory (for error recovery)."""
273
+ if self.project_dir.exists():
274
+ shutil.rmtree(self.project_dir)
275
+
276
+
277
+ def generate_project(config: ProjectConfig, output_dir: Path | None = None) -> list[Path]:
278
+ """
279
+ Generate a FastAPI project from templates.
280
+
281
+ Args:
282
+ config: Project configuration
283
+ output_dir: Output directory (default: current directory)
284
+
285
+ Returns:
286
+ List of paths to generated files
287
+ """
288
+ generator = ProjectGenerator(config, output_dir)
289
+
290
+ try:
291
+ generated_files = generator.generate()
292
+ console.print(f"\n[green]✅ Project generated at:[/green] {generator.project_dir}")
293
+ return generated_files
294
+ except FileExistsError as e:
295
+ console.print(f"\n[red]❌ Error:[/red] {e}")
296
+ raise
297
+ except Exception as e:
298
+ console.print(f"\n[red]❌ Error generating project:[/red] {e}")
299
+ generator.cleanup()
300
+ raise