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 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ word-stack = word_stack.main:main
@@ -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.