diff --git a/README.md b/README.md index 8ddef00..50727dc 100644 --- a/README.md +++ b/README.md @@ -11,25 +11,28 @@ Flask-Muck is a batteries-included framework for automatically generating RESTful APIs with Create, Read, Update and Delete (CRUD) endpoints in a Flask/SqlAlchemy application stack. -With Flask-Muck you don't have to worry about the CRUD. +With Flask-Muck you don't have to worry about the CRUD. ```python from flask import Blueprint -from flask_muck.views import MuckApiView +from flask_muck.views import FlaskMuckApiView import marshmallow as ma from marshmallow import fields as mf from myapp import db + class MyModel(db.Model): id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(255), nullable=False) + name = db.Column(db.String, nullable=False) + class MyModelSchema(ma.Schema): id = mf.Integer(dump_only=True) name = mf.String() -class MyModelApiView(MuckApiView): + +class MyModelApiView(FlaskMuckApiView): api_name = "my-model" session = db.session Model = MyModel @@ -39,8 +42,9 @@ class MyModelApiView(MuckApiView): UpdateSchema = MyModelSchema searchable_columns = [MyModel.name] + blueprint = Blueprint("api", __name__, url_prefix="/api/") -MyModelApiView.add_crud_to_blueprint(blueprint) +MyModelApiView.add_rules_to_blueprint(blueprint) # Available Endpoints: # CREATE | curl -X POST "/api/v1/my-model" -H "Content-Type: application/json" \-d "{\"name\": \"Ayla\"}" diff --git a/docs/docs/quickstart.md b/docs/docs/quickstart.md new file mode 100644 index 0000000..db97343 --- /dev/null +++ b/docs/docs/quickstart.md @@ -0,0 +1,115 @@ +# Quick Start + +Flask-Muck provides standard REST APIs for resources in your Flask/SqlAlchemy application. This +is accomplishing by creating subclasses of the FlaskMuckApiView and configuring them by setting a series of class +variables. + +The quick start guide will walk you through creating your first basic API. The subsequent chapters covering using the +APIs and configuring advanced features. + + +## Define a base view +Flask-Muck works by subsclassing the FlaskMuckApiView and setting class variables on the concrete view classes. In almost +all projects there will be a basic set of class variables shared by all FlaskMuckApiView subclasses. The two most common +settings to be shared across all views is the database session used for committing changes and a set of +decorators that should be applied to all views. + +In this example a base class is defined with with the app's database session and authentication decorator set. + +Application using [SqlAlchemy in Flask](https://flask.palletsprojects.com/en/3.0.x/patterns/sqlalchemy/) session setup: +```python +from flask_muck import FlaskMuckApiView +from myapp.database import db_session +from myapp.auth.decorators import login_required + + +class BaseApiView(FlaskMuckApiView): + session = db_session + decorators = [login_required] + +``` + +Application using [Flask-SqlAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/quickstart/#quick-start) exension: +```python +from flask_muck import FlaskMuckApiView +from myapp import db +from myapp.auth.decorators import login_required + + +class BaseApiView(FlaskMuckApiView): + session = db.session + decorators = [login_required] +``` + +NOTE: For the remainder of this guide we'll assume the usage of the [Flask-SqlAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/3.1.x/quickstart/#quick-start) extension. + +## Create SqlAlchemy Model +Flask-Muck requires the use of SqlAlchemy's [declarative system](). If you are not using the declarative system you will +need to review those [docs]() and re-evaluate whether Flask-Muck is the right choice. Explaining the full process of +creating and registering a SqlAlchemy model in your Flask app is outside the scope of this guide. The example code below +shows the model class we will be creating an API for in the rest of the guide. + +```python +from myapp import db + +class Teacher(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, nullable=False) + years_teaching = db.Column(db.Integer) +``` + +## Create input and response Marshmallow schemas +Flask-Muck requires configuring [Marshmallow](https://marshmallow.readthedocs.io/en/stable/) schemas that will be used +to validate the payload data for the Create, Update, Patch and (optionally) Delete endpoints. Additionally a schema must +be supplied that will serialize the endpoint's resource in responses. In this example simple schema is defined that +can be re-used for all validation and serialization. + +```python +from marshmallow import Schema +from marshmallow import fields as mf + + +class TeacherSchema(Schema): + id = mf.Integer(dump_only=True) + name = mf.String(required=True) + years_teaching = mf.Integer() +``` + +## Create concrete FlaskMuckApiView +Inherit from the project's base api view class and define the required class variables. + +```python +class TeacherApiView(BaseApiView): + api_name = "teachers" # Name used as the url endpoint in the REST API. + Model = Teacher # Model class that will be queried and updated by this API. + ResponseSchema = TeacherSchema # Marshmallow schema used to serialize and Teachers returned by the API. + CreateSchema = TeacherSchema # Marshmallow schema used to validate payload data sent to the Create endpoint. + PatchSchema = TeacherSchema # Marshmallow schema used to validate payload data sent to the Patch endpoint. + UpdateSchema = TeacherSchema # Marshmallow schema used to validate payload data sent to the Update endpoint. + searchable_columns = [Teacher.name] # List of model columns that can be searched when listing Teachers using the API. +``` + +## Add URL rules to a Flask Blueprint. +The final step is to add the correct URL rules to an existing [Flask Blueprint](https://flask.palletsprojects.com/en/3.0.x/blueprints/) +object. A classmethod is included that handles adding all necessary rules to the given Blueprint. + +```python +from flask import Blueprint + +blueprint = Blueprint("api", __name__, url_prefix="/api/") +TeacherApiView.add_rules_to_blueprint(blueprint) +``` + +This produces the following views, a standard REST API! + +| URL Path | Method | Description | +|----------------------|--------|----------------------------------------------------------------------------------------------------| +| /api/teachers/ | GET | List all teachers - querystring options available for sorting, filtering, searching and pagination | +| /api/teachers/ | POST | Create a teacher | +| /api/teachers/\/ | GET | Fetch a single teacher | +| /api/teachers/\/ | PUT | Update a single teacher | +| /api/teachers/\/ | PATCH | Patch a single teacher | +| /api/teachers/\/ | DELETE | Delete a single teacher | + + + diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 5f09e19..af79b4b 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -35,5 +35,6 @@ extra_css: - stylesheets/extra.css nav: - - Home: index.md + - About: index.md - Installation: installation.md + - Quick Start: quickstart.md diff --git a/examples/00_quickstart/app.py b/examples/00_quickstart/app.py index ea6a7de..a2f2c05 100644 --- a/examples/00_quickstart/app.py +++ b/examples/00_quickstart/app.py @@ -4,7 +4,7 @@ from marshmallow import fields as mf from sqlalchemy.orm import DeclarativeBase -from flask_muck.views import MuckApiView +from flask_muck.views import FlaskMuckApiView # Create a Flask app app = Flask(__name__) @@ -42,7 +42,7 @@ class TodoSchema(ma.Schema): # Add Muck views to generate CRUD REST API. -class BaseApiView(MuckApiView): +class BaseApiView(FlaskMuckApiView): """Base view to inherit from. Helpful for setting class variables shared with all API views such as "session" and "decorators". """ @@ -63,7 +63,7 @@ class TodoApiView(BaseApiView): # Add all url rules to the blueprint. -TodoApiView.add_crud_to_blueprint(api_blueprint) +TodoApiView.add_rules_to_blueprint(api_blueprint) # Register api blueprint with the app. app.register_blueprint(api_blueprint) diff --git a/examples/01_authentication/app.py b/examples/01_authentication/app.py index 4574dec..0e8b392 100644 --- a/examples/01_authentication/app.py +++ b/examples/01_authentication/app.py @@ -11,7 +11,7 @@ from marshmallow import fields as mf from sqlalchemy.orm import DeclarativeBase -from flask_muck.views import MuckApiView +from flask_muck.views import FlaskMuckApiView # Create a Flask app app = Flask(__name__) @@ -81,7 +81,7 @@ def logout_view(): # Add Muck views to generate CRUD REST API. -class BaseApiView(MuckApiView): +class BaseApiView(FlaskMuckApiView): """Base view to inherit from. Helpful for setting class variables shared with all API views such as "sqlalchemy_db" and "decorators". """ @@ -103,7 +103,7 @@ class TodoApiView(BaseApiView): # Add all url rules to the blueprint. -TodoApiView.add_crud_to_blueprint(api_blueprint) +TodoApiView.add_rules_to_blueprint(api_blueprint) # Register api blueprint with the app. app.register_blueprint(api_blueprint) diff --git a/src/flask_muck/__init__.py b/src/flask_muck/__init__.py index 2e299b2..34add82 100644 --- a/src/flask_muck/__init__.py +++ b/src/flask_muck/__init__.py @@ -1,4 +1,4 @@ -from .views import MuckApiView +from .views import FlaskMuckApiView from .callback import MuckCallback VERSION = "0.0.3b2" diff --git a/src/flask_muck/utils.py b/src/flask_muck/utils.py index 6240bb7..a58dd1e 100644 --- a/src/flask_muck/utils.py +++ b/src/flask_muck/utils.py @@ -8,10 +8,10 @@ from flask_muck.types import SqlaModelType if TYPE_CHECKING: - from flask_muck.views import MuckApiView + from flask_muck.views import FlaskMuckApiView -def get_url_rule(muck_view: type[MuckApiView], append_rule: Optional[str]) -> str: +def get_url_rule(muck_view: type[FlaskMuckApiView], append_rule: Optional[str]) -> str: """Recursively build the url rule for a MuckApiView by looking at its parent if it exists.""" rule = muck_view.api_name if append_rule: @@ -40,7 +40,7 @@ def get_fk_column( def get_query_filters_from_request_path( - view: Union[type[MuckApiView], MuckApiView], query_filters: list + view: Union[type[FlaskMuckApiView], FlaskMuckApiView], query_filters: list ) -> list: """Recursively builds query kwargs from the request path based on nested MuckApiViews. If the view has no parent then nothing is done and original query_kwargs are returned. @@ -57,7 +57,8 @@ def get_query_filters_from_request_path( def get_join_models_from_parent_views( - view: Union[type[MuckApiView], MuckApiView], join_models: list[SqlaModelType] + view: Union[type[FlaskMuckApiView], FlaskMuckApiView], + join_models: list[SqlaModelType], ) -> list[SqlaModelType]: """Recursively builds a list of models that need to be joined in queries based on the view's parents..""" if view.parent: diff --git a/src/flask_muck/views.py b/src/flask_muck/views.py index da851d4..e0ece3b 100644 --- a/src/flask_muck/views.py +++ b/src/flask_muck/views.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -from copy import deepcopy from json import JSONDecodeError from logging import getLogger from typing import Optional, Union, Any @@ -11,7 +10,7 @@ from flask.views import MethodView from marshmallow import Schema from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session, Query +from sqlalchemy.orm import Query, scoped_session from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.sql.elements import ( BinaryExpression, @@ -41,8 +40,8 @@ } -class MuckApiView(MethodView): - session: Session +class FlaskMuckApiView(MethodView): + session: scoped_session api_name: str Model: SqlaModelType @@ -64,7 +63,7 @@ class MuckApiView(MethodView): post_delete_callbacks: list[type[MuckCallback]] = [] searchable_columns: Optional[list[InstrumentedAttribute]] = None - parent: Optional[type[MuckApiView]] = None + parent: Optional[type[FlaskMuckApiView]] = None default_pagination_limit: int = 20 one_to_one_api: bool = False allowed_methods: set[str] = {"GET", "POST", "PUT", "PATCH", "DELETE"} @@ -374,7 +373,7 @@ def _get_query_search_filter( return or_(*searches), join_models @classmethod - def add_crud_to_blueprint(cls, blueprint: Blueprint) -> None: + def add_rules_to_blueprint(cls, blueprint: Blueprint) -> None: """Adds CRUD endpoints to a blueprint.""" url_rule = get_url_rule(cls, None) api_view = cls.as_view(f"{cls.api_name}_api") diff --git a/tests/app.py b/tests/app.py index a8a0bb2..49109ed 100644 --- a/tests/app.py +++ b/tests/app.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import DeclarativeBase, Mapped from flask_muck import MuckCallback -from flask_muck.views import MuckApiView +from flask_muck.views import FlaskMuckApiView login_manager = LoginManager() @@ -120,7 +120,7 @@ def execute(self) -> None: return -class BaseApiView(MuckApiView): +class BaseApiView(FlaskMuckApiView): """Base view to inherit from. Helpful for setting class variables shared with all API views such as "sqlalchemy_db" and "decorators". """ @@ -171,9 +171,9 @@ class ToyApiView(BaseApiView): # Add all url rules to the blueprint. -GuardianApiView.add_crud_to_blueprint(api_blueprint) -ChildApiView.add_crud_to_blueprint(api_blueprint) -ToyApiView.add_crud_to_blueprint(api_blueprint) +GuardianApiView.add_rules_to_blueprint(api_blueprint) +ChildApiView.add_rules_to_blueprint(api_blueprint) +ToyApiView.add_rules_to_blueprint(api_blueprint) def create_app() -> Flask: