Home > flask, python > Flask (parte 2): struttura progetto complesso

Flask (parte 2): struttura progetto complesso

26 Giugno 2015

Come si può notare nella maggiorparte delle guide presenti sul web, molto spesso gli esempi sono codificati
all’interno di un unico script, eccezion fatta per le templates che devono risiedere in una sottodirectory “templates”.
Utilizzando però un unico modulo, al crescere della complessità dell’applicazione, cresce anche la difficoltà
di gestione del codice stesso.
Flask non dà direttive in merito pertanto lo sviluppatore di turno può agire come crede.
Una buona soluzione, guarda caso, la si trova al capitolo 7 del libro di Grinberg e ne
mostrerò qui la struttura:

|-fantamanager
  |-app/
    |-templates/
    |-static/
    |-main/
      |-__init__.py
      |-errors.py
      |-forms.py
      |-views.py
    |-__init__.py
    |-email.py
    |-models.py
  |-migrations/
  |-tests/
    |-__init__.py
    |-test*.py
  |-venv/
  |-requirements.txt
  |-config.py
  |-manage.py

Questa struttura è composta da 4 directories principali…

app: il package dove risiede la Flask-application
migrations: dove risiedono i migrations-scripts che vedremo dopo
tests: il package dove risiedono gli Unit tests
venv: l’ambiente virtuale trattato nella parte 1

…e da 3 file fondamentali:

requirements.txt: contenente le dipendenze della nostra applicazione (parte 1)
config.py: contenente tutte le configurazioni e i settaggi della app
manage.py: lo script che lancia la nostra applicazione.

Da qui in avanti cominceremo a mettere il codice al posto giusto e lo analizzeremo passo
passo.

Per questa parte sono necessari 3 moduli che andremo ad installare:

(venv) >pip install flask-script flask-sqlalchemy flask-migrate

poi periodicamente o alla fine, è bene rinfrescare il file requirements.txt con il comando:

(venv) >pip freeze>requirements.txt

Il file Config

Di requirements.txt abbiamo già parlato, vediamo config.py:

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


class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True
    FANTAMANAGER_MAIL_SUBJECT_PREFIX = '[FANTAMANAGER]'
    FANTAMANAGER_MAIL_SENDER = 'FANTAMANAGER Admin <[email protected]>'
    FANTAMANAGER_ADMIN = os.environ.get('FANTAMANAGER_ADMIN')

    @staticmethod
    def init_app(app):
        pass


class DevelopmentConfig(Config):
    DEBUG = True
    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')


class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')


class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')


config = {'development': DevelopmentConfig,
          'testing': TestingConfig,
          'production': ProductionConfig,
          'default': DevelopmentConfig}

La classe Config contiene i settaggi comuni alle altre configurazioni, le altre classi invece
si distinguono tra loro in base ai diversi settaggi ed ognuna subclassa la prima.
Tutte queste classi vengono registrate in un dizionario “config”.

Tutti i valori di tali variabili, sono raggiungibili tramite il dizionario os.environ.
In questo modo possono essere settate in maniera invisibile da shell e poi recuperate (password, email, username, ecc),
senza la necessità di mostrarle nel codice sorgente.

(venv) C:\fantamanager>set SECRET_VAR=secretvalue

(venv) C:\fantamanager>python
Python 2.7.8 (default, Jun 30 2014, 16:03:49) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.environ.get("SECRET_VAR")
'secretvalue'

Nella superclass Config viene definito anche un metodo di classe init_app, che accetta come
argomento app e permette, in caso di necessità, nelle varie sottoclassi di configurazione, di specializzare
l’inizializzazione della app stessa.

Per capire il funzionamento dell’applicazione, parleremo ora di application package e application factory.
Prima un passo indietro.
Quando negli esempi classici (unico file) viene creata l’applicazione, questo avviene a livello globale (global scope)

from flask import Flask

app = Flask(__name__)
#...

il problema è che, una volta lanciato lo script, viene creata l’istanza di app e non è più possibile
apportare cambiamenti dinamicamente.

