winebox 0.1.3__tar.gz → 0.1.5__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.
- {winebox-0.1.3 → winebox-0.1.5}/PKG-INFO +4 -1
- winebox-0.1.5/docs/_static/screenshots/cellar.png +0 -0
- winebox-0.1.5/docs/_static/screenshots/checkin.png +0 -0
- winebox-0.1.5/docs/_static/screenshots/dashboard.png +0 -0
- winebox-0.1.5/docs/_static/screenshots/search.png +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/docs/index.md +2 -0
- {winebox-0.1.3 → winebox-0.1.5}/docs/user-guide.md +64 -0
- {winebox-0.1.3 → winebox-0.1.5}/pyproject.toml +4 -1
- winebox-0.1.5/scripts/__init__.py +1 -0
- winebox-0.1.5/scripts/migrations/__init__.py +20 -0
- winebox-0.1.5/scripts/migrations/db_migrate_0_to_1.py +46 -0
- winebox-0.1.5/scripts/migrations/db_revert_1_to_0.py +108 -0
- winebox-0.1.5/scripts/migrations/runner.py +606 -0
- {winebox-0.1.3 → winebox-0.1.5}/tasks.py +137 -3
- winebox-0.1.5/tests/test_checkin_e2e.py +342 -0
- winebox-0.1.5/tests/test_wines.py +385 -0
- {winebox-0.1.3 → winebox-0.1.5}/uv.lock +210 -1
- {winebox-0.1.3 → winebox-0.1.5}/winebox/__init__.py +1 -1
- winebox-0.1.5/winebox/config.py +78 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/main.py +48 -1
- {winebox-0.1.3 → winebox-0.1.5}/winebox/models/user.py +2 -0
- winebox-0.1.5/winebox/routers/auth.py +204 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/routers/wines.py +130 -71
- winebox-0.1.5/winebox/services/image_storage.py +219 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/services/vision.py +50 -23
- {winebox-0.1.3 → winebox-0.1.5}/winebox/static/css/style.css +201 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/static/index.html +176 -19
- {winebox-0.1.3 → winebox-0.1.5}/winebox/static/js/app.js +343 -62
- winebox-0.1.3/tests/test_wines.py +0 -212
- winebox-0.1.3/winebox/config.py +0 -47
- winebox-0.1.3/winebox/routers/auth.py +0 -90
- winebox-0.1.3/winebox/services/image_storage.py +0 -90
- {winebox-0.1.3 → winebox-0.1.5}/.github/workflows/ci.yml +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/.github/workflows/publish.yml +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/.gitignore +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/.python-version +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/LICENSE +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/README.md +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/docs/api-reference.md +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/docs/conf.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/docs/screenshots/cellar.png +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/docs/screenshots/checkin.png +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/docs/screenshots/dashboard.png +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/docs/screenshots/login.png +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/tests/__init__.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/tests/conftest.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/tests/test_ocr.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/tests/test_search.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/tests/test_transactions.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/cli/__init__.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/cli/server.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/cli/user_admin.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/database.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/models/__init__.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/models/inventory.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/models/transaction.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/models/wine.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/routers/__init__.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/routers/cellar.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/routers/search.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/routers/transactions.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/schemas/__init__.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/schemas/transaction.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/schemas/wine.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/services/__init__.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/services/auth.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/services/ocr.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/services/wine_parser.py +0 -0
- {winebox-0.1.3 → winebox-0.1.5}/winebox/static/favicon.svg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: winebox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Wine Cellar Management Application with OCR label scanning
|
|
5
5
|
Project-URL: Homepage, https://github.com/jdrumgoole/winebox
|
|
6
6
|
Project-URL: Repository, https://github.com/jdrumgoole/winebox
|
|
@@ -33,6 +33,7 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
33
33
|
Requires-Dist: pytesseract>=0.3.10
|
|
34
34
|
Requires-Dist: python-jose[cryptography]>=3.3.0
|
|
35
35
|
Requires-Dist: python-multipart>=0.0.6
|
|
36
|
+
Requires-Dist: slowapi>=0.1.9
|
|
36
37
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
37
38
|
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
38
39
|
Provides-Extra: dev
|
|
@@ -41,6 +42,8 @@ Requires-Dist: httpx>=0.26.0; extra == 'dev'
|
|
|
41
42
|
Requires-Dist: invoke>=2.2.0; extra == 'dev'
|
|
42
43
|
Requires-Dist: myst-parser>=2.0.0; extra == 'dev'
|
|
43
44
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest-playwright>=0.4.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest-xdist>=3.5.0; extra == 'dev'
|
|
44
47
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
45
48
|
Requires-Dist: sphinx>=7.0.0; extra == 'dev'
|
|
46
49
|
Description-Content-Type: text/markdown
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -162,6 +162,8 @@ winebox/
|
|
|
162
162
|
│ ├── routers/ # API endpoints
|
|
163
163
|
│ ├── services/ # Business logic
|
|
164
164
|
│ └── static/ # Web interface
|
|
165
|
+
├── scripts/ # Utility scripts
|
|
166
|
+
│ └── migrations/ # Database migration system
|
|
165
167
|
├── tests/ # Test suite
|
|
166
168
|
├── docs/ # Documentation
|
|
167
169
|
├── data/ # Database and images
|
|
@@ -41,6 +41,11 @@ The check-in process adds bottles to your cellar inventory.
|
|
|
41
41
|
6. **Add notes** (optional):
|
|
42
42
|
- Where you purchased it, price, occasion, etc.
|
|
43
43
|
7. Click **Check In Wine**
|
|
44
|
+
8. **Review in confirmation dialog**:
|
|
45
|
+
- A confirmation dialog appears with all editable fields
|
|
46
|
+
- Make any final adjustments to wine details
|
|
47
|
+
- View raw label text by expanding the "Raw Label Text" section
|
|
48
|
+
- Click **Confirm** to save or **Cancel** to return to the form
|
|
44
49
|
|
|
45
50
|
### Using the API
|
|
46
51
|
|
|
@@ -178,3 +183,62 @@ Your data is stored in:
|
|
|
178
183
|
- **Images**: `data/images/`
|
|
179
184
|
|
|
180
185
|
Back up these files regularly to preserve your cellar records.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
### Running Tests
|
|
190
|
+
|
|
191
|
+
WineBox has both unit tests and end-to-end (E2E) browser tests.
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Run all tests (unit + E2E)
|
|
195
|
+
invoke test
|
|
196
|
+
|
|
197
|
+
# Run only unit tests (fast, no server required)
|
|
198
|
+
invoke test-unit
|
|
199
|
+
|
|
200
|
+
# Run only E2E tests (requires running server)
|
|
201
|
+
invoke test-e2e
|
|
202
|
+
|
|
203
|
+
# Run E2E tests with more workers for faster execution
|
|
204
|
+
invoke test-e2e --workers 8
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Note**: E2E tests use Playwright for browser automation and create unique test users for parallel execution. The server must be running (`invoke start-background`) before running E2E tests.
|
|
208
|
+
|
|
209
|
+
### Invoke Tasks
|
|
210
|
+
|
|
211
|
+
Common development tasks:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Server management
|
|
215
|
+
invoke start # Start server in foreground
|
|
216
|
+
invoke start-background # Start server in background
|
|
217
|
+
invoke stop # Stop the server
|
|
218
|
+
invoke restart # Restart the server
|
|
219
|
+
invoke status # Check server status
|
|
220
|
+
invoke logs # View server logs
|
|
221
|
+
|
|
222
|
+
# Database management
|
|
223
|
+
invoke init-db # Initialize database
|
|
224
|
+
invoke purge --force # Delete database and images
|
|
225
|
+
invoke purge-wines --force # Delete wines but keep users
|
|
226
|
+
|
|
227
|
+
# Database migrations
|
|
228
|
+
uv run python -m scripts.migrations.runner status # Show version
|
|
229
|
+
uv run python -m scripts.migrations.runner up # Migrate to latest
|
|
230
|
+
uv run python -m scripts.migrations.runner down --to 0 # Revert to version
|
|
231
|
+
uv run python -m scripts.migrations.runner history # Show history
|
|
232
|
+
|
|
233
|
+
# User management
|
|
234
|
+
invoke add-user <username> --password <pass>
|
|
235
|
+
invoke remove-user <username> --force
|
|
236
|
+
invoke list-users
|
|
237
|
+
invoke disable-user <username>
|
|
238
|
+
invoke enable-user <username>
|
|
239
|
+
invoke passwd <username> --password <newpass>
|
|
240
|
+
|
|
241
|
+
# Documentation
|
|
242
|
+
invoke docs-build # Build Sphinx documentation
|
|
243
|
+
invoke docs-serve # Build and serve docs locally
|
|
244
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "winebox"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.5"
|
|
4
4
|
description = "Wine Cellar Management Application with OCR label scanning"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -37,6 +37,7 @@ dependencies = [
|
|
|
37
37
|
"bcrypt>=4.0.0,<4.1.0",
|
|
38
38
|
"python-jose[cryptography]>=3.3.0",
|
|
39
39
|
"anthropic>=0.40.0",
|
|
40
|
+
"slowapi>=0.1.9",
|
|
40
41
|
]
|
|
41
42
|
|
|
42
43
|
[project.urls]
|
|
@@ -48,6 +49,8 @@ Issues = "https://github.com/jdrumgoole/winebox/issues"
|
|
|
48
49
|
dev = [
|
|
49
50
|
"pytest>=8.0.0",
|
|
50
51
|
"pytest-asyncio>=0.23.0",
|
|
52
|
+
"pytest-playwright>=0.4.0",
|
|
53
|
+
"pytest-xdist>=3.5.0",
|
|
51
54
|
"httpx>=0.26.0",
|
|
52
55
|
"invoke>=2.2.0",
|
|
53
56
|
"sphinx>=7.0.0",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Scripts package for WineBox utility scripts."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Database migration system for WineBox.
|
|
2
|
+
|
|
3
|
+
This package provides versioned database migrations with forward and reverse support.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
# Show current version and available migrations
|
|
7
|
+
uv run python -m scripts.migrations.runner status
|
|
8
|
+
|
|
9
|
+
# Migrate to latest version
|
|
10
|
+
uv run python -m scripts.migrations.runner up
|
|
11
|
+
|
|
12
|
+
# Migrate to specific version
|
|
13
|
+
uv run python -m scripts.migrations.runner up --to 2
|
|
14
|
+
|
|
15
|
+
# Revert to specific version
|
|
16
|
+
uv run python -m scripts.migrations.runner down --to 0
|
|
17
|
+
|
|
18
|
+
# Show migration history
|
|
19
|
+
uv run python -m scripts.migrations.runner history
|
|
20
|
+
"""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Migration: Add full_name and anthropic_api_key columns to users table.
|
|
3
|
+
|
|
4
|
+
This migration adds user settings columns:
|
|
5
|
+
- full_name: Optional display name for the user
|
|
6
|
+
- anthropic_api_key: Optional API key for Claude Vision (per-user override)
|
|
7
|
+
|
|
8
|
+
Version: 0 -> 1
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sqlite3
|
|
12
|
+
|
|
13
|
+
# Migration metadata
|
|
14
|
+
SOURCE_VERSION = 0
|
|
15
|
+
TARGET_VERSION = 1
|
|
16
|
+
DESCRIPTION = "Add full_name and anthropic_api_key to users table"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def migrate(cursor: sqlite3.Cursor) -> None:
|
|
20
|
+
"""Apply the forward migration.
|
|
21
|
+
|
|
22
|
+
Adds full_name and anthropic_api_key columns to the users table.
|
|
23
|
+
These are nullable columns, so existing users will have NULL values.
|
|
24
|
+
"""
|
|
25
|
+
# Check existing columns
|
|
26
|
+
cursor.execute("PRAGMA table_info(users)")
|
|
27
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
28
|
+
|
|
29
|
+
# Add full_name column if it doesn't exist
|
|
30
|
+
if "full_name" not in columns:
|
|
31
|
+
cursor.execute("ALTER TABLE users ADD COLUMN full_name VARCHAR(255)")
|
|
32
|
+
|
|
33
|
+
# Add anthropic_api_key column if it doesn't exist
|
|
34
|
+
if "anthropic_api_key" not in columns:
|
|
35
|
+
cursor.execute("ALTER TABLE users ADD COLUMN anthropic_api_key VARCHAR(255)")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate(cursor: sqlite3.Cursor) -> bool:
|
|
39
|
+
"""Validate the migration was successful.
|
|
40
|
+
|
|
41
|
+
Returns True if both columns exist in the users table.
|
|
42
|
+
"""
|
|
43
|
+
cursor.execute("PRAGMA table_info(users)")
|
|
44
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
45
|
+
|
|
46
|
+
return "full_name" in columns and "anthropic_api_key" in columns
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Revert migration: Remove full_name and anthropic_api_key columns from users table.
|
|
3
|
+
|
|
4
|
+
This revert removes the user settings columns added in version 1.
|
|
5
|
+
WARNING: Data in these columns will be lost!
|
|
6
|
+
|
|
7
|
+
Version: 1 -> 0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sqlite3
|
|
11
|
+
|
|
12
|
+
# Migration metadata
|
|
13
|
+
SOURCE_VERSION = 1
|
|
14
|
+
TARGET_VERSION = 0
|
|
15
|
+
DESCRIPTION = "Remove full_name and anthropic_api_key from users table"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def migrate(cursor: sqlite3.Cursor) -> None:
|
|
19
|
+
"""Apply the reverse migration (revert).
|
|
20
|
+
|
|
21
|
+
Removes full_name and anthropic_api_key columns from the users table.
|
|
22
|
+
|
|
23
|
+
Since SQLite doesn't support DROP COLUMN in older versions, we use
|
|
24
|
+
the table rebuild pattern:
|
|
25
|
+
1. Create new table without the columns
|
|
26
|
+
2. Copy data from old table
|
|
27
|
+
3. Drop old table
|
|
28
|
+
4. Rename new table
|
|
29
|
+
5. Recreate indexes
|
|
30
|
+
|
|
31
|
+
WARNING: Data in full_name and anthropic_api_key columns will be lost!
|
|
32
|
+
"""
|
|
33
|
+
# Check if columns exist (may have already been removed)
|
|
34
|
+
cursor.execute("PRAGMA table_info(users)")
|
|
35
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
36
|
+
|
|
37
|
+
if "full_name" not in columns and "anthropic_api_key" not in columns:
|
|
38
|
+
# Columns already removed, nothing to do
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Disable foreign key checks during table rebuild
|
|
42
|
+
cursor.execute("PRAGMA foreign_keys = OFF")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# 1. Create new table without full_name and anthropic_api_key
|
|
46
|
+
cursor.execute("""
|
|
47
|
+
CREATE TABLE users_new (
|
|
48
|
+
id CHAR(36) PRIMARY KEY,
|
|
49
|
+
username VARCHAR(50) NOT NULL UNIQUE,
|
|
50
|
+
email VARCHAR(255) UNIQUE,
|
|
51
|
+
hashed_password VARCHAR(255) NOT NULL,
|
|
52
|
+
is_active BOOLEAN DEFAULT 1 NOT NULL,
|
|
53
|
+
is_admin BOOLEAN DEFAULT 0 NOT NULL,
|
|
54
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
55
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
56
|
+
last_login DATETIME
|
|
57
|
+
)
|
|
58
|
+
""")
|
|
59
|
+
|
|
60
|
+
# 2. Copy data from old table (excluding removed columns)
|
|
61
|
+
cursor.execute("""
|
|
62
|
+
INSERT INTO users_new (
|
|
63
|
+
id, username, email, hashed_password,
|
|
64
|
+
is_active, is_admin, created_at, updated_at, last_login
|
|
65
|
+
)
|
|
66
|
+
SELECT
|
|
67
|
+
id, username, email, hashed_password,
|
|
68
|
+
is_active, is_admin, created_at, updated_at, last_login
|
|
69
|
+
FROM users
|
|
70
|
+
""")
|
|
71
|
+
|
|
72
|
+
# 3. Drop old table
|
|
73
|
+
cursor.execute("DROP TABLE users")
|
|
74
|
+
|
|
75
|
+
# 4. Rename new table
|
|
76
|
+
cursor.execute("ALTER TABLE users_new RENAME TO users")
|
|
77
|
+
|
|
78
|
+
# 5. Recreate indexes
|
|
79
|
+
cursor.execute(
|
|
80
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS ix_users_username ON users (username)"
|
|
81
|
+
)
|
|
82
|
+
cursor.execute(
|
|
83
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS ix_users_email ON users (email)"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
finally:
|
|
87
|
+
# Re-enable foreign key checks
|
|
88
|
+
cursor.execute("PRAGMA foreign_keys = ON")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate(cursor: sqlite3.Cursor) -> bool:
|
|
92
|
+
"""Validate the revert was successful.
|
|
93
|
+
|
|
94
|
+
Returns True if both columns have been removed from the users table.
|
|
95
|
+
"""
|
|
96
|
+
cursor.execute("PRAGMA table_info(users)")
|
|
97
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
98
|
+
|
|
99
|
+
# Verify columns are removed
|
|
100
|
+
if "full_name" in columns or "anthropic_api_key" in columns:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
# Verify essential columns still exist
|
|
104
|
+
required_columns = {
|
|
105
|
+
"id", "username", "email", "hashed_password",
|
|
106
|
+
"is_active", "is_admin", "created_at", "updated_at", "last_login"
|
|
107
|
+
}
|
|
108
|
+
return required_columns.issubset(columns)
|