kahoot-to-anki 1.1.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 +9 -0
- kahoot_to_anki/processing.py +4 -2
- {kahoot_to_anki-1.1.0.dist-info → kahoot_to_anki-1.2.0.dist-info}/METADATA +2 -1
- kahoot_to_anki-1.2.0.dist-info/RECORD +13 -0
- tests/test_cli.py +111 -0
- tests/test_processing.py +340 -0
- kahoot_to_anki-1.1.0.dist-info/RECORD +0 -12
- tests/test_project.py +0 -226
- {kahoot_to_anki-1.1.0.dist-info → kahoot_to_anki-1.2.0.dist-info}/WHEEL +0 -0
- {kahoot_to_anki-1.1.0.dist-info → kahoot_to_anki-1.2.0.dist-info}/entry_points.txt +0 -0
- {kahoot_to_anki-1.1.0.dist-info → kahoot_to_anki-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {kahoot_to_anki-1.1.0.dist-info → kahoot_to_anki-1.2.0.dist-info}/top_level.txt +0 -0
kahoot_to_anki/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "1.
|
1
|
+
__version__ = "1.2.0"
|
kahoot_to_anki/cli.py
CHANGED
@@ -5,6 +5,9 @@ import logging
|
|
5
5
|
import os
|
6
6
|
import glob
|
7
7
|
|
8
|
+
from kahoot_to_anki import __version__
|
9
|
+
|
10
|
+
|
8
11
|
# Constants
|
9
12
|
DEFAULT_INPUT_DIRECTORY = "./data"
|
10
13
|
DEFAULT_OUTPUT_DIRECTORY = "./"
|
@@ -65,6 +68,12 @@ def get_commandline_arguments() -> CLIArgs:
|
|
65
68
|
f"If not specified, the default deck name '{DEFAULT_DECK_TITLE}' will be used.",
|
66
69
|
type=str,
|
67
70
|
)
|
71
|
+
parser.add_argument(
|
72
|
+
"--version",
|
73
|
+
action="version",
|
74
|
+
version=f"%(prog)s {__version__}",
|
75
|
+
help="Show the version number and exit.",
|
76
|
+
)
|
68
77
|
args = parser.parse_args()
|
69
78
|
|
70
79
|
return CLIArgs(
|
kahoot_to_anki/processing.py
CHANGED
@@ -83,14 +83,16 @@ def df_processing(data: pd.DataFrame) -> pd.DataFrame:
|
|
83
83
|
:param data: DataFrame with Kahoot question data
|
84
84
|
:return: Processed DataFrame
|
85
85
|
"""
|
86
|
+
if data.empty:
|
87
|
+
return pd.DataFrame(columns=["Question", "Possible Answers", "Correct Answers"])
|
88
|
+
|
86
89
|
# delete duplicated questions
|
87
90
|
data = data.drop_duplicates(subset=["Question Number"])
|
88
|
-
|
89
91
|
data = data.fillna("")
|
90
92
|
|
91
93
|
data["Possible Answers"] = data[
|
92
94
|
["Answer 1", "Answer 2", "Answer 3", "Answer 4", "Answer 5", "Answer 6"]
|
93
|
-
].agg("<br>".join, axis=1)
|
95
|
+
].astype(str).agg("<br>".join, axis=1)
|
94
96
|
|
95
97
|
# keep only needed columns
|
96
98
|
data = data[["Question", "Possible Answers", "Correct Answers"]]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: kahoot-to-anki
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
4
4
|
Summary: CLI tool to convert Kahoot quiz reports into Anki flashcards
|
5
5
|
Author: Simon Hardmeier
|
6
6
|
License-Expression: MIT
|
@@ -69,6 +69,7 @@ All valid Excel files in the directory will be processed.
|
|
69
69
|
| `--sheet` | The Excel Sheet with the raw Kahoot quiz data (default: `RawReportData Data`) |
|
70
70
|
| `--csv`, `--no-csv` | Enable or disable CSV export of the questions (default: disabled) |
|
71
71
|
| `-t`, `--title` | Title of the generated Anki deck (default: `"Kahoot"`) |
|
72
|
+
| `--version` | Show the version of the installed kahoot-to-anki package |
|
72
73
|
|
73
74
|
|
74
75
|
## Example
|
@@ -0,0 +1,13 @@
|
|
1
|
+
kahoot_to_anki/__init__.py,sha256=Btl_98iBIXFtvGx47MqpfbaEVYoOMPBQn9bao7UASkQ,21
|
2
|
+
kahoot_to_anki/cli.py,sha256=2tRiIcnbcrPXVPQ8mdeUOHXYP3LO9ExNfp3Uy1FSaFk,4484
|
3
|
+
kahoot_to_anki/main.py,sha256=oVmOLqXUnzy1Q0Xnn4Dzr65bFf8Jaw4vYRwvDTSd_ro,936
|
4
|
+
kahoot_to_anki/processing.py,sha256=Ku8nFhgWpprmYWdLL6f-T8P7J_SmMuQD2NLVCQQErCs,4346
|
5
|
+
kahoot_to_anki-1.2.0.dist-info/licenses/LICENSE,sha256=HxPBlT4sSfEgRBrX0jZd8WTfM0c31VFgnLaCEWzGMZc,1122
|
6
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
tests/test_cli.py,sha256=SL1WEJoA59-G7hRdWg5wOUyCPrjzZvsgPJxAZMPXpWg,3195
|
8
|
+
tests/test_processing.py,sha256=swLG2gkcwWm1ZG0T5u6izlhV-2AIsaMKdg174jp3za8,10951
|
9
|
+
kahoot_to_anki-1.2.0.dist-info/METADATA,sha256=I8iBo7bvvBQMyMticSKPHsILRASFMaB0VRi45EWhv3E,2953
|
10
|
+
kahoot_to_anki-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
11
|
+
kahoot_to_anki-1.2.0.dist-info/entry_points.txt,sha256=VqCk_PPVpGFgnGCBIlEnz8Y0qUqYV8J2tYNZZa8IRWA,60
|
12
|
+
kahoot_to_anki-1.2.0.dist-info/top_level.txt,sha256=aTMCk83rMZjWFZ556EHLIxVOgEawIWsMZzRpR3IPQ1w,21
|
13
|
+
kahoot_to_anki-1.2.0.dist-info/RECORD,,
|
tests/test_cli.py
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
import sys
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
from kahoot_to_anki.cli import get_commandline_arguments, validation
|
7
|
+
|
8
|
+
|
9
|
+
# --- get_commandline_arguments ---
|
10
|
+
def test_get_commandline_arguments(monkeypatch):
|
11
|
+
"""Test that CLI arguments are correctly parsed."""
|
12
|
+
|
13
|
+
test_args = [
|
14
|
+
"kahoot-to-anki",
|
15
|
+
"-i", "tests/data",
|
16
|
+
"-o", "output_dir",
|
17
|
+
"--csv",
|
18
|
+
"--sheet", "CustomSheet",
|
19
|
+
"--title", "My Deck"
|
20
|
+
]
|
21
|
+
|
22
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
23
|
+
|
24
|
+
args = get_commandline_arguments()
|
25
|
+
|
26
|
+
assert Path(args.input_path).name == "data"
|
27
|
+
assert Path(args.output_path).name == "output_dir"
|
28
|
+
assert args.export_csv is True
|
29
|
+
assert args.sheet == "CustomSheet"
|
30
|
+
assert args.deck_title == "My Deck"
|
31
|
+
|
32
|
+
|
33
|
+
def test_get_commandline_arguments_no_csv(monkeypatch):
|
34
|
+
"""Test that --no-csv CLI argument is correctly parsed."""
|
35
|
+
|
36
|
+
test_args = [
|
37
|
+
"kahoot-to-anki",
|
38
|
+
"--no-csv",
|
39
|
+
]
|
40
|
+
|
41
|
+
monkeypatch.setattr(sys, "argv", test_args)
|
42
|
+
args = get_commandline_arguments()
|
43
|
+
|
44
|
+
assert args.export_csv is False
|
45
|
+
|
46
|
+
|
47
|
+
# --- validation ---
|
48
|
+
def test_validation_valid_excel_file(tmp_path):
|
49
|
+
# Create dummy Excel file
|
50
|
+
excel_file = tmp_path / "valid.xlsx"
|
51
|
+
excel_file.write_text("Excel content")
|
52
|
+
|
53
|
+
# Create output directory
|
54
|
+
output_dir = tmp_path / "output"
|
55
|
+
output_dir.mkdir()
|
56
|
+
|
57
|
+
# Should not raise
|
58
|
+
validation(str(excel_file), str(output_dir))
|
59
|
+
|
60
|
+
|
61
|
+
def test_validation_valid_input_directory_with_excel(tmp_path):
|
62
|
+
input_dir = tmp_path / "input"
|
63
|
+
input_dir.mkdir()
|
64
|
+
(input_dir / "file1.xlsx").write_text("Excel content")
|
65
|
+
|
66
|
+
output_dir = tmp_path / "output"
|
67
|
+
output_dir.mkdir()
|
68
|
+
|
69
|
+
# Should not raise
|
70
|
+
validation(str(input_dir), str(output_dir))
|
71
|
+
|
72
|
+
|
73
|
+
def test_validation_input_path_does_not_exist(tmp_path):
|
74
|
+
output_dir = tmp_path / "output"
|
75
|
+
output_dir.mkdir()
|
76
|
+
|
77
|
+
with pytest.raises(FileNotFoundError):
|
78
|
+
validation(str(tmp_path / "missing.xlsx"), str(output_dir))
|
79
|
+
|
80
|
+
|
81
|
+
def test_validation_input_not_excel_file(tmp_path):
|
82
|
+
file = tmp_path / "file.txt"
|
83
|
+
file.write_text("Not Excel")
|
84
|
+
output_dir = tmp_path / "output"
|
85
|
+
output_dir.mkdir()
|
86
|
+
|
87
|
+
with pytest.raises(ValueError, match="Input file is not an excel file"):
|
88
|
+
validation(str(file), str(output_dir))
|
89
|
+
|
90
|
+
|
91
|
+
def test_validation_input_directory_no_excel_files(tmp_path):
|
92
|
+
input_dir = tmp_path / "input"
|
93
|
+
input_dir.mkdir()
|
94
|
+
(input_dir / "file.txt").write_text("Just text")
|
95
|
+
|
96
|
+
output_dir = tmp_path / "output"
|
97
|
+
output_dir.mkdir()
|
98
|
+
|
99
|
+
with pytest.raises(FileNotFoundError, match="does not contain any excel files"):
|
100
|
+
validation(str(input_dir), str(output_dir))
|
101
|
+
|
102
|
+
|
103
|
+
def test_validation_output_not_a_directory(tmp_path):
|
104
|
+
excel_file = tmp_path / "valid.xlsx"
|
105
|
+
excel_file.write_text("Excel content")
|
106
|
+
|
107
|
+
output_path = tmp_path / "output.txt"
|
108
|
+
output_path.write_text("Not a directory")
|
109
|
+
|
110
|
+
with pytest.raises(ValueError, match="Output is not a directory"):
|
111
|
+
validation(str(excel_file), str(output_path))
|
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)
|
@@ -1,12 +0,0 @@
|
|
1
|
-
kahoot_to_anki/__init__.py,sha256=0y91PW-fcytGzZxMWcdcVL1uY8YfJz4Qb4CVpB1hAXg,21
|
2
|
-
kahoot_to_anki/cli.py,sha256=MaG5527TI1fp4fVYdYGgg3P4eJiEW6wKZLsAjOPvQJA,4263
|
3
|
-
kahoot_to_anki/main.py,sha256=oVmOLqXUnzy1Q0Xnn4Dzr65bFf8Jaw4vYRwvDTSd_ro,936
|
4
|
-
kahoot_to_anki/processing.py,sha256=l-EuDOMp0NK-Uxj9M-VaN5_01W-YUmN-HN3ROM9M6yA,4220
|
5
|
-
kahoot_to_anki-1.1.0.dist-info/licenses/LICENSE,sha256=HxPBlT4sSfEgRBrX0jZd8WTfM0c31VFgnLaCEWzGMZc,1122
|
6
|
-
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
-
tests/test_project.py,sha256=nGwU_Lys2QKJu3VrAnZiP1NS4ydEdkkqWPKQTCOZl8k,5355
|
8
|
-
kahoot_to_anki-1.1.0.dist-info/METADATA,sha256=SUbPtw_U-YWaI0KJ7HnB6ekerE_7nYvNp919DV14U0k,2846
|
9
|
-
kahoot_to_anki-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
-
kahoot_to_anki-1.1.0.dist-info/entry_points.txt,sha256=VqCk_PPVpGFgnGCBIlEnz8Y0qUqYV8J2tYNZZa8IRWA,60
|
11
|
-
kahoot_to_anki-1.1.0.dist-info/top_level.txt,sha256=aTMCk83rMZjWFZ556EHLIxVOgEawIWsMZzRpR3IPQ1w,21
|
12
|
-
kahoot_to_anki-1.1.0.dist-info/RECORD,,
|
tests/test_project.py
DELETED
@@ -1,226 +0,0 @@
|
|
1
|
-
# Standard library imports
|
2
|
-
import shutil
|
3
|
-
import os
|
4
|
-
|
5
|
-
# Third-party library imports
|
6
|
-
import numpy as np
|
7
|
-
import pandas as pd
|
8
|
-
import pytest
|
9
|
-
|
10
|
-
# Local application/library imports
|
11
|
-
import converter
|
12
|
-
|
13
|
-
|
14
|
-
def generate_test_data():
|
15
|
-
# Create multiple lists
|
16
|
-
question_number = [
|
17
|
-
"Question 1",
|
18
|
-
"Question 1",
|
19
|
-
"Question 1",
|
20
|
-
"Question 2",
|
21
|
-
"Question 2",
|
22
|
-
"Question 2",
|
23
|
-
]
|
24
|
-
question = [
|
25
|
-
"How is a code block indicated in Python?",
|
26
|
-
"How is a code block indicated in Python?",
|
27
|
-
"How is a code block indicated in Python?",
|
28
|
-
"Which of the following concepts is not a part of Python?",
|
29
|
-
"Which of the following concepts is not a part of Python?",
|
30
|
-
"Which of the following concepts is not a part of Python?",
|
31
|
-
]
|
32
|
-
answer_1 = [
|
33
|
-
"Brackets",
|
34
|
-
"Brackets",
|
35
|
-
"Brackets",
|
36
|
-
"Pointers",
|
37
|
-
"Pointers",
|
38
|
-
"Pointers",
|
39
|
-
]
|
40
|
-
answer_2 = [
|
41
|
-
"Indentation",
|
42
|
-
"Indentation",
|
43
|
-
"Indentation",
|
44
|
-
"Loops",
|
45
|
-
"Loops",
|
46
|
-
"Loops",
|
47
|
-
]
|
48
|
-
answer_3 = [
|
49
|
-
"Key",
|
50
|
-
"Key",
|
51
|
-
"Key",
|
52
|
-
"Dynamic Typing",
|
53
|
-
"Dynamic Typing",
|
54
|
-
"Dynamic Typing",
|
55
|
-
]
|
56
|
-
answer_4 = [
|
57
|
-
np.nan,
|
58
|
-
np.nan,
|
59
|
-
np.nan,
|
60
|
-
np.nan,
|
61
|
-
np.nan,
|
62
|
-
np.nan,
|
63
|
-
]
|
64
|
-
answer_5 = [
|
65
|
-
np.nan,
|
66
|
-
np.nan,
|
67
|
-
np.nan,
|
68
|
-
np.nan,
|
69
|
-
np.nan,
|
70
|
-
np.nan,
|
71
|
-
]
|
72
|
-
answer_6 = [
|
73
|
-
np.nan,
|
74
|
-
np.nan,
|
75
|
-
np.nan,
|
76
|
-
np.nan,
|
77
|
-
np.nan,
|
78
|
-
np.nan,
|
79
|
-
]
|
80
|
-
correct_answers = [
|
81
|
-
"Indentation",
|
82
|
-
"Indentation",
|
83
|
-
"Indentation",
|
84
|
-
"Pointers",
|
85
|
-
"Pointers",
|
86
|
-
"Pointers",
|
87
|
-
]
|
88
|
-
time_to_answer = [
|
89
|
-
"10",
|
90
|
-
"10",
|
91
|
-
"10",
|
92
|
-
"10",
|
93
|
-
"10",
|
94
|
-
"10",
|
95
|
-
]
|
96
|
-
player = [
|
97
|
-
"Harry",
|
98
|
-
"Hermione ",
|
99
|
-
"Ron",
|
100
|
-
"Harry",
|
101
|
-
"Hermione ",
|
102
|
-
"Ron",
|
103
|
-
]
|
104
|
-
answer = [
|
105
|
-
"Indentation",
|
106
|
-
"Indentation",
|
107
|
-
"Brackets",
|
108
|
-
"Dynamic Typing",
|
109
|
-
"Pointers",
|
110
|
-
"Dynamic Typing",
|
111
|
-
]
|
112
|
-
correct_incorrect = [
|
113
|
-
True,
|
114
|
-
True,
|
115
|
-
False,
|
116
|
-
False,
|
117
|
-
True,
|
118
|
-
False,
|
119
|
-
]
|
120
|
-
columns = [
|
121
|
-
"Question Number",
|
122
|
-
"Question",
|
123
|
-
"Answer 1",
|
124
|
-
"Answer 2",
|
125
|
-
"Answer 3",
|
126
|
-
"Answer 4",
|
127
|
-
"Answer 5",
|
128
|
-
"Answer 6",
|
129
|
-
"Correct Answers",
|
130
|
-
"Time Allotted to Answer (seconds)",
|
131
|
-
"Player",
|
132
|
-
"Answer",
|
133
|
-
"Correct / Incorrect",
|
134
|
-
]
|
135
|
-
|
136
|
-
# Create DataFrame from multiple lists
|
137
|
-
df = pd.DataFrame(
|
138
|
-
list(
|
139
|
-
zip(
|
140
|
-
question_number,
|
141
|
-
question,
|
142
|
-
answer_1,
|
143
|
-
answer_2,
|
144
|
-
answer_3,
|
145
|
-
answer_4,
|
146
|
-
answer_5,
|
147
|
-
answer_6,
|
148
|
-
correct_answers,
|
149
|
-
time_to_answer,
|
150
|
-
player,
|
151
|
-
answer,
|
152
|
-
correct_incorrect,
|
153
|
-
)
|
154
|
-
),
|
155
|
-
columns=columns,
|
156
|
-
)
|
157
|
-
return df
|
158
|
-
|
159
|
-
|
160
|
-
@pytest.fixture(scope="session", autouse=True)
|
161
|
-
def setup_and_teardown():
|
162
|
-
test_data_dir = "test_data"
|
163
|
-
test_data_dir_empty = "test_data_empty"
|
164
|
-
input_file_name = "input.xlsx"
|
165
|
-
empty_file_name = "empty.xlsx"
|
166
|
-
|
167
|
-
input_file_path = os.path.join(test_data_dir, input_file_name)
|
168
|
-
empty_file_path = os.path.join(test_data_dir, empty_file_name)
|
169
|
-
|
170
|
-
df = generate_test_data()
|
171
|
-
|
172
|
-
os.makedirs(test_data_dir, exist_ok=True)
|
173
|
-
df.to_excel(input_file_path, sheet_name="RawReportData Data", index=False)
|
174
|
-
|
175
|
-
os.makedirs(test_data_dir_empty, exist_ok=True)
|
176
|
-
|
177
|
-
# create empty excel
|
178
|
-
df_empty = pd.DataFrame()
|
179
|
-
df_empty.to_excel(empty_file_path)
|
180
|
-
|
181
|
-
# Yield to allow the test to run
|
182
|
-
yield
|
183
|
-
|
184
|
-
# Teardown: Cleanup the created files
|
185
|
-
if os.path.exists(test_data_dir):
|
186
|
-
shutil.rmtree(test_data_dir)
|
187
|
-
if os.path.exists(test_data_dir_empty):
|
188
|
-
shutil.rmtree(test_data_dir_empty)
|
189
|
-
|
190
|
-
|
191
|
-
def test_validation_wrong_output():
|
192
|
-
with pytest.raises(ValueError):
|
193
|
-
assert converter.validation("test_data/", "test_data/input.xlsx")
|
194
|
-
|
195
|
-
|
196
|
-
def test_validation_wrong_input_file():
|
197
|
-
with pytest.raises(FileNotFoundError):
|
198
|
-
assert converter.validation("test_data/in.txt", "test_data/")
|
199
|
-
|
200
|
-
|
201
|
-
def test_validation_empty_input_directory():
|
202
|
-
with pytest.raises(FileNotFoundError):
|
203
|
-
assert converter.validation("test_data_empty/", "./output")
|
204
|
-
|
205
|
-
|
206
|
-
def test_validation():
|
207
|
-
assert converter.validation('./test_data/', "./test_data/") is None
|
208
|
-
|
209
|
-
|
210
|
-
def test_get_questions_wrong_excel():
|
211
|
-
assert converter.get_questions("test_data/empty.xlsx").shape[0] == 0
|
212
|
-
|
213
|
-
|
214
|
-
def test_get_questions_file():
|
215
|
-
assert converter.get_questions("test_data/input.xlsx").shape[0] == 2
|
216
|
-
|
217
|
-
|
218
|
-
def test_get_questions_directory():
|
219
|
-
assert converter.get_questions("test_data/").shape[0] == 2
|
220
|
-
|
221
|
-
|
222
|
-
def test_make_anki():
|
223
|
-
df = converter.get_questions("test_data/input.xlsx")
|
224
|
-
converter.make_anki(df, "test_data/", "test")
|
225
|
-
# Check if output file exists
|
226
|
-
assert os.path.exists("test_data/anki.apkg") is True
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|