tbr-deal-finder 0.2.0__py3-none-any.whl → 0.2.1__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/book.py CHANGED
@@ -55,7 +55,10 @@ class Book:
55
55
  self.format = format
56
56
 
57
57
  def discount(self) -> int:
58
- return int((self.list_price/self.current_price - 1) * 100)
58
+ if not self.current_price:
59
+ return 100
60
+
61
+ return int((1 - self.current_price/self.list_price) * 100)
59
62
 
60
63
  @staticmethod
61
64
  def price_to_string(price: float) -> str:
@@ -127,6 +130,15 @@ class Book:
127
130
  "book_id": self.title_id,
128
131
  }
129
132
 
133
+ def unknown_book_dict(self):
134
+ return {
135
+ "retailer": self.retailer,
136
+ "title": self.title,
137
+ "authors": self.authors,
138
+ "format": self.format.value,
139
+ "book_id": self.deal_id,
140
+ }
141
+
130
142
 
131
143
  def get_deals_found_at(timepoint: datetime) -> list[Book]:
132
144
  db_conn = get_duckdb_conn()
tbr_deal_finder/cli.py CHANGED
@@ -13,7 +13,7 @@ from tbr_deal_finder.migrations import make_migrations
13
13
  from tbr_deal_finder.book import get_deals_found_at, print_books, get_active_deals
14
14
  from tbr_deal_finder.retailer import RETAILER_MAP
15
15
  from tbr_deal_finder.retailer_deal import get_latest_deals
