Skip to content

Commit 7369812

Browse files
committed
many more changes and fixes, add api methods to package root
1 parent a0609c9 commit 7369812

File tree

2 files changed

+116
-34
lines changed

2 files changed

+116
-34
lines changed

pybpsapi/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .main import *
2+
3+
4+
_default_api = API()
5+
6+
latest = _default_api.latest
7+
list = _default_api.list
8+
search = _default_api.search
9+
getpng = _default_api.getpng

pybpsapi.py renamed to pybpsapi/main.py

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -107,44 +107,46 @@ def getpng(self, url: str) -> list | None:
107107

108108

109109
class CircularChecker:
110-
def __init__(self, category: str | int = None, url: str = "https://bpsapi.rajtech.me/", cache_method: str = 'sqlite', **kwargs):
111-
self.url = url
110+
def __init__(
111+
self, category: str | int = None,
112+
api_url: str = "https://bpsapi.rajtech.me/",
113+
fallback_api_url: str = None,
114+
cache_method: str = 'pickle', **kwargs
115+
):
116+
self.api_url = api_url
117+
self.fallback_api_url = fallback_api_url
112118
self.category = category
113119
self.cache_method = cache_method
114-
self.latest_circular_id = self.get_cache()
115120

116121
# Get category names from API
117-
json = requests.get(self.url + "categories").json()
122+
categories = self._send_api_request("categories")
118123

119-
if json['http_status'] == 200:
120-
categories = json['data']
121-
else:
122-
raise ConnectionError("Invalid API Response. API says there are no categories.")
123-
124-
# If category id is passed
124+
# If this circular checker is supposed to be for a specific category of circulars only
125+
# Check if the category name or id is valid
125126
if category is not None:
126127
if type(self.category) is int:
127128
if not _min_category_id <= self.category:
128129
raise ValueError("Invalid category Number")
129130
else: # If category name is passed
130131
if self.category not in categories:
131-
raise ValueError("Invalid category Name")
132+
raise ValueError(f"Invalid category Name ({self.category})."
133+
f"Allowed are {categories}")
132134

135+
# Check if all required variables for each cache method are passed in kwargs
136+
# And create a pickle file or database file on disk (sqlite) if it doesn't exist
133137
# For the sqlite cache method
134138
if self.cache_method == "sqlite":
135139
try:
136-
self.db_name = kwargs['db_name']
137140
self.db_path = kwargs['db_path']
138141
self.db_table = kwargs['db_table']
139142
except KeyError:
140143
raise ValueError(
141144
"Invalid Database Parameters. One of db_name, db_path, db_table not passed into kwargs")
142145

143146
# Create local db if it does not exist
144-
if not os.path.exists(self.db_path + f"/{self.db_name}.db"):
147+
if not os.path.exists(self.db_path):
145148
os.mkdir(self.db_path)
146149

147-
148150
# For the mysql/mariadb cache method
149151
elif cache_method == "mysql":
150152
try:
@@ -159,37 +161,53 @@ def __init__(self, category: str | int = None, url: str = "https://bpsapi.rajtec
159161
raise ValueError(
160162
"Invalid Database Parameters. One of db_name, db_user, db_host, db_port, db_password, db_table not passed into kwargs")
161163

164+
# For the pickle cache method
162165
elif cache_method == 'pickle':
163166
try:
164167
self.cache_file = kwargs['cache_file']
165168
except KeyError:
166169
raise ValueError("Invalid cache file path")
167170

171+
if not os.path.exists(self.cache_file):
172+
with open(self.cache_file, 'wb') as f:
173+
f.write(b'')
174+
168175
else:
169176
raise ValueError("Invalid cache method. Only mysql and sqlite allowed")
170177

171-
# Create a table to cache circulars if it's not there
178+
# For sqlite and mysql, create a table if it doesn't exist in the database
172179
if self.cache_method in ('sqlite', 'mysql'):
173180
con, cur = self._get_db()
174181

175182
cur.execute(
176183
f"""
177184
CREATE TABLE IF NOT EXISTS {self.db_table} (
178-
category TEXT,
179-
id INTEGER UNIQUE,
180-
title TEXT,
181-
link TEXT
185+
category VARCHAR(15) PRIMARY KEY,
186+
latest_circular_id INTEGER
182187
)
183188
"""
184189
)
185190
con.commit()
186191
con.close()
187192

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'')
193+
if self.get_cache() is None:
194+
self.check()
195+
196+
def _send_api_request(self, endpoint: str, fallback=False) -> dict:
197+
try:
198+
api_url = self.fallback_api_url if fallback else self.api_url
199+
request = requests.get(api_url + endpoint, timeout=5)
200+
json = request.json()
201+
except requests.exceptions.ConnectionError:
202+
if fallback:
203+
raise ConnectionError("Both API URLs are down")
204+
if self.fallback_api_url:
205+
warnings.warn("API is down. Trying fallback API URL")
206+
return self._send_api_request(endpoint, True)
207+
else:
208+
raise ConnectionError("API is down")
209+
210+
return json['data']
193211

