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.
- {qums_fetch-0.4.4.dev2 → qums_fetch-0.4.4.dev3}/PKG-INFO +1 -2
- {qums_fetch-0.4.4.dev2 → qums_fetch-0.4.4.dev3}/pyproject.toml +3 -2
- {qums_fetch-0.4.4.dev2 → qums_fetch-0.4.4.dev3}/pyproject.toml.orig +3 -2
- {qums_fetch-0.4.4.dev2 → qums_fetch-0.4.4.dev3}/src/qums_fetch/client.py +36 -32
- {qums_fetch-0.4.4.dev2 → qums_fetch-0.4.4.dev3}/README.md +0 -0
- {qums_fetch-0.4.4.dev2 → qums_fetch-0.4.4.dev3}/src/qums_fetch/__init__.py +0 -0
- {qums_fetch-0.4.4.dev2 → qums_fetch-0.4.4.dev3}/src/qums_fetch/captcha.py +0 -0
- {qums_fetch-0.4.4.dev2 → qums_fetch-0.4.4.dev3}/src/qums_fetch/exceptions.py +0 -0
- {qums_fetch-0.4.4.dev2 → 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")
|
|
@@ -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
|
-
|
|
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 = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|