16
- from tbr_deal_finder.tracked_books import reprocess_incomplete_tbr_books
16
+ from tbr_deal_finder.tracked_books import reprocess_incomplete_tbr_books, clear_unknown_books
17
17
  from tbr_deal_finder.utils import (
18
18
  echo_err,
19
19
  echo_info,
@@ -183,6 +183,7 @@ def setup():
183
183
  # Retailers may have changed causing some books to need reprocessing
184
184
  config = Config.load()
185
185
  reprocess_incomplete_tbr_books(config)
186
+ clear_unknown_books()
186
187
 
187
188
 
188
189
  @cli.command()
tbr_deal_finder/config.py CHANGED
@@ -26,7 +26,7 @@ class Config:
26
26
  library_export_paths: list[str]
27
27
  tracked_retailers: list[str]
28
28
  max_price: float = 8.0
29
- min_discount: int = 35
29
+ min_discount: int = 30
30
30
  run_time: datetime = datetime.now()
31
31
 
32
32
  locale: str = "us" # This will be set as a class attribute below
@@ -62,6 +62,32 @@ _MIGRATIONS = [
62
62
  );
63
63
  """
64
64
  ),
65
+ TableMigration(
66
+ version=1,
67
+ table_name="unknown_book",
68
+ sql="""
69
+ CREATE TABLE unknown_book
70
+ (
71
+ retailer VARCHAR,
72
+ title VARCHAR,
73
+ authors VARCHAR,
74
+ format VARCHAR,
75
+ book_id VARCHAR
76
+ );
77
+ """
78
+ ),
79
+ TableMigration(
80
+ version=1,
81
+ table_name="unknown_book_run_history",
82
+ sql="""
83
+ CREATE TABLE unknown_book_run_history
84
+ (
85
+ timepoint TIMESTAMP_NS,
86
+ ran_successfully BOOLEAN,
87
+ details VARCHAR
88
+ );
89
+ """
90
+ ),
65
91
  ]
66
92
 
67
93
 
@@ -0,0 +1,5 @@
1
+ SELECT timepoint
2
+ FROM unknown_book_run_history
3
+ WHERE ran_successfully = TRUE
4
+ ORDER BY timepoint DESC
5
+ LIMIT 1
@@ -15,7 +15,7 @@ from tbr_deal_finder import TBR_DEALS_PATH
15
15
  from tbr_deal_finder.config import Config
16
16
  from tbr_deal_finder.retailer.models import Retailer
17
17
 
18
- _AUTH_PATH = TBR_DEALS_PATH.joinpath("audible.json")
18
+ AUTH_PATH = TBR_DEALS_PATH.joinpath("audible.json")
19
19
 
20
20
 
21
21
  def login_url_callback(url: str) -> str:
@@ -71,15 +71,15 @@ class Amazon(Retailer):
71
71
  _client: audible.AsyncClient = None
72
72
 
73
73
  async def set_auth(self):
74
- if not os.path.exists(_AUTH_PATH):
74
+ if not os.path.exists(AUTH_PATH):
75
75
  auth = audible.Authenticator.from_login_external(
76
76
  locale=Config.locale,
77
77
  login_url_callback=login_url_callback
78
78
  )
79
79
 
80
80
  # Save credentials to file
81
- auth.to_file(_AUTH_PATH)
81
+ auth.to_file(AUTH_PATH)
82
82
 
83
- self._auth = audible.Authenticator.from_file(_AUTH_PATH)
83
+ self._auth = audible.Authenticator.from_file(AUTH_PATH)
84
84
  self._client = audible.AsyncClient(auth=self._auth)
85
85
 
@@ -1,13 +1,17 @@
1
1
  import asyncio
2
+ import json
2
3
  import readline # type: ignore
3
4
 
4
5
  from tbr_deal_finder.config import Config
5
- from tbr_deal_finder.retailer.amazon import Amazon
6
+ from tbr_deal_finder.retailer.amazon import Amazon, AUTH_PATH
6
7
  from tbr_deal_finder.book import Book, BookFormat, get_normalized_title, get_normalized_authors, is_matching_authors
7
8
 
8
9
 
9
10
  class Kindle(Amazon):
10
11
 
12
+ def __init__(self):
13
+ self._headers = {}
14
+
11
15
  @property
12
16
  def name(self) -> str:
13
17
  return "Kindle"
@@ -19,6 +23,25 @@ class Kindle(Amazon):
19
23
  def _get_base_url(self) -> str:
20
24
  return f"https://www.amazon.{self._auth.locale.domain}"
21
25
 
26
+ def _get_read_base_url(self) -> str:
27
+ return f"https://read.amazon.{self._auth.locale.domain}"
28
+
29
+ async def set_auth(self):
30
+ await super().set_auth()
31
+
32
+ with open(AUTH_PATH, "r") as f:
33
+ auth_info = json.load(f)
34
+
35
+ cookies = auth_info["website_cookies"]
36
+ cookies["x-access-token"] = auth_info["access_token"]
37
+
38
+ self._headers = {
39
+ "User-Agent": "Mozilla/5.0 (Linux; Android 10; Kindle) AppleWebKit/537.3",
40
+ "Accept": "application/json, */*",
41
+ "Cookie": "; ".join([f"{k}={v}" for k, v in cookies.items()])
42
+ }
43
+
44
+
22
45
  async def get_book_asin(
23
46
  self,
24
47
  target: Book,
@@ -90,44 +113,28 @@ class Kindle(Amazon):
90
113
  return []
91
114
 
92
115
  async def get_library(self, config: Config) -> list[Book]:
93
- """Not currently supported
94
-
95
- Getting this info is proving to be a nightmare
96
-
97
- :param config:
98
- :return:
99
- """
100
- return []
101
-
102
- async def _get_library_attempt(self, config: Config) -> list[Book]:
103
- """This should work, but it's returning a redirect
104
-
105
- The user is already authenticated at this point, so I'm not sure what's happening
106
- """
107
- response = []
116
+ books = []
108
117
  pagination_token = 0
109
- total_pages = 1
118
+ url = f"{self._get_read_base_url()}/kindle-library/search"
110
119
 
111
- while pagination_token < total_pages:
120
+ while True:
112
121
  optional_params = {}
113
122
  if pagination_token:
114
123
  optional_params["paginationToken"] = pagination_token
115
124
 
116
125
  response = await self._client.get(
117
- "https://read.amazon.com/kindle-library/search",
126
+ url,
127
+ headers=self._headers,
118
128
  query="",
119
129
  libraryType="BOOKS",
120
130
  sortType="recency",
121
131
  resourceType="EBOOK",
122
- querySize=5,
132
+ querySize=50,
123
133
  **optional_params
124
134
  )
125
135
 
126
- if "paginationToken" in response:
127
- total_pages = int(response["paginationToken"])
128
-
129
136
  for book in response["itemsList"]:
130
- response.append(
137
+ books.append(
131
138
  Book(
132
139
  retailer=self.name,
133
140
  title = book["title"],
@@ -138,4 +145,9 @@ class Kindle(Amazon):
138
145
  )
139
146
  )
140
147
 
141
- return response
148
+ if "paginationToken" in response:
149
+ pagination_token = int(response["paginationToken"])
150
+ else:
151
+ break
152
+
153
+ return books
@@ -10,7 +10,7 @@ from tbr_deal_finder import TBR_DEALS_PATH
10
10
  from tbr_deal_finder.config import Config
11
11
  from tbr_deal_finder.retailer.models import AioHttpSession, Retailer
12
12
  from tbr_deal_finder.book import Book, BookFormat, get_normalized_authors, is_matching_authors, get_normalized_title
13
- from tbr_deal_finder.utils import currency_to_float
13
+ from tbr_deal_finder.utils import currency_to_float, echo_err
14
14
 
15
15
 
16
16
  class LibroFM(AioHttpSession, Retailer):
@@ -77,6 +77,10 @@ class LibroFM(AioHttpSession, Retailer):
77
77
  "password": click.prompt("Libro FM Password", hide_input=True),
78
78
  }
79
79
  )
80
+ if "access_token" not in response:
81
+ echo_err("Login failed. Try again.")
82
+ await self.set_auth()
83
+
80
84
  self.auth_token = response["access_token"]
81
85
  with open(auth_path, "w") as f:
82
86
  json.dump(response, f)
@@ -8,7 +8,7 @@ from tqdm.asyncio import tqdm_asyncio
8
8
 
9
9
  from tbr_deal_finder.book import Book, get_active_deals, BookFormat
10
10
  from tbr_deal_finder.config import Config
11
- from tbr_deal_finder.tracked_books import get_tbr_books
11
+ from tbr_deal_finder.tracked_books import get_tbr_books, get_unknown_books, set_unknown_books
12
12
  from tbr_deal_finder.retailer import RETAILER_MAP
13
13
  from tbr_deal_finder.retailer.models import Retailer
14
14
  from tbr_deal_finder.utils import get_duckdb_conn, echo_warning, echo_info
@@ -55,7 +55,12 @@ def update_retailer_deal_table(config: Config, new_deals: list[Book]):
55
55
  db_conn.unregister("_df")
56
56
 
57
57
 
58
- async def _get_books(config, retailer: Retailer, books: list[Book]) -> list[Book]:
58
+ async def _get_books(
59
+ config,
60
+ retailer: Retailer,
61
+ books: list[Book],
62
+ ignored_deal_ids: set[str],
63
+ ) -> tuple[list[Book], list[Book]]:
59
64
  """Get Books with limited concurrency.
60
65
 
61
66
  - Creates semaphore to limit concurrent requests.
@@ -72,7 +77,7 @@ async def _get_books(config, retailer: Retailer, books: list[Book]) -> list[Book
72
77
  """
73
78
  semaphore = asyncio.Semaphore(10)
74
79
  response = []
75
- unresolved_books = []
80
+ unknown_books = []
76
81
  books = [copy.deepcopy(book) for book in books]
77
82
  for book in books:
78
83
  book.retailer = retailer.name
@@ -81,19 +86,20 @@ async def _get_books(config, retailer: Retailer, books: list[Book]) -> list[Book
81
86
  tasks = [
82
87
  retailer.get_book(book, semaphore)
83
88
  for book in books
89
+ if book.deal_id not in ignored_deal_ids
84
90
  ]
85
91
  results = await tqdm_asyncio.gather(*tasks, desc=f"Getting latest prices from {retailer.name}")
86
92
  for book in results:
87
93
  if book.exists:
88
94
  response.append(book)
89
95
  elif not book.exists:
90
- unresolved_books.append(book)
96
+ unknown_books.append(book)
91
97
 
92
98
  click.echo()
93
- for book in unresolved_books:
99
+ for book in unknown_books:
94
100
  echo_info(f"{book.title} by {book.authors} not found")
95
101
 
96
- return response
102
+ return response, unknown_books
97
103
 
98
104
 
99
105
  def _apply_proper_list_prices(books: list[Book]):
@@ -169,7 +175,10 @@ async def get_latest_deals(config: Config):
169
175
  """
170
176
 
171
177
  books: list[Book] = []
178
+ unknown_books: list[Book] = []
172
179
  tbr_books = await get_tbr_books(config)
180
+ ignore_books: list[Book] = get_unknown_books(config)
181
+ ignored_deal_ids: set[str] = {book.deal_id for book in ignore_books}
173
182
 
174
183
  for retailer_str in config.tracked_retailers:
175
184
  retailer = RETAILER_MAP[retailer_str]()
@@ -182,7 +191,14 @@ async def get_latest_deals(config: Config):
182
191
 
183
192
  echo_info(f"Getting deals from {retailer.name}")
184
193
  click.echo("\n---------------")
185
- books.extend(await _get_books(config, retailer, relevant_tbr_books))
194
+ retailer_books, u_books = await _get_books(
195
+ config,
196
+ retailer,
197
+ relevant_tbr_books,
198
+ ignored_deal_ids
199
+ )
200
+ books.extend(retailer_books)
201
+ unknown_books.extend(u_books)
186
202
  click.echo("---------------\n")
187
203
 
188
204
  _apply_proper_list_prices(books)
@@ -194,3 +210,4 @@ async def get_latest_deals(config: Config):
194
210
  ]
195
211
 
196
212
  update_retailer_deal_table(config, books)
213
+ set_unknown_books(config, unknown_books)
@@ -1,7 +1,9 @@
1
1
  import asyncio
2
2
  import copy
3
3
  import csv
4
+ import functools
4
5
  from collections import defaultdict
6
+ from datetime import datetime, timedelta
5
7
  from typing import Callable, Awaitable, Optional
6
8
 
7
9
  import pandas as pd
@@ -12,7 +14,7 @@ from tbr_deal_finder.owned_books import get_owned_books
12
14
  from tbr_deal_finder.retailer import Chirp, RETAILER_MAP, LibroFM, Kindle
13
15
  from tbr_deal_finder.config import Config
14
16
  from tbr_deal_finder.retailer.models import Retailer
15
- from tbr_deal_finder.utils import execute_query, get_duckdb_conn
17
+ from tbr_deal_finder.utils import execute_query, get_duckdb_conn, get_query_by_name
16
18
 
17
19
 
18
20
  def _library_export_tbr_books(config: Config, tbr_book_map: dict[str: Book]):
@@ -214,6 +216,66 @@ async def _maybe_set_audiobook_isbn(config: Config, new_tbr_books: list[Book]):
214
216
  )
