Home > Fantacalcio, FantaLega, python > Statistiche Fantacalcio: la mia prima applicazione con Flask-SQLAlchemy

Statistiche Fantacalcio: la mia prima applicazione con Flask-SQLAlchemy

19 Settembre 2013

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.

tables
fig.1

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

Categorie:Fantacalcio, FantaLega, python Tag:
I commenti sono chiusi.