Statistiche Fantacalcio: la mia prima applicazione con Flask-SQLAlchemy
Django ed il suo orm, già li ho usati. Stupendo, ma volendo un’applicazione
che lavori sia in locale che su web, il magnifico orm di django (e tutto il suo mondo),
mi diventa pesantuccio da portare appresso.
SQlAlchemy è comodissimo, ma non è compatibile con django (o meglio se devi usare
django e rinunciare al suo orm, lasciamo perdere).
Web2py come WF funziona nativamente con sqla, ma non mi ci trovo granchè.
Invece Flask, essendo un light-web-framework (che ancora non so utilizzare…),
dovrebbe fare a caso mio per il secondo caso… e soprattutto si
appoggia ad SQLAlchemy. Se quindi impostassi la mia app in locale
utilizzando l’estensione di Flask, appunto Flask-SQLAlchemy, nel momento
in cui dovrò trasferire il tutto su web, dovrei essere a posto e soprattutto leggero.
Cominciamo quindi con l’impostare l’applicazione dalle basi.
Come spesso accade sul mio blog, imposto i miei esercizi ed i miei esperimenti,
basandomi sul fantacalcio. Comodissimo per creare un database con più di una tabella
e soprattutto con relazioni che siano almeno di 1 a molti.
Questa volta vorrei un’applicazione che prendesse tutti i calciatori della serie A,
che immagazzinasse tutti i voti che questi prendono ogni giornata e mi restituisse la
media voto.
Mi servirò del magico sqlite per il database e di un pattern MVC per l’app vera e propria.
Inizialmente avremo una UI testuale, poi in futuro faremo anche una GUI.
Iniziamo quindi dal Model, creando un file model.py:
Ci saranno 2 classi (tabelle):
Player (calciatore)
Grade (voto)
collegate tra loro con una relazione 1-a-molti, poichè ovviamente un giocatore
alla fine del campionato riceverà un voto per ogni giornata giocata.
come da guida di Flask-SQLAlchemy, creiamo le classi:
from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///stat.db' db = SQLAlchemy(app) class Player(db.Model): id = db.Column(db.Integer, primary_key=True) code = db.Column(db.Integer(3), unique=True) name = db.Column(db.String(80), unique=True) team = db.Column(db.String(3)) def __init__(self, code, name, team): self.code = code self.name = name self.team = team def __repr__(self): return '<Player %r>' % self.name class Grade(db.Model): id = db.Column(db.Integer, primary_key=True) day = db.Column(db.Integer(3)) f_grade = db.Column(db.Float(4)) # grade with bonus-malus grade = db.Column(db.Float(4)) # naked grade (without bonus-malus) value = db.Column(db.Integer(3)) # Relationship player_id = db.Column(db.Integer, db.ForeignKey('player.id')) player = db.relationship('Player', backref=db.backref('grades', lazy='dynamic')) def __init__(self, day, f_grade, grade, value, player): self.day = day self.f_grade = f_grade self.grade = grade self.value = value self.player = player def __repr__(self): return '<Grade day %r: %r>' % (self.day, self.player)
per creare il database:
db.create_all()
Class Model:
Craieamo ora la classe Model, che conterrà tutte le operazioni che faremo sul database.
I print che appaiono nel codice hanno funzione di debug.
Le vere operazioni di visualizzazione, verranno inserite nel modulo view.py, come
da pattern. Il controller farà il resto.
Comunque, qui di seguito la classe Model con i suoi metodi, tutti per la verità
abbastanza autoesplicativi:
class Model(object): '''Model class for MVC pattern. It creates, destroies, fills and queries the database "stat.db". ''' def __init__(self): self.day = None def _database_exist(self): '''Return True if database already exists''' return 'stat.db' in os.listdir(os.getcwd()) def _delete_database(self): '''Delete database and all data''' ans = raw_input("I'm going to remove database, are you sure? (Y/N): ") if ans.upper() == 'Y': db.session.close() os.remove('stat.db') else: print 'operation canceled!' def _import_all_txt(self): pass def get_import_day(self, path): '''Get the day number from path file''' pattern = 'MCC(\d{2}).txt' try: day = re.compile(pattern).search(path).group(1) except AttributeError: print 'File name must follow "MCCnn_s.txt" pattern! Rename it.' return day def _import_file(self, path): '''Import grade records from txt file and store them in db''' self.day = self.get_import_day(path) with open(path, 'r') as input_file: records = input_file.readlines() self._import_players(records) self._import_grades(records) def _import_players(self, records): for record in records: # input pattern: 102|AGAZZI|CAG|5.0|6.0|11 code, name, team, f_grade, grade, value = record.split('|') if not self._player_exists(code): pl = Player(code=code, name=name, team=team) db.session.add(pl) print '+++ import player {}'.format(pl) else: print '*** player {} existing...'.format(name) db.session.commit() def _import_grades(self, records): for record in records: code, name, team, f_grade, grade, value = record.split('|') if not self._grade_exists(int(self.day)): pl = self.get_player_by_code(code) gr = Grade(day=self.day, f_grade=f_grade, grade=grade, value=value, player=pl) db.session.add(gr) print '<<< import new grade {}'.format(gr) else: print 'this day is not new' db.session.commit() def _player_exists(self, code): return Player.query.filter_by(code=code).first() def _grade_exists(self, day): return Grade.query.filter_by(day=day).first() def _get_imported_days(self): return db.session.query(Grade.day).distinct().all() def get_player_by_code(self, code): return Player.query.filter_by(code=code).first() def get_player_by_name(self, name): return Player.query.filter_by(name=name.upper()).first() def get_player_fgrades(self, player): return [(pl.day, pl.f_grade) for pl in player.grades.all()] def get_avg_player(self, player): ndays = len(player.grades.filter(Grade.grade > 0.0).all()) sum_fg = sum([f_g for day, f_g in self.get_player_fgrades(player)]) try: return sum_fg / ndays except ZeroDivisionError: return 0.0 def get_players_by_team(self, team): return Player.query.filter_by(team=team.upper()[:3]).all() def get_avg_players(self, iterable): return [(pl.name, pl.team, self.get_avg_player(pl)) \ for pl in iterable] def _get_best_avg(self, iterable): # from operator import itemgetter # faster # max(list, key=itemgetter(1)) result= [(pl.code, self.get_avg_player(pl)) \ for pl in iterable] return max(result, key=lambda item: item[1]) def get_best_player(self): code, grade = self._get_best_avg(Player.query.all()) return self.get_player_by_code(code), grade def get_best_team_player(self, team): players = Player.query.filter_by(team=team.upper()[:3]) try: code, grade = self._get_best_avg(players) return self.get_player_by_code(code), grade except ValueError: print "Error: team '%s' does not exist!" % team print "try one of {}".format([t[0] for t in db.session.query(Player.team).distinct().all()]) def get_best_role_player(self, role): lmin, lmax = 0, 0 if role.lower() == 'portiere': lmin, lmax = 100, 200 elif role.lower() == 'difensore': lmin, lmax = 200, 500 elif role.lower() == 'centrocampista': lmin, lmax = 500, 800 elif role.lower() == 'attaccante': lmin, lmax = 800, 1000 else: print "role must be: 'portiere' or 'difensore'" print "or 'centrocampista' or 'attaccante'" return None players = Player.query.filter(Player.code > lmin).filter( Player.code < lmax).all() code, grade = self._get_best_avg(players) return self.get_player_by_code(code), grade def get_players_name_starts_with(self, string): return Player.query.filter( Player.name.startswith(string.upper())).all()
in fondo al file model.py, per comodità di debug, inseriamo il seguente codice:
if __name__ == '__main__': m = Model() if m._database_exist(): print "database exists" else: print "create new database..." db.create_all()
eseguito il software la prima volta, verrà creato il database, la seconda volta,
tramite il metodo _database_exist() faremo un controllo di presenza del file ‘stat.db’.
Se presente, non verrà calpestato da una nuova creazione.
con il metodo _delete_database(self), è possibile cancellare tutto.
quindi, sempre da console:
>>> m._delete_database()
Questo non si dovrebbe fare (l’underscore nel prefisso del metodo parla chiaro), ma siamo
in debug…
Quando faremo la UI nel modulo view, rimarrà tutto più nascosto.
Per proseguire, dobbiamo importare i file dei voti (metodo _import_file(path)).
Ne basta uno, ma sarebbe meglio importare più file.
ad es.
>>> m._import_file('MCC01.txt')
una volta che il database è riempito, il modo di utilizzo viene da sè.
Esempio, voglio sapere il giocatore con la media più alta del campionato?
>>> m.get_best_player() (<Player u'VIDAL'>, 12.5) >>>
Voglio sapere il miglior giocatore di un determinato ruolo?
>>> m.get_best_role_player('a') role must be: 'portiere' or 'difensore' or 'centrocampista' or 'attaccante' >>> m.get_best_role_player('attaccante') (<Player u'ROSSI G.'>, 11.166666666666666) >>>
oppure il miglior giocatore di una squadra?
>>> m.get_best_team_player('inter') (<Player u'ICARDI'>, 10.0) >>>
se volessi la rosa dell’Inter?
>>> m.get_players_by_team('inter') [<Player u'CARRIZO'>, <Player u'CASTELLAZZI'>, <Player u'HANDANOVIC'>, <Player u'ANDREOLLI'>, <Player u'CAMPAGNARO'>, <Player u'CHIVU'>, <Player u'JONATHAN'>, <Player u'JUAN JESUS'>, <Player u'NAGATOMO'>, <Player u'PEREIRA A.'>, <Player u'RANOCCHIA'>, <Player u'SAMUEL'>, <Player u'ROLANDO'>, <Player u'WALLACE'>, <Player u'ALVAREZ R.'>, <Player u'CAMBIASSO'>, <Player u'GUARIN'>, <Player u'KOVACIC'>, <Player u'KUZMANOVIC'>, <Player u'MUDINGAYI'>, <Player u'OBI'>, <Player u'TAIDER'>, <Player u'ZANETTI'>, <Player u'OLSEN'>, <Player u'MARIGA'>, <Player u'BELFODIL'>, <Player u'ICARDI'>, <Player u'MILITO'>, <Player u'PALACIO'>] >>>
se poi vuoi anche vedere la media di costoro,mandi la lista al prossimo metodo:
>>> inter = m.get_players_by_team('inter') >>> m.get_avg_players(inter) [(u'CARRIZO', u'INT', 0.0), (u'CASTELLAZZI', u'INT', 0.0), (u'HANDANOVIC', u'INT', 6.333333333333333), (u'ANDREOLLI', u'INT', 0.0), (u'CAMPAGNARO', u'INT', 7.0), (u'CHIVU', u'INT', 0.0), (u'JONATHAN', u'INT', 7.166666666666667), (u'JUAN JESUS', u'INT', 6.5), (u'NAGATOMO', u'INT', 8.5), (u'PEREIRA A.', u'INT', 0.0), (u'RANOCCHIA', u'INT', 6.333333333333333), (u'SAMUEL', u'INT', 0.0), (u'ROLANDO', u'INT', 0.0), (u'WALLACE', u'INT', 0.0), (u'ALVAREZ R.', u'INT', 9.333333333333334), (u'CAMBIASSO', u'INT', 6.0), (u'GUARIN', u'INT', 5.833333333333333), (u'KOVACIC', u'INT', 5.0), (u'KUZMANOVIC', u'INT', 0.0), (u'MUDINGAYI', u'INT', 0.0), (u'OBI', u'INT', 0.0), (u'TAIDER', u'INT', 6.166666666666667), (u'ZANETTI', u'INT', 0.0), (u'OLSEN', u'INT', 0.0), (u'MARIGA', u'INT', 0.0), (u'BELFODIL', u'INT', 6.0), (u'ICARDI', u'INT', 10.0), (u'MILITO', u'INT', 0.0), (u'PALACIO', u'INT', 9.833333333333334)] >>>
se volessi analizzare un giocatore?
>>> palacio = m.get_player_by_name('palacio') >>> palacio <Player u'PALACIO'> >>> palacio.grades <sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x01A22C10> >>> palacio.grades.all() [<Grade day 1: <Player u'PALACIO'>>, <Grade day 2: <Player u'PALACIO'>>, <Grade day 3: <Player u'PALACIO'>>] >>> for v in palacio.grades.all(): print v.day, v.f_grade 1 11.5 2 11.5 3 6.5 >>> m.get_avg_player(palacio) 9.833333333333334 >>>
e se non conoscessi bene il nome?
come si scrive il nome dello slavo del genoa?
>>> m.get_players_name_starts_with('vr') [<Player u'VRSALJKO'>]
bo’, non resta che creare una UI per rendere più facili, lettura ed utilizzo di
questa app.
Alla prossima.
link utili:
flask-sqlalchemy
model.py
view.py
Commenti recenti