-
Notifications
You must be signed in to change notification settings - Fork 0
/
utils.py
208 lines (174 loc) · 7.37 KB
/
utils.py
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
208
import os
import re
import csv
import configparser
from uuid import uuid4
import requests
from typing import List
from PySide6.QtGui import QShortcut, QKeySequence
from PySide6.QtWidgets import QWidget
from models.Deck import Deck
from models.Flashcard import Flashcard
def is_valid_filename(filename: str) -> bool:
"""
Check if a filename is valid. A valid filename can only include alphanumeric characters, dashes, and hyphens, and
must end with .csv
:param filename: The filename to check
:return: True if the filename is valid, False otherwise
"""
# Filename can only include alphanumeric characters, dashes, and hyphens, and must end with .csv
return re.match(r'^[\w\s-]+\.csv$', filename) is not None
def is_valid_path(basedir, path, follow_symlinks=True):
"""
Check if a path is valid based on a base directory and whether to follow symlinks.
A path is considered valid if it is a subdirectory of the base directory and, if follow_symlinks is False, the path
is not a symlink.
:param basedir: The base directory to check against
:param path: The path to check
:param follow_symlinks: Whether to follow symlinks, like shortcuts
:return: True if the path is valid, False otherwise
"""
if follow_symlinks:
abs_path = os.path.abspath(path)
else:
abs_path = os.path.realpath(path)
basedir = os.path.abspath(basedir)
# Ensure the abs_path starts with basedir and that the next character is a path separator
return abs_path.startswith(os.path.join(basedir, ''))
def save_deck_to_csv(deck: Deck, directory: str) -> None:
"""
Save a deck to a CSV file in the specified directory
:param deck: The deck to save, should be an instance of Deck and include Flashcard instances
:param directory: The directory to save the deck to
:return: None
"""
if not deck.is_modified:
print(f"Deck {deck.name} has not been modified")
return # Skip saving if the deck hasn't been modified
if not os.path.exists(directory):
os.makedirs(directory)
filename = f"{directory}/{deck.name}.csv"
print(f"Saving deck to {filename}")
with open(filename, mode='w', newline='', encoding='utf-8') as file:
writer = csv.writer(file)
writer.writerow(['Deck ID', 'Deck Name', 'Card ID', 'Question', 'Answer', 'Next Review Date', 'Repetitions',
'Easiness Factor', 'Interval', 'Tags'])
for card in deck.cards:
writer.writerow(
[deck.id, deck.name, card.id, card.question, card.answer, card.next_review_date, card.repetitions,
card.easiness_factor, card.interval, ' '.join(card.tags)])
deck.is_modified = False # Reset the modified flag after saving
def save_decks_to_csv(decks: List[Deck], directory: str) -> None:
"""
Save a list of decks to CSV files in the specified directory
:param decks: The list of decks to save
:param directory: The directory to save the decks to, will be validated by is_valid_path
:return: None
"""
for deck in decks:
save_deck_to_csv(deck, directory)
def load_deck_from_csv(filename: str) -> Deck:
"""
Load a deck from a CSV file
:param filename: The filename to load the deck from, including the directory
:return: A Deck instance with the cards loaded from the CSV file
"""
with open(filename, mode='r', newline='', encoding='utf-8') as file:
reader = csv.DictReader(file)
cards = []
deck_name = filename.split('\\')[-1].split('.')[0]
print(f"Loading deck {deck_name}")
for row in reader:
card = Flashcard(
question=row['Question'],
answer=row['Answer'],
next_review_date=row['Next Review Date'],
repetitions=int(row['Repetitions']),
easiness_factor=float(row['Easiness Factor']),
interval=int(row['Interval']),
id=row['Card ID'],
tags=row['Tags'].split(' ')
)
cards.append(card)
deck = Deck(name=deck_name, cards=cards)
return deck
def load_decks_from_csv(directory: str) -> List[Deck]:
"""
Load all decks from a directory
:param directory: The directory to load the decks from, will be validated by is_valid_path
:return: A list of Deck instances with the cards loaded from the CSV files
"""
decks = []
for filename in os.listdir(directory):
filepath = os.path.join(directory, filename)
if is_valid_path(directory, filepath) and is_valid_filename(filename):
deck = load_deck_from_csv(filepath)
deck.is_modified = False
decks.append(deck)
return decks
# TODO: Consider making this more generic so it could be used with other APIs
def download_deck_from_url(url: str, deck_name: str, directory: str) -> None:
"""
Download a deck from a URL and save it to a directory. Note that this was written for a specific API, located at https://jlpt-vocab-api.vercel.app and may need
to be modified for other APIs.
:param url: The URL to download the deck from
:param deck_name: The name of the deck
:param directory: The directory to save the deck to
:return: None
"""
response = requests.get(url)
if response.status_code == 200:
response = response.json()
cards = []
for card in response:
front = card["word"]
furigana = card["furigana"] + ' - ' if card["furigana"] != '' else ''
back = furigana + card["meaning"]
tags = [f'N{card["level"]}']
new_card = Flashcard(front, back, tags=tags, id=str(uuid4()))
cards.append(new_card)
deck = Deck(deck_name, cards)
save_deck_to_csv(deck, directory)
else:
print(f"Failed to download deck from {url}")
def setup_shortcuts(widget: QWidget, shortcuts: dict) -> None:
"""
Set up keyboard shortcuts for a widget
:param widget: The widget to set up the shortcuts for
:param shortcuts: A dictionary of shortcuts, where the key is the shortcut as a string and the value is the method to call
:return: None
"""
for key_sequence, action in shortcuts.items():
shortcut = QShortcut(QKeySequence(key_sequence), widget)
shortcut.activated.connect(action)
# CONFIGURATION
default_config = configparser.ConfigParser()
default_config['DEFAULT'] = {
'decks_directory': 'decks',
'daily_reviews_limit': 100,
'new_card_limit': 20,
'theme': 'blue_dark'
}
def load_config(filename: str) -> configparser.ConfigParser:
"""
Load a configuration file
:param filename: The filename of the configuration file
:return: A ConfigParser instance with the configuration loaded
"""
os.path.exists(filename) or save_config(default_config, filename)
config = configparser.ConfigParser()
config.read(filename)
# If the configuration file is empty, load the default configuration
if not config.sections():
config.read_dict(default_config)
save_config(config, filename)
return config
def save_config(config: configparser.ConfigParser, filename: str) -> None:
"""
Save a configuration file
:param config: The ConfigParser instance to save
:param filename: The filename to save the configuration to
:return: None
"""
with open(filename, 'w') as file:
config.write(file)