215
217
 
216
218
 
219
+ @functools.cache
220
+ def unknown_books_requires_sync() -> bool:
221
+ db_conn = get_duckdb_conn()
222
+ results = execute_query(
223
+ db_conn,
224
+ get_query_by_name("latest_unknown_book_sync.sql")
225
+ )
226
+ if not results:
227
+ return True
228
+
229
+ sync_last_ran = results[0]["timepoint"]
230
+ return datetime.now() - timedelta(days=7) > sync_last_ran
231
+
232
+
233
+ def clear_unknown_books():
234
+ db_conn = get_duckdb_conn()
235
+ db_conn.execute(
236
+ "DELETE FROM unknown_book"
237
+ )
238
+ db_conn.execute(
239
+ "DELETE FROM unknown_book_run_history"
240
+ )
241
+
242
+
243
+ def set_unknown_books(config: Config, unknown_books: list[Book]):
244
+ if not unknown_books_requires_sync():
245
+ return
246
+
247
+ db_conn = get_duckdb_conn()
248
+ db_conn.execute(
249
+ "INSERT INTO unknown_book_run_history (timepoint, ran_successfully, details) VALUES (?, ?, ?)",
250
+ [config.run_time, True, ""]
251
+ )
252
+
253
+ db_conn.execute(
254
+ "DELETE FROM unknown_book"
255
+ )
256
+ if not unknown_books:
257
+ return
258
+
259
+ df = pd.DataFrame([book.unknown_book_dict() for book in unknown_books])
260
+ db_conn = get_duckdb_conn()
261
+ db_conn.register("_df", df)
262
+ db_conn.execute("INSERT INTO unknown_book SELECT * FROM _df;")
263
+ db_conn.unregister("_df")
264
+
265
+
266
+ def get_unknown_books(config: Config) -> list[Book]:
267
+ if unknown_books_requires_sync():
268
+ return []
269
+
270
+ db_conn = get_duckdb_conn()
271
+ unknown_book_data = execute_query(
272
+ db_conn,
273
+ "SELECT * EXCLUDE(book_id) FROM unknown_book"
274
+ )
275
+
276
+ return [Book(timepoint=config.run_time, **b) for b in unknown_book_data]
277
+
278
+
217
279
  async def _maybe_set_ebook_asin(config: Config, new_tbr_books: list[Book]):
