Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

create_app pattern with flask-sqlalchemy

I'm not able to create tables into a database (PostgresSQL) when using Flask's app factory pattern.

I've looked at different examples from Stackoverflow and the Flask-SQLAlchemy source code. My understanding is that with the factory app pattern, I need to set up the so-called context before I can try creating the tables. However, when I create the context, Flask app's config dictionary gets reset and it doesn't propagate the configurations forward.

Here's my model.py

import datetime
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()


class MyModel(db.Model):

    id = db.Column(db.Integer, primary_key=True)
    some = db.Column(db.DateTime, nullable=False)
    random = db.Column(db.String, nullable=False)
    model = db.Column(db.String, nullable=False)
    fields = db.Column(db.Integer, nullable=False)

    def __init__(self, some: datetime.datetime, random: str, model: str,
                 fields: int) -> None:
        self.some = some
        self.random = random
        self.model = model
        self.fields = fields

    def __repr__(self):
        return f"""<MyModel(some={self.some}, random={self.random},
        model={self.model}, fields={self.fields})>"""

Here's the app's __init__.py file

import os

from flask import Flask
from flask_migrate import Migrate
from myapp.models import db

migrate = Migrate()


def create_app(config):

    app = Flask(__name__)
    app.config.from_object(config)
    db.init_app(app)
    migrate.init_app(app, db)
    from .models import MyModel
    with app.app_context():
        db.create_all()

    @app.route('/hello')
    def hello():
        return('Hello World!')

    return app


I also have a main.py file:

from myapp import create_app
from config import Config

app = create_app(Config)

if __name__ == "__main__":
    app.run(host='127.0.0.1', port=8080, debug=True)

The root folder contains the main.py and the MyModule app folder. Furthermore, I've set up a Postgres instance and the required config constants in a config.py file:

import os
from dotenv import load_dotenv

basedir = os.path.abspath(__file__)
load_dotenv(os.path.join(basedir, '.env'))


class Config(object):
    DEBUG = False
    TESTING = False
    CSRF_ENABLED = True
    SECRET_KEY = os.environ.get('SECRET_KEY')
    SQLALCHEMY_DATABASE_URI = os.environ.get('SQLALCHEMY_DATABASE_URI')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

I'm reading the variables from an .env file.

When I run main.py, I get the following error:

(venv) $:project-name username$ python main.py
Traceback (most recent call last):
  File "main.py", line 4, in <module>
    app = create_app(Config)
  File "/Users/username/project-name/myapp/__init__.py", line 18, in create_app
    db.create_all()
  File "/Users/username/venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py", line 1033, in create_all
    self._execute_for_all_tables(app, bind, 'create_all')
  File "/Users/username/venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py", line 1025, in _execute_for_all_tables
    op(bind=self.get_engine(app, bind), **extra)
  File "/Users/username/venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py", line 956, in get_engine
    return connector.get_engine()
  File "/Users/username/venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py", line 560, in get_engine
    options = self.get_options(sa_url, echo)
  File "/Users/username/venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py", line 575, in get_options
    self._sa.apply_driver_hacks(self._app, sa_url, options)
  File "/Users/username/venv/lib/python3.6/site-packages/flask_sqlalchemy/__init__.py", line 877, in apply_driver_hacks
    if sa_url.drivername.startswith('mysql'):
AttributeError: 'NoneType' object has no attribute 'drivername'

Now what's strange here is that when I print print(app.config) inside the create_app function, the configurations are in place, just like I want them to be. So for example SQLALCHEMY_DATABASE_URI=postgresql://testuser:testpassword@localhost:5432/testdb'. However, when I print the same info inside the app.app_context() loop, SQLALCHEMY_DATABASE_URI=None (as an example, the other key-value pairs are also reset).

What am I missing here?

like image 269
P4nd4b0b3r1n0 Avatar asked Oct 15 '22 13:10

P4nd4b0b3r1n0


1 Answers

You are loading your .env file incorrectly:

basedir = os.path.abspath(__file__)
load_dotenv(os.path.join(basedir, '.env'))

basedir is the module file itself, not the directory. So for a file named config.py, basedir is set to /..absolutepath../config.py, not /..absolutepath../config.py. As a result, you are asking dotenv() to load the file /..absolutepath../config.py/.env, which won't exist.

You are missing a os.path.dirname() call:

basedir = os.dirname(os.path.abspath(__file__))

You can avoid this issue altogether by using dotenv.find_dotenv():

load_dotenv(find_dotenv())

which uses the __file__ attribute of the module it is called from.

Other remarks:

  • You are using Flask-Migrate to manage the schema, so don't call db.create_all(). Instead use the Flask CLI and the Flask Migrate commands (flask db init, flask db migrate, and flask db upgrade to create a migration directory and your initial migration script and then upgrade your connected database to use the latest schema version.
  • If you are configuring your database from environment variables, then don't use a variable config object. It's easier to work with a Flask app factory if you just import config from the current project for default configuration that applies to all development. You can always load in more configuration via app.config.from_obj() or app.config.from_envvar(), using optional arguments and an environment variable, as needed.
like image 150
Martijn Pieters Avatar answered Oct 18 '22 04:10

Martijn Pieters