Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
- **🔧 Flexible Configuration**: Customizable paths and settings via environment variables
- **📝 Type Safety**: Full type hints support for better development experience
- **🧪 Testing Ready**: Built-in support for testing with comprehensive error handling
- **📖 Auto Documentation**: Generated code includes professional docstrings
- **� SQLAlchemy Introspection**: Deferred model reflection support via `ReflectedModel` and `FlaskReflection`, allowing models to map table schemas dynamically without explicit field declarations.
- **�📖 Auto Documentation**: Generated code includes professional docstrings
- **🌐 API & Web Support**: Content negotiation for both web and API responses

## 📦 Installation
Expand Down Expand Up @@ -75,20 +76,42 @@ if __name__ == "__main__":
```python
from flask import Flask
from flask_mvc import FlaskMVC
from flask_sqlalchemy import SQLAlchemy

mvc = FlaskMVC()
db = SQLAlchemy()

def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'

# Initialize MVC extension
db.init_app(app)
mvc.init_app(app, path='src') # Custom path (default: 'app')

return app

app = create_app()
```

### SQLAlchemy Introspection Support

Flask MVC now includes Rails-like SQLAlchemy introspection support. With `ReflectedModel` and `FlaskReflection`, you can define a model that only declares `__tablename__` while the extension discovers table columns at runtime.

```python
from flask_mvc.middlewares.base_model import ReflectedModel
from tests.app import db

class Message(ReflectedModel, db.Model):
__tablename__ = 'messages'
```

This feature works best when the database extension is initialized before Flask MVC:

```python
db.init_app(app)
FlaskMVC(app, path='app', db=db)
```

### Generate Your First Controller

```bash
Expand Down
31 changes: 31 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,38 @@ Flask MVC builds on provides the best architecture experience for Flask, and giv
- You can separate routes of business rules
- You can use the before_action to execute a specific code
- You can integrate with other extensions of Flask, Flask-SQLAlchemy, Flask-Migrate, etc.
- **SQLAlchemy Introspection**: deferred model reflection support with `ReflectedModel` and `FlaskReflection` for runtime schema discovery.

## Dependencies

Flask MVC just depends on the Flask extensions to working and requires Python >=3.8.0,<4.0.0.

## SQLAlchemy Introspection

This extension now supports deferred SQLAlchemy reflection for models. By inheriting from `ReflectedModel` and using the `FlaskReflection` integration, models can map to existing database tables without declaring every column in the model class.

Example:

```python
from flask_mvc.middlewares.base_model import ReflectedModel
from tests.app import db

class Message(ReflectedModel, db.Model):
__tablename__ = 'messages'
```

Initialize the database extension before Flask MVC so introspection works correctly:

```python
from flask import Flask
from flask_mvc import FlaskMVC
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'

db.init_app(app)
FlaskMVC(app, path='app', db=db)
```
2 changes: 1 addition & 1 deletion flask_mvc/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.0"
__version__ = "0.5.0"
16 changes: 11 additions & 5 deletions flask_mvc/flask_mvc.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from method_override.wsgi_method_override import MethodOverrideMiddleware

from . import cli
from .middlewares.html_input_method_helper import HTMLInputMethodHelper
from .middlewares.blueprint_binding import BlueprintBinding
from .middlewares.router import Router
from .middlewares.base_model import FlaskReflection


class FlaskMVC:
def __init__(self, app: Flask = None, path="app"):
def __init__(self, app: Flask = None, path="app", db: SQLAlchemy = None):
if app is not None:
self.init_app(app, path)
self.init_app(app, path, db)

def init_app(self, app: Flask = None, path="app"):
self.perform(app, path)
def init_app(self, app: Flask = None, path="app", db: SQLAlchemy = None):
self.perform(app, path, db)

def perform(self, app: Flask, path: str):
def perform(self, app: Flask, path: str, db: SQLAlchemy = None):
self._configure_template_folder(app)
self._configure_method_override_middleware(app)
self._configure_blueprint_middleware(app, path)
self._inject_object_in_jinja_template(app)
self._configure_cli_commands(app)
self._configure_instrospection_database(app, db)

def _configure_template_folder(self, app):
app.template_folder = "views"
Expand All @@ -31,6 +34,9 @@ def _configure_method_override_middleware(self, app):
def _configure_blueprint_middleware(self, app, path):
BlueprintBinding(app, path).register()

def _configure_instrospection_database(self, app, db):
FlaskReflection(app, db)

def _inject_object_in_jinja_template(self, app):
@app.context_processor
def inject_stage_and_region():
Expand Down
52 changes: 52 additions & 0 deletions flask_mvc/middlewares/base_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.declarative import DeferredReflection
from sqlalchemy.exc import OperationalError, InvalidRequestError


# The base model that your classes will inherit
class BaseModel(DeferredReflection):
"""SQLAlchemy base model with deferred table reflection.

This class is abstract and serves as a base for models that need to support
runtime table reflection using
:class:`sqlalchemy.ext.declarative.DeferredReflection`.
"""

__abstract__ = True


class FlaskReflection:
"""Helper to initialize SQLAlchemy reflection in a Flask application.

This class encapsulates the logic to prepare reflected tables via
:meth:`BaseModel.prepare` when the SQLAlchemy extension is available
in the Flask app context.
"""

def __init__(self, app: Flask = None, db: SQLAlchemy = None):
if app is not None and db is not None:
self.init_app(app, db)

def init_app(self, app: Flask, db: SQLAlchemy):
"""Initialize model reflection for a Flask application.

Args:
app: Instance of :class:`flask.Flask` where reflection will be initialized.
db: Instance of :class:`flask_sqlalchemy.SQLAlchemy` used to obtain the
engine and prepare reflected models.

Raises:
ValueError: If ``db`` is ``None``.
"""
if db is None:
raise ValueError("FlaskReflection required a instance of SQLAlchemy")

with app.app_context():
try:
BaseModel.prepare(db.engine)
except (OperationalError, InvalidRequestError):
app.logger.info(
"[FlaskReflection] Deferred introspection: tables or database not "
"found. This is normal on the first migration."
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "flask-mvc2"
version = "0.4.0"
version = "0.5.0"
description = "Transform Flask into a structured MVC architecture with powerful CLI tools"
readme = "README.md"
requires-python = ">=3.12,<3.15"
Expand Down
Loading
Loading