File size: 6,288 Bytes
62f41ca
bb5937d
62f41ca
bb5937d
 
62f41ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb5937d
62f41ca
 
 
 
 
bb5937d
 
 
 
 
 
62f41ca
 
 
 
bb5937d
62f41ca
 
bb5937d
62f41ca
 
 
 
bb5937d
 
62f41ca
 
bb5937d
 
62f41ca
bb5937d
62f41ca
 
bb5937d
 
 
695067b
 
62f41ca
 
 
 
 
 
 
bb5937d
ca10378
 
bb5937d
 
ca10378
bb5937d
62f41ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bb5937d
 
 
62f41ca
 
 
 
 
 
bb5937d
 
 
62f41ca
 
bb5937d
 
 
62f41ca
bb5937d
62f41ca
bb5937d
62f41ca
bb5937d
62f41ca
bb5937d
62f41ca
bb5937d
 
 
 
 
62f41ca
bb5937d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62f41ca
 
 
 
bb5937d
 
62f41ca
 
 
bb5937d
 
 
 
62f41ca
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
"""
Data layer for the browsable MV database app.

Loads data/clean/mv_database.parquet (one row per player-season, MV-matched)
into DuckDB and exposes filter / detail helpers. No scoring — pure browse.
"""

import os
import sys
import time
from pathlib import Path
from threading import Lock

import duckdb

_PARENT = Path(__file__).resolve().parent.parent
if str(_PARENT) not in sys.path:
    sys.path.insert(0, str(_PARENT))

APP_DIR = Path(__file__).parent
DATA_DIR = Path(os.environ.get('DATA_DIR', APP_DIR.parent / 'data' / 'clean')).resolve()
DB_PARQUET = DATA_DIR / 'mv_database.parquet'

DATASET_REPO = os.environ.get('DATASET_REPO_ID')
HF_TOKEN = os.environ.get('HF_TOKEN')
MAX_AGE_HOURS = float(os.environ.get('DATA_MAX_AGE_HOURS', '24'))

# columns shown in the browse table (compact payload)
TABLE_COLS = ['playerId', 'tm_id', 'Jugador', 'Posicion', 'Temporada', 'age',
              'nationality', 'Equipo', 'Competencia', '"Minutos totales"',
              'mv_eos', 'mv_eos_prev', 'tm_mv_now', 'mv_source', 'match_conf',
              'tm_url', 'tm_contract_until', 'tm_foot']

_conn = None
_conn_lock = Lock()


def _is_stale(p):
    if not p.exists():
        return True
    return (time.time() - p.stat().st_mtime) / 3600 > MAX_AGE_HOURS


def bootstrap_data():
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    if not _is_stale(DB_PARQUET):
        print(f'[data] using local {DB_PARQUET.name}', flush=True)
        return
    if not DATASET_REPO:
        if DB_PARQUET.exists():
            print(f'[data] DATASET_REPO_ID not set; using local {DB_PARQUET.name}', flush=True)
            return
        raise RuntimeError(f'No data at {DB_PARQUET} and DATASET_REPO_ID not set.')
    print(f'[data] pulling {DATASET_REPO} -> {DATA_DIR}', flush=True)
    from huggingface_hub import snapshot_download
    snapshot_download(repo_id=DATASET_REPO, repo_type='dataset', local_dir=str(DATA_DIR),
                      local_dir_use_symlinks=False, token=HF_TOKEN,
                      allow_patterns=['mv_database.parquet'])  # app only needs this
    global _conn
    _conn = None
    print('[data] download complete', flush=True)


def get_conn():
    global _conn
    with _conn_lock:
        if _conn is None:
            _conn = duckdb.connect(database=':memory:')
            tmp = os.environ.get('DUCKDB_TEMP_DIR', '/tmp/duckdb')
            os.makedirs(tmp, exist_ok=True)
            _conn.execute(f"PRAGMA memory_limit='{os.environ.get('DUCKDB_MEMORY_LIMIT', '1GB')}'")
            _conn.execute(f"PRAGMA threads={os.environ.get('DUCKDB_THREADS', '2')}")
            _conn.execute(f"PRAGMA temp_directory='{tmp}'")
            _conn.execute(f"CREATE OR REPLACE VIEW base AS SELECT * FROM read_parquet('{DB_PARQUET}');")
        return _conn


