Skip to content

Commit

Permalink
Updates documentation and view class name. (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
dtiesling authored Nov 27, 2023
1 parent 6ac3764 commit 905484b
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 28 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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\"}"
Expand Down
115 changes: 115 additions & 0 deletions docs/docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -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/\<ID>/ | GET | Fetch a single teacher |
| /api/teachers/\<ID>/ | PUT | Update a single teacher |
| /api/teachers/\<ID>/ | PATCH | Patch a single teacher |
| /api/teachers/\<ID>/ | DELETE | Delete a single teacher |



3 changes: 2 additions & 1 deletion docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ extra_css:
- stylesheets/extra.css

nav:
- Home: index.md
- About: index.md
- Installation: installation.md
- Quick Start: quickstart.md
6 changes: 3 additions & 3 deletions examples/00_quickstart/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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".
"""
Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions examples/01_authentication/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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".
"""
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/flask_muck/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .views import MuckApiView
from .views import FlaskMuckApiView
from .callback import MuckCallback

VERSION = "0.0.3b2"
9 changes: 5 additions & 4 deletions src/flask_muck/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down
11 changes: 5 additions & 6 deletions src/flask_muck/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -41,8 +40,8 @@
}


class MuckApiView(MethodView):
session: Session
class FlaskMuckApiView(MethodView):
session: scoped_session
api_name: str
Model: SqlaModelType

Expand All @@ -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"}
Expand Down Expand Up @@ -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")
Expand Down
10 changes: 5 additions & 5 deletions tests/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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".
"""
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 905484b

Please sign in to comment.