Skip to content

Commit 1fab5c7

Browse files
committed
feat(cli): implement CLI commands for MVC component generation and configuration
1 parent e203a8c commit 1fab5c7

File tree

11 files changed

+574
-0
lines changed

11 files changed

+574
-0
lines changed

mvc_flask/cli.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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', '-p',
37+
default=CLIConfig.DEFAULT_CONTROLLERS_PATH,
38+
help='Path where to create the controller'
39+
)
40+
@click.option(
41+
'--force', '-f',
42+
is_flag=True,
43+
help='Overwrite existing controller file'
44+
)
45+
@with_appcontext
46+
def controller(name: str, path: str, force: bool) -> None:
47+
"""Generate a new controller.
48+
49+
Creates a new controller class with RESTful methods following Flask MVC patterns.
50+
The controller will include standard CRUD operations and proper type hints.
51+
52+
Examples:
53+
\b
54+
flask mvc generate controller home
55+
flask mvc generate controller user --path custom/controllers
56+
flask mvc generate controller api_v1_user --force
57+
"""
58+
try:
59+
logger.info(f"Generating controller '{name}' at path '{path}'")
60+
61+
generator = ControllerGenerator()
62+
controller_file = generator.generate(name, path, force=force)
63+
64+
click.echo(
65+
click.style(
66+
f"✓ Controller created successfully at {controller_file}",
67+
fg=CLIConfig.SUCCESS_COLOR
68+
)
69+
)
70+
71+
# Provide helpful next steps
72+
controller_name = name if name.endswith('_controller') else f"{name}_controller"
73+
click.echo(
74+
click.style(
75+
"\nNext steps:",
76+
fg=CLIConfig.INFO_COLOR,
77+
bold=True
78+
)
79+
)
80+
click.echo(
81+
click.style(
82+
"1. Add routes for your controller in your routes.py file",
83+
fg=CLIConfig.INFO_COLOR
84+
)
85+
)
86+
click.echo(
87+
click.style(
88+
f"2. Create templates in views/{name}/ directory",
89+
fg=CLIConfig.INFO_COLOR
90+
)
91+
)
92+
click.echo(
93+
click.style(
94+
f"3. Implement your business logic in {controller_name}.py",
95+
fg=CLIConfig.INFO_COLOR
96+
)
97+
)
98+
99+
except (ControllerGenerationError, InvalidControllerNameError) as e:
100+
logger.error(f"Controller generation failed: {e}")
101+
click.echo(
102+
click.style(f"✗ Error: {e}", fg=CLIConfig.ERROR_COLOR),
103+
err=True
104+
)
105+
raise click.Abort() from e
106+
except Exception as e:
107+
logger.exception(f"Unexpected error during controller generation: {e}")
108+
click.echo(
109+
click.style(
110+
f"✗ Unexpected error: {e}. Please check the logs for more details.",
111+
fg=CLIConfig.ERROR_COLOR
112+
),
113+
err=True
114+
)
115+
raise click.Abort() from e
116+
117+
118+
def init_app(app) -> None:
119+
"""Initialize CLI commands with Flask app.
120+
121+
Args:
122+
app: Flask application instance
123+
"""
124+
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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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',
43+
cls.DEFAULT_CONTROLLERS_PATH
44+
)
45+
46+
@classmethod
47+
def get_templates_dir(cls) -> Path:
48+
"""Get templates directory path.
49+
50+
Returns:
51+
Templates directory path
52+
"""
53+
custom_dir = os.getenv(f'{cls.ENV_PREFIX}TEMPLATES_DIR')
54+
if custom_dir:
55+
return Path(custom_dir)
56+
return cls.TEMPLATES_DIR
57+
58+
@classmethod
59+
def get_file_encoding(cls) -> str:
60+
"""Get file encoding from environment or default.
61+
62+
Returns:
63+
File encoding string
64+
"""
65+
return os.getenv(
66+
f'{cls.ENV_PREFIX}FILE_ENCODING',
67+
cls.FILE_ENCODING
68+
)

mvc_flask/core/exceptions.py

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

0 commit comments

Comments
 (0)