tbr-deal-finder 0.1.6__py3-none-any.whl → 0.1.8__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.
@@ -1,19 +1,18 @@
1
1
  import asyncio
2
+ import copy
2
3
  import csv
4
+ from collections import defaultdict
5
+ from typing import Callable, Awaitable, Optional
3
6
 
7
+ import pandas as pd
4
8
  from tqdm.asyncio import tqdm_asyncio
5
9
 
6
10
  from tbr_deal_finder.book import Book, BookFormat, get_title_id
7
- from tbr_deal_finder.retailer import Chirp, RETAILER_MAP
11
+ from tbr_deal_finder.owned_books import get_owned_books
12
+ from tbr_deal_finder.retailer import Chirp, RETAILER_MAP, LibroFM
8
13
  from tbr_deal_finder.config import Config
9
- from tbr_deal_finder.library_exports import (
10
- get_book_authors,
11
- get_book_title,
12
- is_tbr_book,
13
- requires_audiobook_list_price_default
14
- )
15
14
  from tbr_deal_finder.retailer.models import Retailer
16
- from tbr_deal_finder.utils import currency_to_float
15
+ from tbr_deal_finder.utils import execute_query, get_duckdb_conn
17
16
 
18
17
 
19
18
  def _library_export_tbr_books(config: Config, tbr_book_map: dict[str: Book]):
@@ -35,7 +34,7 @@ def _library_export_tbr_books(config: Config, tbr_book_map: dict[str: Book]):
35
34
  authors = get_book_authors(book_dict)
36
35
 
37
36
  key = get_title_id(title, authors, BookFormat.NA)
38
- if key in tbr_book_map and tbr_book_map[key].audiobook_isbn:
37
+ if key in tbr_book_map:
39
38
  continue
40
39
 
41
40
  tbr_book_map[key] = Book(
@@ -46,33 +45,9 @@ def _library_export_tbr_books(config: Config, tbr_book_map: dict[str: Book]):
46
45
  current_price=0,
47
46
  timepoint=config.run_time,
48
47
  format=BookFormat.NA,
49
- audiobook_isbn=book_dict.get("audiobook_isbn"),
50
- audiobook_list_price=currency_to_float(book_dict.get("audiobook_list_price") or 0),
51
48
  )
52
49
 
53
50
 
54
- async def _apply_dynamic_audiobook_list_price(config: Config, tbr_book_map: dict[str: Book]):
55
- target_books = [
56
- book
57
- for book in tbr_book_map.values()
58
- if book.format == BookFormat.AUDIOBOOK
59
- ]
60
- if not target_books:
61
- return
62
-
63
- chirp = Chirp()
64
- semaphore = asyncio.Semaphore(5)
65
- books_with_pricing: list[Book] = await tqdm_asyncio.gather(
66
- *[
67
- chirp.get_book(book, config.run_time, semaphore)
68
- for book in target_books
69
- ],
70
- desc=f"Getting list prices for Libro.FM wishlist"
71
- )
72
- for book in books_with_pricing:
73
- tbr_book_map[book.title_id].audiobook_list_price = book.list_price
74
-
75
-
76
51
  async def _retailer_wishlist(config: Config, tbr_book_map: dict[str: Book]):
77
52
  """Adds wishlist books in the library export to the provided tbr_book_map
78
53
  Books added here has the format the retailer sells (e.g. Audiobook)
@@ -102,19 +77,244 @@ async def _retailer_wishlist(config: Config, tbr_book_map: dict[str: Book]):
102
77
 
103
78
  tbr_book_map[key] = book
104
79
 
105
- if requires_audiobook_list_price_default(config):
106
- await _apply_dynamic_audiobook_list_price(config, tbr_book_map)
107
80
 
81
+ async def _get_raw_tbr_books(config: Config) -> list[Book]:
82
+ """Gets books in any library export or tracked retailer wishlist
108
83
 
