qums-fetch 0.4.4.dev2__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.dev2
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.dev2"
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.dev2"
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")
@@ -257,7 +238,8 @@ class Client:
257
238
  def _extract_error_message(soup: BeautifulSoup) -> str | None:
258
239
  error_div = soup.select_one(".validation-summary-errors")
259
240
  if error_div:
260
- error_text = error_div.select_one("li").get_text(strip=True)
241
+ list_item = error_div.select_one("li")
242
+ error_text = list_item.get_text(strip=True) if list_item else ""
261
243
  if "password" and "incorrect" in error_text:
262
244
  raise CredentialsError(error_text)
263
245
  else:
@@ -269,10 +251,16 @@ class Client:
269
251
  def is_logged_in(self) -> bool:
270
252
  return self._logged_in
271
253
 
254
+ @property
255
+ def get_cookies(self) -> dict:
256
+ return self._session.cookies.get_dict()
257
+
272
258
  @property
273
259
  def student_details(self) -> dict:
274
260
  if not self._details:
275
261
  self.get_student_details()
262
+
263
+ assert self._details is not None
276
264
  return self._details
277
265
 
278
266
  @property
@@ -281,6 +269,8 @@ class Client:
281
269
  if not self._details:
282
270
  self.get_student_details()
283
271
  self.get_tile_data()
272
+
273
+ assert self._tile_data is not None
284
274
  return self._tile_data
285
275
 
286
276
  @property
@@ -289,6 +279,8 @@ class Client:
289
279
  if not self._details:
290
280
  self.get_student_details()
291
281
  self.get_today_attendance()
282
+
283
+ assert self._today_attendance is not None
292
284
  return self._today_attendance
293
285
 
294
286
  @property
@@ -297,6 +289,8 @@ class Client:
297
289
  if not self._details:
298
290
  self.get_student_details()
299
291
  self.get_month_attendance()
292
+
293
+ assert self._month_attendance is not None
300
294
  return self._month_attendance
301
295
 
302
296
  @property
@@ -305,6 +299,8 @@ class Client:
305
299
  if not self._details:
306
300
  self.get_student_details()
307
301
  self.get_sem_attendance()
302
+
303
+ assert self._sem_attendance is not None
308
304
  return self._sem_attendance
309
305
 
310
306
  # Authenticated data fetches
@@ -329,6 +325,8 @@ class Client:
329
325
 
330
326
  def get_tile_data(self) -> int:
331
327
  self._ensure_session()
328
+ assert self._details is not None
329
+
332
330
  payload = {
333
331
  "RegID": self._details.get("RegID"),
334
332
  }
@@ -353,6 +351,8 @@ class Client:
353
351
 
354
352
  def get_today_attendance(self) -> int:
355
353
  self._ensure_session()
354
+ assert self._details is not None
355
+
356
356
  today = datetime.now(tz=UTC).strftime("%d/%m/%Y")
357
357
  payload = {
358
358
  "RegID": self._details.get("RegID"),
@@ -379,6 +379,8 @@ class Client:
379
379
 
380
380
  def get_month_attendance(self, month: int | None = None) -> int:
381
381
  self._ensure_session()
382
+ assert self._details is not None
383
+
382
384
  if month is None:
383
385
  month = datetime.now(tz=UTC).month
384
386
  payload = {
@@ -409,6 +411,8 @@ class Client:
409
411
 
410
412
  def get_sem_attendance(self, sem: int | None = None) -> int:
411
413
  self._ensure_session()
414
+ assert self._details is not None
415
+
412
416
  if sem is None:
413
417
  sem = self._details.get("YearSem")
414
418
  payload = {