Skip to content

Instantly share code, notes, and snippets.

@jsheridanwells
Last active April 3, 2024 19:10
Show Gist options
  • Save jsheridanwells/67289130318db06a47ae75464591c6b8 to your computer and use it in GitHub Desktop.
Save jsheridanwells/67289130318db06a47ae75464591c6b8 to your computer and use it in GitHub Desktop.
From Zero to Flask Notes

Blueprints

A typical setup:

app_dir/
  manage.py
  application.py
  module_dir/
    views.py

In views.py:

from flask import Blueprint

this_app_module = Blueprint('this_app_module', __name__)

@this_app_module.route('/')
def init():
    return 'My App, YO!!!'

In application.py: in the create_app() method:

def create_app():
    app = Flask(__name__)
    app.config.from_pyfile('settings.py')
    db.init_app(app)
    migrate = Migrate(app, db)

    from counter.views import this_app_module
    app.register_blueprint(this_app_module)

    return app

A Flask Factory Setup

In this setup, application.py is a factory that initialized routes and the ORM for a Flask app

requirements.txt:

alembic==1.0.7
Click==7.0
Flask==1.0.2
Flask-Migrate==2.4.0
Flask-SQLAlchemy==2.3.2
itsdangerous==1.1.0
Jinja2==2.10
Mako==1.0.7
MarkupSafe==1.1.1
PyMySQL==0.9.3
python-dateutil==2.8.0
python-dotenv==0.10.1
python-editor==1.0.4
six==1.12.0
SQLAlchemy==1.2.18
Werkzeug==0.14.1

.flaskenv: (remember to .gitignore this)

FLASK_APP='manage.py'
FLASK_ENV=development
SECRET_KEY='my_secret_key'
DB_HOST=localhost
DB_USERNAME='my_app_user'
DB_PASSWORD='my_password'
DATABASE_NAME='my_database'

settings.py:

import os

SECRET_KEY = os.getenv('SECRET_KEY')
DB_USERNAME=os.environ['DB_USERNAME']
DB_PASSWORD=os.environ['DB_PASSWORD']
DB_HOST=os.environ['DB_HOST']
DATABASE_NAME=os.environ['DATABASE_NAME']
DB_URI = 'mysql+pymysql://%s:%s@%s:3306/%s' % (DB_USERNAME, DB_PASSWORD, DB_HOST, DATABASE_NAME)
SQL_ALCHEMY_DATABASE_URL = DB_URI

application.py:

from flask import(
    Flask
)

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    app.config.from_pyfile('settings.py')
    db.init_app(app)
    migrate = Migrate(app, db)
    return app

in manage.py:

from application import create_app

app = create_app()

From Zero to Flask Notes

Virtual Envs

-- libs and source in their own environment rather than interfering with machine

to make an env:

$ python -m venv venv

to activate:

$ source venv/bin/activate

to deactivate:

$ deactivate

Installing Flask

To upgrade PIP:

$ pip install --upgrade pip

w/ venv activated, install w/ version number:

$ pip install Flask==1.0.2

Simple Hello World

from flask import Flask

# instance of flask class
# __name__ tells flask which module it is running from
app = Flask(__name__)

# this is a decorator
@app.route('/')
def hola():
    return 'Hola, huevon!'

Running the application

(w/ virtualenv activated)

Set an environment variable to indicate what application to run

$ export FLASK_APP=hola

Then: $ flask run It will run at http://localhost:5000/

Debugging

Set up the environment

$ export FLASK_DEBUG=1

Routing

In Flask, it's done through the route decorator

Templates (MVC Views)

Flask will look for a templates directory.

If there's: `./templates/index.html'

It can be rendered as

from flask import Flask, render_template

@app.route('/')
def index():
    return render_template('index.html')

Template variables:

<h1>
    Hello {{ t_name }}
</h1>
@app.route('/hello/<name>')
def hello(name):
    return render_template('hello.html', t_name=name)

Dynamic urls:

    <a href="{{ url_for('hello', name='artie') }}">Say hi to Artie</a>
@app.route('/hello/<name>')
def hello(name):
    return render_template('hello.html', t_name=name)

Block Templates

In base.html:

<html>
<head>
    <title>
        {% block title %} Default Title {% endblock %}
    </title>
    <link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">
</head>
<body>
    {% block content %} {% endblock %}
</body>
</html>

In extending template:

{% extends "base.html" %}
{% block title %}
    HELLO!
{% endblock %}
{% block content %}
<h1>
    Hello {{ t_name }}