Per ovviare a questo spostiamo tutta l’inizializzazione dell’applicazione all’interno del costruttore __init__.py,
dell’application package, in esso importiamo tutte le estensioni che ci servono ma non passiamo nessun parametro in fase
di inizializzazione delle stesse, questo perchè l’istanza app non è ancora stata creata.
Questo avverrà infatti all’interno della factory function, nella quale provvederemo al completamento delle inizializzazioni
precedenti:

from flask import Flask, render_template
from flask.ext.sqlalchemy import SQLAlchemy
from config import config # il dizionario config!

db = SQLAlchemy()

def create_app(config_name): # la factory function
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)
    db.init_app(app)
    return app

come si nota la factory function, quando chiamata (quando vogliamo e anche più volte) ritorna l’istanza app
debitamente inizializzata.

Ora che abbiamo introdotto l’application factory sorge il problema delle route, che nello script singolo venivano
definite tramite il decoratore app.route a livello globale come la stessa app, ora invece il decoratore che deve definirle
viene utilizzato prima che la app sia creata dalla factory function.
Per questo Flask mette a disposizione i Blueprints. I bluebprint sono simili ad una applicazione, possono cioè
registrare le route con i decoratori appositi. Queste route rimangono “dormienti” fintanto che il Blueprint non viene
registrato sulla applicazione app, a quel punto, essendo il blueprint definito a livello globale, le route diventano disponibili.
Per migliorare la flessibilità e la leggibilità della struttura del progetto, dentro app verranno create tante subdir,
quanti sono i blueprints necessari.
Il blueprint principale verrà chiamato “main” (come si nota dalla struttura precedente).

|-fantamanager
  |-app/
    |-main/
      |-__init__.py
      |-errors.py
      |-forms.py
      |-views.py

Il costruttore __init__.py del blueprint “main” sarà:

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors

main è il blueprint che importeremo nei moduli errors.py e views.py specifici.
Per evitare riferimenti circolari negli imports, i due moduli che faranno uso di “main” (vedi es. app/main/errors.py),
vengono importati per ultimi.

from flask import render_template
from . import main # ecco il blueprint!

@main.app_errorhandler(404) # route specifica
def page_not_found(e):
    return render_template('404.html'), 404

@main.app_errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

es. app/main/views.py

from flask import render_template, session, redirect, url_for
from . import main

@main.route('/', methods=['GET', 'POST'])
def index():
    # ...

Ora il blueprint va registrato nella factory function del costruttore della app (app/_init_.py):

def create_app(config_name):
    # ...
    
    from main import main as main_blueprint
    app.register_blueprint(main_blueprint)
    
    return app

Il file manage.py

Come ultimo fondamentale settaggio, il file manage.py che altri non è che il nostro launch script:

#!/usr/bin/env python
import os
from app import create_app, db
# from app.models import ... qui importiamo gli oggetti del models 
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default') # ecco la factory function
manager = Manager(app)
migrate = Migrate(app, db)

def make_shell_context():
    return dict(app=app, db=db) # qui aggiungeremo gli oggetti dei models

manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

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

Segnalo la funzione make_shell_context che ritorna un dizionario con tutti gli oggetti
che vogliamo avere disponibili lanciando una shell, senza doverli importare ogni volta.
quando lanceremo il comando

python manage.py shell

Shell dell’estensione script ci renderà disponibili senza importarli, tutti gli oggetti forniti al
dizionario ritornato da make_shell_context.

quando invece lanceremo il comando

python manage.py db <command>

utilizzeremo l’estensione migrate, che vedremo più avanti.

Questi erano la struttura ed i settaggi di base per la creazione di una applicazione.

Articoli successivi:
Flask (parte 3): database

Articoli precedenti:
Flask (parte 1): virtualenv

link utili:
“Flask Web Development” di Miguel Grinberg, fondamentale per calarsi nel mondo di Flask.
Altri link fondamentali:
Flask sito ufficiale
il blog dell’autore (Flask mega tutorial)

Categorie:flask, python Tag: ,
I commenti sono chiusi.