experimental
experimental

Reputation: 129

Avoiding importing application factory into module needing application context

This question is an extension on my previous one here. I was suggested to put more to explain the problem. As the heading says, I am trying to find a way to avoid importing the application factory (create_app function) into a module that needs application context and were "import current_app as app" is not sufficient.

My problem is I have a circular import problem due to this create_app function which I need to pass in order to get the app_context.

In my __ini__.py, I have this:

# application/__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_restful import Api
from application.resources.product import Product, Products
from application.resources.offer import Offer, Offers  # HERE IS THE PROBLEM

api = Api()
db = SQLAlchemy()

api.add_resource(Product, "/product/<string:name>")  # GET, POST, DELETE, PUT to my local database
api.add_resource(Products, "/products")  # GET all products from my local database
api.add_resource(Offer, "/offer/<int:id>")  # POST call to the external Offers API microservise
api.add_resource(Offers, "/offers")  # GET all offers from my local database


def create_app(config_filename=None):
    """ Initialize core application. """
    app = Flask(__name__, instance_relative_config=False)
    app.config.from_object("config.Config")

    db.init_app(app)
    api.init_app(app)

    with app.app_context():
        db.create_all()

        return app

The problem is in this line:

from application.resources.offer import Offer, Offers  # HERE IS THE PROBLEM

because in that module, I have:

#application/resources/offer.py 

from flask_restful import Resource
from application.models.offer import OfferModel  # IMPORTING OFFER MODEL

which in turn imports application/models/offer.py where I have the critical part:

#application/models/offer.py

import requests
# from flask import current_app as app 
from application import create_app  # THIS CAUSES THE CIRCULAR IMPORT ERROR
from sqlalchemy.exc import OperationalError

app = create_app() # I NEED TO CREATE THE APP IN ORDER TO GET THE APP CONTEXT BECASE IN THE CLASS I HAVE SOME FUNCTIONS THAT NEED IT

class OfferModel(db.Model):
    """ Data model for offers. """
    # some code to instantiate the class... + other methods..

    # THIS IS ONE OF THE METHODS THAT NEED APP_CONTEXT OR ELSE IT WILL ERROR OUT
    @classmethod
    def update_offer_price(cls):
        """ Call offers api to get new prices. This function will run in a separated thread in a scheduler. """
        with app.app_context():
            headers = {"Bearer": app.config["MS_API_ACCESS_TOKEN"]}
            for offer_id in OfferModel.offer_ids:
                offers_url = app.config["MS_API_OFFERS_BASE_URL"] + "/products/" + str(offer_id) + "/offers"
                res = requests.get(offers_url, headers=headers).json()
                for offer in res:
                    try:
                        OfferModel.query.filter_by(offer_id=offer["id"]).update(dict(price=offer["price"]))
                        db.session.commit()
                    except OperationalError:
                        print("Database does not exists.")
                        db.session.rollback()

I have tried to use from flask import current_app as app to get the context, it did not work. I don't know why it was not sufficient to pass current_app as app and get the context because it now forces me to pass the create_app application factory which causes the circular import problem.

Upvotes: 1

Views: 563

Answers (2)

experimental
experimental

Reputation: 129

ok so this is how I solved it. I made a new file endpoints.py where I put all my Api resources

# application/endpoints.py    

from application import api
from application.resources.product import Product, Products
from application.resources.offer import Offer, Offers

api.add_resource(Product, "/product/<string:name>")  # GET, POST, DELETE, PUT - calls to local database
api.add_resource(Products, "/products")  # GET all products from local database.
api.add_resource(Offer, "/offer/<int:id>")  # POST call to the Offers API microservice.
api.add_resource(Offers, "/offers")  # GET all offers from local database

Then in init.py I import it at the very bottom.

# aplication/__init__.py

from flask import Flask
from flask_restful import Api
from db import db

api = Api()


def create_app():
    app = Flask(__name__, instance_relative_config=False)
    app.config.from_object("config.Config")
    db.init_app(app)
    api.init_app(app)
    
    with app.app_context():
        from application import routes
        db.create_all()

        return app


from application import endpoints # importing here to avoid circular imports

It is not very pretty but it works.

Upvotes: 1

Sergey Shubin
Sergey Shubin

Reputation: 3257

Your update_offer_price method needs database interaction and an access to the configuration. It gets them from the application context but it works only if your Flask application is initialized. This method is run in a separate thread so you create the second instance of Flask application in this thread.

Alternative way is getting standalone database interaction and configuration access outside the application context.

Configuration

Configuration does not seem a problem as your application gets it from another module:

app.config.from_object("config.Config")

So you can directly import this object to your offer.py:

from config import Config

headers = {"Bearer": Config.MS_API_ACCESS_TOKEN}

Database access

To get standalone database access you need to define your models via SQLAlchemy instead of flask_sqlalchemy. It was already described in this answer but I post here the essentials. For your case it may look like this. Your base.py module:

from sqlalchemy import MetaData
from sqlalchemy.ext.declarative import declarative_base

metadata = MetaData()
Base = declarative_base(metadata=metadata)

And offer.py module:

import sqlalchemy as sa

from .base import Base

class OfferModel(Base):
    id = sa.Column(sa.Integer, primary_key=True)
    # Another declarations

The produced metadata object is used to initialize your flask_sqlalchemy object:

from flask_sqlalchemy import SQLAlchemy

from application.models.base import metadata

db = SQLAlchemy(metadata=metadata)

Your models can be queried outside the application context but you need to manually create database engine and sessions. For example:

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

from config import Config

from application.models.offer import Offer

engine = create_engine(Config.YOUR_DATABASE_URL)
# It is recommended to create a single engine
# and use it afterwards to bind database sessions to.
# Perhaps `application.models.base` module
# is better to be used for this declaration.

def your_database_interaction():
    session = Session(engine)
    offers = session.query(Offer).all()
    for offer in offers:
        # Some update here
    session.commit()
    session.close()

Note that with this approach you can't use your models classes for queriing, I mean:

OfferModel.query.all()  # Does not work
db.session.query(OfferModel).all()  # Works

Upvotes: 2

Related Questions