def season_sort_key(s):
    s = str(s).strip()
    if '-' in s:
        a, b = s.split('-')
        return int(a) * 100 + int(b)
    try:
        return int(s) * 100
    except ValueError:
        return 0


def get_meta():
    conn = get_conn()
    seasons = sorted([r[0] for r in conn.execute(
        "SELECT DISTINCT Temporada FROM base WHERE Temporada IS NOT NULL").fetchall()],
        key=season_sort_key)
    leagues = sorted([r[0] for r in conn.execute(
        "SELECT DISTINCT Competencia FROM base WHERE Competencia IS NOT NULL").fetchall()])
    positions = sorted([r[0] for r in conn.execute(
        "SELECT DISTINCT Posicion FROM base WHERE Posicion IS NOT NULL").fetchall()])
    n_players = conn.execute("SELECT COUNT(DISTINCT playerId) FROM base").fetchone()[0]
    n_rows = conn.execute("SELECT COUNT(*) FROM base").fetchone()[0]
    return {'seasons': seasons, 'latest_season': seasons[-1] if seasons else None,
            'leagues': leagues, 'positions': positions,
            'n_rows': n_rows, 'n_players': n_players}


def get_rows(seasons=None, leagues=None, positions=None, age_min=None, age_max=None,
             min_minutes=0, foot=None, mv_min_millions=None, mv_max_millions=None,
             nationality=None, conf=None, sort='mv_eos', sort_dir='desc', limit=2000):
    conn = get_conn()
    where, params = ['Jugador IS NOT NULL'], []
    if seasons:
        where.append(f"Temporada IN ({','.join(['?']*len(seasons))})"); params += seasons
    if leagues:
        where.append(f"Competencia IN ({','.join(['?']*len(leagues))})"); params += leagues
    if positions:
        where.append(f"Posicion IN ({','.join(['?']*len(positions))})"); params += positions
    if min_minutes:
        where.append('"Minutos totales" >= ?'); params.append(float(min_minutes))
    if age_min is not None:
        where.append('age >= ?'); params.append(age_min)
    if age_max is not None:
        where.append('age <= ?'); params.append(age_max)
    if foot:
        where.append('lower(tm_foot) = ?'); params.append(foot.lower())
    if mv_min_millions is not None:
        where.append('mv_eos >= ?'); params.append(mv_min_millions * 1e6)
    if mv_max_millions is not None:
        where.append('mv_eos <= ?'); params.append(mv_max_millions * 1e6)
    if nationality:
        where.append('lower(nationality) LIKE ?'); params.append(f'%{nationality.lower()}%')
    if conf:
        where.append('match_conf = ?'); params.append(conf)

    safe_sort = {'mv_eos', 'mv_eos_prev', 'age', 'Minutos totales', 'Jugador',
                 'Temporada', 'Competencia', 'Posicion', 'nationality'}
    sort_col = sort if sort in safe_sort else 'mv_eos'
    direction = 'DESC' if str(sort_dir).lower() == 'desc' else 'ASC'
    sql = (f"SELECT {', '.join(TABLE_COLS)} FROM base WHERE {' AND '.join(where)} "
           f'ORDER BY "{sort_col}" {direction} NULLS LAST LIMIT {int(limit)}')
    return conn.execute(sql, params).df()


def get_player(player_id, season):
    conn = get_conn()
    df = conn.execute("SELECT * FROM base WHERE playerId = ? AND Temporada = ? LIMIT 1",
                      [player_id, season]).df()
    if df.empty:
        return None
    history = conn.execute(
        """SELECT Temporada, Competencia, "Minutos totales", "Partidos jugados",
                  Goles_totales, asistencias, mv_eos, mv_eos_prev
           FROM base WHERE playerId = ? ORDER BY Temporada""",
        [player_id]).df()
    return {'row': df.iloc[0].to_dict(), 'history': history.to_dict('records')}