</h1>
{% endblock %}

Static folder

/static is for anything that won't change (css, images)

Link to it dynamically: <link rel="stylesheet" href="{{ url_for('static', filename='base.css') }}">

Forms

Route parameters:

@app.route('/form', methods=['GET'])
def form():
    first_name = request.args['first_name']
    last_name = request.args['last_name']
    return f'FirstName: { first_name }, Last Name: { last_name }'

Returns values from url params: http://localhost:5000/form?first_name=hopper&last_name=john

Changing first_name and last_name vars like this will make it so that either parameter is optional:

    first_name = request.args.get('first_name')
    last_name = request.args.get('last_name')

Using an HTML form:

<form action="{{ url_for('form') }}">
    <input type="text" name="first_name" id="first-name">
    <input type="text" name="last_name" id="last-name">
    <input type="submit" name="name_form" value="send me yer name">
</form>

Change flask route to:

@app.route('/form', methods=['GET'])
def form():
    if request.args.get('name_form'):
        first_name = request.args.get('first_name')
        last_name = request.args.get('last_name')
        return f'FirstName: { first_name }, Last Name: { last_name }'
    return render_template('form.html')

request.args.get('name_form'): matches name="name_form" in input:submit.

if 'name_form' is sent with the request, accept that first and last name values but if not, just render the form.html template.

Change it to a POST:

  1. Add method="POST" to the <form> tag.

  2. Change flask method to:

@app.route('/form', methods=['GET', 'POST'])
def form():
    if request.method == 'POST':
        first_name = request.form.get('first_name')
        last_name = request.form.get('last_name')
        return f'FirstName: { first_name }, Last Name: { last_name }'
    return render_template('form.html')

Adds 'POST' to accepted methods, gets data from the form, not the url. (another way is first_name = request.values.get('first_name')) which works with GET and POST

Cookies and Sessions

Adding the following to the end of the form method:

        response = make_response(redirect(url_for('registered')))
        response.set_cookie('first_name', first_name)
        return response

makes the first_name value available in the next method:

@app.route('/thank_you')
def registered():
    first_name = request.cookies.get('first_name')
    return f'Thank You, {first_name}'

Sessions encrypt cookie data

Add a secret to your app:

app.secret_key = 'shhhh_this_is_a_secret'

Load the session:

        session['first_name'] = first_name
        return redirect(url_for('registered'))

Now it's available on other pages:

first_name = session.get('first_name')

Config

Configuration can be saved in requirements.txt at the app root level.

In the requirements file, you can specifify libraries and versions:

Flask==1.0.2

Then for a fresh install:

$ pip install -r requirements.txt

WARNING: Make sure venv is activated or else you'll install those libraries across all code repos.

Python Dot Env makes it easier to store more environment data. Normally this is kept in the gitignore 'cuz it's got connection strings and all that.

Add python-dotenv==0.8.2 (or whatever version you want) to the requirements.txt

then, pip install

Create a file called settings.py that can store the values that flask will push from the environment file to the os settings

import os
SECRET_KEY = os.getenv('SECRET_KEY')

Add the settings to the config where the app is instantiated:

app = Flask(__name__)

app.config.from_pyfile('settings.py')

Create a .flaskenv file. This has got any environment variables. Put this in the .gitignore:

FLASK_APP='hello.py'
FLASK_ENV=development
SECRET_KEY='shhhhhh_dont_tell_the_Secret'

Now SECRET_KEY will be available to the app.

Migrations

Flask-Migrate process:

init

migrate

upgrade

downgrade

(migrate creates the migration files, but upgrade actually applies them to the db)

Model must be registered in application.py:

    db.init_app(app)
    migrate = Migrate(app, db)

Model is 'seen' in the view file that contains it, for example, in views.py:

from counter.models import Counter

Initialize: $ flask db init

Checking CRUD ops in the Flask Console:

