Pouring Django world into Flask

Przemek Lewandowski

@haxoza

One drop at a time - case study

About me

Overview

  • Choosing the right tools
  • Flask way
  • Building basic API
  • Flask vs Django

Feeding the troll
is not intended

Disclaimer

The project to build

  • Scraping a few RSS feeds (10 rows/min)
  • One bag of data
  • Basic API with one endpoint
  • One table view

Do we need a big framework for this?

Chosen toolset

  • Flask
  • PostgreSQL
  • Celery
  • Angular

Let's start with Flask!

Database?

No prob! SQLAlchemy FTW!

Not exactly…

SQLAlchemy with Flask

  • Pure SQLAlchemy
  • Flask-SQLAlchemy library
  • Alembic for migrations

We went for Flask-SQLAlchemy

Flask-SQLAlchemy


db = SQLAlchemy()


class Document(db.Model):
    id = Column(Integer(), primary_key=True)
    data = Column(MutableDict.as_mutable(JSONB), nullable=False)

    def __init__(self, data):
        self.data = data

Alembic with Flask

  • When to create database?
  • How to define data mappers or ORM?
  • How to run migrations?

Do you know manage.py in Django?

flask-script as entry point

  • migrations with Alembic
    • flask-migrate
  • dev server
  • tests entry point
  • flask app creation

flask-script example

#!/usr/bin/env python

from flask.ext.script import Manager, Command
from flask.ext.migrate import MigrateCommand

from app import create_app
from grabber import grab_feeds
from tasks import run_celery
from tests.command import PytestCommand


manager = Manager(create_app)
manager.add_option('-c', '--config', dest='config_file', required=False)
manager.add_command('db', MigrateCommand)
manager.add_command('test', PytestCommand)
manager.add_command('runcelery', Command(run_celery))
manager.command(grab_feeds)

if __name__ == '__main__':
    manager.run()

flask app creation


def create_app(config_file=None, settings_override=None):
    app = Flask(__name__)

    if config_file:
        app.config.from_pyfile(config_file)
    else:
        app.config.from_envvar(config_variable_name)

    if settings_override:
        app.config.update(settings_override)

    init_app(app)

    return app


def init_app(app):
    db.init_app(app)
    migrate.init_app(app, db)
    api.init_app(app)

What about tests?

Issues with tests

  • App initialization
  • Test database
  • Rollback after test
  • HTTP client

py.test fixtures
to the rescue!

py.test app fixture


@pytest.yield_fixture(scope='session')
def app():
    app = create_app(config_file='config/testing.py')
    app.response_class = ApiTestingResponse
    ctx = app.app_context()
    ctx.push()

    yield app

    ctx.pop()

py.test db and client fixtures


@pytest.yield_fixture(scope='session')
def db(app):
    _db.create_all()

    yield _db

    _db.drop_all()


@pytest.fixture(scope='session')
def client(app):
    return app.test_client()

py.test db session fixture


@pytest.yield_fixture(scope='function')
def db_session(db):
    connection = db.engine.connect()
    transaction = connection.begin()

    options = dict(bind=connection)
    session = db.create_scoped_session(options=options)

    db.session = session

    yield session

    transaction.rollback()
    connection.close()
    session.remove()

Let's do REST API

Flask options to REST API

  • Pure Flask
  • Flask-RESTful library
  • Flask-RESTless library
  • Eve library
  • Flask API library
  • ... and more

All suck at some point but
we went for Flask-RESTful

Basic REST API


class FeedsResource(Resource):

    def get(self):
        # request parsing
        return {
            'data': 'transformed to json',
        }


api = FeedsApi()
api.add_resource(FeedsResource, '/feeds')

py.test with API


def test_get_should_return_feed_data(client, session):
    feed_entries_count = 3
    factories.FeedDataFactory.create_batch(feed_entries_count)

    response = client.get('/feeds')

    assert response.status_code == 200
    assert response.json['records_total'] == feed_entries_count
    assert len(response.json['data']) == feed_entries_count

Flask boilerplate

Flask vs Django

Django philosophy

  • Loose coupling 
  • Less code / Fullstack
  • DRY
  • Active Record design pattern

Common Django pitfalls

  • Replacing default ORM
  • Too fat models
  • Business logic in forms / views
  • Mixing application layers

Flask philosophy

  • Small and simple core
  • Flexibility
  • No decisions for developers
    • No database layer
    • No form validations layer
    • ...

Common Flask pitfalls

  • Reinventing the wheel
  • First hours spent on basic stuff
  • A need for your own conventions

Micro-frameworks encourage to

  • Modular structure
  • Design architecture from the beginning
  • Omit pitfalls
    • i.e. Active Record pattern for bigger apps
  • Use POPO (Plain Old Python Object)

Is that true?

It's not about code size.
It's about common sense.

Resources

  • http://flask.pocoo.org/docs/0.10/foreword/#what-does-micro-mean
  • https://docs.djangoproject.com/en/1.9/misc/design-philosophies/
  • https://www.dabapps.com/blog/django-models-and-encapsulation/

Thanks!

Questions?

@haxoza

Pouring Django world to Flask - one drop at a time

By Przemek Lewandowski

Pouring Django world to Flask - one drop at a time

  • 3,429