Skip to content

Commit 7f7c9ff

Browse files
committed
fix circularchecker false positives, rewrite cache
1 parent 5550919 commit 7f7c9ff

2 files changed

Lines changed: 85 additions & 110 deletions

File tree

pybpsapi.py

Lines changed: 84 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import mysql.connector
12
import requests
2-
import pickle
33

44

55
class API:
@@ -41,8 +41,7 @@ def latest(self, category: str or int) -> dict | None:
4141
# /list endpoint
4242
def list_(self, category: str or int, amount: int = -1) -> list | None:
4343
"""The `/list` endpoint returns a list of circulars from a particular category"""
44-
if type(category) == int:
45-
category = int(category)
44+
if type(category) is int:
4645
if not 1 < category < 100:
4746
raise ValueError("Invalid category Number")
4847

@@ -106,7 +105,7 @@ def getpng(self, url: str) -> list | None:
106105

107106

108107
class CircularChecker:
109-
def __init__(self, category, url: str = "https://bpsapi.rajtech.me/", cache_method=None, debug: bool = False,
108+
def __init__(self, category, url: str = "https://bpsapi.rajtech.me/", cache_method='sqlite', debug: bool = False,
110109
**kwargs):
111110
self.url = url
112111
self.category = category
@@ -134,87 +133,69 @@ def __init__(self, category, url: str = "https://bpsapi.rajtech.me/", cache_meth
134133

135134
self.cache_method = cache_method
136135

137-
if cache_method is not None:
138-
if cache_method == "database":
139-
try:
140-
self.db_name = kwargs['db_name']
141-
self.db_path = kwargs['db_path']
142-
self.db_table = kwargs['db_table']
143-
except KeyError:
144-
raise ValueError("Invalid Database Parameters")
145-
146-
import sqlite3
147-
import os
148-
149-
if not os.path.exists(self.db_path + f"/{self.db_name}.db"):
150-
os.mkdir(self.db_path)
151-
152-
self._con = sqlite3.connect(self.db_path + f"/{self.db_name}.db")
153-
self._cur = self._con.cursor()
154-
155-
self._cur.execute(
156-
f"CREATE TABLE IF NOT EXISTS {self.db_table} (title TEXT, category TEXT, data BLOB)")
157-
158-
# check if the cache exists
159-
self._cur.execute(f"SELECT * FROM {self.db_table} WHERE title = ? AND category = ?",
160-
("circular_list", self.category))
161-
if self._cur.fetchone() is None:
162-
self._cur.execute(f"INSERT INTO {self.db_table} VALUES (?, ?, ?)",
163-
("circular_list", self.category, pickle.dumps([])))
164-
self._con.commit()
165-
166-
elif cache_method == "pickle":
167-
try:
168-
self.pickle_path = kwargs['pickle_path']
169-
self.pickle_name = kwargs['pickle_name']
170-
except KeyError:
171-
raise ValueError("Invalid Pickle Path")
172-
173-
if self.pickle_name.endswith(".pickle"):
174-
self.pickle_name = self.pickle_name[:-7]
175-
176-
import os
177-
if not os.path.exists(self.pickle_path):
178-
os.mkdir(self.pickle_path)
179-
180-
# create a pickle file if it doesn't exist
181-
if not os.path.exists(self.pickle_path + f"/{self.pickle_name}.pickle"):
182-
with open(self.pickle_path + f"/{self.pickle_name}.pickle", "wb") as f:
183-
pickle.dump([], f)
184-
185-
else:
186-
raise ValueError("Invalid Cache Method")
136+
if cache_method is None:
137+
raise ValueError("Invalid Cache Method")
187138

188-
def get_cache(self) -> list[list]:
189-
if self.cache_method == "database":
190-
self._cur.execute(f"SELECT * FROM {self.db_table} WHERE category = ?", (self.category,))
191-
res = self._cur.fetchone()
192-
if res is None:
193-
return []
194-
else:
195-
return pickle.loads(res[2])
139+
if cache_method == "sqlite":
140+
try:
141+
self.db_name = kwargs['db_name']
142+
self.db_path = kwargs['db_path']
143+
self.db_table = kwargs['db_table']
144+
except KeyError:
145+
raise ValueError("Invalid Database Parameters. One of db_name, db_path, db_table not passed into kwargs")
196146

197-
elif self.cache_method == "pickle":
147+
import sqlite3
148+
import os
198149

199-
with open(self.pickle_path + f"/{self.pickle_name}.pickle", "rb") as f:
200-
return pickle.load(f)
150+
if not os.path.exists(self.db_path + f"/{self.db_name}.db"):
151+
os.mkdir(self.db_path)
201152

202-
else:
203-
return self._cache
153+
self._con = sqlite3.connect(self.db_path + f"/{self.db_name}.db")
154+
self._cur = self._con.cursor()
204155

205-
def _set_cache(self, data, title: str = "circular_list"):
206-
if self.cache_method == "database":
207-
self._cur.execute(f"DELETE FROM {self.db_table} WHERE category = ?", (self.category,))
208-
self._cur.execute(f"INSERT INTO {self.db_table} VALUES (?, ?, ?)",
209-
(title, self.category, pickle.dumps(data)))
210-
self._con.commit()
156+
elif cache_method == "mysql":
157+
try:
158+
self.db_name = kwargs['db_name']
159+
self.db_user = kwargs['db_user']
160+
self.db_host = kwargs['db_host']
161+
self.db_port = kwargs['db_port']
162+
self.db_password = kwargs['db_password']
163+
self.db_table = kwargs['db_table']
211164

212-
elif self.cache_method == "pickle":
213-
with open(self.pickle_path + f"/{self.pickle_name}.pickle", "wb") as f:
214-
pickle.dump(data, f)
165+
except KeyError:
166+
raise ValueError("Invalid Database Parameters. One of db_name, db_user, db_host, db_port, db_password, db_table not passed into kwargs")
215167

216-
else:
217-
self._cache = data
168+
self._con = mysql.connector.python(
169+
host=self.db_host, port=self.db_port, password=self.db_password,
170+
user=self.db_user, database=self.db_name,
171+
)
172+
173+
self._cur = self._con.cursor()
174+
175+
self._cur.execute(
176+
f"CREATE TABLE IF NOT EXISTS {self.db_table} (category TEXT, id INTEGER, title TEXT, link TEXT)"
177+
)
178+
self._con.commit()
179+
180+
def get_cache(self) -> list[list]:
181+
self._cur.execute(f"SELECT * FROM {self.db_table} WHERE category = ?", (self.category,))
182+
res = self._cur.fetchall()
183+
184+
return res
185+
186+
def _set_cache(self, data):
187+
# data [ (id, title, link) ]
188+
self._cur.executemany(
189+
f"INSERT IGNORE INTO {self.db_table} (category, id, title, link) VALUES ({self.category}, ?, ?, ?)",
190+
data
191+
)
192+
self._con.commit()
193+
194+
def _add_to_cache(self, id_, title, link):
195+
self._cur.execute(
196+
f"INSERT IGNORE INTO {self.db_table} (id, title, link) VALUES (?, ?, ?, ?)",
197+
(self.category, id_, title, link)
198+
)
218199

219200
def _refresh_cache(self):
220201
request = requests.get(f"{self.url}list/{self.category}")
@@ -229,37 +210,34 @@ def _refresh_cache(self):
229210
self._set_cache(json['data'])
230211

231212
def check(self) -> list[dict] | list[None]:
232-
return_dict = []
233-
old_cached = self.get_cache()
234-
235-
if not old_cached:
236-
self._refresh_cache()
237-
return []
238-
239-
self._cur.execute(f"SELECT * FROM {self.db_table} WHERE category = ?", (self.category,))
240-
res = self._cur.fetchone()
241-
242-
if res is None:
243-
cache = []
244-
else:
245-
cache = pickle.loads(res[2])
213+
self._cur.execute(f"SELECT COUNT(*) FROM {self.db_table} WHERE category = ?", (self.category,))
214+
cached_circular_amount = self._cur.fetchone()[0]
246215

247216
self._refresh_cache()
248-
final_dict = self.get_cache()
217+
new_circular_list = self.get_cache()
218+
# data[(id, title, link)]
249219

250-
if final_dict != old_cached: # If the old and new dict are not the same
251-
new_circular_objects = [i for i in final_dict if i not in old_cached]
220+
if len(new_circular_list) != cached_circular_amount:
221+
self._cur.execute(f"SELECT id FROM {self.db_table} WHERE category = ?", (self.category,))
252222

253-
for circular in new_circular_objects:
254-
# check if they are in the database
255-
if circular in cache:
256-
continue
223+
cached_circular_ids = self._cur.fetchall()
224+
cached_circular_ids = [i[0] for i in cached_circular_ids]
257225

258-
return_dict.append(circular)
226+
new_circular_objects = [i for i in new_circular_list if i[0] not in cached_circular_ids]
259227

260-
# sort the return_dict by circular id in ascending order
261-
return_dict.sort(key=lambda x: x['id'])
262-
return return_dict
228+
# (id, title, link) -> {'id': id, 'title': title, 'link': link}
229+
new_circular_objects = [
230+
{
231+
'id': i[0],
232+
'title': i[1],
233+
'link': [2]
234+
}
235+
for i in new_circular_objects
236+
]
237+
238+
# sort the new_circular_objects by circular id in ascending order
239+
new_circular_objects.sort(key=lambda x: x['id'])
240+
return new_circular_objects
263241

264242
else:
265243
return []
@@ -269,13 +247,10 @@ class CircularCheckerGroup:
269247
def __init__(self, *args, **kwargs):
270248
self._checkers = []
271249

272-
if kwargs.get("debug"):
273-
self.debug = True
274-
else:
275-
self.debug = False
250+
self.debug = bool(kwargs.get("debug"))
276251

277252
for arg in args:
278-
if type(arg) != CircularChecker:
253+
if type(arg) is not CircularChecker:
279254
raise ValueError("Invalid CircularChecker Object")
280255
self._checkers.append(arg)
281256

@@ -285,7 +260,7 @@ def __init__(self, *args, **kwargs):
285260
def add(self, checker: CircularChecker, *args: CircularChecker):
286261
self._checkers.append(checker)
287262
for arg in args:
288-
if type(arg) != CircularChecker:
263+
if type(arg) is not CircularChecker:
289264
raise ValueError("Invalid CircularChecker Object")
290265
self._checkers.append(arg)
291266

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "pybpsapi"
33
version = "1.3.4"
4-
description = "This package is a Python wrapper for the BPS API. It supports all the five endpoints of the API, and also contains a good circular-checking system."
4+
description = "This package is a Python wrapper for the BPS Circular API. It supports all the five endpoints of the API, and also contains a good circular-checking system."
55
author = "Raj Dave"
66
author-email = "rajdave8002@gmail.com"
77
homepage = "https://bpsapi.rajtech.me/docs/"

0 commit comments

Comments
 (0)