tbr-deal-finder 0.3.1__py3-none-any.whl → 0.3.3__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.
- tbr_deal_finder/__init__.py +1 -1
- tbr_deal_finder/desktop_updater.py +7 -83
- tbr_deal_finder/gui/main.py +80 -27
- tbr_deal_finder/gui/pages/all_books.py +7 -0
- tbr_deal_finder/gui/pages/base_book_page.py +1 -2
- tbr_deal_finder/gui/pages/book_details.py +2 -3
- tbr_deal_finder/gui/pages/latest_deals.py +24 -20
- tbr_deal_finder/gui/pages/settings.py +1 -0
- tbr_deal_finder/retailer_deal.py +20 -29
- tbr_deal_finder/utils.py +28 -19
- {tbr_deal_finder-0.3.1.dist-info → tbr_deal_finder-0.3.3.dist-info}/METADATA +1 -1
- {tbr_deal_finder-0.3.1.dist-info → tbr_deal_finder-0.3.3.dist-info}/RECORD +15 -15
- {tbr_deal_finder-0.3.1.dist-info → tbr_deal_finder-0.3.3.dist-info}/WHEEL +0 -0
- {tbr_deal_finder-0.3.1.dist-info → tbr_deal_finder-0.3.3.dist-info}/entry_points.txt +0 -0
- {tbr_deal_finder-0.3.1.dist-info → tbr_deal_finder-0.3.3.dist-info}/licenses/LICENSE +0 -0
tbr_deal_finder/__init__.py
CHANGED
@@ -43,12 +43,16 @@ class DesktopUpdater:
|
|
43
43
|
release_data = response.json()
|
44
44
|
latest_version = release_data["tag_name"].lstrip("v")
|
45
45
|
if version.parse(latest_version) > version.parse(self.current_version):
|
46
|
-
|
46
|
+
download_url = release_data["html_url"]
|
47
|
+
if self.platform == "darwin":
|
48
|
+
for asset in release_data["assets"]:
|
49
|
+
if asset["browser_download_url"].endswith(".dmg"):
|
50
|
+
download_url = asset["browser_download_url"]
|
51
|
+
|
47
52
|
return {
|
48
53
|
"version": latest_version,
|
49
|
-
"download_url":
|
54
|
+
"download_url": download_url,
|
50
55
|
"release_notes": release_data.get("body", ""),
|
51
|
-
"release_url": release_url
|
52
56
|
}
|
53
57
|
|
54
58
|
return None
|
@@ -56,86 +60,6 @@ class DesktopUpdater:
|
|
56
60
|
except Exception as e:
|
57
61
|
logger.error(f"Failed to check updates for {self.github_repo}: {e}")
|
58
62
|
return None
|
59
|
-
|
60
|
-
def download_update(self, download_url: str, progress_callback=None) -> Optional[Path]:
|
61
|
-
"""
|
62
|
-
Download the update file.
|
63
|
-
Returns path to downloaded file or None if failed.
|
64
|
-
"""
|
65
|
-
try:
|
66
|
-
response = requests.get(download_url, stream=True, timeout=30)
|
67
|
-
response.raise_for_status()
|
68
|
-
|
69
|
-
# Determine file extension
|
70
|
-
filename = download_url.split("/")[-1]
|
71
|
-
temp_file = Path(tempfile.gettempdir()) / filename
|
72
|
-
|
73
|
-
total_size = int(response.headers.get('content-length', 0))
|
74
|
-
downloaded = 0
|
75
|
-
|
76
|
-
with open(temp_file, 'wb') as f:
|
77
|
-
for chunk in response.iter_content(chunk_size=8192):
|
78
|
-
if chunk:
|
79
|
-
f.write(chunk)
|
80
|
-
downloaded += len(chunk)
|
81
|
-
|
82
|
-
if progress_callback and total_size > 0:
|
83
|
-
progress = (downloaded / total_size) * 100
|
84
|
-
progress_callback(progress)
|
85
|
-
|
86
|
-
return temp_file
|
87
|
-
|
88
|
-
except Exception as e:
|
89
|
-
logger.error(f"Failed to download update: {e}")
|
90
|
-
return None
|
91
|
-
|
92
|
-
def install_update(self, update_file: Path) -> bool:
|
93
|
-
"""
|
94
|
-
Install the downloaded update.
|
95
|
-
Platform-specific installation logic.
|
96
|
-
"""
|
97
|
-
if self.platform == "darwin":
|
98
|
-
return self._install_macos_update(update_file)
|
99
|
-
elif self.platform == "windows":
|
100
|
-
return self._install_windows_update(update_file)
|
101
|
-
elif self.platform == "linux":
|
102
|
-
return self._install_linux_update(update_file)
|
103
|
-
else:
|
104
|
-
return False
|
105
|
-
|
106
|
-
def _install_macos_update(self, dmg_file: Path) -> bool:
|
107
|
-
"""Install .dmg update on macOS."""
|
108
|
-
try:
|
109
|
-
# Open the DMG file - user will need to drag to Applications
|
110
|
-
subprocess.run(["open", str(dmg_file)], check=True)
|
111
|
-
return True
|
112
|
-
except subprocess.CalledProcessError:
|
113
|
-
return False
|
114
|
-
|
115
|
-
def _install_windows_update(self, exe_file: Path) -> bool:
|
116
|
-
"""Install .exe update on Windows."""
|
117
|
-
try:
|
118
|
-
# Run the installer
|
119
|
-
subprocess.run([str(exe_file)], check=True)
|
120
|
-
return True
|
121
|
-
except subprocess.CalledProcessError:
|
122
|
-
return False
|
123
|
-
|
124
|
-
def _install_linux_update(self, appimage_file: Path) -> bool:
|
125
|
-
"""Install AppImage update on Linux."""
|
126
|
-
try:
|
127
|
-
# Make executable and offer to replace current installation
|
128
|
-
os.chmod(appimage_file, 0o755)
|
129
|
-
|
130
|
-
# For AppImage, we'd typically replace the current file
|
131
|
-
# This is more complex and might require user permission
|
132
|
-
return True
|
133
|
-
except Exception:
|
134
|
-
return False
|
135
|
-
|
136
|
-
def open_download_page(self, release_url: str):
|
137
|
-
"""Open the GitHub release page in browser."""
|
138
|
-
webbrowser.open(release_url)
|
139
63
|
|
140
64
|
|
141
65
|
# Global instance
|
tbr_deal_finder/gui/main.py
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
import asyncio
|
2
2
|
import os
|
3
3
|
import base64
|
4
|
+
import subprocess
|
5
|
+
import sys
|
6
|
+
from datetime import datetime
|
7
|
+
from pathlib import Path
|
4
8
|
|
5
9
|
import flet as ft
|
6
10
|
|
@@ -17,6 +21,7 @@ from tbr_deal_finder.gui.pages.all_deals import AllDealsPage
|
|
17
21
|
from tbr_deal_finder.gui.pages.latest_deals import LatestDealsPage
|
18
22
|
from tbr_deal_finder.gui.pages.all_books import AllBooksPage
|
19
23
|
from tbr_deal_finder.gui.pages.book_details import BookDetailsPage
|
24
|
+
from tbr_deal_finder.utils import get_duckdb_conn, get_latest_deal_last_ran
|
20
25
|
|
21
26
|
|
22
27
|
class TBRDealFinderApp:
|
@@ -26,7 +31,9 @@ class TBRDealFinderApp:
|
|
26
31
|
self.current_page = "all_deals"
|
27
32
|
self.selected_book = None
|
28
33
|
self.update_info = None # Store update information
|
29
|
-
|
34
|
+
self.nav_disabled = False # Track navigation disabled state
|
35
|
+
self._last_run_time = None
|
36
|
+
|
30
37
|
# Initialize pages
|
31
38
|
self.settings_page = SettingsPage(self)
|
32
39
|
self.all_deals_page = AllDealsPage(self)
|
@@ -258,6 +265,16 @@ class TBRDealFinderApp:
|
|
258
265
|
|
259
266
|
def nav_changed(self, e):
|
260
267
|
"""Handle navigation rail selection changes"""
|
268
|
+
# Prevent navigation if disabled
|
269
|
+
if self.nav_disabled:
|
270
|
+
# Reset to current page selection to prevent visual change
|
271
|
+
current_indices = {"all_deals": 0, "latest_deals": 1, "all_books": 2}
|
272
|
+
self.nav_rail.selected_index = current_indices.get(self.current_page, 0)
|
273
|
+
# Reapply disabled state after page update
|
274
|
+
self.nav_rail.disabled = True
|
275
|
+
self.page.update()
|
276
|
+
return
|
277
|
+
|
261
278
|
destinations = ["all_deals", "latest_deals", "all_books"]
|
262
279
|
if e.control.selected_index < len(destinations):
|
263
280
|
self.current_page = destinations[e.control.selected_index]
|
@@ -284,10 +301,26 @@ class TBRDealFinderApp:
|
|
284
301
|
self.all_books_page.refresh_page_state()
|
285
302
|
|
286
303
|
def refresh_all_pages(self):
|
287
|
-
"""Refresh all pages by clearing their state and reloading data"""
|
304
|
+
"""Refresh all pages except all_books_page by clearing their state and reloading data"""
|
288
305
|
self.all_deals_page.refresh_page_state()
|
289
306
|
self.latest_deals_page.refresh_page_state()
|
290
|
-
|
307
|
+
|
308
|
+
def disable_navigation(self):
|
309
|
+
"""Disable navigation rail during background operations"""
|
310
|
+
self.nav_disabled = True
|
311
|
+
if hasattr(self, 'nav_rail'):
|
312
|
+
self.nav_rail.disabled = True
|
313
|
+
self.page.update()
|
314
|
+
|
315
|
+
def enable_navigation(self):
|
316
|
+
"""Enable navigation rail after background operations complete"""
|
317
|
+
if not self.nav_disabled:
|
318
|
+
return
|
319
|
+
|
320
|
+
self.nav_disabled = False
|
321
|
+
if hasattr(self, 'nav_rail'):
|
322
|
+
self.nav_rail.disabled = False
|
323
|
+
self.page.update()
|
291
324
|
|
292
325
|
def get_current_page_content(self):
|
293
326
|
"""Get content for the current page"""
|
@@ -309,6 +342,7 @@ class TBRDealFinderApp:
|
|
309
342
|
|
310
343
|
def get_config_prompt(self):
|
311
344
|
"""Show config setup prompt when no config exists"""
|
345
|
+
self.disable_navigation()
|
312
346
|
return ft.Container(
|
313
347
|
content=ft.Column([
|
314
348
|
ft.Icon(ft.Icons.SETTINGS, size=64, color=ft.Colors.GREY_400),
|
@@ -592,10 +626,6 @@ class TBRDealFinderApp:
|
|
592
626
|
def close_dialog(e):
|
593
627
|
dialog.open = False
|
594
628
|
self.page.update()
|
595
|
-
|
596
|
-
def view_release_and_close(e):
|
597
|
-
self.view_release_notes(e)
|
598
|
-
close_dialog(e)
|
599
629
|
|
600
630
|
def download_and_close(e):
|
601
631
|
self.download_update(e)
|
@@ -609,10 +639,13 @@ class TBRDealFinderApp:
|
|
609
639
|
], spacing=10),
|
610
640
|
content=ft.Column([
|
611
641
|
ft.Text(f"Version {self.update_info['version']} is now available!"),
|
612
|
-
ft.
|
613
|
-
|
642
|
+
ft.Divider(),
|
643
|
+
ft.Text(
|
644
|
+
self.update_info.get('release_notes', 'No release notes available.'),
|
645
|
+
selectable=True
|
646
|
+
),
|
647
|
+
], scroll=ft.ScrollMode.AUTO, spacing=10, tight=True),
|
614
648
|
actions=[
|
615
|
-
ft.TextButton("View Release Notes", on_click=view_release_and_close),
|
616
649
|
ft.ElevatedButton("Download Update", on_click=download_and_close),
|
617
650
|
ft.TextButton("Later", on_click=close_dialog),
|
618
651
|
],
|
@@ -646,24 +679,36 @@ class TBRDealFinderApp:
|
|
646
679
|
dialog.open = True
|
647
680
|
self.page.update()
|
648
681
|
|
649
|
-
def view_release_notes(self, e):
|
650
|
-
"""Open release notes in browser."""
|
651
|
-
if self.update_info:
|
652
|
-
import webbrowser
|
653
|
-
webbrowser.open(self.update_info['release_url'])
|
654
|
-
|
655
682
|
def download_update(self, e):
|
656
683
|
"""Handle update download."""
|
657
684
|
if not self.update_info or not self.update_info.get('download_url'):
|
658
685
|
return
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
686
|
+
|
687
|
+
if sys.platform == "darwin":
|
688
|
+
dmg_path = Path(
|
689
|
+
f"~/Downloads/TBR-Deal-Finder-{self.update_info['version']}-mac.dmg"
|
690
|
+
).expanduser()
|
691
|
+
|
692
|
+
# Show download instructions
|
693
|
+
self.show_download_instructions()
|
694
|
+
|
695
|
+
if not dmg_path.exists():
|
696
|
+
# Using curl or urllib to download to prevent Mac warning
|
697
|
+
subprocess.run([
|
698
|
+
"curl", "-L",
|
699
|
+
self.update_info['download_url'],
|
700
|
+
"-o", dmg_path
|
701
|
+
])
|
702
|
+
|
703
|
+
subprocess.run(["open", dmg_path])
|
704
|
+
else:
|
705
|
+
# For now, open download URL in browser
|
706
|
+
# In a more advanced implementation, you could download in-app
|
707
|
+
import webbrowser
|
708
|
+
webbrowser.open(self.update_info['download_url'])
|
709
|
+
|
710
|
+
# Show download instructions
|
711
|
+
self.show_download_instructions()
|
667
712
|
|
668
713
|
def show_download_instructions(self):
|
669
714
|
"""Show instructions for installing the downloaded update."""
|
@@ -672,7 +717,7 @@ class TBRDealFinderApp:
|
|
672
717
|
self.page.update()
|
673
718
|
|
674
719
|
instructions = {
|
675
|
-
"darwin": "1.
|
720
|
+
"darwin": "1. Wait for the update to download (can take a minute or 2)\n2. Close TBR Deal Finder\n3. Once the installer opens, drag the app to Applications folder\n3. When prompted, select Replace\n4. Restart the application",
|
676
721
|
"windows": "1. Download will start in your browser\n2. Run the downloaded .exe installer\n3. Follow the installation wizard\n4. Restart the application",
|
677
722
|
}
|
678
723
|
|
@@ -703,12 +748,20 @@ class TBRDealFinderApp:
|
|
703
748
|
dialog.open = True
|
704
749
|
self.page.update()
|
705
750
|
|
706
|
-
|
707
|
-
|
708
751
|
def check_for_updates_button(self):
|
709
752
|
"""Check for updates when button is clicked."""
|
710
753
|
self.check_for_updates_manual()
|
711
754
|
|
755
|
+
def update_last_run_time(self):
|
756
|
+
db_conn = get_duckdb_conn()
|
757
|
+
self._last_run_time = get_latest_deal_last_ran(db_conn)
|
758
|
+
|
759
|
+
def get_last_run_time(self) -> datetime:
|
760
|
+
if not self._last_run_time:
|
761
|
+
self.update_last_run_time()
|
762
|
+
|
763
|
+
return self._last_run_time
|
764
|
+
|
712
765
|
|
713
766
|
def main():
|
714
767
|
"""Main entry point for the GUI application"""
|
@@ -39,6 +39,9 @@ class AllBooksPage(BaseBookPage):
|
|
39
39
|
|
40
40
|
async def _async_load_items(self):
|
41
41
|
"""Load TBR books asynchronously using Flet's async support"""
|
42
|
+
# Disable navigation during the loading operation
|
43
|
+
self.app.disable_navigation()
|
44
|
+
|
42
45
|
try:
|
43
46
|
# Run the async operation directly
|
44
47
|
await self.app.auth_all_configured_retailers()
|
@@ -50,6 +53,10 @@ class AllBooksPage(BaseBookPage):
|
|
50
53
|
self.filtered_items = []
|
51
54
|
finally:
|
52
55
|
self.set_loading(False)
|
56
|
+
|
57
|
+
# Re-enable navigation after the operation completes
|
58
|
+
self.app.enable_navigation()
|
59
|
+
|
53
60
|
# Update the page to reflect the loaded data
|
54
61
|
self.app.page.update()
|
55
62
|
|
@@ -142,8 +142,7 @@ class BaseBookPage(ABC):
|
|
142
142
|
item_tiles.append(tile)
|
143
143
|
|
144
144
|
return ft.Container(
|
145
|
-
content=ft.
|
146
|
-
height=700,
|
145
|
+
content=ft.Column(item_tiles, spacing=5),
|
147
146
|
border=ft.border.all(1, ft.Colors.OUTLINE),
|
148
147
|
border_radius=8,
|
149
148
|
padding=10
|
@@ -11,7 +11,7 @@ from tbr_deal_finder.utils import get_duckdb_conn, execute_query, float_to_curre
|
|
11
11
|
logger = logging.getLogger(__name__)
|
12
12
|
|
13
13
|
|
14
|
-
def build_book_price_section(historical_data: list[dict]) -> ft.Column:
|
14
|
+
def build_book_price_section(max_dt: datetime, historical_data: list[dict]) -> ft.Column:
|
15
15
|
retailer_data = dict()
|
16
16
|
available_colors = [
|
17
17
|
ft.Colors.AMBER,
|
@@ -27,7 +27,6 @@ def build_book_price_section(historical_data: list[dict]) -> ft.Column:
|
|
27
27
|
min_price = None
|
28
28
|
max_price = None
|
29
29
|
min_time = None
|
30
|
-
max_dt = datetime.now()
|
31
30
|
max_time = max_dt.timestamp()
|
32
31
|
|
33
32
|
for record in historical_data:
|
@@ -461,7 +460,7 @@ class BookDetailsPage:
|
|
461
460
|
)
|
462
461
|
|
463
462
|
# Create the chart
|
464
|
-
chart_fig = build_book_price_section(self.historical_data)
|
463
|
+
chart_fig = build_book_price_section(self.app.get_last_run_time(), self.historical_data)
|
465
464
|
|
466
465
|
return ft.Container(
|
467
466
|
content=ft.Column([
|
@@ -10,7 +10,6 @@ from tbr_deal_finder.gui.pages.base_book_page import BaseBookPage
|
|
10
10
|
class LatestDealsPage(BaseBookPage):
|
11
11
|
def __init__(self, app):
|
12
12
|
super().__init__(app, 4)
|
13
|
-
self.last_run_time = None
|
14
13
|
|
15
14
|
def get_page_title(self) -> str:
|
16
15
|
return "Latest Deals"
|
@@ -27,7 +26,7 @@ class LatestDealsPage(BaseBookPage):
|
|
27
26
|
|
28
27
|
def build(self):
|
29
28
|
"""Build the latest deals page with custom header"""
|
30
|
-
self.
|
29
|
+
self.app.update_last_run_time()
|
31
30
|
|
32
31
|
# Custom header with run button
|
33
32
|
header = self.build_header()
|
@@ -73,15 +72,16 @@ class LatestDealsPage(BaseBookPage):
|
|
73
72
|
def build_header(self):
|
74
73
|
"""Build the header with run button and status"""
|
75
74
|
can_run = self.can_run_latest_deals()
|
76
|
-
|
77
|
-
|
78
|
-
|
75
|
+
last_run_time = self.app.get_last_run_time()
|
76
|
+
|
77
|
+
if not can_run and last_run_time:
|
78
|
+
next_run_time = last_run_time + timedelta(hours=8)
|
79
79
|
time_remaining = next_run_time - datetime.now()
|
80
80
|
hours_remaining = max(0, int(time_remaining.total_seconds() / 3600))
|
81
81
|
status_text = f"Next run available in {hours_remaining} hours"
|
82
82
|
status_color = ft.Colors.ORANGE
|
83
|
-
elif
|
84
|
-
status_text = f"Last run: {
|
83
|
+
elif last_run_time:
|
84
|
+
status_text = f"Last run: {last_run_time.strftime('%Y-%m-%d %H:%M')}"
|
85
85
|
status_color = ft.Colors.GREEN
|
86
86
|
else:
|
87
87
|
status_text = "No previous runs"
|
@@ -194,11 +194,13 @@ class LatestDealsPage(BaseBookPage):
|
|
194
194
|
|
195
195
|
def load_items(self):
|
196
196
|
"""Load deals found at the last run time"""
|
197
|
-
|
197
|
+
last_run_time = self.app.get_last_run_time()
|
198
|
+
|
199
|
+
if last_run_time:
|
198
200
|
try:
|
199
201
|
self.items = [
|
200
202
|
book
|
201
|
-
for book in get_deals_found_at(
|
203
|
+
for book in get_deals_found_at(last_run_time)
|
202
204
|
if is_qualifying_deal(self.app.config, book)
|
203
205
|
]
|
204
206
|
self.apply_filters()
|
@@ -237,19 +239,15 @@ class LatestDealsPage(BaseBookPage):
|
|
237
239
|
)
|
238
240
|
)
|
239
241
|
|
240
|
-
def check_last_run(self):
|
241
|
-
"""Check when deals were last run"""
|
242
|
-
if not self.last_run_time:
|
243
|
-
db_conn = get_duckdb_conn()
|
244
|
-
self.last_run_time = get_latest_deal_last_ran(db_conn)
|
245
|
-
|
246
242
|
def can_run_latest_deals(self) -> bool:
|
247
243
|
"""Check if latest deals can be run (8 hour cooldown)"""
|
248
|
-
|
244
|
+
last_run_time = self.app.get_last_run_time()
|
245
|
+
|
246
|
+
if not last_run_time:
|
249
247
|
return True
|
250
248
|
|
251
249
|
min_age = datetime.now() - timedelta(hours=8)
|
252
|
-
return
|
250
|
+
return last_run_time < min_age
|
253
251
|
|
254
252
|
def run_latest_deals(self, e):
|
255
253
|
"""Run the latest deals check"""
|
@@ -274,6 +272,9 @@ class LatestDealsPage(BaseBookPage):
|
|
274
272
|
self.progress_container.visible = True
|
275
273
|
self.run_button.disabled = True
|
276
274
|
|
275
|
+
# Disable navigation during the operation
|
276
|
+
self.app.disable_navigation()
|
277
|
+
|
277
278
|
# Update the page to show loading state
|
278
279
|
self.app.page.update()
|
279
280
|
|
@@ -283,7 +284,7 @@ class LatestDealsPage(BaseBookPage):
|
|
283
284
|
|
284
285
|
if success:
|
285
286
|
# Update the run time and load new deals
|
286
|
-
self.
|
287
|
+
self.app.update_last_run_time()
|
287
288
|
self.load_items()
|
288
289
|
self.show_success(f"Found {len(self.items)} new deals!")
|
289
290
|
else:
|
@@ -297,7 +298,10 @@ class LatestDealsPage(BaseBookPage):
|
|
297
298
|
self.is_loading = False
|
298
299
|
self.progress_container.visible = False
|
299
300
|
self.run_button.disabled = False
|
300
|
-
self.
|
301
|
+
self.app.update_last_run_time() # Refresh the status
|
302
|
+
|
303
|
+
# Re-enable navigation after the operation
|
304
|
+
self.app.enable_navigation()
|
301
305
|
|
302
306
|
# Update the page to reset loading state
|
303
307
|
self.app.page.update()
|
@@ -366,5 +370,5 @@ class LatestDealsPage(BaseBookPage):
|
|
366
370
|
self.format_dropdown.value = "All"
|
367
371
|
|
368
372
|
# Check last run and reload data
|
369
|
-
self.
|
373
|
+
self.app.update_last_run_time()
|
370
374
|
self.load_items()
|
tbr_deal_finder/retailer_deal.py
CHANGED
@@ -2,16 +2,14 @@ import asyncio
|
|
2
2
|
import copy
|
3
3
|
from collections import defaultdict
|
4
4
|
|
5
|
-
import click
|
6
5
|
import pandas as pd
|
7
|
-
from tqdm.asyncio import tqdm_asyncio
|
8
6
|
|
9
7
|
from tbr_deal_finder.book import Book, get_active_deals, BookFormat
|
10
8
|
from tbr_deal_finder.config import Config
|
11
9
|
from tbr_deal_finder.tracked_books import get_tbr_books, get_unknown_books, set_unknown_books
|
12
10
|
from tbr_deal_finder.retailer import RETAILER_MAP
|
13
11
|
from tbr_deal_finder.retailer.models import Retailer
|
14
|
-
from tbr_deal_finder.utils import get_duckdb_conn, echo_info, echo_err
|
12
|
+
from tbr_deal_finder.utils import get_duckdb_conn, echo_info, echo_err
|
15
13
|
|
16
14
|
|
17
15
|
def update_retailer_deal_table(config: Config, new_deals: list[Book]):
|
@@ -67,6 +65,13 @@ async def _get_books(
|
|
67
65
|
Returns:
|
68
66
|
List of Book objects with updated pricing and availability
|
69
67
|
"""
|
68
|
+
|
69
|
+
echo_info(f"Getting deals from {retailer.name}")
|
70
|
+
books = _get_retailer_relevant_tbr_books(
|
71
|
+
retailer,
|
72
|
+
books,
|
73
|
+
)
|
74
|
+
|
70
75
|
semaphore = asyncio.Semaphore(retailer.max_concurrency)
|
71
76
|
response = []
|
72
77
|
unknown_books = []
|
@@ -81,11 +86,7 @@ async def _get_books(
|
|
81
86
|
if book.deal_id not in ignored_deal_ids
|
82
87
|
]
|
83
88
|
|
84
|
-
|
85
|
-
results = await asyncio.gather(*tasks)
|
86
|
-
else:
|
87
|
-
results = await tqdm_asyncio.gather(*tasks, desc=f"Getting latest prices from {retailer.name}")
|
88
|
-
|
89
|
+
results = await asyncio.gather(*tasks)
|
89
90
|
for book in results:
|
90
91
|
if not book:
|
91
92
|
"""Cases where we know the retailer has the book but it's not coming back.
|
@@ -99,9 +100,7 @@ async def _get_books(
|
|
99
100
|
elif not book.exists:
|
100
101
|
unknown_books.append(book)
|
101
102
|
|
102
|
-
|
103
|
-
for book in unknown_books:
|
104
|
-
echo_info(f"{book.title} by {book.authors} not found")
|
103
|
+
echo_info(f"Finished getting deals from {retailer.name}")
|
105
104
|
|
106
105
|
return response, unknown_books
|
107
106
|
|
@@ -184,34 +183,26 @@ async def _get_latest_deals(config: Config):
|
|
184
183
|
ignore_books: list[Book] = get_unknown_books(config)
|
185
184
|
ignored_deal_ids: set[str] = {book.deal_id for book in ignore_books}
|
186
185
|
|
186
|
+
tasks = []
|
187
187
|
for retailer_str in config.tracked_retailers:
|
188
188
|
retailer = RETAILER_MAP[retailer_str]()
|
189
189
|
await retailer.set_auth()
|
190
190
|
|
191
|
-
|
192
|
-
|
193
|
-
|
191
|
+
tasks.append(
|
192
|
+
_get_books(
|
193
|
+
config,
|
194
|
+
retailer,
|
195
|
+
tbr_books,
|
196
|
+
ignored_deal_ids
|
197
|
+
)
|
194
198
|
)
|
195
199
|
|
196
|
-
|
197
|
-
|
198
|
-
retailer_books, u_books = await _get_books(
|
199
|
-
config,
|
200
|
-
retailer,
|
201
|
-
relevant_tbr_books,
|
202
|
-
ignored_deal_ids
|
203
|
-
)
|
200
|
+
results = await asyncio.gather(*tasks)
|
201
|
+
for retailer_books, u_books in results:
|
204
202
|
books.extend(retailer_books)
|
205
203
|
unknown_books.extend(u_books)
|
206
|
-
click.echo("---------------\n")
|
207
204
|
|
208
205
|
_apply_proper_list_prices(books)
|
209
|
-
|
210
|
-
books = [
|
211
|
-
book
|
212
|
-
for book in books
|
213
|
-
]
|
214
|
-
|
215
206
|
update_retailer_deal_table(config, books)
|
216
207
|
set_unknown_books(config, unknown_books)
|
217
208
|
|
tbr_deal_finder/utils.py
CHANGED
@@ -27,25 +27,34 @@ def get_data_dir() -> Path:
|
|
27
27
|
app_name = "TBR Deal Finder"
|
28
28
|
|
29
29
|
if custom_path := os.getenv("TBR_DEAL_FINDER_CUSTOM_PATH"):
|
30
|
-
path = Path(custom_path)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
30
|
+
path = Path(custom_path).expanduser()
|
31
|
+
else:
|
32
|
+
cli_path = Path.home() / ".tbr_deal_finder"
|
33
|
+
if sys.platform == "win32":
|
34
|
+
# Windows: C:\Users\Username\AppData\Local\AppAuthor\AppName
|
35
|
+
base = os.environ.get("LOCALAPPDATA", os.path.expanduser("~\\AppData\\Local"))
|
36
|
+
gui_path = Path(base) / app_author / app_name
|
37
|
+
|
38
|
+
elif sys.platform == "darwin":
|
39
|
+
# macOS: ~/Library/Application Support/AppName
|
40
|
+
gui_path = Path.home() / "Library" / "Application Support" / app_name
|
41
|
+
|
42
|
+
else: # Linux and others
|
43
|
+
# Linux: ~/.local/share/appname (following XDG spec)
|
44
|
+
xdg_data_home = os.environ.get("XDG_DATA_HOME",
|
45
|
+
os.path.expanduser("~/.local/share"))
|
46
|
+
gui_path = Path(xdg_data_home) / app_name.lower()
|
47
|
+
|
48
|
+
if is_gui_env():
|
49
|
+
path = gui_path
|
50
|
+
if cli_path.exists() and not path.exists():
|
51
|
+
# Use the cli path if it exists and the gui path does not
|
52
|
+
path = cli_path
|
53
|
+
else:
|
54
|
+
path = cli_path
|
55
|
+
if gui_path.exists() and not path.exists():
|
56
|
+
# Use the gui path if it exists and the cli path does not
|
57
|
+
path = gui_path
|
49
58
|
|
50
59
|
# Create directory if it doesn't exist
|
51
60
|
path.mkdir(parents=True, exist_ok=True)
|
@@ -1,24 +1,24 @@
|
|
1
|
-
tbr_deal_finder/__init__.py,sha256=
|
1
|
+
tbr_deal_finder/__init__.py,sha256=Yq_k2n4fUg7OMmkDUL-qq_1rUK2wu1KPureT3sJEKx4,104
|
2
2
|
tbr_deal_finder/__main__.py,sha256=b2-3WiGIno_XVusUDijQXa2MthKahFz6CVH-hIBe7es,165
|
3
3
|
tbr_deal_finder/book.py,sha256=WbvDEeI923iQX_eIsC9H7y-qKVPkfCqwjBCqChssFCM,6740
|
4
4
|
tbr_deal_finder/cli.py,sha256=16vZnWS9TTWPhIjZsjsf7OY7Btb0ULOfws5EkUefbRY,7089
|
5
5
|
tbr_deal_finder/config.py,sha256=pocHMEoJVIoiuRzEtKDAmsGmiiLgD-lN6n6TU9gI8Lc,3982
|
6
|
-
tbr_deal_finder/desktop_updater.py,sha256
|
6
|
+
tbr_deal_finder/desktop_updater.py,sha256=-1hNJt1uAkxGo4fUf1IT_BQc9vcRR3mCBDHcYsO-_DU,2285
|
7
7
|
tbr_deal_finder/migrations.py,sha256=fO7r2JbWb6YG0CsPqauakwvbKaEFPxqX1PP8c8N03Wc,4951
|
8
8
|
tbr_deal_finder/owned_books.py,sha256=Cf1VeiSg7XBi_TXptJfy5sO1mEgMMQWbJ_P6SzAx0nQ,516
|
9
|
-
tbr_deal_finder/retailer_deal.py,sha256=
|
9
|
+
tbr_deal_finder/retailer_deal.py,sha256=l2n-79kSNZfPh73PZBbSE6tkNYYCaHPEehIPcelDPeY,7214
|
10
10
|
tbr_deal_finder/tracked_books.py,sha256=u3KfBNlwvsEwTfM5TAJVLbiTmm1lTe2k70JJOszQz1k,12714
|
11
|
-
tbr_deal_finder/utils.py,sha256=
|
11
|
+
tbr_deal_finder/utils.py,sha256=LJYRNPRO3XBFIlodGzxCxlxiQ9viNYGU5QjNnjz4qMA,3635
|
12
12
|
tbr_deal_finder/version_check.py,sha256=tzuKWngnjSdjPAOdyuPk2ym6Lv3LyeObJAfgsBIH9YQ,1217
|
13
13
|
tbr_deal_finder/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
-
tbr_deal_finder/gui/main.py,sha256=
|
14
|
+
tbr_deal_finder/gui/main.py,sha256=msFtiEVd7um3Ck-dlMhjSUHG2GjSyeVuJSDsLGFarJs,28420
|
15
15
|
tbr_deal_finder/gui/pages/__init__.py,sha256=1xc9Ib3oX_hqxLdhZvbCv3AhwOmVBSklZZC6CAcvoMU,20
|
16
|
-
tbr_deal_finder/gui/pages/all_books.py,sha256=
|
16
|
+
tbr_deal_finder/gui/pages/all_books.py,sha256=uQguQ9_NjM5kq_D5DbOWFB9om1lOdPk1iTcjH0yl9bg,3655
|
17
17
|
tbr_deal_finder/gui/pages/all_deals.py,sha256=rY08w4XXFc_Jbd39BPJ6fO00vstuuu2YHvZterHx5ZI,2389
|
18
|
-
tbr_deal_finder/gui/pages/base_book_page.py,sha256=
|
19
|
-
tbr_deal_finder/gui/pages/book_details.py,sha256=
|
20
|
-
tbr_deal_finder/gui/pages/latest_deals.py,sha256=
|
21
|
-
tbr_deal_finder/gui/pages/settings.py,sha256=
|
18
|
+
tbr_deal_finder/gui/pages/base_book_page.py,sha256=kmHjJE4eIGrMhXtBWePn_eXfO6qJ36kIoQ7hE1xB3ic,10465
|
19
|
+
tbr_deal_finder/gui/pages/book_details.py,sha256=8ZqONLeqZ_1TGzmkJrem00VBFT3VcczEtEIzyCiHyro,22067
|
20
|
+
tbr_deal_finder/gui/pages/latest_deals.py,sha256=XFTzmWjaw8grmbSF_KTGMKFOJ2TUY2kpLljyVdte93k,13896
|
21
|
+
tbr_deal_finder/gui/pages/settings.py,sha256=gdQXi508yd_oZ5lj0mxuDL5qSxmH256LwPi5gTo2Cqg,14357
|
22
22
|
tbr_deal_finder/queries/get_active_deals.sql,sha256=nh0F1lRV6YVrUV7gsQpjsgfXmN9R0peBeMHRifjgpUM,212
|
23
23
|
tbr_deal_finder/queries/get_deals_found_at.sql,sha256=KqrtQk7FS4Hf74RyL1r-oD2D-RJz1urrxKxkwlvjAro,139
|
24
24
|
tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql,sha256=W4cNMAHtcW2DzQyPL8SHHFcbVZQKVK2VfTzazxC3LJU,107
|
@@ -31,8 +31,8 @@ tbr_deal_finder/retailer/chirp.py,sha256=GR8yeXp-1DHMktepy5TecHslrUibpLM7LfueIuT
|
|
31
31
|
tbr_deal_finder/retailer/kindle.py,sha256=kA4SO2kl2SvJHADkBYyYMgq_bditStbiTiW7piQPAFI,5282
|
32
32
|
tbr_deal_finder/retailer/librofm.py,sha256=dn1kaJEQ9g_AOAFNKhUtFLxYxQZa6RAHgXhT_dx-O3k,7514
|
33
33
|
tbr_deal_finder/retailer/models.py,sha256=56xTwcLcw3bFcvTDOb85TktqtksvvyY95hZBbp9-5mY,3340
|
34
|
-
tbr_deal_finder-0.3.
|
35
|
-
tbr_deal_finder-0.3.
|
36
|
-
tbr_deal_finder-0.3.
|
37
|
-
tbr_deal_finder-0.3.
|
38
|
-
tbr_deal_finder-0.3.
|
34
|
+
tbr_deal_finder-0.3.3.dist-info/METADATA,sha256=VEp2T5WOIRAR_c5lc3y5YI8GJaaEiUuacKdl2oBQCdw,3448
|
35
|
+
tbr_deal_finder-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
36
|
+
tbr_deal_finder-0.3.3.dist-info/entry_points.txt,sha256=xjeRw7aX_jbX1ERC--IgYIa2oLNgeRefsMbKeTAVb70,112
|
37
|
+
tbr_deal_finder-0.3.3.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
|
38
|
+
tbr_deal_finder-0.3.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|