nsaintsever commited on
Commit
950504b
·
verified ·
1 Parent(s): 96be0bb

Upload 3 files

Browse files
Files changed (4) hide show
  1. .gitattributes +1 -0
  2. LWINdatabase.xlsx +3 -0
  3. app.py +262 -0
  4. requirements.txt +9 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ LWINdatabase.xlsx filter=lfs diff=lfs merge=lfs -text
LWINdatabase.xlsx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d561772266eb61bfd35713ad0cd52ee5d978f585cf9717411c2fec7342bccdeb
3
+ size 4379782
app.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import cloudscraper
3
+ import pandas as pd
4
+ import plotly.express as px
5
+ from bs4 import BeautifulSoup
6
+ import random
7
+ import time
8
+
9
+ # Définition des cookies disponibles
10
+ cookies_list = [
11
+ {'_vinous_session': "BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiJWQ4MDUyOTllZGNkZDE5YTUyZWE3OTU0NzZjMTRjMDk1BjsAVEkiGXdhcmRlbi51c2VyLnVzZXIua2V5BjsAVFsHWwZpAq5mSSIiJDJhJDEwJFdTaU9uamkydDBNN3RqMG01YXRJVS4GOwBUSSIdd2FyZGVuLnVzZXIudXNlci5zZXNzaW9uBjsAVHsGSSIUbGFzdF9yZXF1ZXN0X2F0BjsAVGwrB5mlAGg%3D--7777dea1a4299409509b5af6636777dd9f84f0b7"},
12
+ {'_vinous_session': "BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiJTAwOTQzOTk1NjdhZjc0YzQ5NzJmZTI4OTVjOWFkZTJkBjsAVEkiGXdhcmRlbi51c2VyLnVzZXIua2V5BjsAVFsHWwZpAls6SSIiJDJhJDEwJEF2VWJZdU1vRVYubmh4WEw0Y1pSbmUGOwBUSSIdd2FyZGVuLnVzZXIudXNlci5zZXNzaW9uBjsAVHsGSSIUbGFzdF9yZXF1ZXN0X2F0BjsAVGwrB3CqAGg%3D--d83482ea4c6c2a043c93ee64546c4cddeec2304f"}
13
+ ]
14
+
15
+ # Sélection d'un cookie unique à utiliser pour tous les appels
16
+ selected_cookie = random.choice(cookies_list)
17
+
18
+ # Définition d'une liste de headers possibles
19
+ headers_list = [
20
+ {
21
+ "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",
22
+ "Accept": "application/json, text/plain, */*",
23
+ "Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
24
+ "Sec-Fetch-Dest": "empty",
25
+ "Sec-Fetch-Mode": "cors",
26
+ "Sec-Fetch-Site": "same-origin"
27
+ }
28
+ ]
29
+
30
+ # Sélection d'un header unique à utiliser
31
+ selected_header = random.choice(headers_list)
32
+
33
+ # Création de l'instance cloudscraper et application du cookie sélectionné
34
+ scraper = cloudscraper.create_scraper()
35
+ scraper.cookies.update(selected_cookie)
36
+
37
+ # Configuration de l'interface Streamlit
38
+ st.set_page_config(page_title="Wine prices", layout="wide")
39
+
40
+ # Initialisation du session state
41
+ if "selected_wines" not in st.session_state:
42
+ st.session_state.selected_wines = []
43
+
44
+ dark_mode_css = """
45
+ <style>
46
+ body {
47
+ background-color: #0e1117;
48
+ color: white;
49
+ }
50
+ .stApp {
51
+ background-color: #0e1117;
52
+ }
53
+ .stSidebar {
54
+ background-color: #161a25;
55
+ }
56
+ .stTextInput, .stSelectbox, .stButton, .stDataFrame, .stTable {
57
+ background-color: #21262d;
58
+ color: white;
59
+ }
60
+ .stPlotlyChart {
61
+ background-color: transparent;
62
+ }
63
+ </style>
64
+ """
65
+ st.markdown(dark_mode_css, unsafe_allow_html=True)
66
+
67
+ # Chargement du fichier Excel contenant la correspondance LWIN7 -> Nom du vin
68
+ @st.cache_data
69
+ def load_wine_data():
70
+ return pd.read_excel("LWINdatabase.xlsx") # Assurez-vous d'avoir ce fichier avec les colonnes LWIN, DISPLAY_NAME, etc.
71
+
72
+ df_wines = load_wine_data()
73
+
74
+ # Sélection du vin par l'utilisateur
75
+ def select_wine():
76
+ return st.sidebar.selectbox("Sélectionnez un vin :", df_wines["DISPLAY_NAME"].unique())
77
+
78
+ # Génération du LWIN11 (LWIN7 + millésime)
79
+ def generate_lwin11(lwin7, vintage):
80
+ return str(lwin7) + str(vintage)
81
+
82
+ # Récupération du taux de change GBP/EUR depuis Boursorama
83
+ def get_exchange_rate():
84
+ url = "https://www.boursorama.com/bourse/devises/taux-de-change-livresterling-euro-GBP-EUR/"
85
+ 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"})
86
+
87
+ if response.status_code == 200:
88
+ soup = BeautifulSoup(response.text, "html.parser")
89
+ rate_tag = soup.find("span", class_="c-instrument c-instrument--last")
90
+
91
+ if rate_tag:
92
+ rate = rate_tag.text.replace(",", ".")
93
+ return float(rate)
94
+
95
+ st.error("Impossible de récupérer le taux de change GBP/EUR.")
96
+ return 1.2 # Valeur par défaut
97
+
98
+ # Fonction unique qui vérifie la disponibilité et récupère les données
99
+ def get_vinous_data(lwin11, display_name):
100
+ url_prices = f"https://vinous.com/api/v2/wine/{lwin11}/livex_price_history?currency=USD"
101
+ response = scraper.get(url_prices, headers=selected_header)
102
+ time.sleep(1.5)
103
+
104
+ st.write("Réponse brute de l'API :", response.json()) # Pour debug temporaire
105
+
106
+ if response.status_code == 200:
107
+ try:
108
+ data = response.json()
109
+ if "error" in data or not data.get("livex_price_history"):
110
+ st.warning(f"🚨 Malheureusement, le vin **{display_name} ({lwin11})** n'est pas disponible.")
111
+ return pd.DataFrame()
112
+ else:
113
+ data_prices = data.get("livex_price_history", [])
114
+ df_prices = pd.DataFrame(data_prices)
115
+ return df_prices
116
+ except Exception as e:
117
+ st.error(f"⚠️ Erreur lors du traitement des données pour **{display_name} ({lwin11})**. Détails : {e}")
118
+ return pd.DataFrame()
119
+ else:
120
+ st.warning(f"🚨 Impossible de récupérer les prix pour **{display_name} ({lwin11})**. Code erreur: {response.status_code} - Détails : {response.content}")
121
+ return pd.DataFrame()
122
+
123
+
124
+
125
+
126
+ # Convertir les prix en EUR et diviser par 12 pour obtenir le prix par bouteille
127
+ def convert_price_per_bottle(df_prices, exchange_rate):
128
+ df_prices["date_string"] = pd.to_datetime(df_prices["date_string"], format="%Y-%m-%d")
129
+ df_prices["value"] = df_prices["value"].astype(float)
130
+ df_prices["price_per_bottle"] = (df_prices["value"] * exchange_rate) / 12
131
+ return df_prices
132
+
133
+ def check_wine_availability(lwin11):
134
+ """Teste la disponibilité d'un vin/millésime via un appel API."""
135
+ headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
136
+ current_cookie = random.choice(cookies_list)
137
+
138
+ url = f"https://vinous.com/api/v2/wine/{lwin11}/livex_price_history"
139
+ response = scraper.get(url, headers=headers, cookies=current_cookie)
140
+
141
+ if response.status_code == 200:
142
+ try:
143
+ data = response.json()
144
+ if "error" in data:
145
+ return False # Vin non disponible
146
+ return True # Vin disponible
147
+ except:
148
+ return False # Erreur JSON
149
+ return False # Code erreur autre que 200
150
+
151
+ # Sélection du vin par l'utilisateur
152
+ st.sidebar.title("Filtres")
153
+
154
+ # 🟢 Sélection de la région
155
+ selected_region = st.sidebar.selectbox("Sélectionnez une région :", df_wines["REGION"].dropna().unique())
156
+
157
+ # 🟢 Filtrer les vins selon la région sélectionnée
158
+ filtered_wines = df_wines[df_wines["REGION"] == selected_region]
159
+
160
+ # 🟢 Sélection **multiple** de vins parmi les vins de la région
161
+ selected_wines = st.sidebar.multiselect("Sélectionnez un ou plusieurs vins :", filtered_wines["DISPLAY_NAME"].unique())
162
+
163
+ # 🟢 Filtrer les millésimes selon les vins sélectionnés
164
+ if selected_wines:
165
+ filtered_vintages = sorted(range(1925, 2026), reverse=True)
166
+ selected_vintages = st.sidebar.multiselect("Sélectionnez un ou plusieurs millésimes :", filtered_vintages)
167
+ else:
168
+ selected_vintages = []
169
+
170
+ # 🟢 Générer les LWIN11 pour chaque combinaison vin-millésime
171
+ wine_selections = []
172
+ for selected_wine in selected_wines:
173
+ wine_data = filtered_wines[filtered_wines["DISPLAY_NAME"] == selected_wine].iloc[0]
174
+ lwin7 = wine_data["LWIN"]
175
+
176
+ for vintage in selected_vintages:
177
+ lwin11 = f"{lwin7}{vintage}"
178
+ wine_selections.append((selected_wine, vintage, lwin11))
179
+
180
+ # 🟢 Vérifier la disponibilité des vins/millésimes avant ajout au graphique
181
+ available_wines = []
182
+ for wine_name, vintage, lwin11 in wine_selections:
183
+ if check_wine_availability(lwin11): # Vérifie si le vin/millésime est dispo via API
184
+ available_wines.append((wine_name, vintage, lwin11))
185
+ else:
186
+ st.sidebar.warning(f"⚠️ {wine_name} ({vintage}) n'est pas disponible.")
187
+
188
+ # 🟢 Bouton pour ajouter au graphique les vins/millésimes disponibles (évite les doublons)
189
+ if available_wines and st.sidebar.button("Ajouter au graphique"):
190
+ for wine in available_wines:
191
+ if wine not in st.session_state.selected_wines: # Ajout uniquement si non présent
192
+ st.session_state.selected_wines.append(wine)
193
+
194
+ # 🟢 Bouton pour réinitialiser le graphique
195
+ if st.sidebar.button("Reset graphique"):
196
+ st.session_state.selected_wines = []
197
+
198
+
199
+
200
+
201
+ # Récupération et affichage des prix
202
+ if st.session_state.selected_wines:
203
+ fig = px.line(
204
+ title="Évolution des Prix",
205
+ color_discrete_sequence=px.colors.qualitative.Safe
206
+ )
207
+
208
+ # Récupérer le taux de change une seule fois
209
+ exchange_rate = get_exchange_rate()
210
+
211
+ all_dates = [] # Stocke toutes les dates pour fixer l'axe X
212
+
213
+ for wine_name, vintage, lwin11 in st.session_state.selected_wines:
214
+ df_prices = get_vinous_data(lwin11, wine_name)
215
+
216
+ if not df_prices.empty:
217
+ df_prices = convert_price_per_bottle(df_prices, exchange_rate)
218
+ all_dates.extend(df_prices["date_string"]) # Ajoute toutes les dates au pool global
219
+
220
+ df_prices["tooltip"] = df_prices.apply(
221
+ lambda row: f"<b>Prix:</b> {row['price_per_bottle']:.2f} EUR<br><b>Date:</b> {row['date_string']}",
222
+ axis=1
223
+ )
224
+
225
+ last_price = df_prices["price_per_bottle"].iloc[-1]
226
+
227
+ fig.add_scatter(
228
+ x=df_prices["date_string"],
229
+ y=df_prices["price_per_bottle"],
230
+ mode="lines+markers",
231
+ name=f"{wine_name} ({vintage}) - {last_price:.2f} EUR",
232
+ text=df_prices["tooltip"],
233
+ hoverinfo="text"
234
+ )
235
+
236
+ # Récupérer la plage complète des dates pour fixer l'axe X
237
+ if all_dates:
238
+ min_date = min(all_dates)
239
+ max_date = max(all_dates)
240
+
241
+ fig.update_layout(
242
+ height=600,
243
+ legend=dict(
244
+ orientation="h",
245
+ yanchor="bottom", y=-0.3,
246
+ xanchor="center", x=0.5
247
+ ),
248
+ xaxis=dict(
249
+ tickangle=45,
250
+ tickmode="array",
251
+ tickvals=pd.date_range(
252
+ start=min_date,
253
+ end=max_date,
254
+ freq='Q'
255
+ ).strftime('%Y-%m').tolist(),
256
+ tickformat="%Y-%m",
257
+ range=[min_date, max_date] # Fixe la plage de l'axe X
258
+ ),
259
+ margin=dict(l=20, r=20, t=50, b=100),
260
+ )
261
+
262
+ st.plotly_chart(fig, use_container_width=True)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.36.0
2
+ pandas==2.2.3
3
+ plotly==5.24.1
4
+ requests==2.32.3
5
+ beautifulsoup4==4.11.2
6
+ openpyxl==3.1.5
7
+ selenium==4.24.0
8
+ webdriver-manager==4.0.2
9
+ cloudscraper==1.2.71