winebox 0.1.3__py3-none-any.whl → 0.1.5__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.
winebox/static/js/app.js CHANGED
@@ -9,6 +9,7 @@ const API_BASE = '/api';
9
9
  let currentPage = 'dashboard';
10
10
  let authToken = localStorage.getItem('winebox_token');
11
11
  let currentUser = null;
12
+ let lastScanResult = null; // Store last scan result to avoid rescanning on checkin
12
13
 
13
14
  // Initialize app
14
15
  document.addEventListener('DOMContentLoaded', () => {
@@ -42,11 +43,21 @@ function initAuth() {
42
43
  // Logout button
43
44
  document.getElementById('logout-btn').addEventListener('click', handleLogout);
44
45
 
45
- // Password toggle
46
- const passwordToggle = document.querySelector('.password-toggle');
47
- if (passwordToggle) {
48
- passwordToggle.addEventListener('click', function() {
49
- const passwordInput = document.getElementById('login-password');
46
+ // Username link to settings
47
+ document.getElementById('username-display').addEventListener('click', (e) => {
48
+ e.preventDefault();
49
+ navigateTo('settings');
50
+ });
51
+
52
+ // Password toggle for all password fields
53
+ initPasswordToggles();
54
+ }
55
+
56
+ function initPasswordToggles() {
57
+ document.querySelectorAll('.password-toggle').forEach(toggle => {
58
+ toggle.addEventListener('click', function() {
59
+ const wrapper = this.closest('.password-input-wrapper');
60
+ const passwordInput = wrapper.querySelector('input[type="password"], input[type="text"]');
50
61
  const eyeIcon = this.querySelector('.eye-icon');
51
62
  const eyeOffIcon = this.querySelector('.eye-off-icon');
52
63
 
@@ -62,7 +73,7 @@ function initAuth() {
62
73
  this.setAttribute('aria-label', 'Show password');
63
74
  }
64
75
  });
65
- }
76
+ });
66
77
  }
67
78
 
68
79
  async function checkAuth() {
@@ -95,7 +106,9 @@ function showMainApp() {
95
106
  document.body.classList.remove('logged-out');
96
107
  document.getElementById('page-login').classList.remove('active');
97
108
  document.getElementById('user-info').style.display = 'flex';
98
- document.getElementById('username-display').textContent = currentUser.username;
109
+ // Display full name if available, otherwise username
110
+ const displayName = currentUser.full_name || currentUser.username;
111
+ document.getElementById('username-display').textContent = displayName;
99
112
  loadDashboard();
100
113
  }
101
114
 
@@ -203,6 +216,9 @@ function navigateTo(page) {
203
216
  case 'search':
204
217
  // Search results loaded on form submit
205
218
  break;
219
+ case 'settings':
220
+ loadSettings();
221
+ break;
206
222
  }
207
223
  }
208
224
 
@@ -215,6 +231,7 @@ function initForms() {
215
231
  document.getElementById('front-preview').innerHTML = 'Tap to take photo or select image';
216
232
  document.getElementById('back-preview').innerHTML = 'Tap to take photo or select image';
217
233
  clearRawLabelText();
234
+ lastScanResult = null; // Clear stored scan result
218
235
  });
219
236
 
220
237
  // Image previews - make clickable to trigger file input
@@ -271,6 +288,12 @@ function initForms() {
271
288
 
272
289
  // History filter
273
290
  document.getElementById('history-filter').addEventListener('change', loadHistory);
291
+
292
+ // Settings forms
293
+ document.getElementById('profile-form').addEventListener('submit', handleProfileUpdate);
294
+ document.getElementById('password-form').addEventListener('submit', handlePasswordChange);
295
+ document.getElementById('api-key-form').addEventListener('submit', handleApiKeyUpdate);
296
+ document.getElementById('delete-api-key-btn').addEventListener('click', handleApiKeyDelete);
274
297
  }
275
298
 
276
299
  function previewImage(input, previewId) {
@@ -317,6 +340,7 @@ async function scanLabels() {
317
340
  }
318
341
 
319
342
  const result = await response.json();
343
+ lastScanResult = result; // Store for checkin
320
344
  populateFormFromScan(result);
321
345
  const methodName = result.method === 'claude_vision' ? 'Claude Vision' : 'Tesseract OCR';
322
346
  showToast(`Label scanned with ${methodName}`, 'success');
@@ -421,70 +445,86 @@ function showScanningIndicator(show) {
421
445
  }
422
446
  }
423
447
 
424
- async function handleCheckin(e) {
448
+ // Store pending checkin data for confirmation
449
+ let pendingCheckinData = null;
450
+
451
+ function handleCheckin(e) {
425
452
  e.preventDefault();
426
- const form = e.target;
427
- const formData = new FormData(form);
428
453
 
429
- try {
430
- const response = await fetchWithAuth(`${API_BASE}/wines/checkin`, {
431
- method: 'POST',
432
- body: formData
433
- });
454
+ const frontLabel = document.getElementById('front-label');
455
+ if (!frontLabel.files || !frontLabel.files[0]) {
456
+ showToast('Please select a front label image', 'error');
457
+ return;
458
+ }
434
459
 
435
- if (!response.ok) {
436
- const error = await response.json();
437
- throw new Error(error.detail || 'Check-in failed');
438
- }
460
+ // Store the form data for later submission
461
+ pendingCheckinData = {
462
+ frontLabel: frontLabel.files[0],
463
+ backLabel: document.getElementById('back-label').files?.[0] || null,
464
+ name: document.getElementById('wine-name').value,
465
+ winery: document.getElementById('winery').value,
466
+ vintage: document.getElementById('vintage').value,
467
+ grapeVariety: document.getElementById('grape-variety').value,
468
+ region: document.getElementById('region').value,
469
+ country: document.getElementById('country').value,
470
+ alcohol: document.getElementById('alcohol').value,
471
+ quantity: document.getElementById('quantity').value || '1',
472
+ notes: document.getElementById('notes').value,
473
+ frontLabelText: lastScanResult?.ocr?.front_label_text || '',
474
+ backLabelText: lastScanResult?.ocr?.back_label_text || ''
475
+ };
439
476
 
440
- const wine = await response.json();
441
- showCheckinConfirmation(wine);
442
- form.reset();
443
- } catch (error) {
444
- showToast(error.message, 'error');
445
- }
477
+ // Show the confirmation modal with editable fields
478
+ showCheckinConfirmation();
446
479
  }
447
480
 
448
- function showCheckinConfirmation(wine) {
481
+ function showCheckinConfirmation() {
449
482
  const modal = document.getElementById('checkin-confirm-modal');
483
+ const data = pendingCheckinData;
450
484
 
451
- // Set wine name
452
- document.getElementById('checkin-confirm-name').textContent = wine.name;
453
-
454
- // Set image
485
+ // Set image preview
455
486
  const imageContainer = document.getElementById('checkin-confirm-image');
456
- if (wine.front_label_image_path) {
457
- imageContainer.innerHTML = `<img src="${API_BASE}/images/${wine.front_label_image_path}" alt="Wine label">`;
487
+ if (data.frontLabel) {
488
+ const reader = new FileReader();
489
+ reader.onload = (e) => {
490
+ imageContainer.innerHTML = `<img src="${e.target.result}" alt="Wine label">`;
491
+ };
492
+ reader.readAsDataURL(data.frontLabel);
458
493
  } else {
459
494
  imageContainer.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);">No image</div>';
460
495
  }
461
496
 
462
- // Set parsed fields
463
- const fieldsContainer = document.getElementById('checkin-confirm-fields');
464
- const fields = [
465
- { label: 'Winery', value: wine.winery },
466
- { label: 'Vintage', value: wine.vintage },
467
- { label: 'Grape Variety', value: wine.grape_variety },
468
- { label: 'Region', value: wine.region },
469
- { label: 'Country', value: wine.country },
470
- { label: 'Alcohol %', value: wine.alcohol_percentage ? `${wine.alcohol_percentage}%` : null },
471
- { label: 'Quantity', value: wine.inventory?.quantity || 1 }
472
- ];
473
-
474
- fieldsContainer.innerHTML = fields.map(field => `
475
- <div class="checkin-confirm-field">
476
- <div class="label">${field.label}</div>
477
- <div class="value ${field.value ? '' : 'empty'}">${field.value || 'Not detected'}</div>
478
- </div>
479
- `).join('');
480
-
481
- // Set OCR text
482
- document.getElementById('checkin-confirm-front-ocr').textContent = wine.front_label_text || 'No text extracted';
497
+ // Populate editable fields
498
+ document.getElementById('confirm-wine-name').value = data.name || '';
499
+ document.getElementById('confirm-winery').value = data.winery || '';
500
+ document.getElementById('confirm-vintage').value = data.vintage || '';
501
+ document.getElementById('confirm-grape-variety').value = data.grapeVariety || '';
502
+ document.getElementById('confirm-region').value = data.region || '';
503
+ document.getElementById('confirm-country').value = data.country || '';
504
+ document.getElementById('confirm-alcohol').value = data.alcohol || '';
505
+ document.getElementById('confirm-quantity').value = data.quantity || '1';
506
+ document.getElementById('confirm-notes').value = data.notes || '';
507
+
508
+ // Set OCR text (hidden by default)
509
+ const ocrSection = document.getElementById('confirm-ocr-section');
510
+ const ocrContent = document.getElementById('confirm-ocr-content');
511
+ const ocrToggle = document.getElementById('confirm-ocr-toggle');
512
+
513
+ if (data.frontLabelText) {
514
+ document.getElementById('checkin-confirm-front-ocr').textContent = data.frontLabelText;
515
+ ocrSection.style.display = 'block';
516
+ ocrContent.style.display = 'none'; // Hidden by default
517
+ ocrSection.classList.remove('open');
518
+ ocrToggle.querySelector('.collapse-icon').textContent = '+';
519
+ ocrToggle.querySelector('.label').textContent = 'Show Raw Label Text';
520
+ } else {
521
+ ocrSection.style.display = 'none';
522
+ }
483
523
 
484
524
  const backOcrSection = document.getElementById('checkin-confirm-back-ocr-section');
485
- if (wine.back_label_text) {
525
+ if (data.backLabelText) {
486
526
  backOcrSection.style.display = 'block';
487
- document.getElementById('checkin-confirm-back-ocr').textContent = wine.back_label_text;
527
+ document.getElementById('checkin-confirm-back-ocr').textContent = data.backLabelText;
488
528
  } else {
489
529
  backOcrSection.style.display = 'none';
490
530
  }
@@ -492,18 +532,91 @@ function showCheckinConfirmation(wine) {
492
532
  // Show modal
493
533
  modal.classList.add('active');
494
534
 
495
- // Set up button handlers
496
- document.getElementById('checkin-confirm-done').onclick = () => {
497
- modal.classList.remove('active');
498
- navigateTo('cellar');
535
+ // Set up OCR toggle
536
+ ocrToggle.onclick = () => {
537
+ ocrSection.classList.toggle('open');
538
+ if (ocrSection.classList.contains('open')) {
539
+ ocrContent.style.display = 'block';
540
+ ocrToggle.querySelector('.collapse-icon').textContent = '-';
541
+ ocrToggle.querySelector('.label').textContent = 'Hide Raw Label Text';
542
+ } else {
543
+ ocrContent.style.display = 'none';
544
+ ocrToggle.querySelector('.collapse-icon').textContent = '+';
545
+ ocrToggle.querySelector('.label').textContent = 'Show Raw Label Text';
546
+ }
499
547
  };
500
548
 
501
- document.getElementById('checkin-confirm-another').onclick = () => {
549
+ // Set up button handlers
550
+ document.getElementById('checkin-confirm-btn').onclick = submitCheckin;
551
+ document.getElementById('checkin-cancel-btn').onclick = cancelCheckin;
552
+ }
553
+
554
+ async function submitCheckin() {
555
+ const modal = document.getElementById('checkin-confirm-modal');
556
+ const data = pendingCheckinData;
557
+
558
+ // Build form data from confirmation modal fields
559
+ const formData = new FormData();
560
+ formData.append('front_label', data.frontLabel);
561
+ if (data.backLabel) {
562
+ formData.append('back_label', data.backLabel);
563
+ }
564
+
565
+ // Get values from confirmation modal (may have been edited)
566
+ formData.append('name', document.getElementById('confirm-wine-name').value);
567
+ formData.append('winery', document.getElementById('confirm-winery').value);
568
+ const vintage = document.getElementById('confirm-vintage').value;
569
+ if (vintage) formData.append('vintage', vintage);
570
+ formData.append('grape_variety', document.getElementById('confirm-grape-variety').value);
571
+ formData.append('region', document.getElementById('confirm-region').value);
572
+ formData.append('country', document.getElementById('confirm-country').value);
573
+ const alcohol = document.getElementById('confirm-alcohol').value;
574
+ if (alcohol) formData.append('alcohol_percentage', alcohol);
575
+ formData.append('quantity', document.getElementById('confirm-quantity').value || '1');
576
+ formData.append('notes', document.getElementById('confirm-notes').value);
577
+
578
+ // Include pre-scanned OCR text to avoid rescanning (saves API costs)
579
+ if (data.frontLabelText) {
580
+ formData.append('front_label_text', data.frontLabelText);
581
+ }
582
+ if (data.backLabelText) {
583
+ formData.append('back_label_text', data.backLabelText);
584
+ }
585
+
586
+ try {
587
+ const response = await fetchWithAuth(`${API_BASE}/wines/checkin`, {
588
+ method: 'POST',
589
+ body: formData
590
+ });
591
+
592
+ if (!response.ok) {
593
+ const error = await response.json();
594
+ throw new Error(error.detail || 'Check-in failed');
595
+ }
596
+
597
+ const wine = await response.json();
598
+ showToast(`Successfully checked in: ${wine.name}`, 'success');
599
+
600
+ // Close modal and reset form
502
601
  modal.classList.remove('active');
503
- // Reset form previews
602
+ document.getElementById('checkin-form').reset();
504
603
  document.getElementById('front-preview').innerHTML = 'Tap to take photo or select image';
505
604
  document.getElementById('back-preview').innerHTML = 'Tap to take photo or select image';
506
- };
605
+ clearRawLabelText();
606
+ lastScanResult = null;
607
+ pendingCheckinData = null;
608
+
609
+ // Navigate to cellar
610
+ navigateTo('cellar');
611
+ } catch (error) {
612
+ showToast(error.message, 'error');
613
+ }
614
+ }
615
+
616
+ function cancelCheckin() {
617
+ const modal = document.getElementById('checkin-confirm-modal');
618
+ modal.classList.remove('active');
619
+ // Keep the form data so user can make changes and try again
507
620
  }
508
621
 
509
622
  async function handleSearch(e) {
@@ -995,3 +1108,171 @@ function debounce(func, wait) {
995
1108
  timeout = setTimeout(later, wait);
996
1109
  };
997
1110
  }
1111
+
1112
+ // Settings
1113
+ function loadSettings() {
1114
+ // Populate profile form with current user data
1115
+ document.getElementById('settings-username').value = currentUser.username;
1116
+ document.getElementById('settings-fullname').value = currentUser.full_name || '';
1117
+
1118
+ // Update API key status
1119
+ updateApiKeyStatus(currentUser.has_api_key);
1120
+
1121
+ // Clear password form
1122
+ document.getElementById('password-form').reset();
1123
+
1124
+ // Clear API key form
1125
+ document.getElementById('api-key').value = '';
1126
+ }
1127
+
1128
+ function updateApiKeyStatus(hasApiKey) {
1129
+ const statusDiv = document.getElementById('api-key-status');
1130
+ const statusText = statusDiv.querySelector('.status-text');
1131
+ const deleteBtn = document.getElementById('delete-api-key-btn');
1132
+
1133
+ statusDiv.classList.remove('configured', 'not-configured');
1134
+
1135
+ if (hasApiKey) {
1136
+ statusDiv.classList.add('configured');
1137
+ statusText.textContent = 'API key is configured';
1138
+ deleteBtn.style.display = 'inline-block';
1139
+ } else {
1140
+ statusDiv.classList.add('not-configured');
1141
+ statusText.textContent = 'No API key configured';
1142
+ deleteBtn.style.display = 'none';
1143
+ }
1144
+ }
1145
+
1146
+ async function handleProfileUpdate(e) {
1147
+ e.preventDefault();
1148
+
1149
+ const fullName = document.getElementById('settings-fullname').value.trim();
1150
+
1151
+ try {
1152
+ const response = await fetchWithAuth(`${API_BASE}/auth/profile`, {
1153
+ method: 'PUT',
1154
+ headers: {
1155
+ 'Content-Type': 'application/json',
1156
+ },
1157
+ body: JSON.stringify({ full_name: fullName || null })
1158
+ });
1159
+
1160
+ if (!response.ok) {
1161
+ const error = await response.json();
1162
+ throw new Error(error.detail || 'Failed to update profile');
1163
+ }
1164
+
1165
+ const updatedUser = await response.json();
1166
+ currentUser = updatedUser;
1167
+
1168
+ // Update display name in header
1169
+ const displayName = currentUser.full_name || currentUser.username;
1170
+ document.getElementById('username-display').textContent = displayName;
1171
+
1172
+ showToast('Profile updated successfully', 'success');
1173
+ } catch (error) {
1174
+ showToast(error.message, 'error');
1175
+ }
1176
+ }
1177
+
1178
+ async function handlePasswordChange(e) {
1179
+ e.preventDefault();
1180
+
1181
+ const currentPassword = document.getElementById('current-password').value;
1182
+ const newPassword = document.getElementById('new-password').value;
1183
+ const confirmPassword = document.getElementById('confirm-password').value;
1184
+
1185
+ if (newPassword !== confirmPassword) {
1186
+ showToast('New passwords do not match', 'error');
1187
+ return;
1188
+ }
1189
+
1190
+ if (newPassword.length < 6) {
1191
+ showToast('Password must be at least 6 characters', 'error');
1192
+ return;
1193
+ }
1194
+
1195
+ try {
1196
+ const response = await fetchWithAuth(`${API_BASE}/auth/password`, {
1197
+ method: 'PUT',
1198
+ headers: {
1199
+ 'Content-Type': 'application/json',
1200
+ },
1201
+ body: JSON.stringify({
1202
+ current_password: currentPassword,
1203
+ new_password: newPassword
1204
+ })
1205
+ });
1206
+
1207
+ if (!response.ok) {
1208
+ const error = await response.json();
1209
+ throw new Error(error.detail || 'Failed to change password');
1210
+ }
1211
+
1212
+ document.getElementById('password-form').reset();
1213
+ showToast('Password changed successfully', 'success');
1214
+ } catch (error) {
1215
+ showToast(error.message, 'error');
1216
+ }
1217
+ }
1218
+
1219
+ async function handleApiKeyUpdate(e) {
1220
+ e.preventDefault();
1221
+
1222
+ const apiKey = document.getElementById('api-key').value.trim();
1223
+
1224
+ if (!apiKey) {
1225
+ showToast('Please enter an API key', 'error');
1226
+ return;
1227
+ }
1228
+
1229
+ if (!apiKey.startsWith('sk-ant-')) {
1230
+ showToast('Invalid API key format. Anthropic API keys start with sk-ant-', 'error');
1231
+ return;
1232
+ }
1233
+
1234
+ try {
1235
+ const response = await fetchWithAuth(`${API_BASE}/auth/api-key`, {
1236
+ method: 'PUT',
1237
+ headers: {
1238
+ 'Content-Type': 'application/json',
1239
+ },
1240
+ body: JSON.stringify({ api_key: apiKey })
1241
+ });
1242
+
1243
+ if (!response.ok) {
1244
+ const error = await response.json();
1245
+ throw new Error(error.detail || 'Failed to update API key');
1246
+ }
1247
+
1248
+ currentUser.has_api_key = true;
1249
+ updateApiKeyStatus(true);
1250
+ document.getElementById('api-key').value = '';
1251
+ showToast('API key saved successfully', 'success');
1252
+ } catch (error) {
1253
+ showToast(error.message, 'error');
1254
+ }
1255
+ }
1256
+
1257
+ async function handleApiKeyDelete() {
1258
+ if (!confirm('Are you sure you want to delete your API key? Wine label scanning will use the default system key if available.')) {
1259
+ return;
1260
+ }
1261
+
1262
+ try {
1263
+ const response = await fetchWithAuth(`${API_BASE}/auth/api-key`, {
1264
+ method: 'DELETE'
1265
+ });
1266
+
1267
+ if (!response.ok) {
1268
+ const error = await response.json();
1269
+ throw new Error(error.detail || 'Failed to delete API key');
1270
+ }
1271
+
1272
+ currentUser.has_api_key = false;
1273
+ updateApiKeyStatus(false);
1274
+ showToast('API key deleted successfully', 'success');
1275
+ } catch (error) {
1276
+ showToast(error.message, 'error');
1277
+ }
1278
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: winebox
3
- Version: 0.1.3
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
@@ -1,36 +1,36 @@
1
- winebox/__init__.py,sha256=1MbD6vmUlpuZKB-RvWkszuH5NNsw0MFclRCQgwErdNQ,75
2
- winebox/config.py,sha256=mxdWkchZdYkPrSpfgn1RV26nZK09_3rql0iGbcAGvM0,1233
1
+ winebox/__init__.py,sha256=ESGS6HomBT7Xl256jY5X0p4xKi9lh7XlrhskSQosf2U,75
2
+ winebox/config.py,sha256=pfwXrKsI4KsXtA4lehEhAc9xfjBGu1OzdhiUq4mixto,2438
3
3
  winebox/database.py,sha256=jTdf9mb48D8644THo6Mx6lHCKDFlAZ18P8Dvuk2eROI,1128
4
- winebox/main.py,sha256=wlXrBwMvJQhG3g8mmOGWp-QbEEbR805SaElBryR61t0,2398
4
+ winebox/main.py,sha256=RCB1oCxWbKXiMtQvfLabhGI_xvDjr_pGLEYeo2SSgeU,4104
5
5
  winebox/cli/__init__.py,sha256=FOUoclklBXVfsfON3ohA8v_coJ9p5LSbVYvTucyXieg,29
6
6
  winebox/cli/server.py,sha256=aN4znSvQRCZFZlQ1gcGag7OCFngwzUJdi084ZdNrnNI,8416
7
7
  winebox/cli/user_admin.py,sha256=xwkd4-YN9uL6hX7-AuiDUNekG7x45l1lKor7AsOUxds,8549
8
8
  winebox/models/__init__.py,sha256=2avQWHx53-5Z6jnzFlhh5u10Dw99w7en2nqCXEbwqRI,312
9
9
  winebox/models/inventory.py,sha256=VLxpEIdYbD-oG9OfIGkHKDKm-cbSSo_aZGx66wRqR7Y,1324
10
10
  winebox/models/transaction.py,sha256=9k1AAK0unYNseDubTaiFnGUGDVtuOqrjNUjIDukFDbc,1812
11
- winebox/models/user.py,sha256=VP7NvbQDJMygTtt9Q5zPiBmtvtOCW5Cvaidi8zXDp98,1570
11
+ winebox/models/user.py,sha256=Ke7DJsVtUzHlKQUiHEDHTi4Rm-tqCbrin4iZyt4SP9A,1734
12
12
  winebox/models/wine.py,sha256=R40TTRunPcTG4_D8kEWhopM2k6ne_awET7ctY4fk-HE,2503
13
13
  winebox/routers/__init__.py,sha256=TEjBLPCkG007ItXMONZ2TsjOxl_TvHis1v84IOkXm1A,167
14
- winebox/routers/auth.py,sha256=7rmld-TOIHmCMJfQKuQfkZFoNAAZd0zuj-mKqmAbHeg,2446
14
+ winebox/routers/auth.py,sha256=0F8tXJSo_oIYEY25dO2QZ-NtuF6-Fi4HnSLTCa6TWzs,5657
15
15
  winebox/routers/cellar.py,sha256=QSyAnPQ2hiNaoojLk-5hJy95OnG8RMHOD0h5LV6bAWM,3404
16
16
  winebox/routers/search.py,sha256=aIqwsjnB9e-D84VVcuye-01V_9WBUjFKzXNsX0gkRak,4971
17
17
  winebox/routers/transactions.py,sha256=K0UDIJbVSTjV7NdI_CS0pvoTflH3Yp89DmneyFyl9ok,2055
18
- winebox/routers/wines.py,sha256=muTwU0T21vkihNOyVtnXjQY7Fl6d2BRKBpJoIyyieA4,14238
18
+ winebox/routers/wines.py,sha256=szr9h7W2Q-Xxn8LYd6rXtEOVWST7hthXGrZ84xSyLpY,16948
19
19
  winebox/schemas/__init__.py,sha256=R4j1wdV1F-6kA45rHA8NUP2cqxBmLb2CMklo8tFjzGQ,357
20
20
  winebox/schemas/transaction.py,sha256=GV_AZGeuFp_nOkg902UUhrHGwB1DxcQmGoG82m1UccI,899
21
21
  winebox/schemas/wine.py,sha256=xHWDiV5bvEJuLPN2qWgOhqVMOTdlDSmfan5mtEy3Nvw,2301
22
22
  winebox/services/__init__.py,sha256=rlAd5FBcqZJ7rsrFqR8MwTqGJVerlQHVUleEmoIaz1Y,277
23
23
  winebox/services/auth.py,sha256=RDXP26hL3cGGxKcniXWMZ0sPUP6ew4LLMnfo4dv4P2E,3854
24
- winebox/services/image_storage.py,sha256=ftiX54dVuDT0DVLpyBOL9N7zhZkapI0l1TFknfPedno,2393
24
+ winebox/services/image_storage.py,sha256=r1QzOFY0KzcJVdQV2fa4df9GtV3T7weQB17oEQZLTTo,6306
25
25
  winebox/services/ocr.py,sha256=zFmQGj3a_jv21MBx8RfNkFIenAOnJ0O28JupSrsNC44,4925
26
- winebox/services/vision.py,sha256=9WKHrDdFK0AmmmnST7vnV50Vgr6UsKR-WUtflKklJLc,9100
26
+ winebox/services/vision.py,sha256=jINJ0RmmIkBhiuAyYAUCtmhxMWTz1pNcTSOUck-xjPg,10263
27
27
  winebox/services/wine_parser.py,sha256=n5ldv2x2XsTfpK3acYDyMNyZHdJ_8WQU0keOql14MJ0,10456
28
28
  winebox/static/favicon.svg,sha256=jYFlQ-dEfhUoHQJepoSEMYQx0jxk8HyuX0wrd4OApzE,1061
29
- winebox/static/index.html,sha256=7T_bgTKKGOQ0eKoo4scyVAD51GyCfXfNrJMTwlNvbYQ,17065
30
- winebox/static/css/style.css,sha256=TBVTdw5eEkm2du7GeS-sPTsWiE-_ttzAWozWLXOuBzU,25940
31
- winebox/static/js/app.js,sha256=RNDdXMaz3uC3emRnsaUPxDCi9Q5SPcUQeuIrTRAmX2U,34741
32
- winebox-0.1.3.dist-info/METADATA,sha256=UcVbvLKE-F1v3AiS8GISrF379XqEN2tF_B63ra0Je-k,8001
33
- winebox-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
- winebox-0.1.3.dist-info/entry_points.txt,sha256=XY-GMf023m8Iof3rmokkITONECm8gCOVkTX0rCkze30,103
35
- winebox-0.1.3.dist-info/licenses/LICENSE,sha256=3popbhCShFhWVwTLKeQE-JNDYuPOYfmCiz-LoQpizTA,1070
36
- winebox-0.1.3.dist-info/RECORD,,
29
+ winebox/static/index.html,sha256=LZKro_4jyebGXCPvP87TbBrKuVh_Nma7n9JRhUGwh2Q,27473
30
+ winebox/static/css/style.css,sha256=QEcmosNtf4_oH7JGYveQYRAzHdS5vh93fTVdyBA9YhM,29987
31
+ winebox/static/js/app.js,sha256=0TVu8WFHZhHt-jhFNFetAHCTn89xLc8exf99GjtFRD4,45251
32
+ winebox-0.1.5.dist-info/METADATA,sha256=kSNMBqDN-ut8IouWmP7cyke4694VrpLQ1S9aB-GsBzY,8138
33
+ winebox-0.1.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
34
+ winebox-0.1.5.dist-info/entry_points.txt,sha256=XY-GMf023m8Iof3rmokkITONECm8gCOVkTX0rCkze30,103
35
+ winebox-0.1.5.dist-info/licenses/LICENSE,sha256=3popbhCShFhWVwTLKeQE-JNDYuPOYfmCiz-LoQpizTA,1070
36
+ winebox-0.1.5.dist-info/RECORD,,