$ flask shell
>>> from counter.models import Counter
>>> from application import db
>>> Counter.query.all()
[]
>>> counter = Counter(count=1)
>>> counter.id
>>> db.session.add(counter)
>>> ^[[A^[[A^[[A^[[B^C
KeyboardInterrupt
>>> Counter.query.all()
[<Count 1>]
>>> db.session.commit()
>>> counter.id
1
>>> counter_1 = Counter.query.get(1)
>>> counter_1
<Count 1>
>>> counter_1.count
1
>>> counter_lit = Counter.query.all()
>>> counter_lit
[<Count 1>]
>>> counter = Counter.query.get(1)
>>> counter.count
1
>>> counter.count = 2
>>> db.session.commit()
>>> counter = Counter.query.get(1)
>>> counter
<Count 2>
>>> db.session.delete(counter)
>>> db.session.commit()
>>> Counter.query.all()
[]
>>>

Models

from application import db

class Counter(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    count = db.Column(db.Integer)

    def __init__(self, count):
        self.count = count
    
    def __repr__(self):
        return f'<Count { self.count }>'

MySql Setup

Basics

Starting: $ mysql.server start

Stopping: $ mysql.server stop

Restarting: $ mysql.server restart

Securing the server: $ mysql_secure_installation

Say 'no' to Validate Password Component. Enter a root user password 'yes' to removing anonymous users. 'yes' to disallowing remote logins 'yes to removing test db. 'yes' to reloading privileged tables.

Creating an Application User

  1. login: $ mysql -uroot -p<MY_PASSWORD>

  2. CREATE DATABASE my_database;

  3. `CREATE USER 'my_app_user'@'%' IDENTIFIED BY 'mypassword';

  4. Grant all privileges: GRANT ALL PRIVILEGES ON my_database.* TO 'my_app_user'@'%'

  5. Reload the privileges: FLUSH PRIVILEGES

  6. Quit and login with the new user: $ mysql -umy_app_user -pmypassword

  7. Check if the user has the right access: USE my_database; (if it says 'Database changed', you're good.)

Tests

The following, in util/test_db.py will create and drop a test database uner the root user:

import os
from flask_sqlalchemy import sqlalchemy

class TestDB:
    def __init__(self):
        self.db_name = os.environ['DATABASE_NAME'] + '_test'
        self.db_host = os.environ['DB_HOST']
        self.db_root_password = os.environ['MYSQL_ROOT_PASSWORD']
        if self.db_root_password:
            self.db_username = 'root'
            self.db_password = self.db_root_password
        else:
            self.db_username = os.environ['DB_USERNAME']
            self.db_password = os.environ['DB_PASSWORD']
        self.db_uri = 'mysql+pymysql://%s:%s@%s' % (self.db_username,
            self.db_password, self.db_host)

    def create_db(self):
        # create the database if root user
        if self.db_root_password:
            engine = sqlalchemy.create_engine(self.db_uri)
            conn = engine.connect()
            conn.execute("COMMIT")
            conn.execute("CREATE DATABASE "  + self.db_name)
            conn.close()
        return self.db_uri + '/' + self.db_name

    def drop_db(self):
        # drop the database if root user
        if self.db_root_password:
            engine = sqlalchemy.create_engine(self.db_uri)
            conn = engine.connect()
            conn.execute("COMMIT")
            conn.execute("DROP DATABASE "  + self.db_name)
            conn.close()

The following shows the setup and teardown for any testing suite. Note that it's run within pure Python riather than Flask:

import os
import unittest
import pathlib

from dotenv import load_dotenv
env_dir = pathlib.Path(__file__).parents[1]
load_dotenv(os.path.join(env_dir, '.flaskenv'))

from counter.models import Counter
from application import db
from application import create_app as create_app_base
from util.test_db import TestDB

class CounterTest(unittest.TestCase):
    def create_app(self):
        return create_app_base(
            SQL_ALCHEMY_DATABASE_URI=self.db_uri,
            TESTING=True,
            SECRET_KEY='mySecret'
        )
    
    def setup(self):
        self.test_db = TestDB()
        self.db_uri = self.test_db.create_db()
        self.app_factory = self.create_app()
        self.app = self.app_factory.test_client()
        with self.app_factory.app_context():
            db.create_all()
    
    def tearDown(self):
        with self.app_factory.app_context():
            db.drop_all()
        self.test_db.drop_db()
- CAREFUL :: For some reason this drops the real database!!!!

Create a test runner in the project root, for example test_runner.py:

import sys, pathlib
sys.path.append(pathlib.Path(__file__).parents[0])

import unittest
loader = unittest.TestLoader()
tests = loader.discover('.')
testRunner = unittest.runner.TextTestRunner()

if __name__ == '__main__':
    testRunner.run(tests)

To run: $ python test_runner.py

(note: I had to pip install cryptography to get the mysql root password to work).

Add an assertion in tests.py:

    def test_counter(self):
        rv = self.app.get('/')
        assert '1' in str(rv.data)
        rv = self.app.get('/')
        assert '2' in str(rv.data)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment