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.
- fastapi_forge/__init__.py +15 -0
- fastapi_forge/cli.py +125 -0
- fastapi_forge/commands/__init__.py +5 -0
- fastapi_forge/commands/create.py +107 -0
- fastapi_forge/generator.py +300 -0
- fastapi_forge/models.py +187 -0
- fastapi_forge/prompts.py +364 -0
- fastapi_forge/templates/base/.dockerignore.jinja +20 -0
- fastapi_forge/templates/base/.github/workflows/ci.yml.jinja +155 -0
- fastapi_forge/templates/base/.gitignore.jinja +68 -0
- fastapi_forge/templates/base/Dockerfile.jinja +69 -0
- fastapi_forge/templates/base/README.md.jinja +141 -0
- fastapi_forge/templates/base/alembic/README.md.jinja +43 -0
- fastapi_forge/templates/base/alembic/env.py.jinja +93 -0
- fastapi_forge/templates/base/alembic/script.py.mako.jinja +26 -0
- fastapi_forge/templates/base/alembic/versions/.gitkeep.jinja +1 -0
- fastapi_forge/templates/base/alembic.ini.jinja +70 -0
- fastapi_forge/templates/base/app/__init__.py.jinja +5 -0
- fastapi_forge/templates/base/app/api/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/api/auth_api.py.jinja +72 -0
- fastapi_forge/templates/base/app/api/health_api.py.jinja +41 -0
- fastapi_forge/templates/base/app/config/__init__.py.jinja +31 -0
- fastapi_forge/templates/base/app/config/base.py.jinja +52 -0
- fastapi_forge/templates/base/app/config/env.py.jinja +75 -0
- fastapi_forge/templates/base/app/core/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/core/auth.py.jinja +96 -0
- fastapi_forge/templates/base/app/core/config.py.jinja +56 -0
- fastapi_forge/templates/base/app/core/database.py.jinja +68 -0
- fastapi_forge/templates/base/app/core/deps.py.jinja +55 -0
- fastapi_forge/templates/base/app/core/redis.py.jinja +41 -0
- fastapi_forge/templates/base/app/daos/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/exceptions/__init__.py.jinja +7 -0
- fastapi_forge/templates/base/app/exceptions/exception.py.jinja +34 -0
- fastapi_forge/templates/base/app/exceptions/handler.py.jinja +56 -0
- fastapi_forge/templates/base/app/main.py.jinja +24 -0
- fastapi_forge/templates/base/app/models/__init__.py.jinja +7 -0
- fastapi_forge/templates/base/app/models/base.py.jinja +13 -0
- fastapi_forge/templates/base/app/schemas/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/schemas/api_schema.py.jinja +20 -0
- fastapi_forge/templates/base/app/schemas/auth_schema.py.jinja +21 -0
- fastapi_forge/templates/base/app/server.py.jinja +99 -0
- fastapi_forge/templates/base/app/services/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/utils/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/utils/log.py.jinja +21 -0
- fastapi_forge/templates/base/config.yaml.jinja +71 -0
- fastapi_forge/templates/base/docker-compose.yml.jinja +117 -0
- fastapi_forge/templates/base/pyproject.toml.jinja +86 -0
- fastapi_forge/templates/base/pytest.ini.jinja +10 -0
- fastapi_forge/templates/base/ruff.toml.jinja +33 -0
- fastapi_forge/templates/base/tests/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/tests/conftest.py.jinja +31 -0
- fastapi_forge/templates/base/tests/test_health.py.jinja +24 -0
- forgeapi-0.1.0.dist-info/METADATA +182 -0
- forgeapi-0.1.0.dist-info/RECORD +57 -0
- forgeapi-0.1.0.dist-info/WHEEL +4 -0
- forgeapi-0.1.0.dist-info/entry_points.txt +2 -0
- 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,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
|