qums-fetch 0.4.4.dev1__tar.gz → 0.4.4.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qums-fetch
3
- Version: 0.4.4.dev1
3
+ Version: 0.4.4.dev3
4
4
  Summary: Login and fetch data from QUMS-ERP Dashboard
5
5
  Author: Ishaan Gupta
6
6
  Author-email: Ishaan Gupta <ishaan.ishu@proton.me>
7
7
  Requires-Dist: beautifulsoup4>=4.15.0
8
8
  Requires-Dist: pillow>=12.3.0
9
9
  Requires-Dist: pytesseract>=0.3.13
10
- Requires-Dist: python-dotenv>=1.2.2
11
10
  Requires-Dist: requests>=2.34.2
12
11
  Requires-Python: >=3.14
13
12
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "qums-fetch"
7
- version = "0.4.4.dev1"
7
+ version = "0.4.4.dev3"
8
8
  description = "Login and fetch data from QUMS-ERP Dashboard"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.14"
@@ -12,7 +12,6 @@ dependencies = [
12
12
  "beautifulsoup4>=4.15.0",
13
13
  "pillow>=12.3.0",
14
14
  "pytesseract>=0.3.13",
15
- "python-dotenv>=1.2.2",
16
15
  "requests>=2.34.2",
17
16
  ]
18
17
 
@@ -23,7 +22,9 @@ email = "ishaan.ishu@proton.me"
23
22
  [dependency-groups]
24
23
  dev = [
25
24
  "pytest>=9.1.1",
25
+ "python-dotenv>=1.2.3",
26
26
  "ruff>=0.16.5",
27
+ "ty>=0.0.78",
27
28
  ]
28
29
 
29
30
  [tool.pytest.ini_options]
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "qums-fetch"
7
- version = "0.4.4.dev1"
7
+ version = "0.4.4.dev3"
8
8
  description = "Login and fetch data from QUMS-ERP Dashboard"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -15,14 +15,15 @@ dependencies = [
15
15
  "beautifulsoup4>=4.15.0",
16
16
  "pillow>=12.3.0",
17
17
  "pytesseract>=0.3.13",
18
- "python-dotenv>=1.2.2",
19
18
  "requests>=2.34.2",
20
19
  ]
21
20
 
22
21
  [dependency-groups]
23
22
  dev = [
24
23
  "pytest>=9.1.1",
24
+ "python-dotenv>=1.2.3",
25
25
  "ruff>=0.16.5",
26
+ "ty>=0.0.78",
26
27
  ]
27
28
 
28
29
  [tool.pytest.ini_options]
@@ -57,6 +57,7 @@ class Client:
57
57
  password: str,
58
58
  auto_login: bool = True,
59
59
  login_retries: int = 3,
60
+ cookies: dict | None = None,
60
61
  ):
61
62
  self._validate_credentials(username, password)
62
63
  self._validate_retries(login_retries)
@@ -71,43 +72,24 @@ class Client:
71
72
  self._month_attendance: dict | None = None
72
73
  self._sem_attendance: list[dict] | None = None
73
74
 
75
+ if cookies:
76
+ self._session.cookies.update(cookies)
77
+
74
78
  if auto_login:
75
- if self._load_session() and self._is_session_valid():
79
+ if self._is_session_valid():
76
80
  self._logged_in = True
77
81
  logger.info("Session restored, login skipped.")
78
82
  else:
79
83
  self.login(max_attempts=login_retries)
80
84
 
81
- @property
82
- def _cookie_path(self) -> str:
83
- return f"cookies_{self.username}.json"
84
-
85
- # Save session cookies
86
- def _save_session(self) -> None:
87
- with open(self._cookie_path, "w") as f:
88
- cookies = self._session.cookies.get_dict()
89
- json.dump(cookies, f)
90
- logger.info("Session saved to %s", self._cookie_path)
91
-
92
- # Load session cookies
93
- def _load_session(self) -> bool:
94
- try:
95
- with open(self._cookie_path) as f:
96
- data = json.load(f)
97
- cookies = data
98
- self._session.cookies.update(cookies)
99
- return True
100
- except FileNotFoundError, json.JSONDecodeError, KeyError:
101
- return False
102
-
103
85
  # Check if the session is valid
104
86
  def _is_session_valid(self) -> bool:
105
87
  try:
106
- resp = self._session.post(f"{BASE_URL}/Account/GetStudentDetail", timeout=5)
107
- data = resp.json()
108
- json.loads(data["state"])
109
- return True
110
- except requests.RequestException, json.JSONDecodeError, KeyError:
88
+ resp = self._session.post(
89
+ f"{BASE_URL}/Account/GetStudentDetail", timeout=5, allow_redirects=False
90
+ )
91
+ return resp.status_code == 200
92
+ except requests.RequestException:
111
93
  return False
112
94
 
113
95
  # Re-login if session is expired
@@ -179,7 +161,7 @@ class Client:
179
161
  f"Could not find hidden input '{TOKEN_INPUT_NAME}' on the login page. "
180
162
  "The site's markup may have changed."
181
163
  )
182
- token = token_input["value"]
164
+ token = str(token_input["value"])
183
165
 
184
166
  img_tag = soup.select_one(CAPTCHA_IMG_SELECTOR)
