word-stack 0.2.0__tar.gz
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-0.2.0/.env.example +1 -0
- word_stack-0.2.0/.gitignore +17 -0
- word_stack-0.2.0/.python-version +1 -0
- word_stack-0.2.0/LICENSE +21 -0
- word_stack-0.2.0/PKG-INFO +10 -0
- word_stack-0.2.0/README.md +109 -0
- word_stack-0.2.0/TODO.txt +28 -0
- word_stack-0.2.0/pyproject.toml +18 -0
- word_stack-0.2.0/tests/test_api.py +55 -0
- word_stack-0.2.0/tests/test_storage.py +27 -0
- word_stack-0.2.0/uv.lock +228 -0
- word_stack-0.2.0/word_stack/__init__.py +0 -0
- word_stack-0.2.0/word_stack/api.py +34 -0
- word_stack-0.2.0/word_stack/main.py +66 -0
- word_stack-0.2.0/word_stack/storage.py +367 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
WORD_STACK_ENV=development
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
word_stack-0.2.0/LICENSE
ADDED
|
@@ -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.
|
|
@@ -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,109 @@
|
|
|
1
|
+
# Word-Stack
|
|
2
|
+
|
|
3
|
+
A sleek, fast, terminal-based vocabulary builder. Add words from your command line, automatically fetch their definitions and pronunciations, and build a daily study habit without ever leaving your terminal.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
* **Lightning Fast:** Built with modern Python and `uv`.
|
|
7
|
+
* **Zero-Friction Adding:** Just type `word-stack add <word>`. The Dictionary API automatically fetches definitions, examples, and phonetics.
|
|
8
|
+
* **Bulk Import:** Quickly add multiple words at once with beautiful progress tracking.
|
|
9
|
+
* **Daily Study Mode:** Uses a built-in SQLite database to track your learning and serves you a daily quiz of unstudied words.
|
|
10
|
+
* **Beautiful UI:** Terminal output is fully styled with `rich` for gorgeous tables, panels, and loading animations.
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
Before installing, make sure you have the [uv package manager](https://docs.astral.sh/uv/) installed on your system.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Word-Stack is easily installable globally:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/kemalbsoylu/word-stack.git
|
|
21
|
+
cd word-stack
|
|
22
|
+
uv tool install .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
*Note: The application creates a local SQLite database safely tucked away in `~/.word-stack/words.db` to keep your system clean.*
|
|
26
|
+
|
|
27
|
+
### Setting up your Alias
|
|
28
|
+
To make the tool lightning fast to use, set up a permanent alias in your shell configuration (like `~/.zshrc` or `~/.bashrc`):
|
|
29
|
+
```bash
|
|
30
|
+
alias ws="word-stack"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Updating your Installation
|
|
34
|
+
When you pull new code from this repository, you can force `uv` to update your global installation with the latest features:
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install --force .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Once installed, you can use the `ws` alias from anywhere on your system.
|
|
42
|
+
|
|
43
|
+
**Check your installed version:**
|
|
44
|
+
```bash
|
|
45
|
+
ws --version
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Add a word (Auto-fetches definition):**
|
|
49
|
+
```bash
|
|
50
|
+
ws add "persevere"
|
|
51
|
+
ws add "equilibrium" "denge"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Add multiple words at once:**
|
|
55
|
+
```bash
|
|
56
|
+
ws bulk "apple" "banana" "ephemeral"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**List your latest saved words:**
|
|
60
|
+
```bash
|
|
61
|
+
ws list
|
|
62
|
+
ws list --limit 20
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**View word details:**
|
|
66
|
+
```bash
|
|
67
|
+
ws show "persevere"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Start your daily study session:**
|
|
71
|
+
```bash
|
|
72
|
+
ws study
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Delete a word:**
|
|
76
|
+
```bash
|
|
77
|
+
ws delete "persevere"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Development Setup
|
|
81
|
+
|
|
82
|
+
Word-Stack uses `uv` for lightning-fast development. You **do not** need to manually create virtual environments or run `pip install`. `uv` handles everything for you automatically.
|
|
83
|
+
|
|
84
|
+
If you want to contribute or test features without altering your personal vocabulary database, set up an isolated development environment:
|
|
85
|
+
|
|
86
|
+
**1. Set up your environment variables:**
|
|
87
|
+
```bash
|
|
88
|
+
cp .env.example .env
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**2. Set up a temporary development alias in your terminal:**
|
|
92
|
+
```bash
|
|
93
|
+
alias dev="uv run --env-file .env -m word_stack.main"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Now you can run commands like `dev list` or `dev bulk test`. The application will detect the development environment and safely route all data to a local `.dev_data/words.db` file instead of your global database!
|
|
97
|
+
|
|
98
|
+
### Running Tests
|
|
99
|
+
To run the test suite, simply use:
|
|
100
|
+
```bash
|
|
101
|
+
uv run pytest
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### 🎓 For Python Beginners
|
|
107
|
+
Are you learning Python and want to see exactly how this tool was built?
|
|
108
|
+
|
|
109
|
+
This repository is the production version of the tool. If you want to see a step-by-step, 3-phase educational history of how to build a CLI app from scratch, please visit the [Word-Stack-CLI Educational Repository](https://github.com/kemalbsoylu/word-stack-cli). It is designed specifically for beginners to explore and submit their first Open Source contributions!
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
feat: deletion of app, update of app
|
|
2
|
+
|
|
3
|
+
styling
|
|
4
|
+
|
|
5
|
+
add suggestions with difflib and rich
|
|
6
|
+
feat: if a word not found, suggest?
|
|
7
|
+
|
|
8
|
+
spaced-repetition algorithms?
|
|
9
|
+
CSV or an Anki deck?
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
later:
|
|
14
|
+
testing db: testing add_word, list_words, or delete_word. advanced database mock tests
|
|
15
|
+
keep logs, logging
|
|
16
|
+
check --verbose, cyclopts
|
|
17
|
+
|
|
18
|
+
question:
|
|
19
|
+
https://audreyfeldroy.github.io/cookiecutter-pypackage/tutorial/
|
|
20
|
+
https://github.com/cookiecutter/cookiecutter
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
done:
|
|
25
|
+
fix: if a word not found, do not add it,
|
|
26
|
+
feat: add multiple words at once
|
|
27
|
+
feat: -v --version
|
|
28
|
+
fix: make list show latest words and total number
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "word-stack"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "A powerful, terminal-based vocabulary builder and daily study tool."
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"pytest>=9.0.2",
|
|
8
|
+
"requests>=2.32.5",
|
|
9
|
+
"rich>=14.3.3",
|
|
10
|
+
"rich-argparse>=1.7.2",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
word-stack = "word_stack.main:main"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["hatchling"]
|
|
18
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
from word_stack.api import get_word_info
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# The @patch decorator intercepts 'requests.get' inside our api.py file
|
|
9
|
+
@patch("word_stack.api.requests.get")
|
|
10
|
+
def test_get_word_info_success(mock_get):
|
|
11
|
+
"""Test fetching a word successfully using a fake API response."""
|
|
12
|
+
|
|
13
|
+
# 1. Setup the fake response (This is what the API would normally return)
|
|
14
|
+
mock_get.return_value.status_code = 200
|
|
15
|
+
mock_get.return_value.json.return_value = [{
|
|
16
|
+
"phonetic": "/tɛst/",
|
|
17
|
+
"meanings": [{
|
|
18
|
+
"definitions": [{
|
|
19
|
+
"definition": "A procedure intended to establish the quality of something.",
|
|
20
|
+
"example": "Both tests were successful."
|
|
21
|
+
}]
|
|
22
|
+
}]
|
|
23
|
+
}]
|
|
24
|
+
|
|
25
|
+
# 2. Call function. It thinks it's hitting the internet, but it gets fake data!
|
|
26
|
+
result = get_word_info("test")
|
|
27
|
+
|
|
28
|
+
# 3. Assert it parsed fake JSON correctly
|
|
29
|
+
assert result is not None
|
|
30
|
+
assert result["phonetic"] == "/tɛst/"
|
|
31
|
+
assert result["definition"] == "A procedure intended to establish the quality of something."
|
|
32
|
+
assert result["example"] == "Both tests were successful."
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@patch("word_stack.api.requests.get")
|
|
36
|
+
def test_get_word_info_not_found(mock_get):
|
|
37
|
+
"""Test how the app handles a 404 Not Found error."""
|
|
38
|
+
|
|
39
|
+
# Simulate the API returning a 404 error
|
|
40
|
+
mock_get.return_value.status_code = 404
|
|
41
|
+
|
|
42
|
+
# Tell pytest that we EXPECT a ValueError to be raised here
|
|
43
|
+
with pytest.raises(ValueError, match="not_found"):
|
|
44
|
+
get_word_info("notarealword123")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@patch("word_stack.api.requests.get")
|
|
48
|
+
def test_get_word_info_connection_error(mock_get):
|
|
49
|
+
"""Test how the app handles a complete network failure."""
|
|
50
|
+
|
|
51
|
+
# .side_effect is used in mocks to simulate an exception being thrown
|
|
52
|
+
mock_get.side_effect = requests.exceptions.ConnectionError("No internet")
|
|
53
|
+
|
|
54
|
+
with pytest.raises(ConnectionError):
|
|
55
|
+
get_word_info("wifi_is_down_word")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from word_stack.storage import format_date
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_format_date_valid_iso():
|
|
5
|
+
"""Test that a valid ISO timestamp formats correctly."""
|
|
6
|
+
# This is a fixed date: March 9, 2026 at 12:30 PM
|
|
7
|
+
iso_string = "2026-03-09T12:30:00"
|
|
8
|
+
|
|
9
|
+
# Call our function
|
|
10
|
+
result = format_date(iso_string)
|
|
11
|
+
|
|
12
|
+
# Assert (check) that the result matches what we expect
|
|
13
|
+
assert result == "Mar 09, 2026 at 12:30 PM"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_format_date_empty_or_na():
|
|
17
|
+
"""Test that empty strings or 'N/A' return the default 'Never studied' message."""
|
|
18
|
+
assert format_date(None) == "Never studied"
|
|
19
|
+
assert format_date("N/A") == "Never studied"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_format_date_invalid_string():
|
|
23
|
+
"""Test that an invalid date string just returns the string itself."""
|
|
24
|
+
# If the database accidentally got corrupted with bad data,
|
|
25
|
+
# the function should safely return the bad data instead of crashing.
|
|
26
|
+
bad_data = "Not a real date"
|
|
27
|
+
assert format_date(bad_data) == "Not a real date"
|
word_stack-0.2.0/uv.lock
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "certifi"
|
|
7
|
+
version = "2026.2.25"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "charset-normalizer"
|
|
16
|
+
version = "3.4.5"
|
|
17
|
+
source = { registry = "https://pypi.org/simple" }
|
|
18
|
+
sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" }
|
|
19
|
+
wheels = [
|
|
20
|
+
{ url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" },
|
|
21
|
+
{ url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" },
|
|
22
|
+
{ url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" },
|
|
23
|
+
{ url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" },
|
|
24
|
+
{ url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" },
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" },
|
|
26
|
+
{ url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" },
|
|
27
|
+
{ url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" },
|
|
28
|
+
{ url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" },
|
|
29
|
+
{ url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" },
|
|
30
|
+
{ url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" },
|
|
31
|
+
{ url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" },
|
|
32
|
+
{ url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" },
|
|
33
|
+
{ url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" },
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" },
|
|
35
|
+
{ url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" },
|
|
36
|
+
{ url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" },
|
|
37
|
+
{ url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" },
|
|
38
|
+
{ url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" },
|
|
39
|
+
{ url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" },
|
|
40
|
+
{ url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" },
|
|
41
|
+
{ url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" },
|
|
42
|
+
{ url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" },
|
|
43
|
+
{ url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" },
|
|
44
|
+
{ url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" },
|
|
45
|
+
{ url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" },
|
|
46
|
+
{ url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" },
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" },
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" },
|
|
49
|
+
{ url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" },
|
|
50
|
+
{ url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" },
|
|
51
|
+
{ url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" },
|
|
52
|
+
{ url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" },
|
|
53
|
+
{ url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" },
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" },
|
|
55
|
+
{ url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" },
|
|
56
|
+
{ url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" },
|
|
57
|
+
{ url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" },
|
|
58
|
+
{ url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" },
|
|
59
|
+
{ url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" },
|
|
60
|
+
{ url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" },
|
|
61
|
+
{ url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" },
|
|
62
|
+
{ url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" },
|
|
63
|
+
{ url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" },
|
|
64
|
+
{ url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" },
|
|
65
|
+
{ url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" },
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" },
|
|
67
|
+
{ url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" },
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[[package]]
|
|
72
|
+
name = "colorama"
|
|
73
|
+
version = "0.4.6"
|
|
74
|
+
source = { registry = "https://pypi.org/simple" }
|
|
75
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
76
|
+
wheels = [
|
|
77
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[[package]]
|
|
81
|
+
name = "idna"
|
|
82
|
+
version = "3.11"
|
|
83
|
+
source = { registry = "https://pypi.org/simple" }
|
|
84
|
+
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
|
85
|
+
wheels = [
|
|
86
|
+
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
[[package]]
|
|
90
|
+
name = "iniconfig"
|
|
91
|
+
version = "2.3.0"
|
|
92
|
+
source = { registry = "https://pypi.org/simple" }
|
|
93
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
94
|
+
wheels = [
|
|
95
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
[[package]]
|
|
99
|
+
name = "markdown-it-py"
|
|
100
|
+
version = "4.0.0"
|
|
101
|
+
source = { registry = "https://pypi.org/simple" }
|
|
102
|
+
dependencies = [
|
|
103
|
+
{ name = "mdurl" },
|
|
104
|
+
]
|
|
105
|
+
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
|
106
|
+
wheels = [
|
|
107
|
+
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
[[package]]
|
|
111
|
+
name = "mdurl"
|
|
112
|
+
version = "0.1.2"
|
|
113
|
+
source = { registry = "https://pypi.org/simple" }
|
|
114
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
115
|
+
wheels = [
|
|
116
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
[[package]]
|
|
120
|
+
name = "packaging"
|
|
121
|
+
version = "26.0"
|
|
122
|
+
source = { registry = "https://pypi.org/simple" }
|
|
123
|
+
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
|
124
|
+
wheels = [
|
|
125
|
+
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
[[package]]
|
|
129
|
+
name = "pluggy"
|
|
130
|
+
version = "1.6.0"
|
|
131
|
+
source = { registry = "https://pypi.org/simple" }
|
|
132
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
133
|
+
wheels = [
|
|
134
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
[[package]]
|
|
138
|
+
name = "pygments"
|
|
139
|
+
version = "2.19.2"
|
|
140
|
+
source = { registry = "https://pypi.org/simple" }
|
|
141
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
|
142
|
+
wheels = [
|
|
143
|
+
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
[[package]]
|
|
147
|
+
name = "pytest"
|
|
148
|
+
version = "9.0.2"
|
|
149
|
+
source = { registry = "https://pypi.org/simple" }
|
|
150
|
+
dependencies = [
|
|
151
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
152
|
+
{ name = "iniconfig" },
|
|
153
|
+
{ name = "packaging" },
|
|
154
|
+
{ name = "pluggy" },
|
|
155
|
+
{ name = "pygments" },
|
|
156
|
+
]
|
|
157
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
|
158
|
+
wheels = [
|
|
159
|
+
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
[[package]]
|
|
163
|
+
name = "requests"
|
|
164
|
+
version = "2.32.5"
|
|
165
|
+
source = { registry = "https://pypi.org/simple" }
|
|
166
|
+
dependencies = [
|
|
167
|
+
{ name = "certifi" },
|
|
168
|
+
{ name = "charset-normalizer" },
|
|
169
|
+
{ name = "idna" },
|
|
170
|
+
{ name = "urllib3" },
|
|
171
|
+
]
|
|
172
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
|
173
|
+
wheels = [
|
|
174
|
+
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
[[package]]
|
|
178
|
+
name = "rich"
|
|
179
|
+
version = "14.3.3"
|
|
180
|
+
source = { registry = "https://pypi.org/simple" }
|
|
181
|
+
dependencies = [
|
|
182
|
+
{ name = "markdown-it-py" },
|
|
183
|
+
{ name = "pygments" },
|
|
184
|
+
]
|
|
185
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
|
186
|
+
wheels = [
|
|
187
|
+
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
[[package]]
|
|
191
|
+
name = "rich-argparse"
|
|
192
|
+
version = "1.7.2"
|
|
193
|
+
source = { registry = "https://pypi.org/simple" }
|
|
194
|
+
dependencies = [
|
|
195
|
+
{ name = "rich" },
|
|
196
|
+
]
|
|
197
|
+
sdist = { url = "https://files.pythonhosted.org/packages/4c/f7/1c65e0245d4c7009a87ac92908294a66e7e7635eccf76a68550f40c6df80/rich_argparse-1.7.2.tar.gz", hash = "sha256:64fd2e948fc96e8a1a06e0e72c111c2ce7f3af74126d75c0f5f63926e7289cd1", size = 38500, upload-time = "2025-11-01T10:35:44.232Z" }
|
|
198
|
+
wheels = [
|
|
199
|
+
{ url = "https://files.pythonhosted.org/packages/04/80/97b6f357ac458d9ad9872cc3183ca09ef7439ac89e030ea43053ba1294b6/rich_argparse-1.7.2-py3-none-any.whl", hash = "sha256:0559b1f47a19bbeb82bf15f95a057f99bcbbc98385532f57937f9fc57acc501a", size = 25476, upload-time = "2025-11-01T10:35:42.681Z" },
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
[[package]]
|
|
203
|
+
name = "urllib3"
|
|
204
|
+
version = "2.6.3"
|
|
205
|
+
source = { registry = "https://pypi.org/simple" }
|
|
206
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
|
207
|
+
wheels = [
|
|
208
|
+
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
[[package]]
|
|
212
|
+
name = "word-stack"
|
|
213
|
+
version = "0.2.0"
|
|
214
|
+
source = { editable = "." }
|
|
215
|
+
dependencies = [
|
|
216
|
+
{ name = "pytest" },
|
|
217
|
+
{ name = "requests" },
|
|
218
|
+
{ name = "rich" },
|
|
219
|
+
{ name = "rich-argparse" },
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
[package.metadata]
|
|
223
|
+
requires-dist = [
|
|
224
|
+
{ name = "pytest", specifier = ">=9.0.2" },
|
|
225
|
+
{ name = "requests", specifier = ">=2.32.5" },
|
|
226
|
+
{ name = "rich", specifier = ">=14.3.3" },
|
|
227
|
+
{ name = "rich-argparse", specifier = ">=1.7.2" },
|
|
228
|
+
]
|
|
File without changes
|
|
@@ -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))
|
|
@@ -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()
|
|
@@ -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]")
|