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.
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/PKG-INFO +1 -2
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/pyproject.toml +3 -2
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/pyproject.toml.orig +3 -2
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/src/qums_fetch/client.py +43 -43
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/README.md +0 -0
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/src/qums_fetch/__init__.py +0 -0
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/src/qums_fetch/captcha.py +0 -0
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/src/qums_fetch/exceptions.py +0 -0
- {qums_fetch-0.4.4.dev1 → qums_fetch-0.4.4.dev3}/src/qums_fetch/py.typed +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: qums-fetch
|
|
3
|
-
Version: 0.4.4.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
110
|
-
except requests.RequestException
|
|
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
|
-
|
|
259
|
-
if
|
|
260
|
-
|
|
261
|
-
if
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|