185
167
  if not img_tag or not img_tag.get("src"):
@@ -188,7 +170,7 @@ class Client:
188
170
  "The site's markup may have changed."
189
171
  )
190
172
 
191
- src = img_tag["src"]
173
+ src = str(img_tag["src"])
192
174
  match = re.match(r"data:(image/\w+);base64,(.+)", src, re.DOTALL)
193
175
  if not match:
194
176
  raise LoginPageParseError(
@@ -234,7 +216,6 @@ class Client:
234
216
  if FEEDBACK_PATH in resp.url:
235
217
  logger.info("Skipped the course feedback survey")
236
218
  self._logged_in = True
237
- self._save_session()
238
219
  return True
239
220
 
240
221
  soup = BeautifulSoup(resp.text, "html.parser")
@@ -245,7 +226,7 @@ class Client:
245
226
 
246
227
  # Check if login was successful
247
228
  def _looks_like_login_success(self, resp: requests.Response) -> bool:
248
- if resp.url == BASE_URL:
229
+ if resp.url.rstrip("/") == BASE_URL:
249
230
  soup = BeautifulSoup(resp.text, "html.parser")
250
231
  still_has_captcha = soup.select_one(CAPTCHA_IMG_SELECTOR) is not None
251
232
  return not still_has_captcha
@@ -255,17 +236,14 @@ class Client:
255
236
  # Extract error message from the login page
256
237
  @staticmethod
257
238
  def _extract_error_message(soup: BeautifulSoup) -> str | None:
258
- captcha_error = soup.select_one(".field-validation-error")
259
- if captcha_error:
260
- text = captcha_error.get_text(strip=True)
261
- if text:
262
- raise CaptchaError(text)
263
-
264
- credentials_error = soup.select_one(".validation-summary-errors")
265
- if credentials_error:
266
- text = credentials_error.get_text(strip=True)
267
- if text:
268
- raise CredentialsError(text)
239
+ error_div = soup.select_one(".validation-summary-errors")
240
+ if error_div:
241
+ list_item = error_div.select_one("li")
242
+ error_text = list_item.get_text(strip=True) if list_item else ""
243
+ if "password" and "incorrect" in error_text:
244
+ raise CredentialsError(error_text)
245
+ else:
246
+ raise CaptchaError(error_text)
269
247
 
270
248
  return None
271
249
 
@@ -273,10 +251,16 @@ class Client:
273
251
  def is_logged_in(self) -> bool:
274
252
  return self._logged_in
275
253
 
254
+ @property
255
+ def get_cookies(self) -> dict:
256
+ return self._session.cookies.get_dict()
257
+
276
258
  @property
277
259
  def student_details(self) -> dict:
278
260
  if not self._details:
279
261
  self.get_student_details()
262
+
263
+ assert self._details is not None
280
264
  return self._details
281
265
 
282
266
  @property
@@ -285,6 +269,8 @@ class Client:
285
269
  if not self._details:
286
270
  self.get_student_details()
287
271
  self.get_tile_data()
272
+
273
+ assert self._tile_data is not None
288
274
  return self._tile_data
289
275
 
290
276
  @property
@@ -293,6 +279,8 @@ class Client:
293
279
  if not self._details:
294
280
  self.get_student_details()
295
281
  self.get_today_attendance()
282
+
283
+ assert self._today_attendance is not None
296
284
  return self._today_attendance
297
285
 
298
286
  @property
@@ -301,6 +289,8 @@ class Client:
301
289
  if not self._details:
302
290
  self.get_student_details()
303
291
  self.get_month_attendance()
292
+
293
+ assert self._month_attendance is not None
304
294
  return self._month_attendance
305
295
 
306
296
  @property
@@ -309,6 +299,8 @@ class Client:
309
299
  if not self._details:
310
300
  self.get_student_details()
311
301
  self.get_sem_attendance()
302
+
303
+ assert self._sem_attendance is not None
312
304
  return self._sem_attendance
313
305
 
314
306
  # Authenticated data fetches
@@ -333,6 +325,8 @@ class Client:
333
325
 
334
326
  def get_tile_data(self) -> int:
335
327
  self._ensure_session()
328
+ assert self._details is not None
329
+
336
330
  payload = {
337
331
  "RegID": self._details.get("RegID"),
338
332
  }
@@ -357,6 +351,8 @@ class Client:
357
351
 
358
352
  def get_today_attendance(self) -> int:
359
353
  self._ensure_session()
354
+ assert self._details is not None
355
+
360
356
  today = datetime.now(tz=UTC).strftime("%d/%m/%Y")
361
357
  payload = {
362
358
  "RegID": self._details.get("RegID"),
@@ -383,6 +379,8 @@ class Client:
383
379
 
384
380
  def get_month_attendance(self, month: int | None = None) -> int:
385
381
  self._ensure_session()
382
+ assert self._details is not None
383
+
386
384
  if month is None:
387
385
  month = datetime.now(tz=UTC).month
388
386
  payload = {
@@ -413,6 +411,8 @@ class Client:
413
411
 
414
412
  def get_sem_attendance(self, sem: int | None = None) -> int:
415
413
  self._ensure_session()
414
+ assert self._details is not None
415
+
416
416
  if sem is None:
417
417
  sem = self._details.get("YearSem")
418
418
  payload = {