218
280
  """To get the price from kindle for a book, you need its asin
219
281
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tbr-deal-finder
3
- Version: 0.2.0
3
+ Version: 0.2.1
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
@@ -23,7 +23,7 @@ Track price drops and find deals on books in your TBR (To Be Read) and wishlist
23
23
  ## Features
24
24
  - Use your StoryGraph exports, Goodreads exports, and custom csvs (spreadsheet) to track book deals
25
25
  - Supports multiple of the library exports above
26
- - Tracks deals on the wishlist of all your configured retailers like audible
26
+ - Tracks deals on the wishlist of all your configured retailers like audible (excluding kindle)
27
27
  - Supports multiple locales and currencies
28
28
  - Find the latest and active deals from supported sellers
29
29
  - Simple CLI interface for setup and usage
@@ -0,0 +1,25 @@
1
+ tbr_deal_finder/__init__.py,sha256=WCoj0GZrRiCQlrpkLTw1VUeJmX-RtBLdLqnFYn1Es_4,208
2
+ tbr_deal_finder/book.py,sha256=mJzuDgyd5UQqlfubj8e9uhmbUxliYvD1y-oQkfenIAM,6414
3
+ tbr_deal_finder/cli.py,sha256=YnfYyjoeQ0pefPdT-fcz19NQWlEWTHldAR52owoAxd8,7493
4
+ tbr_deal_finder/config.py,sha256=Bpr9C9NClsHrFJuykcITrtRTr2vxAzbcZrMQ85neQ-Y,3978
5
+ tbr_deal_finder/migrations.py,sha256=fO7r2JbWb6YG0CsPqauakwvbKaEFPxqX1PP8c8N03Wc,4951
6
+ tbr_deal_finder/owned_books.py,sha256=Cf1VeiSg7XBi_TXptJfy5sO1mEgMMQWbJ_P6SzAx0nQ,516
7
+ tbr_deal_finder/retailer_deal.py,sha256=_0ZxsTB3frWHKFmfIWZOdsXGe5zOylNh04oamuWd45c,6955
8
+ tbr_deal_finder/tracked_books.py,sha256=1zKv1aRqP9AnyZlzfyAWWUNamrFG6UhWXubZ-1HzBm4,12486
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/queries/latest_unknown_book_sync.sql,sha256=d4ewoYP5otnCj0_TqsXCCLI8BEmHzqTyJrGxTvl2l-I,108
14
+ tbr_deal_finder/retailer/__init__.py,sha256=OD6jUYV8LaURxqHnZq-aiFi7OdWG6qWznRlF_g246lo,316
15
+ tbr_deal_finder/retailer/amazon.py,sha256=sUjNkKEBEOeT_mYPDyi4XKvijM8kM8ZT4hKAe7-j7yI,2468
16
+ tbr_deal_finder/retailer/audible.py,sha256=qwXDKc1W8vGGhqvU2YI7hNfD1rHz2yL-8foXstxb8t8,3991
17
+ tbr_deal_finder/retailer/chirp.py,sha256=f_O-6X9duR_gBT8UWDxDr-KQYSRFOOiYOX9Az2pCi6Y,9183
18
+ tbr_deal_finder/retailer/kindle.py,sha256=ZkOqNu43C5JbngB6lZhdK0373NH-iJKHE1fFIjGuMow,4829
19
+ tbr_deal_finder/retailer/librofm.py,sha256=TRey_38sf3VGpvXxLwhdqwYKP7Izj07c9Qs3n1pu5_Y,6494
20
+ tbr_deal_finder/retailer/models.py,sha256=xm99ngt_Ze7yyEwttddkpwL7xjy0YfcFAduV6Rsx63M,2510
21
+ tbr_deal_finder-0.2.1.dist-info/METADATA,sha256=QKG3ak5XtMq_TLYHF10BFDak_tycxBCJaeNce6qKVLg,4381
22
+ tbr_deal_finder-0.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ tbr_deal_finder-0.2.1.dist-info/entry_points.txt,sha256=y_KG1k8xVCY8gngSZ-na2bkK-tTLUdOc_qZ9Djwldv0,60
24
+ tbr_deal_finder-0.2.1.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
25
+ tbr_deal_finder-0.2.1.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- tbr_deal_finder/__init__.py,sha256=WCoj0GZrRiCQlrpkLTw1VUeJmX-RtBLdLqnFYn1Es_4,208
2
- tbr_deal_finder/book.py,sha256=vCvkjU98mI0Z7WW_Z3GppnI4aem9ht-flB8HB4RCujQ,6107
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=TdVD5kIgdkDoQNwBIsOikrvFiQiIezmSYf64F5cBN0o,10856
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=OD6jUYV8LaURxqHnZq-aiFi7OdWG6qWznRlF_g246lo,316
14
- tbr_deal_finder/retailer/amazon.py,sha256=rYJlBT4JsOg7QVIhJ8a7xJKUjczP_RMK2m-ddH7FjlQ,2472
15
- tbr_deal_finder/retailer/audible.py,sha256=qwXDKc1W8vGGhqvU2YI7hNfD1rHz2yL-8foXstxb8t8,3991
16
- tbr_deal_finder/retailer/chirp.py,sha256=f_O-6X9duR_gBT8UWDxDr-KQYSRFOOiYOX9Az2pCi6Y,9183
17
- tbr_deal_finder/retailer/kindle.py,sha256=ELdKSzKMCmZWw8TjGHihuyZcgqwVygi94mZFOYQ61qY,4489
18
- tbr_deal_finder/retailer/librofm.py,sha256=Oi9UlkyCqdI-PSRApGSCv_TWP7yWwvYEADOZleAOSmM,6357
19
- tbr_deal_finder/retailer/models.py,sha256=xm99ngt_Ze7yyEwttddkpwL7xjy0YfcFAduV6Rsx63M,2510
20
- tbr_deal_finder-0.2.0.dist-info/METADATA,sha256=ZS-Q4WHHC3Y7BGbSC7j6ClKizcdVMSVfHJ2KpbV7DXk,4363
21
- tbr_deal_finder-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- tbr_deal_finder-0.2.0.dist-info/entry_points.txt,sha256=y_KG1k8xVCY8gngSZ-na2bkK-tTLUdOc_qZ9Djwldv0,60
23
- tbr_deal_finder-0.2.0.dist-info/licenses/LICENSE,sha256=rNc0wNPn4d4HHu6ZheJzeUaz_FbJ4rj2Dr2FjAivkNg,1064
24
- tbr_deal_finder-0.2.0.dist-info/RECORD,,