Files
turf_saas/pmu_results.py

969 lines
39 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pmu_results.py — Récupération complète des données PMU via API client/7
=========================================================================
Sources : https://online.turfinfo.api.pmu.fr/rest/client/7
Endpoints utilisés :
/programme/DDMMYYYY?meteo=true → programme + météo + ordreArrivee
/programme/.../R{n}/C{n}/participants → partants + cotes + stats + résultats
/programme/.../R{n}/C{n}/rapports-definitifs → gains
Schéma DB (nouvelles tables, compatibles avec turf.db existant) :
pmu_reunions — une ligne par réunion du jour
pmu_courses — une ligne par course
pmu_partants — un cheval par course (stats + cotes + résultat)
pmu_rapports — gains par type de pari
pmu_meteo — météo par réunion
Usage :
python3 pmu_results.py # aujourd'hui
python3 pmu_results.py --date 23032026 # date en DDMMYYYY
python3 pmu_results.py --show # afficher résultats BDD sans scraper
python3 pmu_results.py --bilan # bilan prédictions vs arrivées
python3 pmu_results.py --yesterday # raccourci hier
"""
import sqlite3
import requests
import json
import base64
import time
import argparse
import re
from datetime import datetime, timedelta
from pathlib import Path
# ─────────────────────────────────────────────────────────
# CONFIG
# ─────────────────────────────────────────────────────────
DB_PATH = "/home/h3r7/turf_saas/turf_saas.db"
OUTPUT_DIR = Path("/home/h3r7/turf_scraper")
API_BASE = "https://online.turfinfo.api.pmu.fr/rest/client/7"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "application/json",
"Accept-Language": "fr-FR,fr;q=0.9",
"Referer": "https://www.pmu.fr/",
}
SESSION = requests.Session()
SESSION.headers.update(HEADERS)
# ─────────────────────────────────────────────────────────
# SCHÉMA BASE DE DONNÉES
# ─────────────────────────────────────────────────────────
SCHEMA = """
CREATE TABLE IF NOT EXISTS pmu_reunions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_programme TEXT NOT NULL,
num_reunion INTEGER NOT NULL,
num_externe INTEGER,
nature TEXT,
statut TEXT,
audience TEXT,
hippodrome_code TEXT,
hippodrome_court TEXT,
hippodrome_long TEXT,
pays_code TEXT,
pays_libelle TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date_programme, num_reunion)
);
CREATE TABLE IF NOT EXISTS pmu_meteo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_programme TEXT NOT NULL,
num_reunion INTEGER NOT NULL,
nebulositecode TEXT,
nebulosite_court TEXT,
nebulosite_long TEXT,
temperature INTEGER,
force_vent INTEGER,
direction_vent TEXT,
date_prevision INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date_programme, num_reunion)
);
CREATE TABLE IF NOT EXISTS pmu_courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_programme TEXT NOT NULL,
num_reunion INTEGER NOT NULL,
num_course INTEGER NOT NULL,
num_externe INTEGER,
libelle TEXT,
libelle_court TEXT,
heure_depart INTEGER,
heure_depart_str TEXT,
distance INTEGER,
distance_unit TEXT,
parcours TEXT,
discipline TEXT,
specialite TEXT,
type_piste TEXT,
corde TEXT,
condition_age TEXT,
condition_sexe TEXT,
categorie_particularite TEXT,
nb_declares_partants INTEGER,
montant_prix INTEGER,
montant_1er INTEGER,
montant_2eme INTEGER,
montant_3eme INTEGER,
montant_4eme INTEGER,
montant_5eme INTEGER,
penetrometre_intitule TEXT,
penetrometre_valeur TEXT,
statut TEXT,
categorie_statut TEXT,
arrivee_definitive INTEGER DEFAULT 0,
rapports_disponibles INTEGER DEFAULT 0,
duree_course_ms INTEGER,
conditions TEXT,
ordre_arrivee_json TEXT,
incidents_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date_programme, num_reunion, num_course)
);
CREATE TABLE IF NOT EXISTS pmu_partants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_programme TEXT NOT NULL,
num_reunion INTEGER NOT NULL,
num_course INTEGER NOT NULL,
num_pmu INTEGER NOT NULL,
id_cheval TEXT,
nom TEXT,
age INTEGER,
sexe TEXT,
race TEXT,
robe TEXT,
pays TEXT,
place_corde INTEGER,
nom_pere TEXT,
nom_mere TEXT,
nom_pere_mere TEXT,
driver TEXT,
driver_change INTEGER DEFAULT 0,
entraineur TEXT,
proprietaire TEXT,
eleveur TEXT,
pays_entrainement TEXT,
oeilleres TEXT,
supplement INTEGER DEFAULT 0,
handicap_valeur REAL,
handicap_poids INTEGER,
musique TEXT,
nombre_courses INTEGER DEFAULT 0,
nombre_victoires INTEGER DEFAULT 0,
nombre_places INTEGER DEFAULT 0,
nombre_places_2eme INTEGER DEFAULT 0,
nombre_places_3eme INTEGER DEFAULT 0,
gains_carriere INTEGER DEFAULT 0,
gains_victoires INTEGER DEFAULT 0,
gains_place INTEGER DEFAULT 0,
gains_annee_en_cours INTEGER DEFAULT 0,
gains_annee_precedente INTEGER DEFAULT 0,
cote_direct REAL,
cote_reference REAL,
tendance_cote TEXT,
favoris INTEGER DEFAULT 0,
ordre_arrivee INTEGER DEFAULT 0,
statut_partant TEXT,
distance_cheval_prec TEXT,
distance_cheval_prec_code INTEGER,
commentaire_apres_course TEXT,
indicateur_inedit INTEGER DEFAULT 0,
tx_victoire REAL,
tx_place REAL,
forme_recente REAL,
tendance_forme REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date_programme, num_reunion, num_course, num_pmu)
);
CREATE TABLE IF NOT EXISTS pmu_rapports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_programme TEXT NOT NULL,
num_reunion INTEGER NOT NULL,
num_course INTEGER NOT NULL,
type_pari TEXT NOT NULL,
famille_pari TEXT,
mise_base INTEGER,
combinaison TEXT NOT NULL,
dividende INTEGER,
dividende_euro REAL,
nb_gagnants REAL,
libelle TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(date_programme, num_reunion, num_course, type_pari, combinaison)
);
"""
# Vues recréées séparément (DROP + CREATE)
VUES = [
("v_resultats_complets", """
CREATE VIEW v_resultats_complets AS
SELECT
p.date_programme,
p.num_reunion,
p.num_course,
c.libelle AS course,
c.heure_depart_str AS heure,
r.hippodrome_long AS hippodrome,
c.discipline,
c.distance,
c.type_piste,
c.penetrometre_intitule AS penetrometre,
m.temperature,
m.nebulosite_court AS meteo,
m.force_vent,
m.direction_vent,
p.num_pmu,
p.nom AS cheval,
p.age,
p.sexe,
p.race,
p.driver,
p.entraineur,
p.oeilleres,
p.handicap_valeur,
p.musique,
p.nombre_courses,
p.nombre_victoires,
p.nombre_places,
p.gains_carriere,
p.gains_annee_en_cours,
p.cote_direct,
p.cote_reference,
p.tendance_cote,
p.tx_victoire,
p.tx_place,
p.forme_recente,
p.tendance_forme,
p.ordre_arrivee AS position_finale,
p.distance_cheval_prec,
p.commentaire_apres_course,
p.nom_pere,
p.nom_mere,
p.pays_entrainement
FROM pmu_partants p
JOIN pmu_courses c ON c.date_programme=p.date_programme
AND c.num_reunion=p.num_reunion
AND c.num_course=p.num_course
JOIN pmu_reunions r ON r.date_programme=p.date_programme
AND r.num_reunion=p.num_reunion
LEFT JOIN pmu_meteo m ON m.date_programme=p.date_programme
AND m.num_reunion=p.num_reunion
"""),
("v_bilan_predictions", """
CREATE VIEW v_bilan_predictions AS
SELECT
pr.date,
pr.race_name,
pr.horse_name,
pr.horse_number,
pr.prediction_rank,
pr.odds AS cote_pred,
pr.source,
pa.ordre_arrivee AS position_reelle,
pa.cote_direct AS cote_finale,
pa.commentaire_apres_course,
CASE
WHEN pa.ordre_arrivee = 1 THEN 'GAGNANT'
WHEN pa.ordre_arrivee <= 3 THEN 'PLACE'
WHEN pa.ordre_arrivee <= 5 THEN 'TOP5'
WHEN pa.ordre_arrivee > 5 THEN 'HORS'
ELSE 'INCONNU'
END AS resultat
FROM predictions pr
LEFT JOIN pmu_partants pa
ON pa.date_programme = pr.date
AND pa.nom = pr.horse_name
AND pa.num_pmu = pr.horse_number
"""),
]
# ─────────────────────────────────────────────────────────
# INITIALISATION DB
# ─────────────────────────────────────────────────────────
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.executescript(SCHEMA)
for vue_name, vue_sql in VUES:
conn.execute(f"DROP VIEW IF EXISTS {vue_name}")
conn.execute(vue_sql)
conn.commit()
conn.close()
print("✅ Schéma DB initialisé (5 tables + 2 vues)")
# ─────────────────────────────────────────────────────────
# APPELS API
# ─────────────────────────────────────────────────────────
def api_get(path, params=None, retries=3):
url = API_BASE + path
for attempt in range(retries):
try:
r = SESSION.get(url, params=params, timeout=15)
if r.status_code == 200:
return r.json()
elif r.status_code == 404:
return None
else:
print(f" ⚠️ HTTP {r.status_code}")
time.sleep(1)
except Exception as e:
if attempt == retries - 1:
print(f" ❌ Erreur réseau : {e}")
return None
def ts_to_hhmm(ts_ms):
if not ts_ms:
return ""
try:
return datetime.fromtimestamp(ts_ms / 1000).strftime("%H:%M")
except Exception:
return ""
# ─────────────────────────────────────────────────────────
# CALCUL STATS MUSIQUE
# ─────────────────────────────────────────────────────────
def parse_musique(musique):
if not musique:
return None, None, None, None
clean = re.sub(r'\(\d+\)', '', musique)
resultats = re.findall(r'(\d+|D|0)([amphsc]?)', clean)
positions = []
for pos, _ in resultats[:10]:
positions.append(99 if pos == 'D' else int(pos))
if not positions:
return None, None, None, None
nb = len(positions)
vict = positions.count(1)
plac = sum(1 for p in positions if 1 <= p <= 3)
rec = [p for p in positions[:3] if p != 99]
forme_recente = round(sum(rec) / len(rec), 1) if rec else None
if len(positions) >= 4:
tendance = round(sum(positions[-4:]) / 4 - sum(positions[:4]) / 4, 1)
else:
tendance = 0.0
return (
round(vict / nb * 100, 1) if nb else None,
round(plac / nb * 100, 1) if nb else None,
forme_recente,
tendance,
)
# ─────────────────────────────────────────────────────────
# SAUVEGARDE RÉUNION
# ─────────────────────────────────────────────────────────
def save_reunion(conn, date_str, reunion):
hipp = reunion.get("hippodrome", {})
pays = reunion.get("pays", {})
conn.execute("""
INSERT OR REPLACE INTO pmu_reunions
(date_programme, num_reunion, num_externe, nature, statut, audience,
hippodrome_code, hippodrome_court, hippodrome_long, pays_code, pays_libelle)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
""", (
date_str,
reunion.get("numOfficiel"),
reunion.get("numExterne"),
reunion.get("nature"),
reunion.get("statut"),
reunion.get("audience"),
hipp.get("code"),
hipp.get("libelleCourt"),
hipp.get("libelleLong"),
pays.get("code"),
pays.get("libelle"),
))
# ─────────────────────────────────────────────────────────
# SAUVEGARDE MÉTÉO
# ─────────────────────────────────────────────────────────
def save_meteo(conn, date_str, num_reunion, meteo):
if not meteo:
return
conn.execute("""
INSERT OR REPLACE INTO pmu_meteo
(date_programme, num_reunion, nebulositecode, nebulosite_court,
nebulosite_long, temperature, force_vent, direction_vent, date_prevision)
VALUES (?,?,?,?,?,?,?,?,?)
""", (
date_str, num_reunion,
meteo.get("nebulositeCode"),
meteo.get("nebulositeLibelleCourt"),
meteo.get("nebulositeLibelleLong"),
meteo.get("temperature"),
meteo.get("forceVent"),
meteo.get("directionVent"),
meteo.get("datePrevision"),
))
# ─────────────────────────────────────────────────────────
# SAUVEGARDE COURSE
# ─────────────────────────────────────────────────────────
def save_course(conn, date_str, num_reunion, course):
heure_ts = course.get("heureDepart")
penet = course.get("penetrometre") or {}
incidents = course.get("incidents", [])
ordre_arr = course.get("ordreArrivee", [])
conn.execute("""
INSERT OR REPLACE INTO pmu_courses
(date_programme, num_reunion, num_course, num_externe,
libelle, libelle_court, heure_depart, heure_depart_str,
distance, distance_unit, parcours, discipline, specialite,
type_piste, corde, condition_age, condition_sexe,
categorie_particularite, nb_declares_partants,
montant_prix, montant_1er, montant_2eme, montant_3eme, montant_4eme, montant_5eme,
penetrometre_intitule, penetrometre_valeur,
statut, categorie_statut, arrivee_definitive, rapports_disponibles,
duree_course_ms, conditions, ordre_arrivee_json, incidents_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", (
date_str, num_reunion,
course.get("numOrdre"),
course.get("numExterne"),
course.get("libelle"),
course.get("libelleCourt"),
heure_ts,
ts_to_hhmm(heure_ts),
course.get("distance"),
course.get("distanceUnit"),
course.get("parcours"),
course.get("discipline"),
course.get("specialite"),
course.get("typePiste"),
course.get("corde"),
course.get("conditionAge"),
course.get("conditionSexe"),
course.get("categorieParticularite"),
course.get("nombreDeclaresPartants"),
course.get("montantPrix"),
course.get("montantOffert1er"),
course.get("montantOffert2eme"),
course.get("montantOffert3eme"),
course.get("montantOffert4eme"),
course.get("montantOffert5eme"),
penet.get("intitule"),
penet.get("commentaire"),
course.get("statut"),
course.get("categorieStatut"),
int(bool(course.get("arriveeDefinitive"))),
int(bool(course.get("rapportsDefinitifsDisponibles"))),
course.get("dureeCourse"),
course.get("conditions"),
json.dumps(ordre_arr) if ordre_arr else None,
json.dumps(incidents) if incidents else None,
))
# ─────────────────────────────────────────────────────────
# SAUVEGARDE PARTANTS
# ─────────────────────────────────────────────────────────
def save_partants(conn, date_str, num_reunion, num_course, participants):
saved = 0
for p in participants:
robe = p.get("robe") or {}
gains = p.get("gainsParticipant") or {}
rdr = p.get("dernierRapportDirect") or {}
rref = p.get("dernierRapportReference") or {}
dist_prec = p.get("distanceChevalPrecedent") or {}
comm = p.get("commentaireApresCourse") or {}
musique = p.get("musique", "")
tx_vic, tx_plac, forme_rec, tendance_f = parse_musique(musique)
conn.execute("""
INSERT OR REPLACE INTO pmu_partants
(date_programme, num_reunion, num_course,
num_pmu, id_cheval, nom, age, sexe, race, robe, pays, place_corde,
nom_pere, nom_mere, nom_pere_mere,
driver, driver_change, entraineur, proprietaire, eleveur, pays_entrainement,
oeilleres, supplement, handicap_valeur, handicap_poids,
musique, nombre_courses, nombre_victoires, nombre_places,
nombre_places_2eme, nombre_places_3eme,
gains_carriere, gains_victoires, gains_place,
gains_annee_en_cours, gains_annee_precedente,
cote_direct, cote_reference, tendance_cote, favoris,
ordre_arrivee, statut_partant,
distance_cheval_prec, distance_cheval_prec_code,
commentaire_apres_course, indicateur_inedit,
tx_victoire, tx_place, forme_recente, tendance_forme)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", (
date_str, num_reunion, num_course,
p.get("numPmu"),
p.get("idCheval"),
p.get("nom"),
p.get("age"),
p.get("sexe"),
p.get("race"),
robe.get("libelleLong"),
p.get("pays"),
p.get("placeCorde"),
p.get("nomPere"),
p.get("nomMere"),
p.get("nomPereMere"),
p.get("driver"),
int(bool(p.get("driverChange"))),
p.get("entraineur"),
p.get("proprietaire"),
p.get("eleveur"),
p.get("paysEntrainement"),
p.get("oeilleres"),
p.get("supplement", 0),
p.get("handicapValeur"),
p.get("handicapPoids"),
musique,
p.get("nombreCourses", 0),
p.get("nombreVictoires", 0),
p.get("nombrePlaces", 0),
p.get("nombrePlacesSecond", 0),
p.get("nombrePlacesTroisieme", 0),
gains.get("gainsCarriere", 0),
gains.get("gainsVictoires", 0),
gains.get("gainsPlace", 0),
gains.get("gainsAnneeEnCours", 0),
gains.get("gainsAnneePrecedente", 0),
rdr.get("rapport"),
rref.get("rapport"),
rref.get("indicateurTendance"),
int(bool(rdr.get("favoris"))),
p.get("ordreArrivee", 0),
p.get("statut"),
dist_prec.get("libelleLong"),
dist_prec.get("code"),
comm.get("texte"),
int(bool(p.get("indicateurInedit"))),
tx_vic, tx_plac, forme_rec, tendance_f,
))
saved += 1
return saved
# ─────────────────────────────────────────────────────────
# SAUVEGARDE RAPPORTS
# ─────────────────────────────────────────────────────────
def save_rapports(conn, date_str, num_reunion, num_course, data):
saved = 0
if not data or not isinstance(data, list):
return 0
for bloc in data:
type_pari = bloc.get("typePari", "")
famille = bloc.get("famillePari", "")
mise_base = bloc.get("miseBase", 0)
for rap in bloc.get("rapports", []):
combinaison = str(rap.get("combinaison", ""))
dividende = rap.get("dividende", 0) or 0
try:
conn.execute("""
INSERT OR IGNORE INTO pmu_rapports
(date_programme, num_reunion, num_course,
type_pari, famille_pari, mise_base,
combinaison, dividende, dividende_euro, nb_gagnants, libelle)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
""", (
date_str, num_reunion, num_course,
type_pari, famille, mise_base,
combinaison,
dividende,
round(dividende / 100, 2),
rap.get("nombreGagnants"),
rap.get("libelle"),
))
saved += 1
except Exception:
pass
return saved
# ─────────────────────────────────────────────────────────
# BILAN PRÉDICTIONS VS ARRIVÉES
# ─────────────────────────────────────────────────────────
def afficher_bilan(date_str):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM v_bilan_predictions WHERE date=? ORDER BY race_name, prediction_rank",
(date_str,)
).fetchall()
conn.close()
if not rows:
print(f"Aucune donnée de bilan pour {date_str} (prédictions ou partants manquants)")
return
from collections import defaultdict
par_course = defaultdict(list)
for r in rows:
par_course[r["race_name"]].append(dict(r))
total_g = total_p = total_t = total = 0
print(f"\n{'='*65}")
print(f" 📊 BILAN PRÉDICTIONS vs ARRIVÉES — {date_str}")
print(f"{'='*65}")
for course, chevaux in sorted(par_course.items()):
print(f"\n 🏇 {course}")
print(f" {'':<4} {'CHEVAL':<22} {'RANG PRÉDIT':<12} {'POSITION':<9} RÉSULTAT")
print(f" {'-'*60}")
for ch in chevaux:
pos = ch["position_reelle"] or "?"
res = ch["resultat"] or "-"
em = {"GAGNANT": "🥇", "PLACE": "🥈", "TOP5": "", "HORS": ""}.get(res, "·")
print(f" {ch['horse_number']:<4} {ch['horse_name']:<22} #{ch['prediction_rank']:<11} {str(pos):<9} {em} {res}")
total += 1
if res == "GAGNANT": total_g += 1
if res in ("GAGNANT", "PLACE"): total_p += 1
if res in ("GAGNANT", "PLACE", "TOP5"): total_t += 1
print(f"\n TOTAL : {total} prédictions analysées")
print(f" 🥇 Gagnants : {total_g} | 🥈 Placés top3 : {total_p} | ✅ Top5 : {total_t}")
print(f"{'='*65}\n")
# ─────────────────────────────────────────────────────────
# AFFICHAGE RÉSULTATS BDD
# ─────────────────────────────────────────────────────────
def afficher_resultats(date_str):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
courses = conn.execute("""
SELECT c.num_reunion, c.num_course, c.libelle, c.heure_depart_str,
r.hippodrome_court, c.discipline, c.distance,
c.penetrometre_intitule, c.nb_declares_partants,
m.temperature, m.nebulosite_court, m.direction_vent, m.force_vent
FROM pmu_courses c
JOIN pmu_reunions r ON r.date_programme=c.date_programme AND r.num_reunion=c.num_reunion
LEFT JOIN pmu_meteo m ON m.date_programme=c.date_programme AND m.num_reunion=c.num_reunion
WHERE c.date_programme=? AND c.arrivee_definitive=1
ORDER BY c.num_reunion, c.num_course
""", (date_str,)).fetchall()
if not courses:
print(f"\nAucune course terminée en BDD pour {date_str}")
conn.close()
return
print(f"\n{'='*65}")
print(f" RÉSULTATS PMU — {date_str} ({len(courses)} course(s))")
print(f"{'='*65}")
for co in courses:
print(f"\n R{co['num_reunion']}C{co['num_course']} | {co['libelle']}")
meteo_str = ""
if co["temperature"]:
meteo_str = f" | {co['temperature']}°C {co['nebulosite_court']} vent {co['direction_vent']} {co['force_vent']}km/h"
print(f" {co['hippodrome_court']}{co['heure_depart_str']}{co['discipline']} {co['distance']}m — {co['nb_declares_partants']} partants{meteo_str}")
if co["penetrometre_intitule"]:
print(f" Terrain : {co['penetrometre_intitule']}")
top5 = conn.execute("""
SELECT num_pmu, nom, driver, ordre_arrivee,
cote_direct, distance_cheval_prec, commentaire_apres_course
FROM pmu_partants
WHERE date_programme=? AND num_reunion=? AND num_course=?
AND ordre_arrivee BETWEEN 1 AND 5
ORDER BY ordre_arrivee
""", (date_str, co["num_reunion"], co["num_course"])).fetchall()
if top5:
print(f" {'Pos':<4} {'':<4} {'CHEVAL':<22} {'Driver':<16} {'Cote':>6} Ecart")
print(f" {'-'*64}")
for ch in top5:
cote = f"{ch['cote_direct']:.1f}" if ch["cote_direct"] else "-"
ecart = ch["distance_cheval_prec"] or ""
print(f" {ch['ordre_arrivee']:<4} {ch['num_pmu']:<4} {ch['nom']:<22} {ch['driver'] or '':<16} {cote:>6} {ecart}")
if ch["commentaire_apres_course"]:
print(f"{ch['commentaire_apres_course'][:80]}")
# Rapports principaux
raps = conn.execute("""
SELECT type_pari, combinaison, dividende_euro, nb_gagnants
FROM pmu_rapports
WHERE date_programme=? AND num_reunion=? AND num_course=?
AND type_pari IN ('SIMPLE_GAGNANT','SIMPLE_PLACE','COUPLE_GAGNANT',
'TIERCE','QUARTE_PLUS','QUINTE_PLUS')
ORDER BY CASE type_pari
WHEN 'SIMPLE_GAGNANT' THEN 1
WHEN 'SIMPLE_PLACE' THEN 2
WHEN 'COUPLE_GAGNANT' THEN 3
WHEN 'TIERCE' THEN 4
WHEN 'QUARTE_PLUS' THEN 5
WHEN 'QUINTE_PLUS' THEN 6 END, combinaison
""", (date_str, co["num_reunion"], co["num_course"])).fetchall()
if raps:
print(f" Rapports :")
for rap in raps:
nb = f"({int(rap['nb_gagnants'])} gagnants)" if rap["nb_gagnants"] else ""
print(f" {rap['type_pari']:<22} {rap['combinaison']:<12} {rap['dividende_euro']:>8.2f}{nb}")
conn.close()
print()
# ─────────────────────────────────────────────────────────
# EXPORT JSON
# ─────────────────────────────────────────────────────────
def export_json(date_str, date_ddmmyyyy):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
def rows(sql):
return [dict(r) for r in conn.execute(sql, (date_str,)).fetchall()]
out = {
"date": date_str,
"generated_at": datetime.now().isoformat(),
"reunions": rows("SELECT * FROM pmu_reunions WHERE date_programme=?"),
"courses": rows("SELECT * FROM pmu_courses WHERE date_programme=?"),
"partants": rows("SELECT * FROM pmu_partants WHERE date_programme=?"),
"rapports": rows("SELECT * FROM pmu_rapports WHERE date_programme=?"),
}
conn.close()
fname = OUTPUT_DIR / f"pmu_{date_ddmmyyyy}.json"
with open(fname, "w", encoding="utf-8") as f:
json.dump(out, f, indent=2, ensure_ascii=False, default=str)
print(f" 📁 Export JSON : {fname}\n")
# ─────────────────────────────────────────────────────────
# ORCHESTRATEUR PRINCIPAL
# ─────────────────────────────────────────────────────────
def run(date_ddmmyyyy: str):
d = datetime.strptime(date_ddmmyyyy, "%d%m%Y")
date_str = d.strftime("%Y-%m-%d")
print(f"\n{'='*65}")
print(f" 🏇 PMU SCRAPER — {date_str} (API client/7)")
print(f"{'='*65}\n")
init_db()
conn = sqlite3.connect(DB_PATH)
# ── 1. Programme ─────────────────────────────────────
print("📡 Récupération du programme...")
prog = api_get(f"/programme/{date_ddmmyyyy}", params={"meteo": "true"})
if not prog:
print("❌ Programme indisponible.")
conn.close()
return
reunions = prog.get("programme", {}).get("reunions", [])
print(f"{len(reunions)} réunion(s)\n")
total_courses = total_partants = total_rapports = 0
for reunion in reunions:
r_num = reunion.get("numOfficiel")
hipp = reunion.get("hippodrome", {}).get("libelleCourt", "?")
statut_r = reunion.get("statut", "")
courses = reunion.get("courses", [])
print(f" ── R{r_num} {hipp} ({statut_r}) — {len(courses)} course(s)")
save_reunion(conn, date_str, reunion)
save_meteo(conn, date_str, r_num, reunion.get("meteo"))
conn.commit()
for course in courses:
c_num = course.get("numOrdre")
libelle = course.get("libelleCourt", f"C{c_num}")
heure = ts_to_hhmm(course.get("heureDepart"))
terminee = bool(course.get("arriveeDefinitive"))
print(f" C{c_num} {libelle:<20} {heure} ", end="", flush=True)
save_course(conn, date_str, r_num, course)
total_courses += 1
# ── 2. Participants ───────────────────────────
parts = api_get(
f"/programme/{date_ddmmyyyy}/R{r_num}/C{c_num}/participants",
params={"withCotes": "true"}
)
time.sleep(0.2)
if parts and "participants" in parts:
n = save_partants(conn, date_str, r_num, c_num, parts["participants"])
total_partants += n
print(f"{n} partants ", end="", flush=True)
else:
print("(pas de partants) ", end="", flush=True)
# ── 3. Rapports (si terminée) ─────────────────
if terminee:
raps = api_get(
f"/programme/{date_ddmmyyyy}/R{r_num}/C{c_num}/rapports-definitifs"
)
time.sleep(0.2)
if raps and isinstance(raps, list):
n = save_rapports(conn, date_str, r_num, c_num, raps)
total_rapports += n
print(f"{n} rapports", flush=True)
else:
print("(pas de rapports)", flush=True)
else:
print("(en attente)", flush=True)
conn.commit()
calculate_and_save_scores(conn, date_str)
conn.close()
print(f"\n{'='*65}")
print(f" ✅ TERMINÉ — Courses: {total_courses} | Partants: {total_partants} | Rapports: {total_rapports}")
print(f"{'='*65}\n")
export_json(date_str, date_ddmmyyyy)
afficher_resultats(date_str)
afficher_bilan(date_str)
def calculate_and_save_scores(conn, date_str):
"""Calcule et sauvegarde les scores Top 5 pour chaque course terminée"""
conn.row_factory = sqlite3.Row
courses = conn.execute("""
SELECT date_programme, num_reunion, num_course, libelle, heure_depart_str,
ordre_arrivee_json
FROM pmu_courses
WHERE date_programme = ? AND arrivee_definitive = 1
""", (date_str,)).fetchall()
if not courses:
print(" Aucune course terminée pour calcul des scores")
return
for course in courses:
if not course['ordre_arrivee_json']:
continue
arrivee = json.loads(course['ordre_arrivee_json'])
if not arrivee:
continue
race_name = f"R{course['num_reunion']}C{course['num_course']} - {course['libelle']}"
partants = conn.execute("""
SELECT nom, num_pmu, ordre_arrivee, cote_direct
FROM pmu_partants
WHERE date_programme = ? AND num_reunion = ? AND num_course = ?
ORDER BY cote_direct ASC
""", (date_str, course['num_reunion'], course['num_course'])).fetchall()
if not partants:
continue
arrived_names = [p['nom'] for p in partants if p['ordre_arrivee'] and 1 <= p['ordre_arrivee'] <= 5]
top5_cotes = [p['nom'] for p in partants[:5] if p['nom']]
bases = [p['nom'] for p in conn.execute("""
SELECT DISTINCT pr.horse_name
FROM predictions pr
WHERE pr.date = ? AND pr.source = 'Bases'
""", (date_str,)).fetchall()]
chances = [p['nom'] for p in conn.execute("""
SELECT DISTINCT pr.horse_name
FROM predictions pr
WHERE pr.date = ? AND pr.source = 'Chances'
""", (date_str,)).fetchall()]
outsiders = [p['nom'] for p in conn.execute("""
SELECT DISTINCT pr.horse_name
FROM predictions pr
WHERE pr.date = ? AND pr.source = 'Outsiders'
""", (date_str,)).fetchall()]
top5_bc = (bases + chances)[:5]
top5_bo = (bases + outsiders)[:5]
hits_cotes = sum(1 for h in top5_cotes if h in arrived_names)
hits_bc = sum(1 for h in top5_bc if h in arrived_names)
hits_bo = sum(1 for h in top5_bo if h in arrived_names)
hippodrome = conn.execute("""
SELECT r.hippodrome_long
FROM pmu_reunions r
WHERE r.date_programme = ? AND r.num_reunion = ?
""", (date_str, course['num_reunion'])).fetchone()
hippodrome_name = hippodrome['hippodrome_long'] if hippodrome else ''
data = {
'date': date_str,
'race_name': race_name,
'race_time': course['heure_depart_str'],
'hippodrome': hippodrome_name,
'top5_cotes': top5_cotes,
'top5_cotes_hits': hits_cotes,
'top5_bc': top5_bc,
'top5_bc_hits': hits_bc,
'top5_bo': top5_bo,
'top5_bo_hits': hits_bo,
'results_top5': arrived_names
}
try:
auth = base64.b64encode(b'h3r7:h3r7').decode('ascii')
resp = requests.post(
'http://localhost:8765/turf/api/scores',
json=data,
headers={'Authorization': f'Basic {auth}'},
timeout=10
)
print(f" 💾 Scores sauvegardés: {race_name} - Hits: Cotes:{hits_cotes} BC:{hits_bc} BO:{hits_bo}")
except Exception as e:
print(f" ⚠️ Erreur sauvegarde scores: {e}")
# ─────────────────────────────────────────────────────────
# POINT D'ENTRÉE
# ─────────────────────────────────────────────────────────
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="PMU Results — API client/7")
parser.add_argument("--date", "-d",
default=datetime.now().strftime("%d%m%Y"),
help="Date DDMMYYYY (défaut: aujourd'hui)")
parser.add_argument("--yesterday", "-y", action="store_true")
parser.add_argument("--show", "-s", action="store_true",
help="Afficher résultats BDD sans scraper")
parser.add_argument("--bilan", "-b", action="store_true",
help="Afficher bilan prédictions vs arrivées")
args = parser.parse_args()
if args.yesterday:
date_fmt = (datetime.now() - timedelta(days=1)).strftime("%d%m%Y")
else:
date_fmt = args.date
date_iso = datetime.strptime(date_fmt, "%d%m%Y").strftime("%Y-%m-%d")
if args.show:
init_db()
afficher_resultats(date_iso)
elif args.bilan:
init_db()
afficher_bilan(date_iso)
else:
run(date_fmt)