Skip to content

Commit 81d3e33

Browse files
authored
feat(cli): implement CLI commands for MVC component generation and configuration (#76)
Implement CLI commands for MVC component generation and configuration
2 parents 3790975 + 2e5702d commit 81d3e33

File tree

11 files changed

+561
-0
lines changed

11 files changed

+561
-0
lines changed

mvc_flask/cli.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""Flask MVC CLI commands for generating MVC components.
2+
3+
This module provides command-line tools for scaffolding MVC components
4+
following Flask and Python best practices.
5+
"""
6+
7+
import logging
8+
import click
9+
from flask.cli import with_appcontext
10+
from typing import Optional
11+
12+
from .core.generators import ControllerGenerator
13+
from .core.exceptions import ControllerGenerationError, InvalidControllerNameError
14+
from .core.config import CLIConfig
15+
16+
# Configure logging
17+
logging.basicConfig(level=logging.INFO)
18+
logger = logging.getLogger(__name__)
19+
20+
21+
@click.group()
22+
def mvc():
23+
"""MVC Flask commands."""
24+
pass
25+
26+
27+
@mvc.group()
28+
def generate():
29+
"""Generate MVC components."""
30+
pass
31+
32+
33+
@generate.command()
34+
@click.argument("name")
35+
@click.option(
36+
"--path",
37+
"-p",
38+
default=CLIConfig.DEFAULT_CONTROLLERS_PATH,
39+
help="Path where to create the controller",
40+
)
41+
@click.option("--force", "-f", is_flag=True, help="Overwrite existing controller file")
42+
@with_appcontext
43+
def controller(name: str, path: str, force: bool) -> None:
44+
"""Generate a new controller.
45+
46+
Creates a new controller class with RESTful methods following Flask MVC patterns.
47+
The controller will include standard CRUD operations and proper type hints.
48+
49+
Examples:
50+
\b
51+
flask mvc generate controller home
52+
flask mvc generate controller user --path custom/controllers
53+
flask mvc generate controller api_v1_user --force
54+
"""
55+
try:
56+
logger.info(f"Generating controller '{name}' at path '{path}'")
57+
58+
generator = ControllerGenerator()
59+
controller_file = generator.generate(name, path, force=force)
60+
61+
click.echo(
62+
click.style(
63+
f"✓ Controller created successfully at {controller_file}",
64+
fg=CLIConfig.SUCCESS_COLOR,
65+
)
66+
)
67+
68+
# Provide helpful next steps
69+
controller_name = name if name.endswith("_controller") else f"{name}_controller"
70+
click.echo(click.style("\nNext steps:", fg=CLIConfig.INFO_COLOR, bold=True))
71+
click.echo(
72+
click.style(
73+
"1. Add routes for your controller in your routes.py file",
74+
fg=CLIConfig.INFO_COLOR,
75+
)
76+
)
77+
click.echo(
78+
click.style(
79+
f"2. Create templates in views/{name}/ directory",
80+
fg=CLIConfig.INFO_COLOR,
81+
)
82+
)
83+
click.echo(
84+
click.style(
85+
f"3. Implement your business logic in {controller_name}.py",
86+
fg=CLIConfig.INFO_COLOR,
87+
)
88+
)
89+
90+
except (ControllerGenerationError, InvalidControllerNameError) as e:
91+
logger.error(f"Controller generation failed: {e}")
92+
click.echo(click.style(f"✗ Error: {e}", fg=CLIConfig.ERROR_COLOR), err=True)
93+
raise click.Abort() from e
94+
except Exception as e:
95+
logger.exception(f"Unexpected error during controller generation: {e}")
96+
click.echo(
97+
click.style(
98+
f"✗ Unexpected error: {e}. Please check the logs for more details.",
99+
fg=CLIConfig.ERROR_COLOR,
100+
),
101+
err=True,
102+
)
103+
raise click.Abort() from e
104+
105+
106+
def init_app(app) -> None:
107+
"""Initialize CLI commands with Flask app.
108+
109+
Args:
110+
app: Flask application instance
111+
"""
112+
app.cli.add_command(mvc)

mvc_flask/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Core package for MVC Flask."""

mvc_flask/core/config.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Configuration for MVC Flask CLI."""
2+
3+
import os
4+
from pathlib import Path
5+
from typing import Dict, Any, Optional
6+
7+
8+
class CLIConfig:
9+
"""Configuration settings for CLI commands."""
10+
11+
# Default paths
12+
DEFAULT_CONTROLLERS_PATH = "app/controllers"
13+
DEFAULT_VIEWS_PATH = "app/views"
14+
DEFAULT_MODELS_PATH = "app/models"
15+
16+
# Template configuration
17+
TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
18+
19+
# Controller template settings
20+
CONTROLLER_TEMPLATE = "base_controller.jinja2"
21+
22+
# File encoding
23+
FILE_ENCODING = "utf-8"
24+
25+
# CLI styling
26+
SUCCESS_COLOR = "green"
27+
ERROR_COLOR = "red"
28+
WARNING_COLOR = "yellow"
29+
INFO_COLOR = "blue"
30+
31+
# Environment variables for overriding defaults
32+
ENV_PREFIX = "FLASK_MVC_"
33+
34+
@classmethod
35+
def get_controllers_path(cls) -> str:
36+
"""Get controllers path from environment or default.
37+
38+
Returns:
39+
Controllers directory path
40+
"""
41+
return os.getenv(
42+
f"{cls.ENV_PREFIX}CONTROLLERS_PATH", cls.DEFAULT_CONTROLLERS_PATH
43+
)
44+
45+
@classmethod
46+
def get_templates_dir(cls) -> Path:
47+
"""Get templates directory path.
48+
49+
Returns:
50+
Templates directory path
51+
"""
52+
custom_dir = os.getenv(f"{cls.ENV_PREFIX}TEMPLATES_DIR")
53+
if custom_dir:
54+
return Path(custom_dir)
55+
return cls.TEMPLATES_DIR
56+
57+
@classmethod
58+
def get_file_encoding(cls) -> str:
59+
"""Get file encoding from environment or default.
60+
61+
Returns:
62+
File encoding string
63+
"""
64+
return os.getenv(f"{cls.ENV_PREFIX}FILE_ENCODING", cls.FILE_ENCODING)

mvc_flask/core/exceptions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Custom exceptions for MVC Flask CLI."""
2+
3+
4+
class MVCFlaskError(Exception):
5+
"""Base exception for MVC Flask CLI errors."""
6+
7+
pass
8+
9+
10+
class ControllerGenerationError(MVCFlaskError):
11+
"""Exception raised when controller generation fails."""
12+
13+
pass
14+
15+
16+
class TemplateNotFoundError(MVCFlaskError):
17+
"""Exception raised when template file is not found."""
18+
19+
pass
20+
21+
22+
class InvalidControllerNameError(MVCFlaskError):
23+
"""Exception raised when controller name is invalid."""
24+
25+
pass

mvc_flask/core/file_handler.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""File system utilities for MVC Flask CLI."""
2+
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
from .exceptions import ControllerGenerationError
7+
8+
9+
class FileHandler:
10+
"""Handles file system operations for code generation."""
11+
12+
@staticmethod
13+
def ensure_directory_exists(directory_path: Path) -> None:
14+
"""Create directory if it doesn't exist.
15+
16+
Args:
17+
directory_path: Path to the directory to create
18+
"""
19+
try:
20+
directory_path.mkdir(parents=True, exist_ok=True)
21+
except Exception as e:
22+
raise ControllerGenerationError(
23+
f"Failed to create directory {directory_path}: {e}"
24+
) from e
25+
26+
@staticmethod
27+
def file_exists(file_path: Path) -> bool:
28+
"""Check if file exists.
29+
30+
Args:
31+
file_path: Path to the file to check
32+
33+
Returns:
34+
True if file exists, False otherwise
35+
"""
36+
return file_path.exists()
37+
38+
@staticmethod
39+
def write_file(file_path: Path, content: str) -> None:
40+
"""Write content to file.
41+
42+
Args:
43+
file_path: Path to the file to write
44+
content: Content to write to the file
45+
46+
Raises:
47+
ControllerGenerationError: If file writing fails
48+
"""
49+
try:
50+
with open(file_path, "w", encoding="utf-8") as f:
51+
f.write(content)
52+
except Exception as e:
53+
raise ControllerGenerationError(
54+
f"Failed to write file {file_path}: {e}"
55+
) from e

mvc_flask/core/generators.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Generators for MVC components."""
2+
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
from .template_renderer import TemplateRenderer
7+
from .file_handler import FileHandler
8+
from .name_utils import NameUtils
9+
from .config import CLIConfig
10+
from .exceptions import ControllerGenerationError, InvalidControllerNameError
11+
12+
13+
class ControllerGenerator:
14+
"""Generates controller files using templates."""
15+
16+
def __init__(self, templates_dir: Optional[Path] = None):
17+
"""Initialize the controller generator.
18+
19+
Args:
20+
templates_dir: Path to templates directory. If None, uses default location.
21+
"""
22+
templates_dir = templates_dir or CLIConfig.get_templates_dir()
23+
24+
self.template_renderer = TemplateRenderer(templates_dir)
25+
self.file_handler = FileHandler()
26+
self.name_utils = NameUtils()
27+
self.config = CLIConfig()
28+
29+
def generate(
30+
self, name: str, output_path: Optional[str] = None, force: bool = False
31+
) -> Path:
32+
"""Generate a new controller file.
33+
34+
Args:
35+
name: Name of the controller
36+
output_path: Directory where to create the controller
37+
force: Whether to overwrite existing files
38+
39+
Returns:
40+
Path to the created controller file
41+
42+
Raises:
43+
ControllerGenerationError: If generation fails
44+
InvalidControllerNameError: If controller name is invalid
45+
"""
46+
try:
47+
# Validate controller name
48+
self.name_utils.validate_controller_name(name)
49+
50+
# Use default path if none provided
51+
output_path = output_path or CLIConfig.get_controllers_path()
52+
53+
# Normalize names
54+
controller_name = self.name_utils.normalize_controller_name(name)
55+
class_name = self.name_utils.generate_class_name(controller_name)
56+
57+
# Prepare paths
58+
output_dir = Path(output_path)
59+
controller_file = output_dir / f"{controller_name}.py"
60+
61+
# Check if file already exists (unless force is True)
62+
if not force and self.file_handler.file_exists(controller_file):
63+
raise ControllerGenerationError(
64+
f"Controller '{controller_name}' already exists at {controller_file}. "
65+
f"Use --force to overwrite."
66+
)
67+
68+
# Ensure output directory exists
69+
self.file_handler.ensure_directory_exists(output_dir)
70+
71+
# Render template
72+
content = self.template_renderer.render(
73+
self.config.CONTROLLER_TEMPLATE, {"class_name": class_name}
74+
)
75+
76+
# Write file
77+
self.file_handler.write_file(controller_file, content)
78+
79+
return controller_file
80+
81+
except Exception as e:
82+
if isinstance(e, (ControllerGenerationError, InvalidControllerNameError)):
83+
raise
84+
raise ControllerGenerationError(f"Failed to generate controller: {e}") from e

0 commit comments

Comments
 (0)