kahoot-to-anki 1.0.0__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kahoot_to_anki/__init__.py +1 -1
- kahoot_to_anki/cli.py +127 -0
- kahoot_to_anki/main.py +40 -0
- kahoot_to_anki/processing.py +143 -0
- kahoot_to_anki-1.2.0.dist-info/METADATA +79 -0
- kahoot_to_anki-1.2.0.dist-info/RECORD +13 -0
- kahoot_to_anki-1.2.0.dist-info/entry_points.txt +2 -0
- tests/test_cli.py +111 -0
- tests/test_processing.py +340 -0
- kahoot_to_anki/converter.py +0 -254
- kahoot_to_anki-1.0.0.dist-info/METADATA +0 -16
- kahoot_to_anki-1.0.0.dist-info/RECORD +0 -10
- kahoot_to_anki-1.0.0.dist-info/entry_points.txt +0 -2
- tests/test_project.py +0 -226
- {kahoot_to_anki-1.0.0.dist-info → kahoot_to_anki-1.2.0.dist-info}/WHEEL +0 -0
- {kahoot_to_anki-1.0.0.dist-info → kahoot_to_anki-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {kahoot_to_anki-1.0.0.dist-info → kahoot_to_anki-1.2.0.dist-info}/top_level.txt +0 -0
tests/test_processing.py
ADDED
@@ -0,0 +1,340 @@
|
|
1
|
+
import logging
|
2
|
+
import zipfile
|
3
|
+
|
4
|
+
import pandas as pd
|
5
|
+
|
6
|
+
from kahoot_to_anki.processing import get_questions, get_excels, get_excel_data, df_processing, make_anki
|
7
|
+
|
8
|
+
logging.basicConfig(level=logging.DEBUG)
|
9
|
+
|
10
|
+
KAHOOT_SHEET_NAME = "RawReportData Data"
|
11
|
+
|
12
|
+
def write_excel(df: pd.DataFrame, tmp_path, filename="sample.xlsx", sheet_name=KAHOOT_SHEET_NAME):
|
13
|
+
path = tmp_path / filename
|
14
|
+
df.to_excel(path, sheet_name=sheet_name, index=False)
|
15
|
+
return path
|
16
|
+
|
17
|
+
# --- get_questions ---
|
18
|
+
def test_get_questions_single_file(tmp_path):
|
19
|
+
"""Test processing a single valid Kahoot Excel file."""
|
20
|
+
|
21
|
+
df = pd.DataFrame({
|
22
|
+
"Question Number": [1],
|
23
|
+
"Question": ["What is 2+2?"],
|
24
|
+
"Answer 1": ["4"],
|
25
|
+
"Answer 2": ["3"],
|
26
|
+
"Answer 3": [""],
|
27
|
+
"Answer 4": [""],
|
28
|
+
"Answer 5": [""],
|
29
|
+
"Answer 6": [""],
|
30
|
+
"Correct Answers": ["4"]
|
31
|
+
})
|
32
|
+
|
33
|
+
excel_file = write_excel(df, tmp_path)
|
34
|
+
result_df = get_questions(input_directory=str(excel_file), sheet_name=KAHOOT_SHEET_NAME)
|
35
|
+
|
36
|
+
# Assertions
|
37
|
+
assert not result_df.empty
|
38
|
+
assert result_df.shape[0] == 1
|
39
|
+
assert "Question" in result_df.columns
|
40
|
+
assert result_df.iloc[0]["Question"] == "What is 2+2?"
|
41
|
+
expected_columns = ["Question", "Possible Answers", "Correct Answers"]
|
42
|
+
assert list(result_df.columns) == expected_columns
|
43
|
+
|
44
|
+
def test_get_questions_deduplicates_questions(tmp_path):
|
45
|
+
"""Test processing a single valid Kahoot Excel file with duplicated questions."""
|
46
|
+
|
47
|
+
df1 = pd.DataFrame({
|
48
|
+
"Question Number": [1],
|
49
|
+
"Question": ["What is 2+2?"],
|
50
|
+
"Answer 1": ["4"],
|
51
|
+
"Answer 2": ["3"],
|
52
|
+
"Answer 3": [""],
|
53
|
+
"Answer 4": [""],
|
54
|
+
"Answer 5": [""],
|
55
|
+
"Answer 6": [""],
|
56
|
+
"Correct Answers": ["4"]
|
57
|
+
})
|
58
|
+
|
59
|
+
df2 = df1.copy()
|
60
|
+
df = pd.concat([df1, df2], ignore_index=True)
|
61
|
+
|
62
|
+
write_excel(df, tmp_path)
|
63
|
+
result_df = get_questions(input_directory=str(tmp_path), sheet_name=KAHOOT_SHEET_NAME)
|
64
|
+
|
65
|
+
# Assertions
|
66
|
+
assert result_df.shape[0] == 1
|
67
|
+
|
68
|
+
|
69
|
+
def test_get_questions_handles_numeric_answers(tmp_path):
|
70
|
+
"""Test processing a single valid Kahoot Excel file with mixed data types."""
|
71
|
+
|
72
|
+
df = pd.DataFrame({
|
73
|
+
"Question Number": [1],
|
74
|
+
"Question": ["What is 2+2?"],
|
75
|
+
"Answer 1": ["2"],
|
76
|
+
"Answer 2": [4],
|
77
|
+
"Answer 3": ["6"],
|
78
|
+
"Answer 4": [""],
|
79
|
+
"Answer 5": [""],
|
80
|
+
"Answer 6": [""],
|
81
|
+
"Correct Answers": ["4"]
|
82
|
+
})
|
83
|
+
|
84
|
+
write_excel(df, tmp_path)
|
85
|
+
result_df = get_questions(input_directory=str(tmp_path), sheet_name=KAHOOT_SHEET_NAME)
|
86
|
+
|
87
|
+
assert result_df.shape[0] == 1
|
88
|
+
assert "4" in result_df.iloc[0]["Possible Answers"]
|
89
|
+
|
90
|
+
|
91
|
+
def test_get_questions_returns_empty_for_empty_file(tmp_path):
|
92
|
+
"""Test that an empty Excel file returns an empty DataFrame."""
|
93
|
+
df = pd.DataFrame(columns=[
|
94
|
+
"Question Number", "Question", "Answer 1", "Answer 2", "Answer 3",
|
95
|
+
"Answer 4", "Answer 5", "Answer 6", "Correct Answers"
|
96
|
+
])
|
97
|
+
write_excel(df, tmp_path)
|
98
|
+
result_df = get_questions(input_directory=str(tmp_path), sheet_name=KAHOOT_SHEET_NAME)
|
99
|
+
assert result_df.empty
|
100
|
+
|
101
|
+
|
102
|
+
def test_get_questions_merges_multiple_files(tmp_path):
|
103
|
+
df1 = pd.DataFrame({
|
104
|
+
"Question Number": [1],
|
105
|
+
"Question": ["What is 2+2?"],
|
106
|
+
"Answer 1": ["4"],
|
107
|
+
"Answer 2": ["3"],
|
108
|
+
"Answer 3": [""],
|
109
|
+
"Answer 4": [""],
|
110
|
+
"Answer 5": [""],
|
111
|
+
"Answer 6": [""],
|
112
|
+
"Correct Answers": ["4"]
|
113
|
+
})
|
114
|
+
|
115
|
+
df2 = pd.DataFrame({
|
116
|
+
"Question Number": [2],
|
117
|
+
"Question": ["What is the capital of France?"],
|
118
|
+
"Answer 1": ["Berlin"],
|
119
|
+
"Answer 2": ["Paris"],
|
120
|
+
"Answer 3": ["Madrid"],
|
121
|
+
"Answer 4": [""],
|
122
|
+
"Answer 5": [""],
|
123
|
+
"Answer 6": [""],
|
124
|
+
"Correct Answers": ["Paris"]
|
125
|
+
})
|
126
|
+
|
127
|
+
# Write both files into the same temp directory
|
128
|
+
write_excel(df1, tmp_path, filename="quiz1.xlsx")
|
129
|
+
write_excel(df2, tmp_path, filename="quiz2.xlsx")
|
130
|
+
|
131
|
+
result_df = get_questions(input_directory=str(tmp_path), sheet_name=KAHOOT_SHEET_NAME)
|
132
|
+
|
133
|
+
assert result_df.shape[0] == 2
|
134
|
+
assert "What is 2+2?" in result_df["Question"].values
|
135
|
+
assert "What is the capital of France?" in result_df["Question"].values
|
136
|
+
|
137
|
+
|
138
|
+
def test_get_questions_skips_invalid_sheets(tmp_path):
|
139
|
+
df1 = pd.DataFrame({
|
140
|
+
"Question Number": [1],
|
141
|
+
"Question": ["What is 2+2?"],
|
142
|
+
"Answer 1": ["4"],
|
143
|
+
"Answer 2": ["3"],
|
144
|
+
"Answer 3": [""],
|
145
|
+
"Answer 4": [""],
|
146
|
+
"Answer 5": [""],
|
147
|
+
"Answer 6": [""],
|
148
|
+
"Correct Answers": ["4"]
|
149
|
+
})
|
150
|
+
|
151
|
+
df2 = pd.DataFrame({
|
152
|
+
"Question Number": [2],
|
153
|
+
"Question": ["What is the capital of France?"],
|
154
|
+
"Answer 1": ["Berlin"],
|
155
|
+
"Answer 2": ["Paris"],
|
156
|
+
"Answer 3": ["Madrid"],
|
157
|
+
"Answer 4": [""],
|
158
|
+
"Answer 5": [""],
|
159
|
+
"Answer 6": [""],
|
160
|
+
"Correct Answers": ["Paris"]
|
161
|
+
})
|
162
|
+
|
163
|
+
df3 = pd.DataFrame({
|
164
|
+
"Question Number": [2],
|
165
|
+
"Question": ["What is the capital of Spain?"],
|
166
|
+
"Answer 1": ["Berlin"],
|
167
|
+
"Answer 2": ["Paris"],
|
168
|
+
"Answer 3": ["Madrid"],
|
169
|
+
"Answer 4": [""],
|
170
|
+
"Answer 5": [""],
|
171
|
+
"Answer 6": [""],
|
172
|
+
"Correct Answers": ["Madrid"]
|
173
|
+
})
|
174
|
+
|
175
|
+
# Write both files into the same temp directory
|
176
|
+
write_excel(df1, tmp_path, filename="quiz1.xlsx")
|
177
|
+
write_excel(df2, tmp_path, filename="quiz2.xlsx")
|
178
|
+
write_excel(df3, tmp_path, filename="quiz3.xlsx", sheet_name="TEST")
|
179
|
+
|
180
|
+
result_df = get_questions(input_directory=str(tmp_path), sheet_name=KAHOOT_SHEET_NAME)
|
181
|
+
|
182
|
+
assert result_df.shape[0] == 2
|
183
|
+
assert "What is 2+2?" in result_df["Question"].values
|
184
|
+
assert "What is the capital of France?" in result_df["Question"].values
|
185
|
+
|
186
|
+
|
187
|
+
# --- get_excels ---
|
188
|
+
def test_get_excels_single_file(tmp_path):
|
189
|
+
"""Test that get_excels yields a single file when given a single .xlsx file path."""
|
190
|
+
file = tmp_path / "single.xlsx"
|
191
|
+
file.write_text("dummy content")
|
192
|
+
|
193
|
+
result = list(get_excels(str(file)))
|
194
|
+
|
195
|
+
assert len(result) == 1
|
196
|
+
assert result[0].endswith("single.xlsx")
|
197
|
+
|
198
|
+
|
199
|
+
def test_get_excels_multiple_files_in_directory(tmp_path):
|
200
|
+
"""Test that get_excels yields all .xlsx files in a directory."""
|
201
|
+
file1 = tmp_path / "file1.xlsx"
|
202
|
+
file2 = tmp_path / "file2.xlsx"
|
203
|
+
file3 = tmp_path / "file3.csv" # should be ignored
|
204
|
+
|
205
|
+
for f in [file1, file2]:
|
206
|
+
f.write_text("dummy content")
|
207
|
+
file3.write_text("should be ignored")
|
208
|
+
|
209
|
+
result = list(get_excels(str(tmp_path)))
|
210
|
+
|
211
|
+
assert len(result) == 2
|
212
|
+
assert all(f.endswith(".xlsx") for f in result)
|
213
|
+
assert str(file3) not in result
|
214
|
+
|
215
|
+
|
216
|
+
# --- get_excel_data ---
|
217
|
+
def test_get_excel_data_valid(tmp_path):
|
218
|
+
"""Test reading a valid Excel file with correct sheet name."""
|
219
|
+
df = pd.DataFrame({"Question": ["Q1"], "Correct Answers": ["A1"]})
|
220
|
+
path = tmp_path / "valid.xlsx"
|
221
|
+
df.to_excel(path, sheet_name=KAHOOT_SHEET_NAME, index=False)
|
222
|
+
|
223
|
+
result = get_excel_data(str(path), sheet_name=KAHOOT_SHEET_NAME)
|
224
|
+
|
225
|
+
assert isinstance(result, pd.DataFrame)
|
226
|
+
assert not result.empty
|
227
|
+
assert "Question" in result.columns
|
228
|
+
|
229
|
+
def test_get_excel_data_missing_sheet(tmp_path, caplog):
|
230
|
+
"""Test behavior when the specified sheet name does not exist."""
|
231
|
+
df = pd.DataFrame({"Question": ["Q1"]})
|
232
|
+
path = tmp_path / "missing_sheet.xlsx"
|
233
|
+
df.to_excel(path, sheet_name="WrongSheet", index=False)
|
234
|
+
|
235
|
+
with caplog.at_level(logging.WARNING):
|
236
|
+
result = get_excel_data(str(path), sheet_name=KAHOOT_SHEET_NAME)
|
237
|
+
|
238
|
+
assert result is None
|
239
|
+
assert "Skipping file" in caplog.text
|
240
|
+
|
241
|
+
def test_get_excel_data_invalid_file(tmp_path, caplog):
|
242
|
+
"""Test behavior when trying to read a corrupted Excel file."""
|
243
|
+
path = tmp_path / "fake.xlsx"
|
244
|
+
path.write_text("This is not a real Excel file.")
|
245
|
+
|
246
|
+
with caplog.at_level(logging.WARNING):
|
247
|
+
result = get_excel_data(str(path), sheet_name=KAHOOT_SHEET_NAME)
|
248
|
+
|
249
|
+
assert result is None
|
250
|
+
assert "Skipping file" in caplog.text
|
251
|
+
|
252
|
+
|
253
|
+
# --- df_processing ---
|
254
|
+
def test_df_processing_empty():
|
255
|
+
df = pd.DataFrame(columns=[
|
256
|
+
"Question Number", "Question", "Answer 1", "Answer 2", "Answer 3",
|
257
|
+
"Answer 4", "Answer 5", "Answer 6", "Correct Answers"
|
258
|
+
])
|
259
|
+
result = df_processing(df)
|
260
|
+
|
261
|
+
assert result.empty
|
262
|
+
assert list(result.columns) == ["Question", "Possible Answers", "Correct Answers"]
|
263
|
+
|
264
|
+
|
265
|
+
def test_df_processing_normal_case():
|
266
|
+
df = pd.DataFrame({
|
267
|
+
"Question Number": [1],
|
268
|
+
"Question": ["What is 2+2?"],
|
269
|
+
"Answer 1": ["2"],
|
270
|
+
"Answer 2": ["4"],
|
271
|
+
"Answer 3": ["3"],
|
272
|
+
"Answer 4": [""],
|
273
|
+
"Answer 5": [""],
|
274
|
+
"Answer 6": [""],
|
275
|
+
"Correct Answers": ["4"]
|
276
|
+
})
|
277
|
+
result = df_processing(df)
|
278
|
+
|
279
|
+
assert result.shape[0] == 1
|
280
|
+
assert "What is 2+2?" in result["Question"].values
|
281
|
+
assert "4" in result["Possible Answers"].iloc[0]
|
282
|
+
|
283
|
+
|
284
|
+
def test_df_processing_duplicate_question_number():
|
285
|
+
df = pd.DataFrame({
|
286
|
+
"Question Number": [1, 1],
|
287
|
+
"Question": ["What is 2+2?", "What is 2+2?"],
|
288
|
+
"Answer 1": ["2", "2"],
|
289
|
+
"Answer 2": ["4", "4"],
|
290
|
+
"Answer 3": ["3", "3"],
|
291
|
+
"Answer 4": ["", ""],
|
292
|
+
"Answer 5": ["", ""],
|
293
|
+
"Answer 6": ["", ""],
|
294
|
+
"Correct Answers": ["4", "4"]
|
295
|
+
})
|
296
|
+
result = df_processing(df)
|
297
|
+
|
298
|
+
assert result.shape[0] == 1
|
299
|
+
|
300
|
+
|
301
|
+
def test_df_processing_mixed_types():
|
302
|
+
df = pd.DataFrame({
|
303
|
+
"Question Number": [1],
|
304
|
+
"Question": ["How many continents?"],
|
305
|
+
"Answer 1": ["7"],
|
306
|
+
"Answer 2": [6], # int type
|
307
|
+
"Answer 3": [""],
|
308
|
+
"Answer 4": [None],
|
309
|
+
"Answer 5": ["Five"],
|
310
|
+
"Answer 6": [""],
|
311
|
+
"Correct Answers": ["7"]
|
312
|
+
})
|
313
|
+
result = df_processing(df)
|
314
|
+
|
315
|
+
assert result.shape[0] == 1
|
316
|
+
assert "6" in result["Possible Answers"].iloc[0] # check that int was converted
|
317
|
+
assert "None" not in result["Possible Answers"].iloc[0] # check fillna
|
318
|
+
|
319
|
+
|
320
|
+
# --- make_anki ---
|
321
|
+
def test_make_anki_creates_apkg(tmp_path):
|
322
|
+
"""Test that make_anki creates a valid .apkg file."""
|
323
|
+
|
324
|
+
# Prepare test data
|
325
|
+
df = pd.DataFrame({
|
326
|
+
"Question": ["What is 2+2?"],
|
327
|
+
"Possible Answers": ["2<br>3<br>4"],
|
328
|
+
"Correct Answers": ["4"]
|
329
|
+
})
|
330
|
+
|
331
|
+
# Run the function
|
332
|
+
make_anki(df=df, out=str(tmp_path), title="Test Deck")
|
333
|
+
|
334
|
+
# Check output file exists
|
335
|
+
output_file = tmp_path / "anki.apkg"
|
336
|
+
assert output_file.exists()
|
337
|
+
assert output_file.stat().st_size > 0
|
338
|
+
|
339
|
+
# Check that it's a valid ZIP file (as .apkg is zip format internally)
|
340
|
+
assert zipfile.is_zipfile(output_file)
|
kahoot_to_anki/converter.py
DELETED
@@ -1,254 +0,0 @@
|
|
1
|
-
# Standard library imports
|
2
|
-
import argparse
|
3
|
-
import logging
|
4
|
-
import os
|
5
|
-
import glob
|
6
|
-
|
7
|
-
# Third-party library imports
|
8
|
-
import genanki
|
9
|
-
import pandas as pd
|
10
|
-
|
11
|
-
# Configure logging settings
|
12
|
-
logging.basicConfig(level=logging.INFO)
|
13
|
-
|
14
|
-
# Constants
|
15
|
-
DEFAULT_INPUT_DIRECTORY = "./data"
|
16
|
-
DEFAULT_OUTPUT_DIRECTORY = "./"
|
17
|
-
DEFAULT_DECK_TITLE = "Kahoot"
|
18
|
-
KAHOOT_EXCEL_SHEET_NAME_RAW_DATA = "RawReportData Data"
|
19
|
-
|
20
|
-
|
21
|
-
def main():
|
22
|
-
# Check command line arguments
|
23
|
-
inp, out, csv, title = get_commandline_arguments()
|
24
|
-
|
25
|
-
validation(inp, out)
|
26
|
-
|
27
|
-
df = get_questions(inp)
|
28
|
-
|
29
|
-
if csv:
|
30
|
-
df.to_csv(
|
31
|
-
os.path.join(out, "kahoot.csv"),
|
32
|
-
sep=";",
|
33
|
-
index=False,
|
34
|
-
encoding="utf-8-sig",
|
35
|
-
)
|
36
|
-
|
37
|
-
make_anki(df, out, title)
|
38
|
-
|
39
|
-
|
40
|
-
def get_commandline_arguments() -> tuple:
|
41
|
-
"""
|
42
|
-
Parses the command line arguments and returns a tuple with the input path, output path, CSV option, and deck title.
|
43
|
-
|
44
|
-
:return: A tuple with the input path, the output path, the csv and the title of the anki deck
|
45
|
-
:rtype: tuple
|
46
|
-
"""
|
47
|
-
parser = argparse.ArgumentParser(description="Create Anki Deck from Kahoot answer")
|
48
|
-
parser.add_argument(
|
49
|
-
"-i",
|
50
|
-
"--inp",
|
51
|
-
default=DEFAULT_INPUT_DIRECTORY,
|
52
|
-
help=f"Path to the directory containing input Excel files or a single input Excel file. If a directory is "
|
53
|
-
f"provided, all Excel files in the directory will be processed. Default: {DEFAULT_INPUT_DIRECTORY}",
|
54
|
-
type=str,
|
55
|
-
)
|
56
|
-
parser.add_argument(
|
57
|
-
"-o",
|
58
|
-
"--out",
|
59
|
-
default=DEFAULT_OUTPUT_DIRECTORY,
|
60
|
-
help="Path to the directory where the Anki flashcards package will be generated. "
|
61
|
-
"If not specified, the package will be created in the current working directory.",
|
62
|
-
type=str,
|
63
|
-
)
|
64
|
-
parser.add_argument(
|
65
|
-
"--csv",
|
66
|
-
action="store_true",
|
67
|
-
help="Generate a CSV file with the question data.",
|
68
|
-
)
|
69
|
-
parser.add_argument(
|
70
|
-
"-t",
|
71
|
-
"--title",
|
72
|
-
default=DEFAULT_DECK_TITLE,
|
73
|
-
help="Name of the Anki deck to be created. "
|
74
|
-
f"If not specified, the default deck name '{DEFAULT_DECK_TITLE}' will be used.",
|
75
|
-
type=str,
|
76
|
-
)
|
77
|
-
args = parser.parse_args()
|
78
|
-
|
79
|
-
# return absolute paths
|
80
|
-
return os.path.abspath(args.inp), os.path.abspath(args.out), args.csv, args.title
|
81
|
-
|
82
|
-
|
83
|
-
def validation(input_directory: str, output_directory: str) -> None:
|
84
|
-
"""
|
85
|
-
This function validates the command line arguments, checking if the input path is a valid Excel file or directory
|
86
|
-
and if the output path is a valid directory.
|
87
|
-
The input path needs to be an Excel file or a directory that contains Excel files.
|
88
|
-
The output path needs to be a directory and not a file.
|
89
|
-
|
90
|
-
:param input_directory: The path of the input Excel or directory
|
91
|
-
:param output_directory: The path of the output directory
|
92
|
-
:return: None
|
93
|
-
:rtype: None
|
94
|
-
"""
|
95
|
-
# Check if input is a file
|
96
|
-
if not os.path.exists(input_directory):
|
97
|
-
logging.error(f"Input directory {input_directory} does not exist!")
|
98
|
-
raise FileNotFoundError(f"Input directory {input_directory} does not exist!")
|
99
|
-
elif (
|
100
|
-
os.path.isfile(input_directory)
|
101
|
-
and os.path.splitext(input_directory)[-1] != ".xlsx"
|
102
|
-
):
|
103
|
-
logging.error("Input file is not an excel file!")
|
104
|
-
raise ValueError("Input file is not an excel file!")
|
105
|
-
elif os.path.isdir(input_directory):
|
106
|
-
input_excels = os.path.join(input_directory, "*.xlsx")
|
107
|
-
if not glob.glob(input_excels):
|
108
|
-
logging.error("Input directory does not contain any excel files!")
|
109
|
-
raise FileNotFoundError("Input directory does not contain any excel files!")
|
110
|
-
|
111
|
-
# Check output directory and create when not existing
|
112
|
-
if not os.path.isdir(output_directory):
|
113
|
-
logging.error("Output is not a directory!")
|
114
|
-
raise ValueError("Output is not a directory!")
|
115
|
-
if not os.path.exists(output_directory):
|
116
|
-
try:
|
117
|
-
os.makedirs(output_directory)
|
118
|
-
except OSError as e:
|
119
|
-
logging.error(
|
120
|
-
"Failed to create output directory '%s': %s", output_directory, str(e)
|
121
|
-
)
|
122
|
-
raise
|
123
|
-
|
124
|
-
|
125
|
-
def get_questions(input_directory: str) -> pd.DataFrame:
|
126
|
-
"""
|
127
|
-
Extracts all the kahoot questions out of the Excel file(s)
|
128
|
-
|
129
|
-
:param input_directory: The path to the input directory or Excel file
|
130
|
-
:return: All the questions with the possible answers and the solution
|
131
|
-
:rtype: pd.DataFrame
|
132
|
-
"""
|
133
|
-
|
134
|
-
def get_excels(path):
|
135
|
-
"""
|
136
|
-
Returns a list with all Excels in the given path
|
137
|
-
:param path: the path to an Excel file or a directory with Excel files
|
138
|
-
:return: a list with all excels
|
139
|
-
"""
|
140
|
-
if os.path.isfile(path):
|
141
|
-
yield path
|
142
|
-
else:
|
143
|
-
yield from glob.glob(os.path.join(input_directory, "*.xlsx"))
|
144
|
-
return [f for f in glob.glob(os.path.join(path, "*.xlsx"))]
|
145
|
-
|
146
|
-
def get_excel_data(excel_file: str) -> pd.DataFrame:
|
147
|
-
"""
|
148
|
-
Returns a pd.DataFrame with the kahoot raw data
|
149
|
-
:param excel_file: an Excel file with Kahoot raw data
|
150
|
-
:return: a DataFrame with the data
|
151
|
-
"""
|
152
|
-
try:
|
153
|
-
# read file
|
154
|
-
return pd.read_excel(
|
155
|
-
excel_file, sheet_name=KAHOOT_EXCEL_SHEET_NAME_RAW_DATA
|
156
|
-
)
|
157
|
-
except ValueError:
|
158
|
-
logging.warning(
|
159
|
-
"Skipping file '%s' as it is not a valid Excel file.", excel_file
|
160
|
-
)
|
161
|
-
return None
|
162
|
-
except Exception as e:
|
163
|
-
logging.error("Failed to read file '%s': %s", excel_file, str(e))
|
164
|
-
return None
|
165
|
-
|
166
|
-
def df_processing(data: pd.DataFrame) -> pd.DataFrame:
|
167
|
-
"""
|
168
|
-
Processes the Kahoot question data.
|
169
|
-
:param data: DataFrame with Kahoot question data
|
170
|
-
:return: Processed DataFrame
|
171
|
-
"""
|
172
|
-
# delete duplicated questions
|
173
|
-
data = data.drop_duplicates(subset=["Question Number"])
|
174
|
-
|
175
|
-
data = data.fillna("")
|
176
|
-
|
177
|
-
data["Possible Answers"] = data[
|
178
|
-
["Answer 1", "Answer 2", "Answer 3", "Answer 4", "Answer 5", "Answer 6"]
|
179
|
-
].agg("<br>".join, axis=1)
|
180
|
-
|
181
|
-
# keep only needed columns
|
182
|
-
data = data[["Question", "Possible Answers", "Correct Answers"]]
|
183
|
-
|
184
|
-
return data
|
185
|
-
|
186
|
-
out = pd.DataFrame(columns=["Question", "Possible Answers", "Correct Answers"])
|
187
|
-
|
188
|
-
questions_cnt = 0
|
189
|
-
files_cnt = 0
|
190
|
-
|
191
|
-
for file in get_excels(input_directory):
|
192
|
-
df = get_excel_data(file)
|
193
|
-
if df is None:
|
194
|
-
continue
|
195
|
-
files_cnt += 1
|
196
|
-
|
197
|
-
df = df_processing(df)
|
198
|
-
|
199
|
-
# add to out dataframe
|
200
|
-
out = pd.concat([out, df], axis=0, ignore_index=True)
|
201
|
-
|
202
|
-
questions_cnt += len(df)
|
203
|
-
|
204
|
-
logging.info("Read input files: %d", files_cnt)
|
205
|
-
logging.info("Read questions: %d", questions_cnt)
|
206
|
-
out = out.drop_duplicates(subset=["Question"])
|
207
|
-
return out
|
208
|
-
|
209
|
-
|
210
|
-
def make_anki(df: pd.DataFrame, out: str, title: str) -> None:
|
211
|
-
"""
|
212
|
-
Creates an Anki deck from the given Kahoot questions
|
213
|
-
|
214
|
-
:param df: The kahoot questions in a pd.DataFrame
|
215
|
-
:param out: The path to the output directory
|
216
|
-
:param title: The title of the Anki deck
|
217
|
-
:return: None
|
218
|
-
"""
|
219
|
-
my_model = genanki.Model(
|
220
|
-
1607392319,
|
221
|
-
"Simple Model",
|
222
|
-
fields=[
|
223
|
-
{"name": "Question"},
|
224
|
-
{"name": "Answer"},
|
225
|
-
{"name": "selects"},
|
226
|
-
],
|
227
|
-
templates=[
|
228
|
-
{
|
229
|
-
"name": "Card 1",
|
230
|
-
"qfmt": "{{Question}}<br><br>{{selects}}",
|
231
|
-
"afmt": '{{FrontSide}}<hr id="answer">{{Answer}}',
|
232
|
-
},
|
233
|
-
],
|
234
|
-
)
|
235
|
-
|
236
|
-
my_deck = genanki.Deck(2059400110, title)
|
237
|
-
|
238
|
-
for index, row in df.iterrows():
|
239
|
-
my_note = genanki.Note(
|
240
|
-
model=my_model,
|
241
|
-
fields=[row["Question"], row["Correct Answers"], row["Possible Answers"]],
|
242
|
-
)
|
243
|
-
my_deck.add_note(my_note)
|
244
|
-
|
245
|
-
try:
|
246
|
-
genanki.Package(my_deck).write_to_file(
|
247
|
-
os.path.join(out, "anki.apkg"),
|
248
|
-
)
|
249
|
-
except Exception as e:
|
250
|
-
logging.error("Failed to write Anki package file: %s", str(e))
|
251
|
-
|
252
|
-
|
253
|
-
if __name__ == "__main__":
|
254
|
-
main()
|
@@ -1,16 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: kahoot-to-anki
|
3
|
-
Version: 1.0.0
|
4
|
-
Summary: CLI tool to convert Kahoot quiz reports into Anki flashcards
|
5
|
-
Author: Simon Hardmeier
|
6
|
-
License-Expression: MIT
|
7
|
-
Requires-Python: >=3.8
|
8
|
-
Description-Content-Type: text/markdown
|
9
|
-
License-File: LICENSE
|
10
|
-
Requires-Dist: genanki
|
11
|
-
Requires-Dist: pandas
|
12
|
-
Requires-Dist: openpyxl
|
13
|
-
Dynamic: license-file
|
14
|
-
|
15
|
-
# kahoot-to-anki
|
16
|
-
> python cli program to convert Kahoot quiz results to Anki flashcards
|
@@ -1,10 +0,0 @@
|
|
1
|
-
kahoot_to_anki/__init__.py,sha256=pquCLjCONKW_xGgL78_TyYqF9PQs7Ws2Tb5KeKCOPE4,67
|
2
|
-
kahoot_to_anki/converter.py,sha256=hBjm4A8zcZeXK4dnXr7jojt-ntPaBdwJq68Nq_TYiko,8387
|
3
|
-
kahoot_to_anki-1.0.0.dist-info/licenses/LICENSE,sha256=HxPBlT4sSfEgRBrX0jZd8WTfM0c31VFgnLaCEWzGMZc,1122
|
4
|
-
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
tests/test_project.py,sha256=nGwU_Lys2QKJu3VrAnZiP1NS4ydEdkkqWPKQTCOZl8k,5355
|
6
|
-
kahoot_to_anki-1.0.0.dist-info/METADATA,sha256=PCRUoo8tOnnuttRuZLv6o90T7YiLICaBjLTqc-26LaQ,457
|
7
|
-
kahoot_to_anki-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
-
kahoot_to_anki-1.0.0.dist-info/entry_points.txt,sha256=Hp06_kNd2MJ0-ShO7KOI4TBreq0fqIVwx_a4i42C-sU,65
|
9
|
-
kahoot_to_anki-1.0.0.dist-info/top_level.txt,sha256=aTMCk83rMZjWFZ556EHLIxVOgEawIWsMZzRpR3IPQ1w,21
|
10
|
-
kahoot_to_anki-1.0.0.dist-info/RECORD,,
|