exlive / app.py
nsaintsever's picture
Update app.py
c7b7123 verified
raw
history blame
10.3 kB
import streamlit as st
import cloudscraper
import pandas as pd
import plotly.express as px
from bs4 import BeautifulSoup
import random
import time
# Définition des cookies disponibles
cookies_list = [
{'_vinous_session': "BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiJWQ4MDUyOTllZGNkZDE5YTUyZWE3OTU0NzZjMTRjMDk1BjsAVEkiGXdhcmRlbi51c2VyLnVzZXIua2V5BjsAVFsHWwZpAq5mSSIiJDJhJDEwJFdTaU9uamkydDBNN3RqMG01YXRJVS4GOwBUSSIdd2FyZGVuLnVzZXIudXNlci5zZXNzaW9uBjsAVHsGSSIUbGFzdF9yZXF1ZXN0X2F0BjsAVGwrB5mlAGg%3D--7777dea1a4299409509b5af6636777dd9f84f0b7"},
{'_vinous_session': "BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiJTAwOTQzOTk1NjdhZjc0YzQ5NzJmZTI4OTVjOWFkZTJkBjsAVEkiGXdhcmRlbi51c2VyLnVzZXIua2V5BjsAVFsHWwZpAls6SSIiJDJhJDEwJEF2VWJZdU1vRVYubmh4WEw0Y1pSbmUGOwBUSSIdd2FyZGVuLnVzZXIudXNlci5zZXNzaW9uBjsAVHsGSSIUbGFzdF9yZXF1ZXN0X2F0BjsAVGwrB3CqAGg%3D--d83482ea4c6c2a043c93ee64546c4cddeec2304f"}
]
# Sélection d'un cookie unique à utiliser pour tous les appels
selected_cookie = random.choice(cookies_list)
# Définition d'une liste de headers possibles
headers_list = [
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin"
}
]
# Sélection d'un header unique à utiliser
selected_header = random.choice(headers_list)
# Création de l'instance cloudscraper et application du cookie sélectionné
scraper = cloudscraper.create_scraper()
scraper.cookies.update(selected_cookie)
# Configuration de l'interface Streamlit
st.set_page_config(page_title="Wine prices", layout="wide")
# Initialisation du session state
if "selected_wines" not in st.session_state:
st.session_state.selected_wines = []
dark_mode_css = """
<style>
body {
background-color: #0e1117;
color: white;
}
.stApp {
background-color: #0e1117;
}
.stSidebar {
background-color: #161a25;
}
.stTextInput, .stSelectbox, .stButton, .stDataFrame, .stTable {
background-color: #21262d;
color: white;
}
.stPlotlyChart {
background-color: transparent;
}
</style>
"""
st.markdown(dark_mode_css, unsafe_allow_html=True)
# Chargement du fichier Excel contenant la correspondance LWIN7 -> Nom du vin
@st.cache_data
def load_wine_data():
return pd.read_excel("LWINdatabase.xlsx") # Assurez-vous d'avoir ce fichier avec les colonnes LWIN, DISPLAY_NAME, etc.
df_wines = load_wine_data()
# Sélection du vin par l'utilisateur
def select_wine():
return st.sidebar.selectbox("Sélectionnez un vin :", df_wines["DISPLAY_NAME"].unique())
# Génération du LWIN11 (LWIN7 + millésime)
def generate_lwin11(lwin7, vintage):
return str(lwin7) + str(vintage)
# Récupération du taux de change GBP/EUR depuis Boursorama
def get_exchange_rate():
url = "https://www.boursorama.com/bourse/devises/taux-de-change-livresterling-euro-GBP-EUR/"
response = scraper.get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"})
if response.status_code == 200:
soup = BeautifulSoup(response.text, "html.parser")
rate_tag = soup.find("span", class_="c-instrument c-instrument--last")
if rate_tag:
rate = rate_tag.text.replace(",", ".")
return float(rate)
st.error("Impossible de récupérer le taux de change GBP/EUR.")
return 1.2 # Valeur par défaut
# Fonction unique qui vérifie la disponibilité et récupère les données
def get_vinous_data(lwin11, display_name):
url_prices = f"https://vinous.com/api/v2/wine/{lwin11}/livex_price_history?currency=USD"
response = scraper.get(url_prices, headers=selected_header)
time.sleep(1.5)
st.write(response.content)
if response.status_code == 200:
try:
data = response.json()
if "error" in data or not data.get("livex_price_history"):
st.warning(f"🚨 Malheureusement, le vin **{display_name} ({lwin11})** n'est pas disponible.")
return pd.DataFrame()
else:
data_prices = data.get("livex_price_history", [])
df_prices = pd.DataFrame(data_prices)
return df_prices
except Exception as e:
st.error(f"⚠️ Erreur lors du traitement des données pour **{display_name} ({lwin11})**. Détails : {e}")
return pd.DataFrame()
else:
st.warning(f"🚨 Impossible de récupérer les prix pour **{display_name} ({lwin11})**. Code erreur: {response.status_code} - Détails : {response.content}")
return pd.DataFrame()
# Convertir les prix en EUR et diviser par 12 pour obtenir le prix par bouteille
def convert_price_per_bottle(df_prices, exchange_rate):
df_prices["date_string"] = pd.to_datetime(df_prices["date_string"], format="%Y-%m-%d")
df_prices["value"] = df_prices["value"].astype(float)
df_prices["price_per_bottle"] = (df_prices["value"] * exchange_rate) / 12
return df_prices
def check_wine_availability(lwin11):
"""Teste la disponibilité d'un vin/millésime via un appel API."""
headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
current_cookie = random.choice(cookies_list)
url = f"https://vinous.com/api/v2/wine/{lwin11}/livex_price_history"
response = scraper.get(url, headers=headers, cookies=current_cookie)
if response.status_code == 200:
try:
data = response.json()
if "error" in data:
return False # Vin non disponible
return True # Vin disponible
except:
return False # Erreur JSON
return False # Code erreur autre que 200
# Sélection du vin par l'utilisateur
st.sidebar.title("Filtres")
# 🟢 Sélection de la région
selected_region = st.sidebar.selectbox("Sélectionnez une région :", df_wines["REGION"].dropna().unique())
# 🟢 Filtrer les vins selon la région sélectionnée
filtered_wines = df_wines[df_wines["REGION"] == selected_region]
# 🟢 Sélection **multiple** de vins parmi les vins de la région
selected_wines = st.sidebar.multiselect("Sélectionnez un ou plusieurs vins :", filtered_wines["DISPLAY_NAME"].unique())
# 🟢 Filtrer les millésimes selon les vins sélectionnés
if selected_wines:
filtered_vintages = sorted(range(1925, 2026), reverse=True)
selected_vintages = st.sidebar.multiselect("Sélectionnez un ou plusieurs millésimes :", filtered_vintages)
else:
selected_vintages = []
# 🟢 Générer les LWIN11 pour chaque combinaison vin-millésime
wine_selections = []
for selected_wine in selected_wines:
wine_data = filtered_wines[filtered_wines["DISPLAY_NAME"] == selected_wine].iloc[0]
lwin7 = wine_data["LWIN"]
for vintage in selected_vintages:
lwin11 = f"{lwin7}{vintage}"
wine_selections.append((selected_wine, vintage, lwin11))
# 🟢 Vérifier la disponibilité des vins/millésimes avant ajout au graphique
available_wines = []
for wine_name, vintage, lwin11 in wine_selections:
if check_wine_availability(lwin11): # Vérifie si le vin/millésime est dispo via API
available_wines.append((wine_name, vintage, lwin11))
else:
st.sidebar.warning(f"⚠️ {wine_name} ({vintage}) n'est pas disponible.")
# 🟢 Bouton pour ajouter au graphique les vins/millésimes disponibles (évite les doublons)
if available_wines and st.sidebar.button("Ajouter au graphique"):
for wine in available_wines:
if wine not in st.session_state.selected_wines: # Ajout uniquement si non présent
st.session_state.selected_wines.append(wine)
# 🟢 Bouton pour réinitialiser le graphique
if st.sidebar.button("Reset graphique"):
st.session_state.selected_wines = []
# Récupération et affichage des prix
if st.session_state.selected_wines:
fig = px.line(
title="Évolution des Prix",
color_discrete_sequence=px.colors.qualitative.Safe
)
# Récupérer le taux de change une seule fois
exchange_rate = get_exchange_rate()
all_dates = [] # Stocke toutes les dates pour fixer l'axe X
for wine_name, vintage, lwin11 in st.session_state.selected_wines:
df_prices = get_vinous_data(lwin11, wine_name)
if not df_prices.empty:
df_prices = convert_price_per_bottle(df_prices, exchange_rate)
all_dates.extend(df_prices["date_string"]) # Ajoute toutes les dates au pool global
df_prices["tooltip"] = df_prices.apply(
lambda row: f"<b>Prix:</b> {row['price_per_bottle']:.2f} EUR<br><b>Date:</b> {row['date_string']}",
axis=1
)
last_price = df_prices["price_per_bottle"].iloc[-1]
fig.add_scatter(
x=df_prices["date_string"],
y=df_prices["price_per_bottle"],
mode="lines+markers",
name=f"{wine_name} ({vintage}) - {last_price:.2f} EUR",
text=df_prices["tooltip"],
hoverinfo="text"
)
# Récupérer la plage complète des dates pour fixer l'axe X
if all_dates:
min_date = min(all_dates)
max_date = max(all_dates)
fig.update_layout(
height=600,
legend=dict(
orientation="h",
yanchor="bottom", y=-0.3,
xanchor="center", x=0.5
),
xaxis=dict(
tickangle=45,
tickmode="array",
tickvals=pd.date_range(
start=min_date,
end=max_date,
freq='Q'
).strftime('%Y-%m').tolist(),
tickformat="%Y-%m",
range=[min_date, max_date] # Fixe la plage de l'axe X
),
margin=dict(l=20, r=20, t=50, b=100),
)
st.plotly_chart(fig, use_container_width=True)