0

I'm using the application factory pattern and I've got a Article object that relates to a Category in a M2M relationship. So far all the routes in the API work as expected. I can create articles with categories via POST MethodViews.

However, I'm trying to seed some sample data to my database via click in a separate file. At first I thought the problem was with Flask CLI and the app context, which I originally had in a blueprint, but then I realized the issue went a bit deeper. I see this issue, but I've updated my Marshmallow/Flask-Marshmallow/Marshmallow-Sqlalchemy to the latest:

https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/20#issuecomment-136400602

// model.py

class CRUDMixin():

    @classmethod
    def find_by_id(cls, _id):
        return cls.query.filter_by(id=_id).first()

    @classmethod
    def find_all(cls):
        return cls.query.all()

    def save_to_db(self):
        db.session.add(self)
        return db.session.commit()

    def update(self):
        return db.session.commit()

    def delete_from_db(self):
        db.session.delete(self)
        return db.session.commit()

class CategoryModel(db.Model, CRUDMixin):
    __tablename__ = "api_category"

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)

    def __repr__(self):
        return '<id {}>'.format(self.id)


class ArticleModel(db.Model, CRUDMixin):
    __tablename__ = 'api_article'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    description = db.Column(db.Text)

    categories = db.relationship('CategoryModel', secondary=api_category_article, lazy='subquery',
                             backref=db.backref('articles', lazy=True))

    def __repr__(self):
        return '<id {}>'.format(self.id)

// schema.py

class CategoryPostSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = CategoryModel
        dump_only = ("name", )
        load_only = ("articles", )
        load_instance = True


class ArticlePostSchema(ma.SQLAlchemyAutoSchema):
    categories = ma.Nested(CategoryPostSchema, many=True)

    class Meta:
        model = ArticleModel
        dump_only = ("id",)
        include_fk = True
        load_instance = True
        include_relationships = True
        sqla_session = db.session

// resource.py

class ArticleListView(MethodView):

    def __init__(self):
        pass

    @classmethod
    def get(cls):
        data = ArticleModel.find_all()
        return jsonify({"data": article_list_schema.dump(data),
                        "count": len(data),
                        "status": 200
                        })

    @classmethod
    def post(cls):

        req_json = request.get_json()
        errors = article_post_schema.validate(req_json)

        if errors:
            response = jsonify({'errors': errors, "status": 400})
            response.status_code = 400
            return response

        data = article_post_schema.load(req_json)
        data.save_to_db()

        response = jsonify({"data": article_post_schema.dump(data), "errors": {}, "status": 201})
        response.status_code = 201
        return response

// initialize.py (in root directory)

import click

from marshmallow import ValidationError

from api import create_app
from api.db import db
from api.schemas import ArticlePostSchema


app = create_app()

@click.group()
def cli():
    pass

@cli.command()
def article():
    with app.app_context():

        article_post_schema  = ArticlePostSchema()

        entry = {"name":"My Family Vacation 5",
                "description":"That time we took a road trip to Europe",
                "categories":[{"id": 1}]
                }

        data = article_post_schema.load(entry, session=db.session)
        data.save_to_db()
        print("Success")

if __name__ == '__main__':
    cli()

// Error

Traceback (most recent call last):
  File "initialize.py", line 41, in <module>
    cli()
...
  File "initialize.py", line 29, in article
    data = article_post_schema.validate(entry)
...
    return self.session.query(self.opts.model).filter_by(**filters).first()
AttributeError: 'DummySession' object has no attribute 'query'

1 Answers1

0

Alright! I think I figured it out. I ultimately had two problems to contend with in Flask 1) Application Context and 2) Request Context. The problem had nothing to do with the Marshmallow Schemas, but I will say it's very confusing that objects without M2M relationships work, but with M2M relationships it requires a request context. If anyone would care to explain, that would be nice.

I ran into the following errors in this process:

AttributeError: 'DummySession' object has no attribute 'query'
RuntimeError: No application found. Either work inside a view function or push an application context.
AssertionError: Popped wrong app context.

I basically needed to add with app.app_context() to my __init__.py file and with current_app.test_request_context('/path/to/route/') and @with_appcontext in my initialize.py file. I also needed to import the functions to my __init__.py. To keep things tidy, I moved the file to my application folder

// init.py

def create_app():

    app = Flask(__name__)

    # Config
    ...

    # Initialize DB and Marshmallow
    db.init_app(app)
    ma.init_app(app)

    with app.app_context():

        # Import Models
        ...

        # Import MethodViews
        ...

        # Import Blueprints
        ...

        # Commands

        from api.initialize import article

        app.cli.add_command(article)

        return app

// initialize.py (now in /api directory)

@click.command()
@with_appcontext
def article():
    article_post_schema  = ArticlePostSchema()
    entry = {"name":"My Family Vacation 5",
                "description":"That time we took a road trip to Europe",
                "categories":[{"id": 1}]
                }

    with current_app.test_request_context('/article/'):
        try:
            data = article_post_schema.load(entry)
            data.save_to_db()
            print("Success")
        except ValidationError as err:
            print("Errors...", err)

Running flask article now does the trick.

Note: There are many examples on Flask-Click using single page files and defining the commands in create_app(). This is great for simple examples or to illuminate the basic idea or functionality. However, it's also very confusing because it seems most people in any production environment will be using Blueprints and the Application Factory pattern as their default. The only useful mention of test_request_context is here in Testing Flask Applications.

So to clarify, if you are putting everything into your create_app you'll need to use with app.test_request_context('/path/to/route/') instead in your __init__.py. Otherwise, as in the above example, you'll need to use current_app if you separate this out into other files.

I found this article very useful to explain the Application Context as well as the lack of clarity in the documentation on this matter:

https://hackingandslacking.com/demystifying-flasks-application-context-c7bd31a53817