· 8 years ago · Jun 30, 2018, 05:12 PM
1import os
2import sqlite3
3import logging
4import re
5from functools import wraps
6from datetime import datetime, timedelta
7from book_review_scraper.exceptions import BookIdCacheMissError, BookIdCacheExpiredError, ISBNError
8
9
10class SqliteCache:
11
12 connection = None
13
14 def __init__(self):
15 self.logger = logging.getLogger(__name__)
16 root_dir = os.path.dirname(os.path.abspath(__file__))
17 self.path = os.path.join(root_dir, '.cache')
18 self.logger.debug('Instantiated with cache_db path as {path}'.format(path=self.path))
19 self.expire_time = timedelta(days=7)
20 # prepare the directory for the cache sqlite db
21 if not os.path.exists(self.path):
22 os.mkdir(self.path)
23 self.logger.debug('Successfully created the storage path for {path}'.format(path=self.path))
24
25 def _get_conn(self, cache_table):
26
27 if self.connection:
28 return self.connection
29
30 cache_db_path = os.path.join(self.path, 'cache.sqlite')
31
32 conn = sqlite3.connect(cache_db_path, timeout=60,
33 detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
34 self.logger.debug('Connected to {path}'.format(path=cache_db_path))
35
36 with conn:
37 cur = conn.cursor()
38 cur.execute("CREATE TABLE IF NOT EXISTS {} "
39 "(isbn INTEGER PRIMARY KEY, "
40 "book_id INTEGER NOT NULL, "
41 "updated TIMESTAMP )".format(cache_table))
42 self.logger.debug('Ran the create table')
43
44 self.connection = conn
45 return self.connection
46
47 def get(self, cache_table, isbn):
48 with self._get_conn(cache_table) as conn:
49 cur = conn.cursor()
50 cur.execute('SELECT book_id, updated '
51 'FROM {} '
52 'WHERE isbn = ?'.format(cache_table), (isbn,))
53 rows = cur.fetchone()
54 if not rows:
55 self.logger.debug(f'{cache_table}, {isbn} cache miss ')
56 raise BookIdCacheMissError(table=cache_table, isbn=isbn)
57 updated = rows[1]
58 now = datetime.now()
59 if updated + self.expire_time < now:
60 raise BookIdCacheExpiredError(table=cache_table, isbn=isbn)
61 return rows[0]
62
63 def set(self, cache_table, isbn, book_id):
64 with self._get_conn(cache_table) as conn:
65 cur = conn.cursor()
66 try:
67 cur.execute('INSERT INTO {} '
68 '(isbn, book_id, updated) '
69 'VALUES (?, ?, ?)'.format(cache_table), (isbn, book_id, datetime.now()))
70 self.logger.debug(f'{cache_table}, insert {isbn} : {book_id}')
71 except sqlite3.IntegrityError:
72 self.update(cache_table, isbn, book_id)
73
74 def remove(self, cache_table, isbn):
75 with self._get_conn(cache_table) as conn:
76 conn.cursor().execute('DELETE FROM {}'
77 ' WHERE isbn = ?'.format(cache_table), (isbn,))
78 self.logger.debug(f'{cache_table}, delete {isbn}')
79
80 def update(self, cache_table, isbn, book_id):
81 with self._get_conn(cache_table) as conn:
82 cur = conn.cursor()
83 cur.execute('UPDATE {} '
84 'SET book_id=?, updated=? '
85 'WHERE isbn = ?'.format(cache_table), (book_id, datetime.now(), isbn))
86 self.logger.debug(f'{cache_table}, update {isbn} : {book_id}')
87
88 def clear(self, cache_table):
89 with self._get_conn(cache_table) as conn:
90 conn.cursor().execute('DELETE FROM {}'.format(cache_table))
91 self.logger.debug(f'{cache_table}, clear')
92
93 def __del__(self):
94 self.logger.debug('Cleans up the object by destroying the sqlite connection')
95 if self.connection:
96 self.connection.close()
97
98
99cache = SqliteCache()
100
101
102def book_id(cache_table):
103 def decorator(fn):
104 @wraps(fn)
105 def wrapped(*args, **kwargs):
106 isbn13 = args[1]
107 if re.match('[\d+]{13}', str(isbn13)) is None:
108 raise ISBNError(bookstore=cache_table, isbn13=isbn13)
109 try:
110 book_id = cache.get(cache_table, isbn13)
111 return book_id
112 except (BookIdCacheMissError, BookIdCacheExpiredError):
113 review_info = fn(*args, **kwargs)
114 cache.set(cache_table, isbn13, review_info.bid)
115 return review_info
116 return wrapped
117 return decorator