Skip to content

Commit dfb66e7

Browse files
committed
Minor improvements and bugfix
1 parent e79344b commit dfb66e7

4 files changed

Lines changed: 106 additions & 79 deletions

File tree

CHANGELOG.MD

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,22 @@ All notable changes to this project will be documented in this file.
6565
### Improved
6666
- `CircularChecker` class now returns a sorted dict of new circulars
6767

68-
# v1.3.1 - 23/6/2023
68+
## v1.3.1 - 23/6/2023
6969

7070
### Fixed
7171
- Changed the param name of /search to `query` from `title` to match API change
7272

73-
# v1.3.2 - 24/7/2023
73+
## v1.3.2 - 24/7/2023
7474

7575
### Fixed
7676
- Fixed dict key error in `CircularChecker` class.
7777

78-
# v1.3.3 - 8/4/2024
78+
## v1.3.3 - 8/4/2024
7979

80-
## Improved
80+
### Improved
8181
- The package now uses bpsapi.rajtech.me/latest/{category} instead of the params method for list and latest
8282

83-
# v1.4 - 12/8/24
83+
## v1.4 - 12/8/24
8484

8585
### Fixed
8686
- CircularChecker's false positives
@@ -89,4 +89,13 @@ All notable changes to this project will be documented in this file.
8989
- Pickle method for cache storage
9090

9191
### Added
92-
- mysql method for cache storage
92+
- mysql method for cache storage
93+
94+
## v1.4.1 - 24/8/24
95+
96+
### Fixed
97+
- A small bug which caused an error when `amount` wasn't passed in list
98+
- Fixed type hints having `or` instead of |
99+
100+
### Improved
101+
- Many little improvements

pybpsapi.py

Lines changed: 90 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import warnings
12
import mysql.connector
23
import requests
34
import sqlite3
45
import os
56

7+
_min_category_id = 23
68

79
class 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

109108
class 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

270277
class 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

Comments
 (0)