109
- async def get_tbr_books(config: Config) -> list[Book]:
110
- tbr_book_map: dict[str: Book] = {}
84
+ Excludes books in the format they are owned.
85
+ Example: User owns Dungeon Crawler Carl Audiobook but it's on the user TBR.
86
+ Result - Deals will be tracked for the Dungeon Crawler Carl EBook (if tracking kindle deals)
87
+
88
+ :param config:
89
+ :return:
90
+ """
91
+
92
+ owned_books = await get_owned_books(config)
93
+ tracking_audiobooks = config.is_tracking_format(book_format=BookFormat.AUDIOBOOK)
94
+ tracking_ebooks = config.is_tracking_format(book_format=BookFormat.EBOOK)
111
95
 
96
+ tbr_book_map: dict[str: Book] = {}
112
97
  # Get TBRs specified in the user library (StoryGraph/GoodReads) export
113
98
  _library_export_tbr_books(config, tbr_book_map)
114
-
115
99
  # Pull wishlist from tracked retailers
116
100
  await _retailer_wishlist(config, tbr_book_map)
101
+ raw_tbr_books = list(tbr_book_map.values())
102
+
103
+ response: list[Book] = []
104
+
105
+ owned_book_title_map: dict[str, set] = defaultdict(set)
106
+ for book in owned_books:
107
+ owned_book_title_map[book.full_title_str].add(book.format)
108
+
109
+ for book in raw_tbr_books:
110
+ owned_formats = owned_book_title_map.get(book.full_title_str)
111
+ if not owned_formats:
112
+ response.append(book)
113
+ elif BookFormat.NA in owned_formats:
114
+ continue
115
+ elif tracking_audiobooks and BookFormat.AUDIOBOOK not in owned_formats:
116
+ book.format = BookFormat.AUDIOBOOK
117
+ response.append(book)
118
+ elif tracking_ebooks and BookFormat.EBOOK not in owned_formats:
119
+ book.format = BookFormat.EBOOK
120
+ response.append(book)
121
+
122
+ return response
123
+
124
+
125
+ async def _set_tbr_book_attr(
126
+ tbr_books: list[Book],
127
+ target_attr: str,
128
+ get_book_callable: Callable[[Book, asyncio.Semaphore], Awaitable[Book]],
129
+ tbr_book_attr: Optional[str] = None
130
+ ):
131
+ if not tbr_books:
132
+ return
133
+
134
+ if not tbr_book_attr:
135
+ tbr_book_attr = target_attr
136
+
137
+ tbr_books_map = {b.full_title_str: b for b in tbr_books}
138
+ tbr_books_copy = copy.deepcopy(tbr_books)
139
+ semaphore = asyncio.Semaphore(5)
140
+ human_readable_name = target_attr.replace("_", " ").title()
141
+
142
+ # Get books with the appropriate transform applied
143
+ # Responsibility is on the callable here
144
+ enriched_books = await tqdm_asyncio.gather(
145
+ *[
146
+ get_book_callable(book, semaphore) for book in tbr_books_copy
147
+ ],
148
+ desc=f"Getting required {human_readable_name} info"
149
+ )
150
+ for enriched_book in enriched_books:
151
+ book = tbr_books_map[enriched_book.full_title_str]
152
+ setattr(
153
+ book,
154
+ tbr_book_attr,
155
+ getattr(enriched_book, target_attr)
156
+ )
157
+
158
+
159
+ def _requires_audiobook_list_price(config: Config):
160
+ return bool(
161
+ "Libro.FM" in config.tracked_retailers
162
+ and "Audible" not in config.tracked_retailers
163
+ and "Chirp" not in config.tracked_retailers
164
+ )
165
+
166
+
167
+ async def _maybe_set_audiobook_list_price(config: Config, new_tbr_books: list[Book]):
168
+ """Set a default list price for audiobooks
169
+
170
+ Only set if not currently set and the only audiobook retailer is Libro.FM
171
+ Libro.FM doesn't include the actual default price in its response, so this grabs the price reported by Chirp.
172
+ Chirp doesn't require a login to get this price info making it ideal in this instance.
173
+
174
+ :param config:
175
+ :return:
176
+ """
177
+ if not _requires_audiobook_list_price(config):
178
+ return
179
+
180
+ chirp = Chirp()
181
+ relevant_tbr_books = [
182
+ book
183
+ for book in new_tbr_books
184
+ if book.format in [BookFormat.AUDIOBOOK, BookFormat.NA]
185
+ ]
186
+
187
+ await _set_tbr_book_attr(
188
+ relevant_tbr_books,
189
+ "list_price",
190
+ chirp.get_book,
191
+ "audiobook_list_price"
192
+ )
193
+
194
+
195
+ async def _maybe_set_audiobook_isbn(config: Config, new_tbr_books: list[Book]):
196
+ """To get the price from Libro.fm for a book, you need its ISBN
197
+
198
+ As opposed to trying to get that every time latest-deals is run
199
+ we're just updating the export csv once to include the ISBN.
200
+
201
+ """
202
+ if "Libro.FM" not in config.tracked_retailers:
203
+ return
204
+
205
+ libro_fm = LibroFM()
206
+ await libro_fm.set_auth()
207
+
208
+ relevant_tbr_books = [
209
+ book
210
+ for book in new_tbr_books
211
+ if book.format in [BookFormat.AUDIOBOOK, BookFormat.NA]
212
+ ]
213
+
214
+ await _set_tbr_book_attr(
215
+ relevant_tbr_books,
216
+ "audiobook_isbn",
217
+ libro_fm.get_book_isbn,
218
+ )
219
+
220
+
221
+ def get_book_authors(book: dict) -> str:
222
+ if authors := book.get('Authors'):
223
+ return authors
224
+
225
+ authors = book['Author']
226
+ if additional_authors := book.get("Additional Authors"):
227
+ authors = f"{authors}, {additional_authors}"
228
+
229
+ return authors
230
+
231
+
232
+ def get_book_title(book: dict) -> str:
233
+ title = book['Title']
234
+ return title.split("(")[0].strip()
235
+
236
+
237
+ def is_tbr_book(book: dict) -> bool:
238
+ if "Read Status" in book:
239
+ return book["Read Status"] == "to-read"
240
+ elif "Bookshelves" in book:
241
+ return "to-read" in book["Bookshelves"]
242
+ else:
243
+ return True
117
244
 
118
- return list(tbr_book_map.values())
119
245
 
246
+ def reprocess_incomplete_tbr_books(config: Config):
247
+ db_conn = get_duckdb_conn()
248
+
249
+ if config.is_tracking_format(BookFormat.EBOOK):
250
+ # Replace any tbr_books missing required attr
251
+ db_conn.execute(
252
+ "DELETE FROM tbr_book WHERE ebook_asin IS NULL AND format != $book_format",
253
+ parameters=dict(book_format=BookFormat.AUDIOBOOK.value)
254
+ )
255
+
256
+ if LibroFM().name in config.tracked_retailers:
257
+ # Replace any tbr_books missing required attr
258
+ db_conn.execute(
259
+ "DELETE FROM tbr_book WHERE audiobook_isbn IS NULL AND format != $book_format",
260
+ parameters=dict(book_format=BookFormat.EBOOK.value)
261
+ )
262
+
263
+ if _requires_audiobook_list_price(config):
264
+ # Replace any tbr_books missing required attr
265
+ db_conn.execute(
266
+ "DELETE FROM tbr_book WHERE audiobook_list_price IS NULL AND format != $book_format",
267
+ parameters=dict(book_format=BookFormat.EBOOK.value)
268
+ )
269
+
270
+
271
+ async def sync_tbr_books(config: Config):
272
+ raw_tbr_books = await _get_raw_tbr_books(config)
273
+ db_conn = get_duckdb_conn()
274
+
275
+ if not raw_tbr_books:
276
+ return
277
+
278
+ df = pd.DataFrame([book.tbr_dict() for book in raw_tbr_books])
279
+ db_conn.register("_df", df)
280
+ db_conn.execute("CREATE OR REPLACE TABLE _latest_tbr_book AS SELECT * FROM _df;")
281
+ db_conn.unregister("_df")
282
+
283
+ # Remove books no longer on user tbr
284
+ db_conn.execute(
285
+ "DELETE FROM tbr_book WHERE book_id NOT IN (SELECT book_id FROM _latest_tbr_book)"
286
+ )
287
+
288
+ # Remove books from _latest_tbr_book for further processing for books already in tbr_book
289
+ db_conn.execute(
290
+ "DELETE FROM _latest_tbr_book WHERE book_id IN (SELECT book_id FROM tbr_book)"
291
+ )
292
+
293
+ new_tbr_book_data = execute_query(
294
+ db_conn,
295
+ "SELECT * EXCLUDE(book_id) FROM _latest_tbr_book"
296
+ )
297
+
298
+ new_tbr_books = [Book(retailer="N/A", timepoint=config.run_time, **b) for b in new_tbr_book_data]
299
+ if not new_tbr_books:
300
+ return
301
+
302
+ await _maybe_set_audiobook_list_price(config, new_tbr_books)
303
+ await _maybe_set_audiobook_isbn(config, new_tbr_books)
304
+
305
+ df = pd.DataFrame([book.tbr_dict() for book in new_tbr_books])
306
+ db_conn.register("_df", df)
307
+ db_conn.execute("INSERT INTO tbr_book SELECT * FROM _df;")
308
+ db_conn.unregister("_df")
309
+
310
+
311
+ async def get_tbr_books(config: Config) -> list[Book]:
312
+ await sync_tbr_books(config)
313
+
314
+ db_conn = get_duckdb_conn()
315
+ tbr_book_data = execute_query(
316
+ db_conn,
317
+ "SELECT * EXCLUDE(book_id) FROM tbr_book"
318
+ )
120
319
 
320
+ return [Book(retailer="N/A", timepoint=config.run_time, **b) for b in tbr_book_data]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tbr-deal-finder
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Track price drops and find deals on books in your TBR list across audiobook and ebook formats.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -9,6 +9,7 @@ Requires-Dist: aiohttp>=3.12.14
9
9
  Requires-Dist: audible==0.8.2
10
10
  Requires-Dist: click>=8.2.1
11
11
  Requires-Dist: duckdb>=1.3.2
12
+ Requires-Dist: levenshtein>=0.27.1
12
13
  Requires-Dist: pandas>=2.3.1
13
14
  Requires-Dist: questionary>=2.1.0
14
15
  Requires-Dist: tqdm>=4.67.1
@@ -17,16 +18,17 @@ Description-Content-Type: text/markdown
17
18
 
18
19
  # tbr-deal-finder
19
20
 
20
- Track price drops and find deals on books in your TBR (To Be Read) list across audiobook and ebook formats.
21
+ Track price drops and find deals on books in your TBR (To Be Read) and wishlist across digital book retailers.
21
22
 
22
23
  ## Features
23
- - Uses your StoryGraph exports, Goodreads exports, and custom csvs (spreadsheet) to track book deals
24
+ - Use your StoryGraph exports, Goodreads exports, and custom csvs (spreadsheet) to track book deals
24
25
  - Supports multiple of the library exports above
25
26
  - Tracks deals on the wishlist of all your configured retailers like audible
26
27
  - Supports multiple locales and currencies
27
- - Finds the latest and active deals from supported sellers
28
+ - Find the latest and active deals from supported sellers
28
29
  - Simple CLI interface for setup and usage
29
30
  - Only get notified for new deals or view all active deals
31
+ - Filters out books you already own to prevent purchasing the same book on multiple retailers
30
32
 
31
33
  ## Support
32
34
 
@@ -54,7 +56,7 @@ Track price drops and find deals on books in your TBR (To Be Read) list across a
54
56
  1. If it's not already on your computer, download Python https://www.python.org/downloads/
55
57
  1. tbr-deal-finder requires Python3.13 or higher
56
58
  2. Optional: Install and use virtualenv
57
- 3. Open your Terminal/Commmand Prompt
59
+ 3. Open your Terminal/Command Prompt
58
60
  4. Run `pip3.13 install tbr-deal-finder`
59
61
 
60
62
  ### UV
@@ -67,7 +69,7 @@ Track price drops and find deals on books in your TBR (To Be Read) list across a
67
69
  https://docs.astral.sh/uv/getting-started/installation/
68
70
 
69
71
  ## Configuration
70
- This tool relies on the csv generated by the app you use to track your TBRs.
72
+ This tool can use the csv generated by the app you use to track your TBRs.
71
73
  Here are the steps to get your export.
72
74
 
73
75
  ### StoryGraph
@@ -0,0 +1,23 @@
1
+ tbr_deal_finder/__init__.py,sha256=WCoj0GZrRiCQlrpkLTw1VUeJmX-RtBLdLqnFYn1Es_4,208
2
+ tbr_deal_finder/book.py,sha256=MYdZ7WeSo9hWy9Af7T5U3-7zPgi5qdMPqlNtRYPMLck,5492
3
+ tbr_deal_finder/cli.py,sha256=C4F2rbPrfYNqlmolx08ZHDCcFJuiPbkc4ECXUO25kmI,7446
4
+ tbr_deal_finder/config.py,sha256=-TtZLv4kVBf56xPkgAdKXeVRV0qw8MZ53XHBQ1HnVX8,3978
5
+ tbr_deal_finder/migrations.py,sha256=_ZxUXzGyEFYlPlpzMvViDVPZJc5BNOiixj150U8HRFc,4224
6
+ tbr_deal_finder/owned_books.py,sha256=Cf1VeiSg7XBi_TXptJfy5sO1mEgMMQWbJ_P6SzAx0nQ,516
7
+ tbr_deal_finder/retailer_deal.py,sha256=jv32WSOtxVxCkxTCLkOqSkcHGHhWfbD4nSxY42Cqk38,6422
8
+ tbr_deal_finder/tracked_books.py,sha256=SOmygViADjr8fa3RiB4dn_bWjIpiAOBNQK37MHOu7nE,10407
9
+ tbr_deal_finder/utils.py,sha256=_4wdGFDtqCdMyoMnwTDiHgCR4WQLAcQr8LlZZZUcq6E,1357
10
+ tbr_deal_finder/queries/get_active_deals.sql,sha256=nh0F1lRV6YVrUV7gsQpjsgfXmN9R0peBeMHRifjgpUM,212
11
+ tbr_deal_finder/queries/get_deals_found_at.sql,sha256=KqrtQk7FS4Hf74RyL1r-oD2D-RJz1urrxKxkwlvjAro,139
12
+ tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql,sha256=W4cNMAHtcW2DzQyPL8SHHFcbVZQKVK2VfTzazxC3LJU,107
13
+ tbr_deal_finder/retailer/__init__.py,sha256=WePMSN7vi4EL_uPiAH6ogNNE-kRQe4OHT4CYGTKvBSk,243
14
+ tbr_deal_finder/retailer/amazon.py,sha256=-U28l3LmVCbOvK0SIXG6MAfYgoRUA--enNQJBxA_NHM,2322
15
+ tbr_deal_finder/retailer/audible.py,sha256=1zM95teGth4UPyK5ZogLM9SIQxVd3kTgjTEiglnQKR8,3979
16
+ tbr_deal_finder/retailer/chirp.py,sha256=s5aj3NwMA1GSymmTylnoWghQsqMp7hFcMdnoA1TRsv4,9241
17
+ tbr_deal_finder/retailer/librofm.py,sha256=ARkUlxMS3OCgfKosX_XKVmMfLcIIDRPzqqmVp0V9IZA,6327
18
+ tbr_deal_finder/retailer/models.py,sha256=I_U8VC5AOozGfPecW7FW8eRoTWEOxnMIHPNbuOJWcjw,1463
19
+ tbr_deal_finder-0.1.8.dist-info/METADATA,sha256=4urAR2x-ABDN0OMscyowYP8Gqc_RXlVsYJoaRxpF7m8,4342
20
+ tbr_deal_finder-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ tbr_deal_finder-0.1.8.dist-info/entry_points.txt,sha256=y_KG1k8xVCY8gngSZ-na2bkK-tTLUdOc_qZ9Djwldv0,60
22
+ tbr_deal_finder-0.1.8.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
23
+ tbr_deal_finder-0.1.8.dist-info/RECORD,,
@@ -1,205 +0,0 @@
1
- import asyncio
2
- import csv
3
- import shutil
4
- import tempfile
5
- from datetime import datetime
6
- from typing import Callable, Awaitable, Optional
7
-
8
- from tqdm.asyncio import tqdm_asyncio
9
-
10
- from tbr_deal_finder.book import Book, BookFormat, get_normalized_authors
11
- from tbr_deal_finder.config import Config
12
- from tbr_deal_finder.retailer import LibroFM, Chirp
13
-
14
-
15
- def get_book_authors(book: dict) -> str:
16
- if authors := book.get('Authors'):
17
- return authors
18
-
19
- authors = book['Author']
20
- if additional_authors := book.get("Additional Authors"):
21
- authors = f"{authors}, {additional_authors}"
22
-
23
- return authors
24
-
25
-
26
- def get_book_title(book: dict) -> str:
27
- title = book['Title']
28
- return title.split("(")[0].strip()
29
-
30
-
31
- def is_tbr_book(book: dict) -> bool:
32
- if "Read Status" in book:
33
- return book["Read Status"] == "to-read"
34
- elif "Bookshelves" in book:
35
- return "to-read" in book["Bookshelves"]
36
- else:
37
- return True
38
-
39
-
40
- def requires_audiobook_list_price_default(config: Config) -> bool:
41
- return bool(
42
- "Libro.FM" in config.tracked_retailers
43
- and "Audible" not in config.tracked_retailers
44
- and "Chirp" not in config.tracked_retailers
45
- )
46
-
47
-
48
- async def _maybe_set_column_for_library_exports(
49
- config: Config,
50
- attr_name: str,
51
- get_book_callable: Callable[[Book, datetime, asyncio.Semaphore], Awaitable[Book]],
52
- column_name: Optional[str] = None,
53
- ):
54
- """Adds a new column to all library exports that are missing it.
55
- Uses get_book_callable to set the column value if a matching record couldn't be found
56
- on that column in any other library export file.
57
-
58
- :param config:
59
- :param attr_name:
60
- :param get_book_callable:
61
- :param column_name:
62
- :return:
63
- """
64
- if not column_name:
65
- column_name = attr_name
66
-
67
- books_requiring_check_map = dict()
68
- book_to_col_val_map = dict()
69
-
70
- # Iterate all library export paths
71
- for library_export_path in config.library_export_paths:
72
- with open(library_export_path, 'r', newline='', encoding='utf-8') as file:
73
- # Use csv.DictReader to get dictionaries with column headers
74
- for book_dict in csv.DictReader(file):
75
- if not is_tbr_book(book_dict):
76
- continue
77
-
78
- title = get_book_title(book_dict)
79
- authors = get_book_authors(book_dict)
80
- key = f'{title}__{get_normalized_authors(authors)}'
81
-
82
- if column_name in book_dict:
83
- # Keep state of value for this book/key
84
- # in the event another export has the same book but the value is not set
85
- book_to_col_val_map[key] = book_dict[column_name]
86
- # Value has been found so a check no longer needs to be performed
87
- books_requiring_check_map.pop(key, None)
88
- elif key not in book_to_col_val_map:
89
- # Not found, add the book to those requiring the column val to be set
90
- books_requiring_check_map[key] = Book(
91
- retailer="N/A",
92
- title=title,
93
- authors=authors,
94
- list_price=0,
95
- current_price=0,
96
- timepoint=config.run_time,
97
- format=BookFormat.NA
98
- )
99
-
100
- if not books_requiring_check_map:
101
- # Everything was resolved, nothing else to do
102
- return
103
-
104
- semaphore = asyncio.Semaphore(5)
105
- human_readable_name = attr_name.replace("_", " ").title()
106
-
107
- # Get books with the appropriate transform applied
108
- # Responsibility is on the callable here
109
- enriched_books = await tqdm_asyncio.gather(
110
- *[
111
- get_book_callable(book, config.run_time, semaphore) for book in books_requiring_check_map.values()
112
- ],
113
- desc=f"Getting required {human_readable_name} info"
114
- )
115
- updated_book_map = {
116
- b.full_title_str: b
117
- for b in enriched_books
118
- }
119
-
120
-
121
- # Go back and now add the new column where it hasn't been set
122
- for library_export_path in config.library_export_paths:
123
- with open(library_export_path, 'r', newline='', encoding='utf-8') as file:
124
- reader = csv.DictReader(file)
125
- field_names = list(reader.fieldnames) + [column_name]
126
- file_content = [book_dict for book_dict in reader]
127
- if not file_content or column_name in file_content[0]:
128
- continue
129
-
130
- with tempfile.NamedTemporaryFile(mode='w', delete=False, newline='') as temp_file:
131
- temp_filename = temp_file.name
132
- writer = csv.DictWriter(temp_file, fieldnames=field_names)
133
- writer.writeheader()
134
-
135
- for book_dict in file_content:
136
- if is_tbr_book(book_dict):
137
- title = get_book_title(book_dict)
138
- authors = get_book_authors(book_dict)
139
- key = f'{title}__{get_normalized_authors(authors)}'
140
-
141
- if key in book_to_col_val_map:
142
- col_val = book_to_col_val_map[key]
143
- elif key in updated_book_map:
144
- book = updated_book_map[key]
145
- col_val = getattr(book, attr_name)
146
- else:
147
- col_val = ""
148
-
149
- book_dict[column_name] = col_val
150
- else:
151
- book_dict[column_name] = ""
152
-
153
- writer.writerow(book_dict)
154
-
155
- shutil.move(temp_filename, library_export_path)
156
-
157
-
158
- async def _maybe_set_library_export_audiobook_isbn(config: Config):
159
- """To get the price from Libro.fm for a book, you need its ISBN
160
-
161
- As opposed to trying to get that every time latest-deals is run
162
- we're just updating the export csv once to include the ISBN.
163
-
164
- Unfortunately, we do have to get it at run time for wishlists.
165
- """
166
- if "Libro.FM" not in config.tracked_retailers:
167
- return
168
-
169
- libro_fm = LibroFM()
170
- await libro_fm.set_auth()
171
-
172
- await _maybe_set_column_for_library_exports(
173
- config,
174
- "audiobook_isbn",
175
- libro_fm.get_book_isbn,
176
- )
177
-
178
-
179
- async def _maybe_set_library_export_audiobook_list_price(config: Config):
180
- """Set a default list price for audiobooks
181
-
182
- Only set if not currently set and the only audiobook retailer is Libro.FM
183
- Libro.FM doesn't include the actual default price in its response, so this grabs the price reported by Chirp.
184
- Chirp doesn't require a login to get this price info making it ideal in this instance.
185
-
186
- :param config:
187
- :return:
188
- """
189
- if not requires_audiobook_list_price_default(config):
190
- return
191
-
192
- chirp = Chirp()
193
- await chirp.set_auth()
194
-
195
- await _maybe_set_column_for_library_exports(
196
- config,
197
- "list_price",
198
- chirp.get_book,
199
- "audiobook_list_price"
200
- )
201
-
202
-
203
- async def maybe_enrich_library_exports(config: Config):
204
- await _maybe_set_library_export_audiobook_isbn(config)
205
- await _maybe_set_library_export_audiobook_list_price(config)
@@ -1,22 +0,0 @@
1
- tbr_deal_finder/__init__.py,sha256=WCoj0GZrRiCQlrpkLTw1VUeJmX-RtBLdLqnFYn1Es_4,208
2
- tbr_deal_finder/book.py,sha256=ZnwuIU2-rP0UC12fSC_HiTXpmgBp5XrRiFe82OLQ-E0,3786
3
- tbr_deal_finder/cli.py,sha256=jIwzyESLGc77Wyxsv4XjEfMHZHjQI_PTmBLamc9tiV0,7284
4
- tbr_deal_finder/config.py,sha256=3fgN92sVsQbVqRBc58QK9w5t35zoPX6pP3k4nnJ_YTg,3441
5
- tbr_deal_finder/library_exports.py,sha256=Hupx3mJyhvqXEslR2R3ifG9ykSKxkOb-H3gGK_CRx68,7133
6
- tbr_deal_finder/migrations.py,sha256=6_WV55bm71UCFrcFrfJXlEX5uDrgnNTWZPq6vZTg18o,3733
7
- tbr_deal_finder/retailer_deal.py,sha256=wMziFXCvrJQ_i4IdsXVPlaXnUIeGTRPt_rwbPfqX2FE,6742
8
- tbr_deal_finder/tracked_books.py,sha256=EKecARSOMPPkmjSSWfWQw9B-TnoSd3-6AQepc2EYTz0,4031
9
- tbr_deal_finder/utils.py,sha256=_4wdGFDtqCdMyoMnwTDiHgCR4WQLAcQr8LlZZZUcq6E,1357
10
- tbr_deal_finder/queries/get_active_deals.sql,sha256=jILZK5UVNPLbbKWgqMW0brEZyCb9XBdQZJLHRULoQC4,195
11
- tbr_deal_finder/queries/get_deals_found_at.sql,sha256=1vAE8PsAvfFi0SbvoUw8pvLwRN9VGYTJ7AVI3rmxXEI,122
12
- tbr_deal_finder/queries/latest_deal_last_ran_most_recent_success.sql,sha256=W4cNMAHtcW2DzQyPL8SHHFcbVZQKVK2VfTzazxC3LJU,107
13
- tbr_deal_finder/retailer/__init__.py,sha256=WePMSN7vi4EL_uPiAH6ogNNE-kRQe4OHT4CYGTKvBSk,243
14
- tbr_deal_finder/retailer/audible.py,sha256=AY8jippIQe0XqCXk9iLb3CHADIYIG3orlctNQRSv2q8,5315
15
- tbr_deal_finder/retailer/chirp.py,sha256=BVtHsrM0nsMmT2fxDnUliXVWfY2xZY8TR3FWZzyaxIA,8042
16
- tbr_deal_finder/retailer/librofm.py,sha256=ZiowAIpDYnuH6KREdPK874t-Handlr0jZ9Mj0QMVGis,6428
17
- tbr_deal_finder/retailer/models.py,sha256=wAGZtp0BWz9vlZCcWZqll8gwXUP6-6oFtsWv3gCXEHM,1415
18
- tbr_deal_finder-0.1.6.dist-info/METADATA,sha256=CbRtsumWOcJ_VaabBoCqzGURzKjCOvvdk5koY11ukZE,4215
19
- tbr_deal_finder-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- tbr_deal_finder-0.1.6.dist-info/entry_points.txt,sha256=y_KG1k8xVCY8gngSZ-na2bkK-tTLUdOc_qZ9Djwldv0,60
21
- tbr_deal_finder-0.1.6.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
22
- tbr_deal_finder-0.1.6.dist-info/RECORD,,