-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.py
More file actions
207 lines (156 loc) · 6.27 KB
/
Copy pathutils.py
File metadata and controls
207 lines (156 loc) · 6.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
"""
utils.py — Fonctions utilitaires de SQLingo.
Ce module contient :
- Le chargement des exercices depuis exercises.yaml
- L'initialisation de la session SRS-like
- La vérification des requêtes utilisateur
- L'affichage des questions et tables
"""
import random
import pandas as pd
import streamlit as st
import yaml
from config import (
EXERCISES_PATH,
ANSWERS_PATH,
QUESTIONS_PATH,
SRS_INCORRECT_DELAY,
)
# ------------------------------------------------------------
# CHARGEMENT DES EXERCICES
# ------------------------------------------------------------
def load_exercises() -> list[dict]:
"""Charge la liste des exercices depuis exercises.yaml.
Returns:
list[dict]: liste de dicts avec les clés theme, name, tables, answer.
"""
with EXERCISES_PATH.open("r") as f:
return yaml.safe_load(f)
# ------------------------------------------------------------
# SESSION SRS-LIKE
# ------------------------------------------------------------
def init_session(themes: list[str] | None = None) -> None:
"""Initialise la session Streamlit avec une queue d'exercices mélangée.
Args:
themes: si fourni, filtre les exercices sur ces thèmes uniquement.
si None, tous les exercices sont inclus.
"""
exercises = load_exercises()
if themes:
exercises = [e for e in exercises if e["theme"] in themes]
random.shuffle(exercises)
st.session_state["queue"] = exercises
st.session_state["score"] = {"correct": 0, "incorrect": 0}
st.session_state["history"] = [] # liste de (exercise, "correct" | "incorrect")
st.session_state["session_started"] = True
def get_current_exercise() -> dict | None:
"""Retourne l'exercice en tête de queue, ou None si la session est terminée."""
queue = st.session_state.get("queue", [])
return queue[0] if queue else None
def record_result(exercise: dict, correct: bool) -> None:
"""Enregistre le résultat et repositionne l'exercice dans la queue.
- Bonne réponse → exercice retiré de la queue (ne revient pas)
- Mauvaise réponse → exercice réinséré à la position SRS_INCORRECT_DELAY
"""
queue = st.session_state["queue"]
result = "correct" if correct else "incorrect"
st.session_state["history"].append((exercise, result))
if correct:
st.session_state["score"]["correct"] += 1
queue.pop(0)
else:
st.session_state["score"]["incorrect"] += 1
queue.pop(0)
insert_at = min(SRS_INCORRECT_DELAY, len(queue))
queue.insert(insert_at, exercise)
# ------------------------------------------------------------
# VÉRIFICATION DE LA REQUÊTE UTILISATEUR
# ------------------------------------------------------------
def check_users_solution(con, solution_df: pd.DataFrame, user_query: str) -> bool:
"""Vérifie que la requête SQL de l'utilisateur produit le bon résultat.
La comparaison est insensible à l'ordre des lignes : les deux DataFrames
sont triés avant d'être comparés.
Args:
con: connexion DuckDB active
solution_df: DataFrame de référence (la solution attendue)
user_query: requête SQL saisie par l'utilisateur
Returns:
bool: True si la requête est correcte, False sinon.
"""
try:
result = con.execute(user_query).df()
st.dataframe(result)
# Colonnes
if list(result.columns) != list(solution_df.columns):
st.warning(
f"Colonnes incorrectes.\n\n"
f"**Attendu :** {list(solution_df.columns)}\n\n"
f"**Obtenu :** {list(result.columns)}"
)
return False
# Nombre de lignes
if len(result) != len(solution_df):
st.warning(
f"Nombre de lignes incorrect : "
f"ta requête retourne **{len(result)}** lignes, "
f"la solution en attend **{len(solution_df)}**."
)
return False
# Valeurs (order-insensitive)
result_sorted = result.sort_values(by=list(result.columns)).reset_index(
drop=True
)
solution_sorted = solution_df.sort_values(
by=list(solution_df.columns)
).reset_index(drop=True) # noqa: E501
if not result_sorted.equals(solution_sorted):
st.warning(
"Les colonnes et le nombre de lignes sont bons, mais certaines valeurs différent."
) # noqa: E501
return False
st.balloons()
return True
except Exception as e:
st.error(f"Erreur lors de l'exécution de ta requête : {e}")
return False
# ------------------------------------------------------------
# AFFICHAGE
# ------------------------------------------------------------
def show_question(exercise: dict) -> None:
"""Affiche l'énoncé markdown de l'exercice courant.
Args:
exercise: dict d'un exercice chargé depuis exercises.yaml
"""
theme = exercise["theme"]
answer = exercise["answer"]
question_file = QUESTIONS_PATH / theme / f"{answer[:-4]}.md"
try:
st.markdown(question_file.read_text())
except FileNotFoundError:
st.warning(f"Énoncé introuvable : `{question_file}`")
def show_tables(con, exercise: dict) -> None:
"""Affiche un aperçu des tables nécessaires à l'exercice.
Args:
con: connexion DuckDB active
exercise: dict d'un exercice chargé depuis exercises.yaml
"""
for table in exercise["tables"]:
st.markdown(f"**`{table}`**")
try:
df = con.execute(f"SELECT * FROM {table} LIMIT 5").df()
st.dataframe(df)
except Exception as e:
st.error(f"Impossible de charger la table `{table}` : {e}")
def load_solution(con, exercise: dict) -> pd.DataFrame:
"""Charge et exécute le fichier SQL solution, retourne le DataFrame résultat.
Args:
con: connexion DuckDB active
exercise: dict d'un exercice chargé depuis exercises.yaml
Returns:
pd.DataFrame: résultat de la requête solution.
Raises:
FileNotFoundError: si le fichier SQL solution est absent.
"""
answer_file = ANSWERS_PATH / exercise["theme"] / exercise["answer"]
sql = answer_file.read_text()
return con.execute(sql).df()