Skip to content

Commit a0609c9

Browse files
committed
implement new circular checking method that uses api endpoint
this uses the /new-circular api endpoint instead of caching every single circular and then manually re-checking
1 parent e6c1c86 commit a0609c9

File tree

1 file changed

+79
-144
lines changed

1 file changed

+79
-144
lines changed

pybpsapi.py

Lines changed: 79 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pickle
12
import warnings
23
import mysql.connector
34
import requests
@@ -106,11 +107,11 @@ def getpng(self, url: str) -> list | None:
106107

107108

108109
class CircularChecker:
109-
def __init__(self, category: str | int, url: str = "https://bpsapi.rajtech.me/", cache_method: str = 'sqlite', **kwargs):
110+
def __init__(self, category: str | int = None, url: str = "https://bpsapi.rajtech.me/", cache_method: str = 'sqlite', **kwargs):
110111
self.url = url
111112
self.category = category
112113
self.cache_method = cache_method
113-
self._cache = []
114+
self.latest_circular_id = self.get_cache()
114115

115116
# Get category names from API
116117
json = requests.get(self.url + "categories").json()
@@ -120,17 +121,14 @@ def __init__(self, category: str | int, url: str = "https://bpsapi.rajtech.me/",
120121
else:
121122
raise ConnectionError("Invalid API Response. API says there are no categories.")
122123

123-
if kwargs.get("debug"):
124-
self.set_cache = self._set_cache
125-
self.refresh_cache = self._refresh_cache
126-
127124
# If category id is passed
128-
if type(self.category) is int:
129-
if not _min_category_id <= self.category:
130-
raise ValueError("Invalid category Number")
131-
else: # If category name is passed
132-
if self.category not in categories:
133-
raise ValueError("Invalid category Name")
125+
if category is not None:
126+
if type(self.category) is int:
127+
if not _min_category_id <= self.category:
128+
raise ValueError("Invalid category Number")
129+
else: # If category name is passed
130+
if self.category not in categories:
131+
raise ValueError("Invalid category Name")
134132

135133
# For the sqlite cache method
136134
if self.cache_method == "sqlite":
@@ -146,8 +144,6 @@ def __init__(self, category: str | int, url: str = "https://bpsapi.rajtech.me/",
146144
if not os.path.exists(self.db_path + f"/{self.db_name}.db"):
147145
os.mkdir(self.db_path)
148146

149-
self._con = sqlite3.connect(self.db_path + f"/{self.db_name}.db")
150-
self._cur = self._con.cursor()
151147

152148
# For the mysql/mariadb cache method
153149
elif cache_method == "mysql":
@@ -163,163 +159,102 @@ def __init__(self, category: str | int, url: str = "https://bpsapi.rajtech.me/",
163159
raise ValueError(
164160
"Invalid Database Parameters. One of db_name, db_user, db_host, db_port, db_password, db_table not passed into kwargs")
165161

166-
self._con = mysql.connector.connect(
167-
host=self.db_host, port=self.db_port, password=self.db_password,
168-
user=self.db_user, database=self.db_name,
169-
)
170-
self._cur = self._con.cursor(prepared=True)
162+
elif cache_method == 'pickle':
163+
try:
164+
self.cache_file = kwargs['cache_file']
165+
except KeyError:
166+
raise ValueError("Invalid cache file path")
171167

172168
else:
173169
raise ValueError("Invalid cache method. Only mysql and sqlite allowed")
174170

175171
# Create a table to cache circulars if it's not there
176-
self._cur.execute(
177-
f"""
178-
CREATE TABLE IF NOT EXISTS {self.db_table} (
179-
category TEXT,
180-
id INTEGER UNIQUE,
181-
title TEXT,
182-
link TEXT
183-
)
184-
"""
185-
)
186-
self._con.commit()
172+
if self.cache_method in ('sqlite', 'mysql'):
173+
con, cur = self._get_db()
174+
175+
cur.execute(
176+
f"""
177+
CREATE TABLE IF NOT EXISTS {self.db_table} (
178+
category TEXT,
179+
id INTEGER UNIQUE,
180+
title TEXT,
181+
link TEXT
182+
)
183+
"""
184+
)
185+
con.commit()
186+
con.close()
187187

188+
# If cache method is pickle, create a file if it doesn't exist
189+
elif self.cache_method == 'pickle':
190+
if not os.path.exists(self.cache_file):
191+
with open(self.cache_file, 'wb') as f:
192+
f.write(b'')
188193

189-
# Method to retrieve cache from the database
190-
def get_cache(self) -> list[list] | list:
191-
self._cur.execute(f"SELECT id, title, link FROM {self.db_table} WHERE category = ?", (self.category,))
192-
res = self._cur.fetchall()
194+
def _get_db(self):
195+
if self.cache_method == 'mysql':
196+
con = mysql.connector.connect(
197+
host=self.db_host, port=self.db_port, password=self.db_password,
198+
user=self.db_user, database=self.db_name,
199+
)
200+
cur = con.cursor(prepared=True)
193201

194-
return res
202+
elif self.cache_method == 'sqlite':
203+
con = sqlite3.connect(self.db_path + f"/{self.db_name}.db")
204+
cur = con.cursor()
205+
else:
206+
raise ValueError("Method not supported for this cache method")
195207

196-
# Method to add multiple items to cache
197-
def _set_cache(self, data):
198-
# data [ (id, title, link) ]
199-
query = f"INSERT OR IGNORE INTO {self.db_table} (category, id, title, link) VALUES (?, ?, ?, ?)"
208+
return con, cur
200209

201-
if self.cache_method == 'mysql':
202-
query = query.replace("OR ", "")
203210

204-
self._cur.executemany(query, tuple((self.category, *d) for d in data))
205-
self._con.commit()
211+
# Method to retrieve cache from the database
212+
def get_cache(self) -> int | None:
213+
if self.cache_method in ('sqlite', 'mysql'):
214+
con, cur = self._get_db()
206215

207-
# Method to add a single item to cache
208-
def _add_to_cache(self, id_: int, title: str, link: str):
209-
query = f"INSERT OR IGNORE INTO {self.db_table} (id, title, link) VALUES (?, ?, ?, ?)"
216+
cur.execute(f"SELECT latest_circular_id FROM {self.db_table} WHERE category = ?", (self.category,))
217+
res = cur.fetchone()
210218

211-
if self.cache_method == 'mysql':
212-
query = query.replace("OR ", "")
219+
elif self.cache_method == 'pickle':
220+
with open(self.cache_file, 'rb') as f:
221+
res = pickle.load(f)
213222

214-
self._cur.execute(query, (self.category, id_, title, link))
223+
return res
215224

216-
# Method to retrieve circulars from the API and insert into cache
217-
def _refresh_cache(self):
218-
request = requests.get(f"{self.url}list/{self.category}")
219-
json: dict = request.json()
225+
# Method to add multiple items to cache
226+
def _set_cache(self, circular_id: int):
220227

221-
if json.get("data") is None or json.get("http_status") is None:
222-
raise ConnectionError("Invalid API Response, it doesn't contain either 'data' or 'http_code'")
228+
if self.cache_method in ('sqlite', 'mysql'):
229+
con, cur = self._get_db()
230+
query = f"INSERT OR IGNORE INTO {self.db_table} (category, latest_circular_id) VALUES (?, ?)"
223231

224-
self._cur.execute(f"SELECT id FROM {self.db_table} WHERE category = ?", (self.category,))
232+
if self.cache_method == 'mysql':
233+
query = query.replace("OR ", "")
225234

226-
# ((1234,), (4567,), ...) -> ('1234', '4567')
227-
cached_ids: list = self._cur.fetchall()
228-
cached_ids: tuple[str, ...] = tuple([str(i[0]) for i in cached_ids])
235+
cur.execute(query, (self.category, circular_id,))
236+
con.commit()
237+
238+
elif self.cache_method == 'pickle':
239+
with open(self.cache_file, 'wb') as f:
240+
pickle.dumps(circular_id, f)
229241

230-
if json['http_status'] == 200:
231-
self._set_cache(
232-
[
233-
(i['id'], i['title'], i['link'])
234-
for i in json['data']
235-
if i['id'] not in cached_ids # Add only new circulars to the database
236-
]
237-
)
238242

239243
# Method to check for new circular(s)
240244
def check(self) -> list[dict] | list:
241-
# First get cached circulars and store them in a variable 'cached_circular_ids'
242-
# Then refresh cache and get the new list of circulars, and then compare and find new ones.
243-
self._cur.execute(f"SELECT id FROM {self.db_table} WHERE category = ?", (self.category,))
245+
res = requests.get(self.url + f"new-circulars/{self.latest_circular_id}").json()['data']
246+
# it's sorted in descending order
244247

245-
cached_circular_ids = self._cur.fetchall()
246-
cached_circular_ids = [i[0] for i in cached_circular_ids] # [(id, title, link)]
248+
if len(res) > 0:
249+
self._set_cache(res[0]['id'])
247250

248-
self._refresh_cache()
249-
new_circular_list = self.get_cache() #
251+
if self.category:
252+
res = [circular for circular in res if circular['category'] == self.category]
250253

251-
# If there are new circulars
252-
if len(new_circular_list) > len(cached_circular_ids):
253-
new_circular_objects = [i for i in new_circular_list if i[0] not in cached_circular_ids]
254-
255-
# (id, title, link) -> {'id': id, 'title': title, 'link': link}
256-
new_circular_objects = [
257-
{
258-
'id': i[0],
259-
'title': i[1],
260-
'link': i[2]
261-
}
262-
for i in new_circular_objects
263-
]
264-
265-
# sort the new_circular_objects by circular id in ascending order
266-
new_circular_objects.sort(key=lambda x: x['id'])
267-
return new_circular_objects
254+
return res
268255

269-
return []
270256

271257
# Close connections when object is deleted
272258
def __del__(self):
273-
if hasattr(self, '_con'):
274-
self._con.close()
275-
276-
277-
class CircularCheckerGroup:
278-
def __init__(self, *circular_checkers: CircularChecker, **kwargs):
279-
self._checkers = []
280-
281-
# Add each checker to self._checkers
282-
for checker in circular_checkers:
283-
if type(checker) is not CircularChecker:
284-
raise ValueError("Invalid CircularChecker Object")
285-
self._checkers.append(checker)
286-
287-
if bool(kwargs.get("debug")):
288-
self.checkers = self._checkers
289-
290-
# Method to add a circular checker to this group
291-
def add(self, checker: CircularChecker, *circular_checkers: CircularChecker):
292-
self._checkers.append(checker)
293-
294-
for checker in circular_checkers:
295-
if type(checker) is not CircularChecker:
296-
raise ValueError("Invalid CircularChecker Object")
297-
self._checkers.append(checker)
298-
299-
# Method to create a circular checker and add it to the group
300-
def create(self, category, url: str = "https://bpsapi.rajtech.me/", cache_method=None, **kwargs):
301-
checker = CircularChecker(category, url, cache_method, **kwargs)
302-
self._checkers.append(checker)
303-
304-
# Method to check for new circulars in each one of the checkers
305-
def check(self) -> dict[list[dict], ...] | dict:
306-
return_dict = {}
307-
for checker in self._checkers:
308-
return_dict[checker.category] = checker.check()
309-
return return_dict
310-
311-
# Method to refresh (sync) cache from API
312-
def refresh_cache(self):
313-
for checker in self._checkers:
314-
checker.refresh_cache()
315-
316-
# Method to get the cache of all checkers
317-
def get_cache(self) -> dict[list[list]] | dict:
318-
return_dict = {}
319-
for checker in self._checkers:
320-
return_dict[checker.category] = checker.get_cache()
321-
return return_dict
259+
pass
322260

323-
def __del__(self):
324-
for checker in self._checkers:
325-
del checker

0 commit comments

Comments
 (0)