Flask (parte 2): struttura progetto complesso
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)
Commenti recenti