Spaces:
Runtime error
Runtime error
version 1 de mon apli
Browse files- .gitattributes +1 -0
- __pycache__/config.cpython-310.pyc +0 -0
- __pycache__/donnees.cpython-310.pyc +0 -0
- __pycache__/fonction.cpython-310.pyc +0 -0
- __pycache__/fonction_recom.cpython-310.pyc +0 -0
- app.py +143 -0
- config.py +1 -0
- data.pkl +3 -0
- donnee_binaire.csv +0 -0
- donnees.py +7 -0
- fonction.py +140 -0
- notes.txt +1 -0
- requirements.txt +66 -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 |
+
data.pkl filter=lfs diff=lfs merge=lfs -text
|
__pycache__/config.cpython-310.pyc
ADDED
Binary file (168 Bytes). View file
|
|
__pycache__/donnees.cpython-310.pyc
ADDED
Binary file (294 Bytes). View file
|
|
__pycache__/fonction.cpython-310.pyc
ADDED
Binary file (5.4 kB). View file
|
|
__pycache__/fonction_recom.cpython-310.pyc
ADDED
Binary file (2.18 kB). View file
|
|
app.py
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import pandas as pd
|
3 |
+
from fonction import prediction, prediction_embe
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
import os
|
6 |
+
import config
|
7 |
+
|
8 |
+
from donnees import data_parfum, data_binaire
|
9 |
+
|
10 |
+
|
11 |
+
|
12 |
+
model = config.model
|
13 |
+
st.set_page_config(layout="wide")
|
14 |
+
|
15 |
+
st.title('Bienvenue sur AKIA PARFUM !')
|
16 |
+
|
17 |
+
##################################################################################################
|
18 |
+
# Barre latérale pour les entrées
|
19 |
+
with st.sidebar:
|
20 |
+
st.markdown("<h1>SAISISSEZ VOS INFORMATIONS</h1>", unsafe_allow_html=True)
|
21 |
+
|
22 |
+
nombre = st.number_input(label='Nombre de parfums souhaités :', step=1, format='%d')
|
23 |
+
nombre = int(nombre)
|
24 |
+
|
25 |
+
|
26 |
+
parfums_prefere = st.multiselect(
|
27 |
+
"Sélectionnez vos parfums préférés.",
|
28 |
+
list(data_binaire.index)
|
29 |
+
)
|
30 |
+
|
31 |
+
|
32 |
+
details_rechercher = st.text_input("Décrivez le parfum de vos rêves.")
|
33 |
+
|
34 |
+
parfums_detestes = st.multiselect(
|
35 |
+
"Sélectionnez des parfums que vous n'aimez pas.",
|
36 |
+
list(data_binaire.index)
|
37 |
+
)
|
38 |
+
|
39 |
+
|
40 |
+
|
41 |
+
if parfums_detestes:
|
42 |
+
liste_deteste = list(prediction(parfums_detestes, data_binaire)[0:10].index)
|
43 |
+
liste_deteste.extend(parfums_detestes)
|
44 |
+
######################################################################################################
|
45 |
+
# Diviser la page en deux colonnes
|
46 |
+
col1, col2 = st.columns([1, 4])
|
47 |
+
|
48 |
+
# Dans la colonne (col2), afficher les résultats
|
49 |
+
with col2:
|
50 |
+
st.markdown("<h3>NOS SUGGESTIONS</h3>", unsafe_allow_html=True)
|
51 |
+
parfums_suggeres = None
|
52 |
+
p = 0 #la variable p sera utiliser dans la suite du code, elle permet de pouvoir noter les parfums apres les recommandation
|
53 |
+
|
54 |
+
####################################################################
|
55 |
+
#EVALUATION DES DEUX MODELS ET PREDICTIONS
|
56 |
+
|
57 |
+
if parfums_prefere:
|
58 |
+
prediction_caracteristique = prediction(parfums_prefere, data_binaire)
|
59 |
+
|
60 |
+
if details_rechercher:
|
61 |
+
prediction_desciption = prediction_embe(details_rechercher, data_parfum, model)
|
62 |
+
|
63 |
+
|
64 |
+
|
65 |
+
|
66 |
+
####################################################################
|
67 |
+
#ELEMENTS A AFFICHES
|
68 |
+
|
69 |
+
#####CAS 1 : Liste de parfums favoris et description du parfum de reve
|
70 |
+
if parfums_prefere and details_rechercher:
|
71 |
+
data = pd.concat([prediction_caracteristique, prediction_desciption])
|
72 |
+
predictions = data.groupby(level=0)['probabilite'].mean()
|
73 |
+
predictions = predictions.drop(parfums_prefere)
|
74 |
+
|
75 |
+
if parfums_detestes:
|
76 |
+
liste_deteste = list(prediction(parfums_detestes, data_binaire)[0:10].index)
|
77 |
+
liste_deteste.extend(parfums_detestes)
|
78 |
+
predictions = predictions.drop(liste_deteste)
|
79 |
+
|
80 |
+
predictions = predictions.sort_values(ascending=False)
|
81 |
+
parfums_suggeres = predictions[0:nombre]
|
82 |
+
p = 1
|
83 |
+
st.write(parfums_suggeres)
|
84 |
+
|
85 |
+
|
86 |
+
##### CAS 2 : description du parfum de reve
|
87 |
+
if not parfums_prefere and details_rechercher:
|
88 |
+
if parfums_detestes:
|
89 |
+
prediction_desciption = prediction_desciption.drop(liste_deteste)
|
90 |
+
parfums_suggeres = prediction_desciption[0:nombre]
|
91 |
+
p = 1
|
92 |
+
st.write(parfums_suggeres)
|
93 |
+
|
94 |
+
|
95 |
+
##### CAS 3 : Liste de parfums favoris
|
96 |
+
if not details_rechercher and parfums_prefere:
|
97 |
+
if parfums_detestes:
|
98 |
+
prediction_caracteristique = prediction_caracteristique.drop(liste_deteste)
|
99 |
+
parfums_suggeres = prediction_caracteristique[0:nombre]
|
100 |
+
p = 1
|
101 |
+
st.write(parfums_suggeres)
|
102 |
+
if not details_rechercher and not parfums_prefere:
|
103 |
+
st.write('Bienvenue ! Veuillez choisir les parfums que vous aimez ou décrire le parfum de vos rêves.')
|
104 |
+
|
105 |
+
|
106 |
+
#############################################################################################
|
107 |
+
|
108 |
+
### NOTEZ NOS SUGGESTIONS
|
109 |
+
liste = []
|
110 |
+
description_ = ''
|
111 |
+
|
112 |
+
with st.sidebar:
|
113 |
+
st.markdown("<h1>Notez nos suggestions sur 10</h1>", unsafe_allow_html=True)
|
114 |
+
if p == 1:
|
115 |
+
form_counter = 0
|
116 |
+
form_counter += 1
|
117 |
+
with st.form(key=f'rating_form_{form_counter}'):
|
118 |
+
parfum = st.selectbox("Choisir un parfum :", list(parfums_suggeres.index))
|
119 |
+
note = st.number_input("Entrez une note :", min_value=1, max_value=10)
|
120 |
+
nom = st.text_input("Entrer votre nom.")
|
121 |
+
if parfum:
|
122 |
+
if parfums_prefere :
|
123 |
+
liste = parfums_prefere
|
124 |
+
if not details_rechercher:
|
125 |
+
proba = parfums_suggeres.loc[parfum].values[0]
|
126 |
+
|
127 |
+
if details_rechercher:
|
128 |
+
description_ = details_rechercher
|
129 |
+
if not parfums_prefere:
|
130 |
+
proba = parfums_suggeres.loc[parfum].values[0]
|
131 |
+
|
132 |
+
#proba = parfums_suggeres.loc[parfum].values[0]
|
133 |
+
|
134 |
+
if details_rechercher and parfums_prefere:
|
135 |
+
proba = parfums_suggeres.loc[parfum]
|
136 |
+
|
137 |
+
submitted = st.form_submit_button("Enregistrer la note")
|
138 |
+
if submitted:
|
139 |
+
with open("notes.txt", "a") as file:
|
140 |
+
file.write(f"{nom},{liste},{description_},{parfum},{proba},{note}\n")
|
141 |
+
st.success("Note enregistrée avec succès !")
|
142 |
+
else:
|
143 |
+
st.write('En notant nos recommandations, vous contribuez à améliorer la qualité des suggestions qui vous sont proposées.')
|
config.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
model = 'models/embedding-001'
|
data.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:7b93fd6100ccde27a1e140293dede6b22b24694fc2cffbdc42348ec4e576ba9a
|
3 |
+
size 19730237
|
donnee_binaire.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
donnees.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
|
3 |
+
data_parfum = pd.read_pickle("data.pkl") #Donner des description et des vecteurs de embedding
|
4 |
+
|
5 |
+
data_binaire = pd.read_csv('donnee_binaire.csv', index_col='nom_parfum') ## donnee des carracteristiques avec des 0 et 1
|
6 |
+
|
7 |
+
|
fonction.py
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
import numpy as np
|
3 |
+
from scipy.spatial.distance import pdist, squareform
|
4 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
5 |
+
import os
|
6 |
+
import google.generativeai as genai
|
7 |
+
import config
|
8 |
+
from dotenv import load_dotenv
|
9 |
+
|
10 |
+
load_dotenv()
|
11 |
+
|
12 |
+
|
13 |
+
|
14 |
+
def prediction(liste_parfums, df):
|
15 |
+
|
16 |
+
"""
|
17 |
+
Cette fonction effectue une prédiction de recommandation de parfums basée sur la similarité de Jaccard.
|
18 |
+
|
19 |
+
Elle prend en entrée une liste de parfums aimés par un utilisateur,
|
20 |
+
calcule un nouveau profil utilisateur en agrégeant les embeddings correspondants à ces parfums,
|
21 |
+
puis calcule la similarité de Jaccard entre ce nouveau profil utilisateur et les autres profils de parfums.
|
22 |
+
Enfin, elle retourne les parfums recommandés triés par ordre décroissant de similarité de Jaccard.
|
23 |
+
|
24 |
+
Args:
|
25 |
+
liste_parfums (list): La liste des parfums aimés par l'utilisateur.
|
26 |
+
df (pandas.DataFrame): Le DataFrame contenant les embeddings des parfums.
|
27 |
+
|
28 |
+
Returns:
|
29 |
+
pandas.DataFrame: Un DataFrame contenant les parfums recommandés et leur probabilité de similarité.
|
30 |
+
|
31 |
+
Raises:
|
32 |
+
ValueError: Si la liste des parfums est vide ou si le DataFrame des embeddings est vide.
|
33 |
+
KeyError: Si la colonne 'Embeddings' n'est pas présente dans le DataFrame.
|
34 |
+
"""
|
35 |
+
|
36 |
+
new_user = np.zeros(df.shape[1]) # Initialisation d'une série de zéros pour un nouveau profil utilisateur
|
37 |
+
|
38 |
+
# Calcul du nouveau profil utilisateur
|
39 |
+
for parfum in liste_parfums:
|
40 |
+
new_user += df.loc[parfum]
|
41 |
+
new_user[new_user > 1] = 1
|
42 |
+
df.loc['new_user'] = new_user
|
43 |
+
|
44 |
+
|
45 |
+
# Calcul de la similarité de Jaccard
|
46 |
+
jaccard_distances = pdist(df.values, metric='jaccard')
|
47 |
+
jaccard_similarity_array = 1 - squareform(jaccard_distances)
|
48 |
+
jaccard_similarity_df = pd.DataFrame(jaccard_similarity_array, index=df.index, columns=df.index)
|
49 |
+
|
50 |
+
# Récupération des similarités pour le nouvel utilisateur
|
51 |
+
jaccard_similarity_series = jaccard_similarity_df.loc['new_user']
|
52 |
+
|
53 |
+
# Suppression des parfums d'entrée
|
54 |
+
jaccard_similarity_series = jaccard_similarity_series.drop(list(liste_parfums))
|
55 |
+
|
56 |
+
# Trier les valeurs de similarité de la plus élevée à la plus basse
|
57 |
+
ordered_similarities = jaccard_similarity_series.sort_values(ascending=False)
|
58 |
+
|
59 |
+
# Supprimer le profil utilisateur après la prédiction
|
60 |
+
df = df.drop(index='new_user')
|
61 |
+
|
62 |
+
dict_of_dicts = ordered_similarities.to_dict()
|
63 |
+
data = pd.DataFrame(list(dict_of_dicts.values()), index=list(dict_of_dicts.keys()), columns=['probabilite'])
|
64 |
+
return data.iloc[1:]
|
65 |
+
|
66 |
+
|
67 |
+
|
68 |
+
|
69 |
+
|
70 |
+
|
71 |
+
|
72 |
+
ma_cle = os.getenv('gemini_key')
|
73 |
+
genai.configure(api_key=ma_cle)
|
74 |
+
model = config.model
|
75 |
+
|
76 |
+
def calcul_emb(description_utilisateur, model):
|
77 |
+
"""
|
78 |
+
Calcule l'embedding d'une description utilisateur en utilisant un modèle spécifié.
|
79 |
+
Args:
|
80 |
+
description_utilisateur (str): La description de l'utilisateur pour laquelle l'embedding doit être calculé.
|
81 |
+
model (str): Le modèle utilisé pour calculer l'embedding.
|
82 |
+
Returns:
|
83 |
+
numpy.ndarray: L'embedding de la description utilisateur, représenté sous forme de tableau NumPy.
|
84 |
+
|
85 |
+
Raises:
|
86 |
+
ValueError: Si le modèle spécifié n'est pas valide ou si la description utilisateur est vide.
|
87 |
+
"""
|
88 |
+
|
89 |
+
embedding = genai.embed_content(model=model,
|
90 |
+
content=description_utilisateur,
|
91 |
+
task_type="retrieval_document")
|
92 |
+
return np.array(embedding["embedding"]).reshape(1, -1)
|
93 |
+
|
94 |
+
|
95 |
+
|
96 |
+
|
97 |
+
def calcul_similarite(embedding_utilisateur, embedding):
|
98 |
+
"""
|
99 |
+
Calcule la similarité cosinus entre deux vecteurs d'embeddings.
|
100 |
+
Args:
|
101 |
+
embedding_utilisateur (numpy.ndarray): L'embedding de la desciption de l'utilisateur, représenté sous forme de tableau NumPy.
|
102 |
+
embedding (numpy.ndarray): L'embedding à comparer avec l'embedding de l'utilisateur, représenté sous forme de tableau NumPy.
|
103 |
+
Returns:
|
104 |
+
float: La similarité cosinus entre les deux embeddings.
|
105 |
+
|
106 |
+
Raises:
|
107 |
+
ValueError: Si l'une des embeddings n'est pas valide ou si elles n'ont pas la même dimension.
|
108 |
+
"""
|
109 |
+
embedding = np.array(embedding).reshape(1, -1) # Convertir en tableau NumPy avant de remodeler
|
110 |
+
return cosine_similarity(embedding, np.array(embedding_utilisateur).reshape(1, -1))[0][0]
|
111 |
+
|
112 |
+
|
113 |
+
|
114 |
+
def prediction_embe(description_utilisateur, df, model):
|
115 |
+
"""
|
116 |
+
Cette fonction effectue une prédiction basée sur l'embedding de la description utilisateur.
|
117 |
+
|
118 |
+
Elle calcule l'embedding de la description utilisateur en utilisant le modèle spécifié,
|
119 |
+
puis calcule la similarité cosinus entre cet embedding et les embeddings stockés dans le DataFrame.
|
120 |
+
Enfin, elle classe les résultats par ordre décroissant de similarité.
|
121 |
+
|
122 |
+
Args:
|
123 |
+
description_utilisateur (str): La description de l'utilisateur pour laquelle la prédiction doit être effectuée.
|
124 |
+
df (pandas.DataFrame): Le DataFrame contenant les embeddings des parfums et d'autres informations.
|
125 |
+
model (str): Le modèle utilisé pour calculer l'embedding de la description utilisateur.
|
126 |
+
|
127 |
+
Returns:
|
128 |
+
pandas.DataFrame: Un DataFrame contenant les noms des parfums et les probabilités de similarité, classés par ordre décroissant de similarité.
|
129 |
+
|
130 |
+
Raises:
|
131 |
+
ValueError: Si le modèle spécifié n'est pas valide ou si la description utilisateur est vide.
|
132 |
+
KeyError: Si la colonne 'Embeddings' n'est pas présente dans le DataFrame.
|
133 |
+
"""
|
134 |
+
vecteur_utilisateur = calcul_emb(description_utilisateur, model)
|
135 |
+
df['probabilite'] = df['Embeddings'].map(lambda emb: calcul_similarite(emb, vecteur_utilisateur))
|
136 |
+
data = df[['nom_parfum', 'probabilite']]
|
137 |
+
data.set_index('nom_parfum', inplace=True)
|
138 |
+
data = data.sort_values(by='probabilite', ascending=False)
|
139 |
+
return data
|
140 |
+
|
notes.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
|
requirements.txt
ADDED
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
altair==5.3.0
|
2 |
+
annotated-types==0.6.0
|
3 |
+
attrs==23.2.0
|
4 |
+
blinker==1.8.2
|
5 |
+
cachetools==5.3.3
|
6 |
+
certifi==2024.2.2
|
7 |
+
charset-normalizer==3.3.2
|
8 |
+
click==8.1.7
|
9 |
+
gitdb==4.0.11
|
10 |
+
GitPython==3.1.43
|
11 |
+
google-ai-generativelanguage==0.6.3
|
12 |
+
google-api-core==2.19.0
|
13 |
+
google-api-python-client==2.129.0
|
14 |
+
google-auth==2.29.0
|
15 |
+
google-auth-httplib2==0.2.0
|
16 |
+
google-generativeai==0.5.3
|
17 |
+
googleapis-common-protos==1.63.0
|
18 |
+
grpcio==1.63.0
|
19 |
+
grpcio-status==1.62.2
|
20 |
+
httplib2==0.22.0
|
21 |
+
idna==3.7
|
22 |
+
Jinja2==3.1.4
|
23 |
+
joblib==1.4.2
|
24 |
+
jsonschema==4.22.0
|
25 |
+
jsonschema-specifications==2023.12.1
|
26 |
+
markdown-it-py==3.0.0
|
27 |
+
MarkupSafe==2.1.5
|
28 |
+
mdurl==0.1.2
|
29 |
+
numpy==1.26.4
|
30 |
+
packaging==24.0
|
31 |
+
pandas==2.2.2
|
32 |
+
pillow==10.3.0
|
33 |
+
proto-plus==1.23.0
|
34 |
+
protobuf==4.25.3
|
35 |
+
pyarrow==16.1.0
|
36 |
+
pyasn1==0.6.0
|
37 |
+
pyasn1_modules==0.4.0
|
38 |
+
pydantic==2.7.1
|
39 |
+
pydantic_core==2.18.2
|
40 |
+
pydeck==0.9.1
|
41 |
+
Pygments==2.18.0
|
42 |
+
pyparsing==3.1.2
|
43 |
+
python-dateutil==2.9.0.post0
|
44 |
+
python-dotenv==1.0.1
|
45 |
+
pytz==2024.1
|
46 |
+
referencing==0.35.1
|
47 |
+
requests==2.31.0
|
48 |
+
rich==13.7.1
|
49 |
+
rpds-py==0.18.1
|
50 |
+
rsa==4.9
|
51 |
+
scikit-learn==1.4.2
|
52 |
+
scipy==1.13.0
|
53 |
+
six==1.16.0
|
54 |
+
smmap==5.0.1
|
55 |
+
streamlit==1.34.0
|
56 |
+
tenacity==8.3.0
|
57 |
+
threadpoolctl==3.5.0
|
58 |
+
toml==0.10.2
|
59 |
+
toolz==0.12.1
|
60 |
+
tornado==6.4
|
61 |
+
tqdm==4.66.4
|
62 |
+
typing_extensions==4.11.0
|
63 |
+
tzdata==2024.1
|
64 |
+
uritemplate==4.1.1
|
65 |
+
urllib3==2.2.1
|
66 |
+
watchdog==4.0.0
|