From a4ef4ff8b73604128abf2ef2a3bb2e5fc093ec4e Mon Sep 17 00:00:00 2001 From: Marcus Almeida Date: Sat, 18 Nov 2023 03:32:49 -0300 Subject: [PATCH] Refactor main code, separate code into modules (#55) * feat(middlewares): add CustomRequestMiddleware to handle custom form and files properties in Flask requests test(callbacks_controller_test): add test for creating a message in callbacks controller * fix(__init__.py): fix import statement for HookMiddleware class feat(hook_middleware.py): add HookMiddleware class to register hooks for controller methods in blueprints The import statement for the `HookMiddleware` class in `__init__.py` was incorrect. It was fixed to import from the correct module. A new file `hook_middleware.py` was added to the `middlewares` directory. This file contains the `HookMiddleware` class which is responsible for registering hooks for controller methods in blueprints. The `register` method takes a controller instance and a blueprint instance as arguments, and registers the appropriate hooks based on the attributes of the controller instance. The `accept_attributes` property defines the list of attributes that are accepted as hooks. * feat(mvc_flask): add InputMethodHelper class to handle HTML-related operations The InputMethodHelper class is added to the mvc_flask/helpers/html module. This class provides methods for generating HTML input elements with specific methods (like PUT and DELETE) that are not natively supported by HTML forms. The class includes the following methods: - input_hidden_method: Determines the appropriate HTML string to return based on the given method string. - _input_html: Generates a hidden HTML input element. - _put: Generates a hidden input field for the PUT method. - _delete: Generates a hidden input field for the DELETE method. This class is intended to be used in the FlaskMVC class in the mvc_flask/__init__.py file. The inject_stage_and_region method in the FlaskMVC class now uses the InputMethodHelper class to generate the appropriate HTML for the method attribute in the returned dictionary. * refactor(__init__.py): rename MethodOverrideMiddleware class to MethodOverrideMiddleware for consistency and clarity refactor(hook_middleware.py): rename HookMidleware class to HookMiddleware for consistency and clarity feat(__init__.py): add import statements for MethodOverrideMiddleware and CustomRequestMiddleware feat(__init__.py): remove unused Hook class and its related code feat(__init__.py): update app.request_class to use CustomRequestMiddleware instead of CustomRequest feat(__init__.py): update app.wsgi_app to use MethodOverrideMiddleware instead of HTTPMethodOverrideMiddleware * feat(callbacks_controller.py): add CallbacksController class with index method and before_request callback feat(routes.py): add route for callbacks with only index method test(routes_test.py): add tests for the newly added callbacks route and controller * fix(messages_controller.py): change query method from `get` to `filter_by` to handle cases where the message with the given id does not exist * fix(hook_middleware.py): format the list comprehension for better readability fix(custom_request_middleware.py): remove extra blank line fix(callbacks_controller.py): change single quotes to double quotes for consistency fix(routes_test.py): format the assert statement for better readability * fix(mvc_flask): fix typo in method_override_middleware filename feat(mvc_flask): add method_override_middleware to handle HTTP method override functionality The typo in the filename of the method_override_middleware module has been fixed. The correct filename is now method_override_middleware.py. A new file, method_override_middleware.py, has been added to the mvc_flask/middlewares/http directory. This file contains the implementation of the MethodOverrideMiddleware class, which is responsible for handling HTTP method override functionality. The middleware allows clients to override the HTTP method of a request by including a special "_method" parameter in the request body. The allowed methods for override are GET, POST, DELETE, PUT, and PATCH. The middleware also handles cases where the overridden method is a bodyless method (GET, HEAD, OPTIONS, DELETE) by setting the appropriate values in the WSGI environment. * fix(__init__.py): update import statement for RouterMiddleware to reflect new file structure feat(router_middleware.py): add RouterMiddleware class to manage routes in a web application feat(namespace_middleware.py): add NamespaceMiddleware class to create namespaces for routes * refactor(__init__.py): remove unused imports and commented out code feat(blueprint_middleware.py): add BlueprintMiddleware class to handle registering blueprints and routes dynamically * refactor(mvc_flask): reorganize code structure and improve readability - Move FlaskMVC class to a separate file `mvc_flask.py` for better organization - Remove unnecessary imports and unused code from `__init__.py` - Rename `init_app` method in `FlaskMVC` class to `perform` for better clarity - Extract configuration logic into separate methods in `FlaskMVC` class for better modularity and readability - Update method names in `FlaskMVC` class to better reflect their purpose - Update variable names in `FlaskMVC` class for better clarity - Update comments and docstrings in `FlaskMVC` class for better understanding --- mvc_flask/__init__.py | 80 +------------------ .../html/input_method_helper.py} | 38 ++++----- mvc_flask/middlewares/blueprint_middleware.py | 38 +++++++++ mvc_flask/middlewares/hook_middleware.py | 24 ++++++ .../http/custom_request_middleware.py | 15 ++++ .../method_override_middleware.py} | 26 +++--- .../http/namespace_middleware.py} | 7 +- .../http/router_middleware.py} | 34 ++++---- mvc_flask/mvc_flask.py | 45 +++++++++++ tests/app/controllers/callbacks_controller.py | 8 ++ tests/app/controllers/messages_controller.py | 4 +- tests/app/routes.py | 1 + tests/callbacks_controller_test.py | 7 ++ tests/routes_test.py | 10 ++- 14 files changed, 200 insertions(+), 137 deletions(-) rename mvc_flask/{middlewares/html.py => helpers/html/input_method_helper.py} (94%) create mode 100644 mvc_flask/middlewares/blueprint_middleware.py create mode 100644 mvc_flask/middlewares/hook_middleware.py create mode 100644 mvc_flask/middlewares/http/custom_request_middleware.py rename mvc_flask/middlewares/{http_method_override.py => http/method_override_middleware.py} (64%) rename mvc_flask/{namespace.py => middlewares/http/namespace_middleware.py} (96%) rename mvc_flask/{router.py => middlewares/http/router_middleware.py} (82%) create mode 100644 mvc_flask/mvc_flask.py create mode 100644 tests/app/controllers/callbacks_controller.py create mode 100644 tests/callbacks_controller_test.py diff --git a/mvc_flask/__init__.py b/mvc_flask/__init__.py index 0d8142d..e4c7e53 100644 --- a/mvc_flask/__init__.py +++ b/mvc_flask/__init__.py @@ -1,79 +1,3 @@ -from importlib import import_module +from .mvc_flask import FlaskMVC, Router -from flask import Flask -from flask.blueprints import Blueprint - -from .router import Router -from .middlewares.html import HTMLMiddleware -from .middlewares.http_method_override import ( - HTTPMethodOverrideMiddleware, - CustomRequest, -) - - -class FlaskMVC: - def __init__(self, app: Flask = None, path="app"): - if app is not None: - self.init_app(app, path) - - def init_app(self, app: Flask = None, path="app"): - self.hook = Hook() - self.path = path - - app.template_folder = "views" - app.request_class = CustomRequest - app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app) - - # register blueprint - self.register_blueprint(app) - - @app.context_processor - def inject_stage_and_region(): - return dict(method=HTMLMiddleware().method) - - def register_blueprint(self, app: Flask): - # load routes defined from users - import_module(f"{self.path}.routes") - - for route in Router._method_route().items(): - controller = route[0] - blueprint = Blueprint(controller, controller) - - obj = import_module(f"{self.path}.controllers.{controller}_controller") - view_func = getattr(obj, f"{controller.title()}Controller") - instance_of_controller = view_func() - self.hook.register(instance_of_controller, blueprint) - - for resource in route[1]: - blueprint.add_url_rule( - rule=resource.path, - endpoint=resource.action, - view_func=getattr(instance_of_controller, resource.action), - methods=resource.method, - ) - - app.register_blueprint(blueprint) - - -class Hook: - def register(self, instance_of_controller, blueprint): - accept_attributes = [ - "before_request", - "after_request", - "teardown_request", - "after_app_request", - "before_app_request", - "teardown_app_request", - "before_app_first_request", - ] - - attrs = [ - attr for attr in dir(instance_of_controller) if attr in accept_attributes - ] - - if attrs: - for attr in attrs: - values = getattr(instance_of_controller, attr) - for value in values: - hook_method = getattr(instance_of_controller, value) - getattr(blueprint, attr)(hook_method) +__all__ = ["FlaskMVC", "Router"] diff --git a/mvc_flask/middlewares/html.py b/mvc_flask/helpers/html/input_method_helper.py similarity index 94% rename from mvc_flask/middlewares/html.py rename to mvc_flask/helpers/html/input_method_helper.py index cb08d58..4583a1a 100644 --- a/mvc_flask/middlewares/html.py +++ b/mvc_flask/helpers/html/input_method_helper.py @@ -1,7 +1,7 @@ import markupsafe -class HTMLMiddleware: +class InputMethodHelper: """ A middleware class for handling HTML-related operations, specifically for creating hidden input fields with specific methods (like PUT and DELETE) that are not natively supported by HTML forms. @@ -13,6 +13,24 @@ class HTMLMiddleware: - method: Public method to handle the generation of appropriate HTML based on a given string. """ + def input_hidden_method(self, input_method): + """ + Determines the appropriate HTML string to return based on the given method string. + + Args: + - string (str): The method string (e.g., 'put', 'delete'). + + Returns: + - Markup: A markupsafe.Markup object containing the appropriate HTML string. + This object is safe to render directly in templates. + """ + result = { + "put": self._put(), + "delete": self._delete(), + }[input_method.lower()] + + return markupsafe.Markup(result) + def _input_html(self, input_method): """ Generates a hidden HTML input element. @@ -42,21 +60,3 @@ def _delete(self): - str: An HTML string for a hidden input element for the DELETE method. """ return self._input_html("delete") - - def method(self, string): - """ - Determines the appropriate HTML string to return based on the given method string. - - Args: - - string (str): The method string (e.g., 'put', 'delete'). - - Returns: - - Markup: A markupsafe.Markup object containing the appropriate HTML string. - This object is safe to render directly in templates. - """ - result = { - "put": self._put(), - "delete": self._delete(), - }[string.lower()] - - return markupsafe.Markup(result) diff --git a/mvc_flask/middlewares/blueprint_middleware.py b/mvc_flask/middlewares/blueprint_middleware.py new file mode 100644 index 0000000..6ce331f --- /dev/null +++ b/mvc_flask/middlewares/blueprint_middleware.py @@ -0,0 +1,38 @@ +from flask import Flask +from importlib import import_module + +from flask.blueprints import Blueprint + +from .hook_middleware import HookMiddleware + +from .http.router_middleware import RouterMiddleware as Router + + +class BlueprintMiddleware: + def __init__(self, app: Flask, path: str) -> None: + self.app = app + self.path = path + + # load routes defined from users + import_module(f"{self.path}.routes") + + def register(self): + for route in Router._method_route().items(): + controller = route[0] + blueprint = Blueprint(controller, controller) + + obj = import_module(f"{self.path}.controllers.{controller}_controller") + view_func = getattr(obj, f"{controller.title()}Controller") + instance_of_controller = view_func() + + HookMiddleware().register(instance_of_controller, blueprint) + + for resource in route[1]: + blueprint.add_url_rule( + rule=resource.path, + endpoint=resource.action, + view_func=getattr(instance_of_controller, resource.action), + methods=resource.method, + ) + + self.app.register_blueprint(blueprint) diff --git a/mvc_flask/middlewares/hook_middleware.py b/mvc_flask/middlewares/hook_middleware.py new file mode 100644 index 0000000..d38991e --- /dev/null +++ b/mvc_flask/middlewares/hook_middleware.py @@ -0,0 +1,24 @@ +class HookMiddleware: + def register(self, controller_instance, blueprint_instance): + attrs = [ + attr for attr in dir(controller_instance) if attr in self.accept_attributes + ] + + if attrs: + for attr in attrs: + values = getattr(controller_instance, attr) + for value in values: + hook_method = getattr(controller_instance, value) + getattr(blueprint_instance, attr)(hook_method) + + @property + def accept_attributes(self): + return [ + "before_request", + "after_request", + "teardown_request", + "after_app_request", + "before_app_request", + "teardown_app_request", + "before_app_first_request", + ] diff --git a/mvc_flask/middlewares/http/custom_request_middleware.py b/mvc_flask/middlewares/http/custom_request_middleware.py new file mode 100644 index 0000000..05735c7 --- /dev/null +++ b/mvc_flask/middlewares/http/custom_request_middleware.py @@ -0,0 +1,15 @@ +from flask import Request + + +class CustomRequestMiddleware(Request): + @property + def form(self): + if "wsgi._post_form" in self.environ: + return self.environ["wsgi._post_form"] + return super().form + + @property + def files(self): + if "wsgi._post_files" in self.environ: + return self.environ["wsgi._post_files"] + return super().files diff --git a/mvc_flask/middlewares/http_method_override.py b/mvc_flask/middlewares/http/method_override_middleware.py similarity index 64% rename from mvc_flask/middlewares/http_method_override.py rename to mvc_flask/middlewares/http/method_override_middleware.py index ccd878f..b4cbd70 100644 --- a/mvc_flask/middlewares/http_method_override.py +++ b/mvc_flask/middlewares/http/method_override_middleware.py @@ -1,8 +1,7 @@ -from flask import Request from werkzeug.formparser import parse_form_data -class HTTPMethodOverrideMiddleware: +class MethodOverrideMiddleware: allowed_methods = frozenset( [ "GET", @@ -12,7 +11,14 @@ class HTTPMethodOverrideMiddleware: "PATCH", ] ) - bodyless_methods = frozenset(["GET", "HEAD", "OPTIONS", "DELETE"]) + bodyless_methods = frozenset( + [ + "GET", + "HEAD", + "OPTIONS", + "DELETE", + ] + ) def __init__(self, app, input_name="_method"): self.app = app @@ -32,17 +38,3 @@ def __call__(self, environ, start_response): environ["CONTENT_LENGTH"] = "0" return self.app(environ, start_response) - - -class CustomRequest(Request): - @property - def form(self): - if "wsgi._post_form" in self.environ: - return self.environ["wsgi._post_form"] - return super().form - - @property - def files(self): - if "wsgi._post_files" in self.environ: - return self.environ["wsgi._post_files"] - return super().files diff --git a/mvc_flask/namespace.py b/mvc_flask/middlewares/http/namespace_middleware.py similarity index 96% rename from mvc_flask/namespace.py rename to mvc_flask/middlewares/http/namespace_middleware.py index eeac72a..843465b 100644 --- a/mvc_flask/namespace.py +++ b/mvc_flask/middlewares/http/namespace_middleware.py @@ -1,8 +1,5 @@ -"""Namespace module.""" - - -class Namespace: - """Namespace.""" +class NamespaceMiddleware: + """NamespaceMiddleware.""" def __init__(self, name: str, router): self.name = name diff --git a/mvc_flask/router.py b/mvc_flask/middlewares/http/router_middleware.py similarity index 82% rename from mvc_flask/router.py rename to mvc_flask/middlewares/http/router_middleware.py index 83fee17..24f27b4 100644 --- a/mvc_flask/router.py +++ b/mvc_flask/middlewares/http/router_middleware.py @@ -1,13 +1,13 @@ from collections import namedtuple -from .namespace import Namespace +from .namespace_middleware import NamespaceMiddleware Model = namedtuple("Model", "method path controller action") -class Router: +class RouterMiddleware: """ - Router class for managing routes in a web application. + RouterMiddleware class for managing routes in a web application. This class provides methods to define and manage different HTTP routes (GET, POST, PUT, DELETE) for the application's controllers and actions. @@ -51,7 +51,7 @@ def _method_route(): routes = {} - for route in Router.ROUTES: + for route in RouterMiddleware.ROUTES: value = list(route.values())[0] for key in route: if key not in routes: @@ -63,16 +63,16 @@ def _method_route(): @staticmethod def namespace(name: str): """ - Creates a namespace for routes. + Creates a namespace middleware for routes. Args: name (str): The name of the namespace. Returns: - Namespace: An instance of Namespace associated with the given name. + NamespaceMiddleware: An instance of NamespaceMiddleware associated with the given name. """ - return Namespace(name, Router) + return NamespaceMiddleware(name, RouterMiddleware) @staticmethod def get(path: str, resource: str): @@ -85,7 +85,9 @@ def get(path: str, resource: str): """ controller, action = resource.split("#") - Router.ROUTES.append({controller: Model(["GET"], path, controller, action)}) + RouterMiddleware.ROUTES.append( + {controller: Model(["GET"], path, controller, action)} + ) @staticmethod def post(path: str, resource: str): @@ -98,7 +100,9 @@ def post(path: str, resource: str): """ controller, action = resource.split("#") - Router.ROUTES.append({controller: Model(["POST"], path, controller, action)}) + RouterMiddleware.ROUTES.append( + {controller: Model(["POST"], path, controller, action)} + ) @staticmethod def put(path: str, resource: str): @@ -111,7 +115,7 @@ def put(path: str, resource: str): """ controller, action = resource.split("#") - Router.ROUTES.append( + RouterMiddleware.ROUTES.append( {controller: Model(["PUT", "PATCH"], path, controller, action)}, ) @@ -126,7 +130,9 @@ def delete(path: str, resource: str): """ controller, action = resource.split("#") - Router.ROUTES.append({controller: Model(["DELETE"], path, controller, action)}) + RouterMiddleware.ROUTES.append( + {controller: Model(["DELETE"], path, controller, action)} + ) @staticmethod def all(resource: str, only=None, base_path=""): @@ -149,7 +155,7 @@ def all(resource: str, only=None, base_path=""): "delete", ] actions = only.split() if isinstance(only, str) else only - Router._add_routes(resource, actions if actions else group, base_path) + RouterMiddleware._add_routes(resource, actions if actions else group, base_path) @staticmethod def _add_routes(name, actions, base_path): @@ -185,7 +191,7 @@ def _add_routes(name, actions, base_path): path = f"{base_path}/{name}{urls.get(action, '')}" if action in parameters: - getattr(Router, parameters[action])(path, f"{name}#{action}") + getattr(RouterMiddleware, parameters[action])(path, f"{name}#{action}") continue - getattr(Router, groups[action])(path, f"{name}#{action}") + getattr(RouterMiddleware, groups[action])(path, f"{name}#{action}") diff --git a/mvc_flask/mvc_flask.py b/mvc_flask/mvc_flask.py new file mode 100644 index 0000000..260e9c4 --- /dev/null +++ b/mvc_flask/mvc_flask.py @@ -0,0 +1,45 @@ +from flask import Flask + +from .middlewares.http.router_middleware import RouterMiddleware as Router + +from .middlewares.http.method_override_middleware import MethodOverrideMiddleware +from .middlewares.http.custom_request_middleware import CustomRequestMiddleware +from .middlewares.blueprint_middleware import BlueprintMiddleware + + +from .helpers.html.input_method_helper import InputMethodHelper + + +class FlaskMVC: + def __init__(self, app: Flask = None, path="app"): + if app is not None: + self.init_app(app, path) + + def init_app(self, app: Flask = None, path="app"): + self.perform(app, path) + + def perform(self, app: Flask, path: str): + self._configure_template_folder(app) + self._configure_custom_request_middleware(app) + self._configure_method_override_middleware(app) + self._configure_blueprint_middleware(app, path) + self._inject_object_in_jinja_template(app) + + def _configure_template_folder(self, app): + app.template_folder = "views" + + def _configure_custom_request_middleware(self, app): + app.request_class = CustomRequestMiddleware + + def _configure_method_override_middleware(self, app): + app.wsgi_app = MethodOverrideMiddleware(app.wsgi_app) + + def _configure_blueprint_middleware(self, app, path): + BlueprintMiddleware(app, path).register() + + def _inject_object_in_jinja_template(self, app): + @app.context_processor + def inject_stage_and_region(): + return { + "method": InputMethodHelper().input_hidden_method, + } diff --git a/tests/app/controllers/callbacks_controller.py b/tests/app/controllers/callbacks_controller.py new file mode 100644 index 0000000..6a09c31 --- /dev/null +++ b/tests/app/controllers/callbacks_controller.py @@ -0,0 +1,8 @@ +class CallbacksController: + before_request = ["before"] + + def index(self): + return self.page + + def before(self): + self.page = "index" diff --git a/tests/app/controllers/messages_controller.py b/tests/app/controllers/messages_controller.py index 48b2b48..23e17b7 100644 --- a/tests/app/controllers/messages_controller.py +++ b/tests/app/controllers/messages_controller.py @@ -28,7 +28,7 @@ def edit(self, id): return render_template("messages/edit.html") def update(self, id): - message = Message.query.get(id) + message = Message.query.filter_by(id=id).first() if request.headers["Content-Type"] == "application/json": message.title = request.json["title"] @@ -44,7 +44,7 @@ def update(self, id): return render_template("messages/show.html", message=message) def delete(self, id): - message = Message.query.get(id) + message = Message.query.filter_by(id=id).first() db.session.delete(message) db.session.commit() diff --git a/tests/app/routes.py b/tests/app/routes.py index b49699b..bc30289 100644 --- a/tests/app/routes.py +++ b/tests/app/routes.py @@ -1,6 +1,7 @@ from mvc_flask import Router Router.all("messages") +Router.all("callbacks", only=["index"]) api = Router.namespace("/api/v1") diff --git a/tests/callbacks_controller_test.py b/tests/callbacks_controller_test.py new file mode 100644 index 0000000..41cd9eb --- /dev/null +++ b/tests/callbacks_controller_test.py @@ -0,0 +1,7 @@ +from flask import url_for + + +def test_create_a_message(client): + response = client.get(url_for("callbacks.index")) + + assert response.text == "index" diff --git a/tests/routes_test.py b/tests/routes_test.py index ec0d0ed..5f81221 100644 --- a/tests/routes_test.py +++ b/tests/routes_test.py @@ -4,7 +4,12 @@ def test_when_blueprints_have_been_registered(client): - assert set(client.application.blueprints) == {"messages", "health", "posts"} + assert set(client.application.blueprints) == { + "messages", + "health", + "posts", + "callbacks", + } def test_when_not_exists_registered_blueprints(client): @@ -12,6 +17,7 @@ def test_when_not_exists_registered_blueprints(client): "messages": 1, "health": 1, "posts": 1, + "callbacks": 1, } @@ -42,7 +48,7 @@ def test_when_there_are_many_registered_routes(client): for route in routes.methods ] - assert methods.count("GET") == 7 + assert methods.count("GET") == 8 assert methods.count("POST") == 1 assert methods.count("PUT") == 1 assert methods.count("PATCH") == 1