1+ import warnings
12import mysql .connector
23import requests
34import sqlite3
45import os
56
7+ _min_category_id = 23
68
79class API :
810 """Methods which communicate with the API"""
@@ -18,100 +20,99 @@ def __init__(self, url="https://bpsapi.rajtech.me/"):
1820 raise ConnectionError ("Invalid API Response. API says there are no categories." )
1921
2022 # /latest endpoint
21- def latest (self , category : str or int ) -> dict | None :
23+ def latest (self , category : str | int ) -> dict | None :
2224 """The `/latest` endpoint returns the latest circular from a particular category"""
23- if type (category ) is int :
24- category = int (category )
2525
26- if not 1 < category < 100 :
27- raise ValueError ("Invalid category Number" )
26+ if type (category ) is str :
27+ if category .isdigit ():
28+ try :
29+ category = int (category )
30+ except ValueError :
31+ warnings .warn (f"Category id: { category } is digit, but cannot be converted to int. "
32+ f"It will be treated as a category name." )
2833
34+ # A category id or category name can be passed in
35+ if type (category ) is int :
36+ if not _min_category_id <= category :
37+ raise ValueError ("Invalid category Number" )
2938 else :
39+ # Check with the API's category list
3040 if category not in self .categories :
3141 raise ValueError ("Invalid category Name" )
3242
3343 request = requests .get (f"{ self .url } latest/{ category } " )
3444 json = request .json ()
3545
36- try :
37- json ['http_status' ]
38- except KeyError :
39- raise ConnectionError ("Invalid API Response" )
40- if json ['http_status' ] == 200 :
41- return json ['data' ]
46+ if json .get ("data" ) is None or json .get ("http_status" ) is None :
47+ raise ConnectionError ("Invalid API Response, it doesn't contain either 'data' or 'http_code'" )
48+
49+ return json ['data' ]
4250
4351 # /list endpoint
44- def list_ (self , category : str or int , amount : int = None ) -> list | None :
52+ def list_ (self , category : str | int , amount : int = None ) -> list | None :
4553 """The `/list` endpoint returns a list of circulars from a particular category"""
4654 if type (category ) is int :
47- if not 1 < category < 100 :
55+ if not _min_category_id <= category :
4856 raise ValueError ("Invalid category Number" )
4957
5058 else :
5159 if category not in self .categories :
5260 raise ValueError ("Invalid category Name" )
5361
54- if amount < 1 :
62+ if type ( amount ) is int and amount < 1 :
5563 amount = None
5664
5765 request = requests .get (f"{ self .url } list/{ category } " )
5866 json = request .json ()
5967
60- try :
61- json ['http_status' ]
62- except KeyError :
63- raise ConnectionError ("Invalid API Response" )
64- if json ['http_status' ] == 200 :
65- return json ['data' ][:amount ]
68+ if json .get ("data" ) is None or json .get ("http_status" ) is None :
69+ raise ConnectionError ("Invalid API Response, it doesn't contain either 'data' or 'http_code'" )
6670
67- def search (self , query : str or int , amount : int = 1 ) -> dict | None :
71+ return json ['data' ][:amount ]
72+
73+ # /search endpoint
74+ def search (self , query : str | int , amount : int = 1 ) -> dict | None :
6875 """The `/search` endpoint lets you search for a circular by its name or ID"""
6976 if query .isdigit () and len (query ) == 4 :
7077 query = int (query )
71- elif type (query ) != str :
72- raise ValueError ("Invalid Query" )
78+ elif type (query ) is not str :
79+ raise ValueError ("Invalid Query. It isn't string " )
7380
7481 params = {'query' : query , 'amount' : amount }
7582
7683 request = requests .get (self .url + "search" , params = params )
7784 json = request .json ()
7885
79- try :
80- json ['http_status' ]
81-
82- except KeyError :
83- raise ConnectionError ("Invalid API Response" )
86+ if json .get ("data" ) is None or json .get ("http_status" ) is None :
87+ raise ConnectionError ("Invalid API Response, it doesn't contain either 'data' or 'http_code'" )
8488
85- if json ['http_status' ] == 200 :
86- return json ['data' ]
89+ return json ['data' ]
8790
8891 # /getpng endpoint
8992 def getpng (self , url : str ) -> list | None :
9093 """The `/getpng` endpoint lets you get the pngs from a circular"""
9194 if type (url ) != str :
92- raise ValueError ("Invalid URL" )
95+ raise ValueError ("Invalid URL. It isn't string. " )
9396
9497 params = {'url' : url }
9598
9699 request = requests .get (self .url + "getpng" , params = params )
97100 json = request .json ()
98101
99- try :
100- json [ 'http_status' ]
102+ if json . get ( "data" ) is None or json . get ( "http_status" ) is None :
103+ raise ConnectionError ( "Invalid API Response, it doesn't contain either 'data' or 'http_code'" )
101104
102- except KeyError :
103- raise ConnectionError ("Invalid API Response" )
104-
105- if json ['http_status' ] == 200 :
106- return json ['data' ]
105+ return json ['data' ]
107106
108107
109108class CircularChecker :
110- def __init__ (self , category , url : str = "https://bpsapi.rajtech.me/" , cache_method = 'sqlite' , ** kwargs ):
109+ def __init__ (self , category : str | int , url : str = "https://bpsapi.rajtech.me/" , cache_method : str = 'sqlite' , ** kwargs ):
111110 self .url = url
112111 self .category = category
112+ self .cache_method = cache_method
113113 self ._cache = []
114114
115+ # Get category names from API
115116 json = requests .get (self .url + "categories" ).json ()
116117
117118 if json ['http_status' ] == 200 :
@@ -125,18 +126,13 @@ def __init__(self, category, url: str = "https://bpsapi.rajtech.me/", cache_meth
125126
126127 # If category id is passed
127128 if type (self .category ) is int :
128- if not 1 < self .category < 100 :
129+ if not _min_category_id <= self .category :
129130 raise ValueError ("Invalid category Number" )
130- # If category name is passed
131- else :
131+ else : # If category name is passed
132132 if self .category not in categories :
133133 raise ValueError ("Invalid category Name" )
134134
135- self .cache_method = cache_method
136-
137- if cache_method is None :
138- raise ValueError ("Invalid Cache Method" )
139-
135+ # For the sqlite cache method
140136 if self .cache_method == "sqlite" :
141137 try :
142138 self .db_name = kwargs ['db_name' ]
@@ -146,12 +142,14 @@ def __init__(self, category, url: str = "https://bpsapi.rajtech.me/", cache_meth
146142 raise ValueError (
147143 "Invalid Database Parameters. One of db_name, db_path, db_table not passed into kwargs" )
148144
145+ # Create local db if it does not exist
149146 if not os .path .exists (self .db_path + f"/{ self .db_name } .db" ):
150147 os .mkdir (self .db_path )
151148
152149 self ._con = sqlite3 .connect (self .db_path + f"/{ self .db_name } .db" )
153150 self ._cur = self ._con .cursor ()
154151
152+ # For the mysql/mariadb cache method
155153 elif cache_method == "mysql" :
156154 try :
157155 self .db_name = kwargs ['db_name' ]
@@ -172,8 +170,9 @@ def __init__(self, category, url: str = "https://bpsapi.rajtech.me/", cache_meth
172170 self ._cur = self ._con .cursor (prepared = True )
173171
174172 else :
175- raise Exception ("Invalid cache method. Only mysql and sqlite allowed" )
173+ raise ValueError ("Invalid cache method. Only mysql and sqlite allowed" )
176174
175+ # Create a table to cache circulars if it's not there
177176 self ._cur .execute (
178177 f"""
179178 CREATE TABLE IF NOT EXISTS { self .db_table } (
@@ -187,13 +186,14 @@ def __init__(self, category, url: str = "https://bpsapi.rajtech.me/", cache_meth
187186 self ._con .commit ()
188187
189188
190-
189+ # Method to retrieve cache from the database
191190 def get_cache (self ) -> list [list ] | list :
192191 self ._cur .execute (f"SELECT id, title, link FROM { self .db_table } WHERE category = ?" , (self .category ,))
193192 res = self ._cur .fetchall ()
194193
195194 return res
196195
196+ # Method to add multiple items to cache
197197 def _set_cache (self , data ):
198198 # data [ (id, title, link) ]
199199 query = f"INSERT OR IGNORE INTO { self .db_table } (category, id, title, link) VALUES (?, ?, ?, ?)"
@@ -204,25 +204,26 @@ def _set_cache(self, data):
204204 self ._cur .executemany (query , tuple ((self .category , * d ) for d in data ))
205205 self ._con .commit ()
206206
207-
208- def _add_to_cache (self , id_ , title , link ):
207+ # Method to add a single item to cache
208+ def _add_to_cache (self , id_ : int , title : str , link : str ):
209209 query = f"INSERT OR IGNORE INTO { self .db_table } (id, title, link) VALUES (?, ?, ?, ?)"
210210
211211 if self .cache_method == 'mysql' :
212212 query = query .replace ("OR " , "" )
213213
214214 self ._cur .execute (query , (self .category , id_ , title , link ))
215215
216+ # Method to retrieve circulars from the API and insert into cache
216217 def _refresh_cache (self ):
217218 request = requests .get (f"{ self .url } list/{ self .category } " )
218219 json : dict = request .json ()
219220
220- try :
221- json ['http_status' ]
222- except KeyError :
223- raise ValueError ("Invalid API Response" )
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'" )
224223
225224 self ._cur .execute (f"SELECT id FROM { self .db_table } WHERE category = ?" , (self .category ,))
225+
226+ # ((1234,), (4567,), ...) -> ('1234', '4567')
226227 cached_ids : list = self ._cur .fetchall ()
227228 cached_ids : tuple [str , ...] = tuple ([str (i [0 ]) for i in cached_ids ])
228229
@@ -231,22 +232,24 @@ def _refresh_cache(self):
231232 [
232233 (i ['id' ], i ['title' ], i ['link' ])
233234 for i in json ['data' ]
234- if i ['id' ] not in cached_ids
235+ if i ['id' ] not in cached_ids # Add only new circulars to the database
235236 ]
236237 )
237238
238- def check (self ) -> list [dict ] | list [None ]:
239+ # Method to check for new circular(s)
240+ 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.
239243 self ._cur .execute (f"SELECT id FROM { self .db_table } WHERE category = ?" , (self .category ,))
240244
241245 cached_circular_ids = self ._cur .fetchall ()
242- cached_circular_ids = [i [0 ] for i in cached_circular_ids ]
246+ cached_circular_ids = [i [0 ] for i in cached_circular_ids ] # [(id, title, link)]
243247
244248 self ._refresh_cache ()
245- new_circular_list = self .get_cache ()
246- # data [(id, title, link)]
247-
248- if len (new_circular_list ) != len (cached_circular_ids ):
249+ new_circular_list = self .get_cache () #
249250
251+ # If there are new circulars
252+ if len (new_circular_list ) > len (cached_circular_ids ):
250253 new_circular_objects = [i for i in new_circular_list if i [0 ] not in cached_circular_ids ]
251254
252255 # (id, title, link) -> {'id': id, 'title': title, 'link': link}
@@ -263,45 +266,60 @@ def check(self) -> list[dict] | list[None]:
263266 new_circular_objects .sort (key = lambda x : x ['id' ])
264267 return new_circular_objects
265268
266- else :
267- return []
269+ return []
270+
271+ # Close connections when object is deleted
272+ def __del__ (self ):
273+ if hasattr (self , '_con' ):
274+ self ._con .close ()
268275
269276
270277class CircularCheckerGroup :
271- def __init__ (self , * args , ** kwargs ):
278+ def __init__ (self , * circular_checkers : CircularChecker , ** kwargs ):
272279 self ._checkers = []
273280
274- for arg in args :
275- if type (arg ) is not CircularChecker :
281+ # Add each checker to self._checkers
282+ for checker in circular_checkers :
283+ if type (checker ) is not CircularChecker :
276284 raise ValueError ("Invalid CircularChecker Object" )
277- self ._checkers .append (arg )
285+ self ._checkers .append (checker )
278286
279287 if bool (kwargs .get ("debug" )):
280288 self .checkers = self ._checkers
281289
282- def add (self , checker : CircularChecker , * args : CircularChecker ):
290+ # Method to add a circular checker to this group
291+ def add (self , checker : CircularChecker , * circular_checkers : CircularChecker ):
283292 self ._checkers .append (checker )
284- for arg in args :
285- if type (arg ) is not CircularChecker :
293+
294+ for checker in circular_checkers :
295+ if type (checker ) is not CircularChecker :
286296 raise ValueError ("Invalid CircularChecker Object" )
287- self ._checkers .append (arg )
297+ self ._checkers .append (checker )
288298
299+ # Method to create a circular checker and add it to the group
289300 def create (self , category , url : str = "https://bpsapi.rajtech.me/" , cache_method = None , ** kwargs ):
290301 checker = CircularChecker (category , url , cache_method , ** kwargs )
291302 self ._checkers .append (checker )
292303
304+ # Method to check for new circulars in each one of the checkers
293305 def check (self ) -> dict [list [dict ], ...] | dict :
294306 return_dict = {}
295307 for checker in self ._checkers :
296308 return_dict [checker .category ] = checker .check ()
297309 return return_dict
298310
311+ # Method to refresh (sync) cache from API
299312 def refresh_cache (self ):
300313 for checker in self ._checkers :
301314 checker .refresh_cache ()
302315
316+ # Method to get the cache of all checkers
303317 def get_cache (self ) -> dict [list [list ]] | dict :
304318 return_dict = {}
305319 for checker in self ._checkers :
306320 return_dict [checker .category ] = checker .get_cache ()
307321 return return_dict
322+
323+ def __del__ (self ):
324+ for checker in self ._checkers :
325+ del checker
0 commit comments