194212
def _get_db(self):
195213
if self.cache_method == 'mysql':
@@ -216,45 +234,100 @@ def get_cache(self) -> int | None:
216234
cur.execute(f"SELECT latest_circular_id FROM {self.db_table} WHERE category = ?", (self.category,))
217235
res = cur.fetchone()
218236

237+
if res is not None:
238+
res = res[0]
239+
219240
elif self.cache_method == 'pickle':
220241
with open(self.cache_file, 'rb') as f:
221242
res = pickle.load(f)
243+
else:
244+
raise ValueError("Method not supported for this cache method")
245+
246+
if res is not None:
247+
res = int(res)
222248

223249
return res
224250

225-
# Method to add multiple items to cache
226251
def _set_cache(self, circular_id: int):
227252

228253
if self.cache_method in ('sqlite', 'mysql'):
229254
con, cur = self._get_db()
230-
query = f"INSERT OR IGNORE INTO {self.db_table} (category, latest_circular_id) VALUES (?, ?)"
255+
# cur.execute(f"DELETE FROM {self.db_table} WHERE category = ?", (self.cate))
231256

232-
if self.cache_method == 'mysql':
233-
query = query.replace("OR ", "")
257+
query = f"REPLACE INTO {self.db_table} (category, latest_circular_id) VALUES (?, ?)"
234258

235259
cur.execute(query, (self.category, circular_id,))
236260
con.commit()
237261

238262
elif self.cache_method == 'pickle':
239263
with open(self.cache_file, 'wb') as f:
240-
pickle.dumps(circular_id, f)
264+
f.write(pickle.dumps(circular_id))
241265

242266

243-
# Method to check for new circular(s)
267+
# Method to check for new circulars
244268
def check(self) -> list[dict] | list:
245-
res = requests.get(self.url + f"new-circulars/{self.latest_circular_id}").json()['data']
269+
270+
if cache := self.get_cache() is not None:
271+
res = self._send_api_request(f'new-circulars/{cache}')
272+
else:
273+
res = self._send_api_request('new-circulars/')
246274
# it's sorted in descending order
247275

276+
# If the API found new circulars
248277
if len(res) > 0:
249278
self._set_cache(res[0]['id'])
250279

280+
# If this circular-checker is meant for only one category,
281+
# remove circulars of any other category
251282
if self.category:
252283
res = [circular for circular in res if circular['category'] == self.category]
253284

285+
# remove the 'category' key from each of the circular objects
286+
for circular in res:
287+
del circular['category']
254288
return res
255289

256290

257291
# Close connections when object is deleted
258-
def __del__(self):
259-
pass
260-
292+
# def __del__(self):
293+
#
294+
# if hasattr(self, '_con'):
295+
# self._con.close()
296+
297+
298+
class CircularCheckerGroup:
299+
def __init__(self, *circular_checkers: CircularChecker):
300+
self._checkers = []
301+
302+
# Add each checker to self._checkers
303+
for checker in circular_checkers:
304+
if type(checker) is not CircularChecker:
305+
raise ValueError("Invalid CircularChecker Object")
306+
self._checkers.append(checker)
307+
308+
309+
# Method to add a circular checker to this group
310+
def add(self, checker: CircularChecker, *circular_checkers: CircularChecker):
311+
self._checkers.append(checker)
312+
313+
for checker in circular_checkers:
314+
if type(checker) is not CircularChecker:
315+
raise ValueError("Invalid CircularChecker Object")
316+
self._checkers.append(checker)
317+
318+
# Method to create a circular checker and add it to the group
319+
def create(self, category, url: str = "https://bpsapi.rajtech.me/", cache_method=None, **kwargs):
320+
checker = CircularChecker(category, url, cache_method, **kwargs)
321+
self._checkers.append(checker)
322+
323+
# Method to check for new circulars in each one of the checkers
324+
def check(self) -> dict[list[dict], ...] | dict:
325+
return_dict = {}
326+
for checker in self._checkers:
327+
return_dict[checker.category] = checker.check()
328+
return return_dict
329+
330+
#
331+
# def __del__(self):
332+
# for checker in self._checkers:
333+
# del checker

0 commit comments

Comments
 (0)