word-stack 0.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.
- word_stack/__init__.py +0 -0
- word_stack/api.py +34 -0
- word_stack/main.py +66 -0
- word_stack/storage.py +367 -0
- word_stack-0.2.0.dist-info/METADATA +10 -0
- word_stack-0.2.0.dist-info/RECORD +9 -0
- word_stack-0.2.0.dist-info/WHEEL +4 -0
- word_stack-0.2.0.dist-info/entry_points.txt +2 -0
- word_stack-0.2.0.dist-info/licenses/LICENSE +21 -0
word_stack/__init__.py
ADDED
|
File without changes
|
word_stack/api.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_word_info(word):
|
|
5
|
+
"""Fetch word details from the Free Dictionary API."""
|
|
6
|
+
url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
response = requests.get(url)
|
|
10
|
+
|
|
11
|
+
if response.status_code == 404:
|
|
12
|
+
raise ValueError("not_found")
|
|
13
|
+
|
|
14
|
+
response.raise_for_status()
|
|
15
|
+
data = response.json()[0]
|
|
16
|
+
|
|
17
|
+
phonetic = data.get("phonetic", "N/A")
|
|
18
|
+
definition = "No definition found"
|
|
19
|
+
example = "No example found"
|
|
20
|
+
|
|
21
|
+
if data.get("meanings"):
|
|
22
|
+
first_meaning = data["meanings"][0]
|
|
23
|
+
if first_meaning.get("definitions"):
|
|
24
|
+
definition = first_meaning["definitions"][0].get("definition", definition)
|
|
25
|
+
example = first_meaning["definitions"][0].get("example", example)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
"phonetic": phonetic,
|
|
29
|
+
"definition": definition,
|
|
30
|
+
"example": example
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
except requests.exceptions.RequestException as e:
|
|
34
|
+
raise ConnectionError(str(e))
|
word_stack/main.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import importlib.metadata
|
|
3
|
+
|
|
4
|
+
from rich_argparse import RawDescriptionRichHelpFormatter
|
|
5
|
+
from word_stack.storage import add_word, list_words, show_word, delete_word, study_words, has_studied_today, \
|
|
6
|
+
add_multiple_words
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_version():
|
|
10
|
+
"""Fetch the package version from pyproject.toml."""
|
|
11
|
+
try:
|
|
12
|
+
return importlib.metadata.version("word-stack")
|
|
13
|
+
except importlib.metadata.PackageNotFoundError:
|
|
14
|
+
return "unknown (not installed as a package)"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
status_msg = "✅ You have studied today!" if has_studied_today() else "❌ You haven't studied today yet."
|
|
19
|
+
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
description=f"Word-Stack: Your personal vocabulary builder.\n\nDaily Status: {status_msg}",
|
|
22
|
+
formatter_class=RawDescriptionRichHelpFormatter
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {get_version()}")
|
|
26
|
+
|
|
27
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
28
|
+
|
|
29
|
+
add_parser = subparsers.add_parser("add", help="Add a new word")
|
|
30
|
+
add_parser.add_argument("word", type=str, help="The English word")
|
|
31
|
+
add_parser.add_argument("translation", type=str, nargs="?", default="N/A", help="Optional translation")
|
|
32
|
+
|
|
33
|
+
bulk_parser = subparsers.add_parser("bulk", help="Add multiple words at once (e.g., word-stack bulk apple banana)")
|
|
34
|
+
bulk_parser.add_argument("words", type=str, nargs="+", help="List of English words separated by spaces")
|
|
35
|
+
|
|
36
|
+
list_parser = subparsers.add_parser("list", help="List latest saved words")
|
|
37
|
+
list_parser.add_argument("-l", "--limit", type=int, default=10, help="Number of latest words to show (default: 10)")
|
|
38
|
+
|
|
39
|
+
show_parser = subparsers.add_parser("show", help="Show details for a specific word")
|
|
40
|
+
show_parser.add_argument("word", type=str, help="The English word to inspect")
|
|
41
|
+
|
|
42
|
+
delete_parser = subparsers.add_parser("delete", help="Delete a saved word")
|
|
43
|
+
delete_parser.add_argument("word", type=str, help="The English word to delete")
|
|
44
|
+
|
|
45
|
+
study_parser = subparsers.add_parser("study", help="Start a daily study session (10 words)")
|
|
46
|
+
|
|
47
|
+
args = parser.parse_args()
|
|
48
|
+
|
|
49
|
+
if args.command == "add":
|
|
50
|
+
add_word(args.word, args.translation)
|
|
51
|
+
elif args.command == "bulk":
|
|
52
|
+
add_multiple_words(args.words)
|
|
53
|
+
elif args.command == "list":
|
|
54
|
+
list_words(args.limit)
|
|
55
|
+
elif args.command == "show":
|
|
56
|
+
show_word(args.word)
|
|
57
|
+
elif args.command == "delete":
|
|
58
|
+
delete_word(args.word)
|
|
59
|
+
elif args.command == "study":
|
|
60
|
+
study_words()
|
|
61
|
+
else:
|
|
62
|
+
parser.print_help()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|
word_stack/storage.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sqlite3
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from word_stack.api import get_word_info
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.progress import Progress
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
if os.getenv("WORD_STACK_ENV") == "development":
|
|
15
|
+
APP_DIR = Path.cwd() / ".dev_data"
|
|
16
|
+
else:
|
|
17
|
+
APP_DIR = Path.home() / ".word-stack"
|
|
18
|
+
|
|
19
|
+
APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
DB_FILE = APP_DIR / "words.db"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_connection():
|
|
24
|
+
"""Create and return a database connection."""
|
|
25
|
+
conn = sqlite3.connect(DB_FILE)
|
|
26
|
+
conn.row_factory = sqlite3.Row
|
|
27
|
+
return conn
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def init_db():
|
|
31
|
+
"""Initialize the database and create the table if it doesn't exist."""
|
|
32
|
+
conn = get_connection()
|
|
33
|
+
cursor = conn.cursor()
|
|
34
|
+
|
|
35
|
+
cursor.execute('''
|
|
36
|
+
CREATE TABLE IF NOT EXISTS words (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
word TEXT UNIQUE NOT NULL,
|
|
39
|
+
translation TEXT,
|
|
40
|
+
phonetic TEXT,
|
|
41
|
+
definition TEXT,
|
|
42
|
+
example TEXT,
|
|
43
|
+
last_studied TEXT
|
|
44
|
+
)
|
|
45
|
+
''')
|
|
46
|
+
conn.commit()
|
|
47
|
+
conn.close()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
init_db()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def format_date(iso_string):
|
|
54
|
+
"""Convert an ISO timestamp into human-readable date."""
|
|
55
|
+
if not iso_string or iso_string == "N/A":
|
|
56
|
+
return "Never studied"
|
|
57
|
+
try:
|
|
58
|
+
dt = datetime.fromisoformat(iso_string)
|
|
59
|
+
# Format: Mar 09, 2026 at 12:00 PM
|
|
60
|
+
return dt.strftime("%b %d, %Y at %I:%M %p")
|
|
61
|
+
except ValueError:
|
|
62
|
+
return iso_string
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def has_studied_today():
|
|
66
|
+
"""Check the database to see if any word was studied today."""
|
|
67
|
+
conn = get_connection()
|
|
68
|
+
cursor = conn.cursor()
|
|
69
|
+
|
|
70
|
+
cursor.execute("SELECT MAX(last_studied) FROM words")
|
|
71
|
+
row = cursor.fetchone()
|
|
72
|
+
conn.close()
|
|
73
|
+
|
|
74
|
+
if row and row[0]:
|
|
75
|
+
last_studied_iso = row[0]
|
|
76
|
+
if last_studied_iso != "N/A":
|
|
77
|
+
last_date = last_studied_iso.split("T")[0]
|
|
78
|
+
today_date = datetime.now().date().isoformat()
|
|
79
|
+
return last_date == today_date
|
|
80
|
+
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def add_word(word, translation="N/A"):
|
|
85
|
+
"""Add a new word and its translation to the database."""
|
|
86
|
+
conn = get_connection()
|
|
87
|
+
cursor = conn.cursor()
|
|
88
|
+
|
|
89
|
+
cursor.execute("SELECT word FROM words WHERE LOWER(word) = LOWER(?)", (word,))
|
|
90
|
+
if cursor.fetchone():
|
|
91
|
+
console.print(f"[bold yellow]The word '{word}' is already in your list![/bold yellow]")
|
|
92
|
+
conn.close()
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with console.status(f"[bold cyan]🔍 Fetching info for '{word}' from the internet...[/bold cyan]",
|
|
97
|
+
spinner="dots"):
|
|
98
|
+
api_info = get_word_info(word)
|
|
99
|
+
|
|
100
|
+
except ValueError:
|
|
101
|
+
console.print(f"[bold yellow]⚠️ '{word}' was not saved. It could not be found in the dictionary.[/bold yellow]")
|
|
102
|
+
conn.close()
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
except ConnectionError as e:
|
|
106
|
+
console.print(f"[bold red]❌ '{word}' was not saved. Network error![/bold red]")
|
|
107
|
+
conn.close()
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
cursor.execute('''
|
|
111
|
+
INSERT INTO words (word, translation, phonetic, definition, example, last_studied)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
113
|
+
''', (
|
|
114
|
+
word,
|
|
115
|
+
translation,
|
|
116
|
+
api_info["phonetic"],
|
|
117
|
+
api_info["definition"],
|
|
118
|
+
api_info["example"],
|
|
119
|
+
None
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
conn.commit()
|
|
123
|
+
conn.close()
|
|
124
|
+
console.print(f"[bold green]✅ Successfully added '{word}'.[/bold green]")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def add_multiple_words(words):
|
|
128
|
+
"""Add multiple words at once with a progress bar and summary."""
|
|
129
|
+
conn = get_connection()
|
|
130
|
+
cursor = conn.cursor()
|
|
131
|
+
|
|
132
|
+
added = []
|
|
133
|
+
skipped = []
|
|
134
|
+
not_found = []
|
|
135
|
+
errors = []
|
|
136
|
+
|
|
137
|
+
unique_words = list(dict.fromkeys(words))
|
|
138
|
+
|
|
139
|
+
console.print()
|
|
140
|
+
|
|
141
|
+
with Progress(console=console) as progress:
|
|
142
|
+
task = progress.add_task("[cyan]Processing words...", total=len(unique_words))
|
|
143
|
+
|
|
144
|
+
for word in unique_words:
|
|
145
|
+
progress.update(task, description=f"[cyan]Fetching '{word}'...")
|
|
146
|
+
|
|
147
|
+
cursor.execute("SELECT word FROM words WHERE LOWER(word) = LOWER(?)", (word,))
|
|
148
|
+
if cursor.fetchone():
|
|
149
|
+
skipped.append(word)
|
|
150
|
+
progress.advance(task)
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
api_info = get_word_info(word)
|
|
155
|
+
|
|
156
|
+
cursor.execute('''
|
|
157
|
+
INSERT INTO words (word, translation, phonetic, definition, example, last_studied)
|
|
158
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
159
|
+
''', (
|
|
160
|
+
word,
|
|
161
|
+
"N/A",
|
|
162
|
+
api_info["phonetic"],
|
|
163
|
+
api_info["definition"],
|
|
164
|
+
api_info["example"],
|
|
165
|
+
None
|
|
166
|
+
))
|
|
167
|
+
conn.commit()
|
|
168
|
+
added.append(word)
|
|
169
|
+
|
|
170
|
+
except ValueError:
|
|
171
|
+
not_found.append(word)
|
|
172
|
+
except ConnectionError:
|
|
173
|
+
errors.append(word)
|
|
174
|
+
|
|
175
|
+
progress.advance(task)
|
|
176
|
+
|
|
177
|
+
conn.close()
|
|
178
|
+
|
|
179
|
+
console.print("\n[bold magenta]Summary[/bold magenta]")
|
|
180
|
+
|
|
181
|
+
if added:
|
|
182
|
+
console.print(f"[bold green]✅ Added ({len(added)}):[/bold green] {', '.join(added)}")
|
|
183
|
+
if skipped:
|
|
184
|
+
console.print(f"[bold yellow]⏭️ Skipped - already saved ({len(skipped)}):[/bold yellow] {', '.join(skipped)}")
|
|
185
|
+
if not_found:
|
|
186
|
+
console.print(f"[bold red]❌ Not Found ({len(not_found)}):[/bold red] {', '.join(not_found)}")
|
|
187
|
+
if errors:
|
|
188
|
+
console.print(f"[bold red]⚠️ Network Errors ({len(errors)}):[/bold red] {', '.join(errors)}")
|
|
189
|
+
|
|
190
|
+
console.print()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def list_words(count=10):
|
|
194
|
+
"""Display latest saved words, newest first."""
|
|
195
|
+
conn = get_connection()
|
|
196
|
+
cursor = conn.cursor()
|
|
197
|
+
|
|
198
|
+
cursor.execute("SELECT COUNT(*) FROM words")
|
|
199
|
+
total_words = cursor.fetchone()[0]
|
|
200
|
+
|
|
201
|
+
if total_words == 0:
|
|
202
|
+
console.print("[yellow]Your word list is empty. Add some words first![/yellow]")
|
|
203
|
+
conn.close()
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
cursor.execute("SELECT word, translation, definition FROM words ORDER BY id DESC LIMIT ?", (count,))
|
|
207
|
+
rows = cursor.fetchall()
|
|
208
|
+
|
|
209
|
+
display_count = len(rows)
|
|
210
|
+
|
|
211
|
+
table = Table(title=f"📚 Your Latest {display_count} Words (Total: {total_words})", show_header=True, header_style="bold magenta")
|
|
212
|
+
|
|
213
|
+
table.add_column("Word", style="cyan", width=15)
|
|
214
|
+
table.add_column("Translation", style="green", width=15)
|
|
215
|
+
table.add_column("Definition", style="white")
|
|
216
|
+
|
|
217
|
+
for row in rows:
|
|
218
|
+
table.add_row(row['word'], row['translation'], row['definition'])
|
|
219
|
+
|
|
220
|
+
console.print()
|
|
221
|
+
console.print(table)
|
|
222
|
+
|
|
223
|
+
remaining_words = total_words - display_count
|
|
224
|
+
|
|
225
|
+
if remaining_words == 1:
|
|
226
|
+
console.print(f"[dim]...and {remaining_words} more word hidden.[/dim]")
|
|
227
|
+
elif remaining_words > 1:
|
|
228
|
+
console.print(f"[dim]...and {remaining_words} more words hidden.[/dim]")
|
|
229
|
+
|
|
230
|
+
if has_studied_today():
|
|
231
|
+
console.print("\n[bold green]✅ Daily Goal: You have studied today![/bold green]")
|
|
232
|
+
else:
|
|
233
|
+
console.print("\n[bold yellow]⚠️ Daily Goal: You haven't studied today yet. Run 'study'![/bold yellow]")
|
|
234
|
+
|
|
235
|
+
console.print()
|
|
236
|
+
conn.close()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def show_word(word):
|
|
240
|
+
"""Show all details for a specific word."""
|
|
241
|
+
conn = get_connection()
|
|
242
|
+
cursor = conn.cursor()
|
|
243
|
+
|
|
244
|
+
cursor.execute("SELECT * FROM words WHERE LOWER(word) = LOWER(?)", (word,))
|
|
245
|
+
row = cursor.fetchone()
|
|
246
|
+
|
|
247
|
+
if row:
|
|
248
|
+
content = (
|
|
249
|
+
f"[bold cyan]Translation :[/bold cyan] {row['translation']}\n"
|
|
250
|
+
f"[bold cyan]Phonetic :[/bold cyan] {row['phonetic']}\n"
|
|
251
|
+
f"[bold cyan]Definition :[/bold cyan] {row['definition']}\n"
|
|
252
|
+
f"[bold cyan]Example :[/bold cyan] {row['example']}\n"
|
|
253
|
+
f"[bold cyan]Last Studied:[/bold cyan] {format_date(row['last_studied'])}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
card = Panel(
|
|
257
|
+
content,
|
|
258
|
+
title=f"📖 [bold magenta]{row['word'].upper()}[/bold magenta]",
|
|
259
|
+
border_style="blue",
|
|
260
|
+
expand=False
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
console.print()
|
|
264
|
+
console.print(card)
|
|
265
|
+
console.print()
|
|
266
|
+
conn.close()
|
|
267
|
+
else:
|
|
268
|
+
conn.close()
|
|
269
|
+
console.print(f"\n[bold yellow]⚠️ The word '{word}' was not found in your list.[/bold yellow]")
|
|
270
|
+
|
|
271
|
+
choice = console.input(f"[dim]Would you like to search the dictionary and add '{word}' now? (y/n): [/dim]")
|
|
272
|
+
if choice.lower() == 'y':
|
|
273
|
+
console.print()
|
|
274
|
+
add_word(word, "N/A")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def delete_word(word):
|
|
278
|
+
"""Delete a word from the saved list."""
|
|
279
|
+
conn = get_connection()
|
|
280
|
+
cursor = conn.cursor()
|
|
281
|
+
|
|
282
|
+
cursor.execute("SELECT id FROM words WHERE LOWER(word) = LOWER(?)", (word,))
|
|
283
|
+
if not cursor.fetchone():
|
|
284
|
+
console.print(f"[bold yellow]⚠️ The word '{word}' was not found in your list.[/bold yellow]")
|
|
285
|
+
conn.close()
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
cursor.execute("DELETE FROM words WHERE LOWER(word) = LOWER(?)", (word,))
|
|
289
|
+
conn.commit()
|
|
290
|
+
conn.close()
|
|
291
|
+
console.print(f"[bold green]✅ Successfully deleted '{word}'.[/bold green]")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def study_words():
|
|
295
|
+
"""Start an interactive study session with words."""
|
|
296
|
+
conn = get_connection()
|
|
297
|
+
cursor = conn.cursor()
|
|
298
|
+
|
|
299
|
+
cursor.execute('''
|
|
300
|
+
SELECT * FROM words
|
|
301
|
+
ORDER BY last_studied ASC NULLS FIRST
|
|
302
|
+
LIMIT 10
|
|
303
|
+
''')
|
|
304
|
+
study_list = cursor.fetchall()
|
|
305
|
+
|
|
306
|
+
if not study_list:
|
|
307
|
+
console.print("[bold yellow]Your word list is empty. Add some words first![/bold yellow]")
|
|
308
|
+
conn.close()
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
312
|
+
console.print(f"\n[bold magenta]🎓 Starting Study Session ({len(study_list)} words)[/bold magenta]")
|
|
313
|
+
|
|
314
|
+
if has_studied_today():
|
|
315
|
+
console.print("[bold cyan]🌟 You already studied today, but extra practice is always great![/bold cyan]\n")
|
|
316
|
+
|
|
317
|
+
console.print("Try to remember the translation and meaning.")
|
|
318
|
+
console.input("\n[dim]Press Enter to begin...[/dim]")
|
|
319
|
+
|
|
320
|
+
for i, row in enumerate(study_list):
|
|
321
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
322
|
+
|
|
323
|
+
front = Panel(
|
|
324
|
+
f"[bold white]Word {i + 1} of {len(study_list)}[/bold white]",
|
|
325
|
+
title=f"🤔 [bold cyan]{row['word'].upper()}[/bold cyan]",
|
|
326
|
+
border_style="cyan",
|
|
327
|
+
expand=False
|
|
328
|
+
)
|
|
329
|
+
console.print(front)
|
|
330
|
+
|
|
331
|
+
user_input = console.input("\n[dim]Press Enter to reveal answer (or 'q' to quit)...[/dim] ")
|
|
332
|
+
if user_input.lower() == 'q':
|
|
333
|
+
console.print("\n[bold yellow]Ending study session early. Great job today![/bold yellow]")
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
back_content = (
|
|
337
|
+
f"[bold green]Translation :[/bold green] {row['translation']}\n"
|
|
338
|
+
f"[bold green]Phonetic :[/bold green] {row['phonetic']}\n"
|
|
339
|
+
f"[bold green]Definition :[/bold green] {row['definition']}\n"
|
|
340
|
+
f"[bold green]Example :[/bold green] {row['example']}\n\n"
|
|
341
|
+
f"[dim]Previously Studied: {format_date(row['last_studied'])}[/dim]"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
back = Panel(
|
|
345
|
+
back_content,
|
|
346
|
+
title=f"💡 [bold green]Answer[/bold green]",
|
|
347
|
+
border_style="green",
|
|
348
|
+
expand=False
|
|
349
|
+
)
|
|
350
|
+
console.print(back)
|
|
351
|
+
|
|
352
|
+
now = datetime.now().isoformat()
|
|
353
|
+
cursor.execute('''
|
|
354
|
+
UPDATE words
|
|
355
|
+
SET last_studied = ?
|
|
356
|
+
WHERE id = ?
|
|
357
|
+
''', (now, row['id']))
|
|
358
|
+
|
|
359
|
+
if i < len(study_list) - 1:
|
|
360
|
+
next_action = console.input("\n[dim]Press Enter for the next word (or 'q' to quit)...[/dim] ")
|
|
361
|
+
if next_action.lower() == 'q':
|
|
362
|
+
console.print("\n[bold yellow]Ending study session early. Great job today![/bold yellow]")
|
|
363
|
+
break
|
|
364
|
+
|
|
365
|
+
conn.commit()
|
|
366
|
+
conn.close()
|
|
367
|
+
console.print("\n[bold green]✅ Study session complete! Progress saved.[/bold green]")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: word-stack
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A powerful, terminal-based vocabulary builder and daily study tool.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: pytest>=9.0.2
|
|
8
|
+
Requires-Dist: requests>=2.32.5
|
|
9
|
+
Requires-Dist: rich-argparse>=1.7.2
|
|
10
|
+
Requires-Dist: rich>=14.3.3
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
word_stack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
word_stack/api.py,sha256=OEW4H04ngPBWSJmbX8XIeUnR8d-MtB9eYiK4IwLRt84,1027
|
|
3
|
+
word_stack/main.py,sha256=V0lYfz0gVLUS4RskdZxElOIb9L_g5IiZ6d5a5U0dRto,2617
|
|
4
|
+
word_stack/storage.py,sha256=lo-fBiZomDjluzY3Xg4DBznhiCNbTOOADwv_EnUCeI8,11708
|
|
5
|
+
word_stack-0.2.0.dist-info/METADATA,sha256=xNVHvWsCzv32mvAFWEm5AS83PsVyNuwLdrOZWSOzWyE,302
|
|
6
|
+
word_stack-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
word_stack-0.2.0.dist-info/entry_points.txt,sha256=YJPkVBlQ9xUpHVNvawM1W4071WsKRPT3wDnhg5rwK3E,52
|
|
8
|
+
word_stack-0.2.0.dist-info/licenses/LICENSE,sha256=q3RaYyFULxk9Piy5GMnbbpJotfiI1iP1lV6Qu25LJgk,1068
|
|
9
|
+
word_stack-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kemal Soylu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|