969 lines
39 KiB
Python
Executable File
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" {'N°':<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} {'N°':<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)
|