smartbvb 0.1.0__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.
- smartbvb-0.1.0/PKG-INFO +45 -0
- smartbvb-0.1.0/README.md +30 -0
- smartbvb-0.1.0/pyproject.toml +27 -0
- smartbvb-0.1.0/setup.cfg +4 -0
- smartbvb-0.1.0/smartbvb/__init__.py +0 -0
- smartbvb-0.1.0/smartbvb/main.py +519 -0
- smartbvb-0.1.0/smartbvb.egg-info/PKG-INFO +45 -0
- smartbvb-0.1.0/smartbvb.egg-info/SOURCES.txt +10 -0
- smartbvb-0.1.0/smartbvb.egg-info/dependency_links.txt +1 -0
- smartbvb-0.1.0/smartbvb.egg-info/entry_points.txt +2 -0
- smartbvb-0.1.0/smartbvb.egg-info/requires.txt +2 -0
- smartbvb-0.1.0/smartbvb.egg-info/top_level.txt +1 -0
smartbvb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartbvb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A fast terminal application for viewing KLE Tech attendance and results.
|
|
5
|
+
Author-email: Mohammad Sadiq Lakkundi <author@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: playwright>=1.30.0
|
|
14
|
+
Requires-Dist: beautifulsoup4>=4.11.0
|
|
15
|
+
|
|
16
|
+
# smartbvb
|
|
17
|
+
|
|
18
|
+
A fast, beautifully styled terminal application for viewing KLE Tech attendance and results natively from your command line!
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Navigate to this folder in your terminal and run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
*Note: Playwright requires a browser to be installed to work in the background. If you haven't installed it before, run:*
|
|
29
|
+
```bash
|
|
30
|
+
playwright install chromium
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Once installed, you can launch the app from anywhere on your terminal just by typing:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
smartbvb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Features
|
|
42
|
+
- **Global `smartbvb` Command:** Run it from any directory on your computer.
|
|
43
|
+
- **Credential Caching:** First-time setup asks for your USN and DOB and securely caches it in `~/.smartbvb_config.json`. You won't be asked again unless you log out!
|
|
44
|
+
- **Persistent Session Manager:** The CLI uses a single, persistent browser session in the background. It will not restart the browser when switching between "View Attendance" and "View Results".
|
|
45
|
+
- **Dynamic Site Status Check:** It validates if the server is up *synchronously* during the "Welcome" loading screen. If the site is down, the viewing options will be grayed out to save you time.
|
smartbvb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# smartbvb
|
|
2
|
+
|
|
3
|
+
A fast, beautifully styled terminal application for viewing KLE Tech attendance and results natively from your command line!
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Navigate to this folder in your terminal and run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
*Note: Playwright requires a browser to be installed to work in the background. If you haven't installed it before, run:*
|
|
14
|
+
```bash
|
|
15
|
+
playwright install chromium
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Once installed, you can launch the app from anywhere on your terminal just by typing:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
smartbvb
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
- **Global `smartbvb` Command:** Run it from any directory on your computer.
|
|
28
|
+
- **Credential Caching:** First-time setup asks for your USN and DOB and securely caches it in `~/.smartbvb_config.json`. You won't be asked again unless you log out!
|
|
29
|
+
- **Persistent Session Manager:** The CLI uses a single, persistent browser session in the background. It will not restart the browser when switching between "View Attendance" and "View Results".
|
|
30
|
+
- **Dynamic Site Status Check:** It validates if the server is up *synchronously* during the "Welcome" loading screen. If the site is down, the viewing options will be grayed out to save you time.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "smartbvb"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A fast terminal application for viewing KLE Tech attendance and results."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{name = "Mohammad Sadiq Lakkundi", email = "author@example.com"}
|
|
12
|
+
]
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.8"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"playwright>=1.30.0",
|
|
23
|
+
"beautifulsoup4>=4.11.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
smartbvb = "smartbvb.main:run"
|
smartbvb-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from playwright.async_api import async_playwright
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# ANSI Color codes
|
|
11
|
+
class Colors:
|
|
12
|
+
RED = '\033[91m'
|
|
13
|
+
GREEN = '\033[92m'
|
|
14
|
+
YELLOW = '\033[93m'
|
|
15
|
+
BLUE = '\033[94m'
|
|
16
|
+
CYAN = '\033[96m'
|
|
17
|
+
WHITE = '\033[97m'
|
|
18
|
+
GRAY = '\033[90m'
|
|
19
|
+
BOLD = '\033[1m'
|
|
20
|
+
END = '\033[0m'
|
|
21
|
+
BG_RED = '\033[41m'
|
|
22
|
+
BG_GREEN = '\033[42m'
|
|
23
|
+
BG_YELLOW = '\033[43m'
|
|
24
|
+
|
|
25
|
+
CONFIG_FILE = Path.home() / ".smartbvb_config.json"
|
|
26
|
+
|
|
27
|
+
def load_config():
|
|
28
|
+
if CONFIG_FILE.exists():
|
|
29
|
+
try:
|
|
30
|
+
with open(CONFIG_FILE, 'r') as f:
|
|
31
|
+
return json.load(f)
|
|
32
|
+
except:
|
|
33
|
+
return None
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def save_config(usn, dob):
|
|
37
|
+
with open(CONFIG_FILE, 'w') as f:
|
|
38
|
+
json.dump({'usn': usn, 'dob': dob}, f)
|
|
39
|
+
|
|
40
|
+
def delete_config():
|
|
41
|
+
if CONFIG_FILE.exists():
|
|
42
|
+
try:
|
|
43
|
+
CONFIG_FILE.unlink()
|
|
44
|
+
except:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
class KLEScraper:
|
|
48
|
+
def __init__(self):
|
|
49
|
+
self.base_url = "https://parents.kletech.ac.in/"
|
|
50
|
+
self.usn = None
|
|
51
|
+
self.dob = None
|
|
52
|
+
self.student_name = None
|
|
53
|
+
|
|
54
|
+
self.playwright = None
|
|
55
|
+
self.browser = None
|
|
56
|
+
self.page = None
|
|
57
|
+
|
|
58
|
+
async def start_browser(self):
|
|
59
|
+
self.playwright = await async_playwright().start()
|
|
60
|
+
self.browser = await self.playwright.chromium.launch(
|
|
61
|
+
headless=True,
|
|
62
|
+
args=['--disable-blink-features=AutomationControlled', '--disable-sync']
|
|
63
|
+
)
|
|
64
|
+
self.page = await self.browser.new_page()
|
|
65
|
+
|
|
66
|
+
async def close_browser(self):
|
|
67
|
+
if self.browser:
|
|
68
|
+
await self.browser.close()
|
|
69
|
+
if self.playwright:
|
|
70
|
+
await self.playwright.stop()
|
|
71
|
+
|
|
72
|
+
async def check_site_status(self):
|
|
73
|
+
try:
|
|
74
|
+
response = await self.page.goto(self.base_url, wait_until='load', timeout=5000)
|
|
75
|
+
return response and response.status == 200
|
|
76
|
+
except:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
async def login(self, usn, dob):
|
|
80
|
+
try:
|
|
81
|
+
await self.page.goto(self.base_url, wait_until='load', timeout=10000)
|
|
82
|
+
|
|
83
|
+
parts = dob.split('-')
|
|
84
|
+
day, month, year = parts[0], parts[1], parts[2]
|
|
85
|
+
|
|
86
|
+
await asyncio.gather(
|
|
87
|
+
self.page.fill('#username', usn),
|
|
88
|
+
self.page.select_option('#dd', day),
|
|
89
|
+
self.page.select_option('#mm', month),
|
|
90
|
+
)
|
|
91
|
+
await self.page.select_option('#yyyy', year)
|
|
92
|
+
await self.page.click('.cn-landing-login1')
|
|
93
|
+
|
|
94
|
+
await self.page.wait_for_function(
|
|
95
|
+
'document.querySelector(".cn-pay-table") !== null',
|
|
96
|
+
timeout=8000
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.usn = usn
|
|
100
|
+
self.dob = dob
|
|
101
|
+
|
|
102
|
+
html = await self.page.content()
|
|
103
|
+
result = self._parse_attendance(html)
|
|
104
|
+
self.student_name = result.get('name', 'Unknown')
|
|
105
|
+
|
|
106
|
+
return True
|
|
107
|
+
except Exception as e:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
async def fetch_attendance(self):
|
|
111
|
+
try:
|
|
112
|
+
await self.page.goto(self.base_url, wait_until='load', timeout=10000)
|
|
113
|
+
|
|
114
|
+
html = await self.page.content()
|
|
115
|
+
if "cn-pay-table" not in html:
|
|
116
|
+
if not await self.login(self.usn, self.dob):
|
|
117
|
+
return {'error': 'Session expired. Please try again.'}
|
|
118
|
+
html = await self.page.content()
|
|
119
|
+
|
|
120
|
+
result = self._parse_attendance(html)
|
|
121
|
+
self.student_name = result.get('name', 'Unknown')
|
|
122
|
+
return result
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return {'error': str(e)}
|
|
125
|
+
|
|
126
|
+
async def fetch_results(self):
|
|
127
|
+
try:
|
|
128
|
+
history_url = f"{self.base_url}index.php?option=com_history&task=getResult&usn={self.usn}"
|
|
129
|
+
await self.page.goto(history_url, wait_until='networkidle', timeout=10000)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
await self.page.wait_for_selector('.credits-sec1', timeout=5000)
|
|
133
|
+
except:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
await self.page.wait_for_selector('.result-table', timeout=5000)
|
|
138
|
+
except:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
await asyncio.sleep(1)
|
|
142
|
+
|
|
143
|
+
html = await self.page.content()
|
|
144
|
+
result = self._parse_results(html)
|
|
145
|
+
return result
|
|
146
|
+
except Exception as e:
|
|
147
|
+
return {'error': str(e)}
|
|
148
|
+
|
|
149
|
+
def _parse_attendance(self, html):
|
|
150
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
151
|
+
try:
|
|
152
|
+
student_name = soup.find('h3').get_text(strip=True) if soup.find('h3') else "Unknown"
|
|
153
|
+
|
|
154
|
+
attendance_map = {}
|
|
155
|
+
scripts = soup.find_all('script')
|
|
156
|
+
|
|
157
|
+
script_count = 0
|
|
158
|
+
for script in scripts:
|
|
159
|
+
content = script.string
|
|
160
|
+
if content and 'bb.generate' in content and 'columns' in content:
|
|
161
|
+
script_count += 1
|
|
162
|
+
if 'gauge' in content or script_count == 2:
|
|
163
|
+
regex = r'\["([A-Z0-9]+)",\s*(\d+)\]'
|
|
164
|
+
matches = re.findall(regex, content)
|
|
165
|
+
for code, percentage in matches:
|
|
166
|
+
attendance_map[code] = int(percentage)
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
courses_list = []
|
|
170
|
+
course_table = soup.find('table', {'class': 'cn-pay-table'})
|
|
171
|
+
|
|
172
|
+
if course_table:
|
|
173
|
+
for row in course_table.find_all('tr')[1:]:
|
|
174
|
+
cols = row.find_all('td')
|
|
175
|
+
if len(cols) >= 2:
|
|
176
|
+
code = cols[0].get_text(strip=True)
|
|
177
|
+
name = cols[1].get_text(strip=True)
|
|
178
|
+
attendance = attendance_map.get(code)
|
|
179
|
+
|
|
180
|
+
if code and name and attendance is not None:
|
|
181
|
+
courses_list.append({
|
|
182
|
+
'code': code,
|
|
183
|
+
'name': name,
|
|
184
|
+
'attendance': attendance
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
'usn': self.usn,
|
|
189
|
+
'name': student_name,
|
|
190
|
+
'courses': courses_list,
|
|
191
|
+
'course_count': len(courses_list)
|
|
192
|
+
}
|
|
193
|
+
except Exception as e:
|
|
194
|
+
return {'error': f'Parse error: {str(e)}'}
|
|
195
|
+
|
|
196
|
+
def _parse_results(self, html):
|
|
197
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
198
|
+
try:
|
|
199
|
+
overall_stats = self._parse_overall_stats(soup)
|
|
200
|
+
semesters = []
|
|
201
|
+
result_tables = soup.find_all('div', class_='result-table')
|
|
202
|
+
|
|
203
|
+
for table_div in result_tables:
|
|
204
|
+
header = table_div.find('h6')
|
|
205
|
+
if not header:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
header_text = header.get_text(strip=True)
|
|
209
|
+
sem_match = re.search(r'(.*?)\s*-\s*Semester', header_text)
|
|
210
|
+
sgpa_match = re.search(r'SGPA:\s*([\d.]+)', header_text)
|
|
211
|
+
credits_reg_match = re.search(r'Credits\s+Registered\s*:\s*([\d.]+)', header_text)
|
|
212
|
+
credits_earned_match = re.search(r'Credits\s+Earned\s*:\s*([\d.]+)', header_text)
|
|
213
|
+
|
|
214
|
+
semester_name = sem_match.group(1).strip() if sem_match else "Unknown"
|
|
215
|
+
sgpa = float(sgpa_match.group(1)) if sgpa_match else 0
|
|
216
|
+
credits_reg = credits_reg_match.group(1) if credits_reg_match else "0"
|
|
217
|
+
credits_earned = credits_earned_match.group(1) if credits_earned_match else "0"
|
|
218
|
+
|
|
219
|
+
courses = []
|
|
220
|
+
table = table_div.find('table', class_='uk-table')
|
|
221
|
+
if table:
|
|
222
|
+
for row in table.find_all('tr')[1:]:
|
|
223
|
+
cols = row.find_all('td')
|
|
224
|
+
if len(cols) >= 6:
|
|
225
|
+
course_code = cols[0].get_text(strip=True)
|
|
226
|
+
course_name = cols[1].get_text(strip=True)
|
|
227
|
+
credits_reg_course = cols[2].get_text(strip=True)
|
|
228
|
+
credits_earned_course = cols[3].get_text(strip=True)
|
|
229
|
+
gpa = cols[4].get_text(strip=True)
|
|
230
|
+
grade = cols[5].get_text(strip=True)
|
|
231
|
+
|
|
232
|
+
courses.append({
|
|
233
|
+
'code': course_code,
|
|
234
|
+
'name': course_name,
|
|
235
|
+
'credits_reg': credits_reg_course,
|
|
236
|
+
'credits_earned': credits_earned_course,
|
|
237
|
+
'gpa': gpa,
|
|
238
|
+
'grade': grade
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
semesters.append({
|
|
242
|
+
'semester': semester_name,
|
|
243
|
+
'sgpa': sgpa,
|
|
244
|
+
'credits_reg': credits_reg,
|
|
245
|
+
'credits_earned': credits_earned,
|
|
246
|
+
'courses': courses
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
'usn': self.usn,
|
|
251
|
+
'overall_stats': overall_stats,
|
|
252
|
+
'semesters': semesters
|
|
253
|
+
}
|
|
254
|
+
except Exception as e:
|
|
255
|
+
return {'error': f'Parse error: {str(e)}'}
|
|
256
|
+
|
|
257
|
+
def _parse_overall_stats(self, soup):
|
|
258
|
+
try:
|
|
259
|
+
stats = {'cgpa': 0, 'credits_earned': 0, 'credits_to_earn': 0}
|
|
260
|
+
cards = soup.find_all('div', class_='credits-sec1')
|
|
261
|
+
|
|
262
|
+
for card in cards:
|
|
263
|
+
header = card.find('h3')
|
|
264
|
+
value_p = card.find('p')
|
|
265
|
+
|
|
266
|
+
if header and value_p:
|
|
267
|
+
header_text = ' '.join(header.get_text(strip=True).lower().split())
|
|
268
|
+
value_text = value_p.get_text(strip=True)
|
|
269
|
+
|
|
270
|
+
value = re.search(r'[\d.]+', value_text)
|
|
271
|
+
if value:
|
|
272
|
+
value_num = float(value.group())
|
|
273
|
+
if 'earned' in header_text and 'so far' in header_text:
|
|
274
|
+
stats['credits_earned'] = value_num
|
|
275
|
+
elif 'to be earned' in header_text:
|
|
276
|
+
stats['credits_to_earn'] = value_num
|
|
277
|
+
elif 'cgpa' in header_text:
|
|
278
|
+
stats['cgpa'] = value_num
|
|
279
|
+
|
|
280
|
+
if stats['credits_earned'] == 0 and stats['credits_to_earn'] == 0:
|
|
281
|
+
all_text = soup.get_text()
|
|
282
|
+
earned_match = re.search(r'Credits\s+Earned\s+So\s+Far\s*[\s\S]*?(\d+(?:\.\d+)?)', all_text)
|
|
283
|
+
if earned_match: stats['credits_earned'] = float(earned_match.group(1))
|
|
284
|
+
|
|
285
|
+
to_earn_match = re.search(r'Credits\s+to\s+be\s+Earned\s*[\s\S]*?(\d+(?:\.\d+)?)', all_text)
|
|
286
|
+
if to_earn_match: stats['credits_to_earn'] = float(to_earn_match.group(1))
|
|
287
|
+
|
|
288
|
+
cgpa_match = re.search(r'CGPA\s*[\s\S]*?(\d+(?:\.\d+)?)', all_text)
|
|
289
|
+
if cgpa_match: stats['cgpa'] = float(cgpa_match.group(1))
|
|
290
|
+
|
|
291
|
+
return stats
|
|
292
|
+
except Exception as e:
|
|
293
|
+
return {'cgpa': 0, 'credits_earned': 0, 'credits_to_earn': 0, 'error': str(e)}
|
|
294
|
+
|
|
295
|
+
def print_header():
|
|
296
|
+
print("\n" + "=" * 80)
|
|
297
|
+
print(f"{Colors.BOLD}{Colors.CYAN}╔════════════════════════════════════════════════════════════════════════════════╗{Colors.END}")
|
|
298
|
+
print(f"{Colors.BOLD}{Colors.CYAN}║ KLE ACADEMIC PORTAL - ATTENDANCE & RESULTS VIEWER ║{Colors.END}")
|
|
299
|
+
print(f"{Colors.BOLD}{Colors.CYAN}╚════════════════════════════════════════════════════════════════════════════════╝{Colors.END}")
|
|
300
|
+
print("=" * 80)
|
|
301
|
+
|
|
302
|
+
def print_attendance(result):
|
|
303
|
+
if 'error' in result:
|
|
304
|
+
print(f"\n{Colors.RED}✗ Error: {result['error']}{Colors.END}\n")
|
|
305
|
+
return
|
|
306
|
+
if not result.get('courses'):
|
|
307
|
+
print(f"\n{Colors.RED}✗ No attendance data found{Colors.END}\n")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
print(f"\n{Colors.BOLD}{Colors.GREEN}✓ Attendance Report{Colors.END}")
|
|
311
|
+
print(f"Student: {Colors.BOLD}{result['name']}{Colors.END}")
|
|
312
|
+
print(f"USN: {Colors.CYAN}{result['usn']}{Colors.END}\n")
|
|
313
|
+
|
|
314
|
+
print(f"{Colors.BOLD}Course Attendance Overview:{Colors.END}\n")
|
|
315
|
+
max_name_len = 45
|
|
316
|
+
print(f"{'CODE':<15} {'COURSE NAME':<{max_name_len}} {'ATT':<6} {'Status'}")
|
|
317
|
+
print("-" * 80)
|
|
318
|
+
|
|
319
|
+
low_count = 0
|
|
320
|
+
for course in result['courses']:
|
|
321
|
+
code = course['code']
|
|
322
|
+
name = course['name'][:max_name_len]
|
|
323
|
+
att = course['attendance']
|
|
324
|
+
|
|
325
|
+
if att < 75:
|
|
326
|
+
color = Colors.RED + Colors.BOLD
|
|
327
|
+
status = f"🔴 CRITICAL (<75%)"
|
|
328
|
+
low_count += 1
|
|
329
|
+
elif att < 85:
|
|
330
|
+
color = Colors.YELLOW
|
|
331
|
+
status = f"🟡 WARNING (75-84%)"
|
|
332
|
+
else:
|
|
333
|
+
color = Colors.GREEN
|
|
334
|
+
status = f"🟢 GOOD (≥85%)"
|
|
335
|
+
|
|
336
|
+
print(f"{code:<15} {name:<{max_name_len}} {color}{att:>3}%{Colors.END} {status}")
|
|
337
|
+
|
|
338
|
+
print("-" * 80)
|
|
339
|
+
print(f"Total Courses: {result['course_count']}")
|
|
340
|
+
|
|
341
|
+
if low_count > 0:
|
|
342
|
+
print(f"\n{Colors.BG_RED}{Colors.WHITE} ⚠️ ALERT: {low_count} subject(s) below 75% attendance {Colors.END}")
|
|
343
|
+
print(f"{Colors.RED}You may NOT be eligible to write exams in these subjects!{Colors.END}\n")
|
|
344
|
+
else:
|
|
345
|
+
print(f"\n{Colors.GREEN}✓ All subjects have adequate attendance!{Colors.END}\n")
|
|
346
|
+
|
|
347
|
+
def print_results(result):
|
|
348
|
+
if 'error' in result:
|
|
349
|
+
print(f"\n{Colors.RED}✗ Error: {result['error']}{Colors.END}\n")
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
overall_stats = result.get('overall_stats', {})
|
|
353
|
+
semesters = result.get('semesters', [])
|
|
354
|
+
|
|
355
|
+
if not semesters:
|
|
356
|
+
print(f"\n{Colors.RED}✗ No results found{Colors.END}\n")
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
print(f"\n{Colors.BOLD}{Colors.GREEN}✓ Cumulative Academic History{Colors.END}")
|
|
360
|
+
print(f"USN: {Colors.CYAN}{result['usn']}{Colors.END}\n")
|
|
361
|
+
|
|
362
|
+
print(f"{Colors.BOLD}{Colors.YELLOW}━━━━ OVERALL STATISTICS ━━━━{Colors.END}")
|
|
363
|
+
print(f" {Colors.BOLD}CGPA:{Colors.END} {Colors.GREEN}{overall_stats.get('cgpa', 0)}{Colors.END}")
|
|
364
|
+
print(f" {Colors.BOLD}Credits Earned So Far:{Colors.END} {Colors.CYAN}{overall_stats.get('credits_earned', 0)}{Colors.END}")
|
|
365
|
+
print(f" {Colors.BOLD}Credits to be Earned:{Colors.END} {Colors.YELLOW}{overall_stats.get('credits_to_earn', 0)}{Colors.END}")
|
|
366
|
+
print(f"{Colors.YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.END}\n")
|
|
367
|
+
|
|
368
|
+
for sem in semesters:
|
|
369
|
+
print(f"\n{Colors.BOLD}{Colors.CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.END}")
|
|
370
|
+
print(f"{Colors.BOLD}{Colors.YELLOW}{sem['semester']}{Colors.END}")
|
|
371
|
+
print(f"{Colors.BOLD}SGPA: {Colors.GREEN}{sem['sgpa']}{Colors.END} | {Colors.BOLD}Credits Reg: {Colors.CYAN}{sem['credits_reg']}{Colors.END} | {Colors.BOLD}Credits Earned: {Colors.CYAN}{sem['credits_earned']}{Colors.END}")
|
|
372
|
+
print(f"{Colors.CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{Colors.END}")
|
|
373
|
+
|
|
374
|
+
courses = sem['courses']
|
|
375
|
+
if not courses:
|
|
376
|
+
print(f"{Colors.GRAY}No courses found for this semester{Colors.END}\n")
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
print(f"{'CODE':<12} {'COURSE NAME':<35} {'C.R':<5} {'C.E':<5} {'GPA':<5} {'Grade':<8}")
|
|
380
|
+
print("-" * 80)
|
|
381
|
+
|
|
382
|
+
for course in courses:
|
|
383
|
+
code = course['code']
|
|
384
|
+
name = course['name'][:33]
|
|
385
|
+
cred_reg = course['credits_reg']
|
|
386
|
+
cred_earned = course['credits_earned']
|
|
387
|
+
gpa = course['gpa']
|
|
388
|
+
grade = course['grade']
|
|
389
|
+
|
|
390
|
+
grade_color = Colors.GREEN if grade == 'S' else (Colors.CYAN if grade in ['A', 'B'] else (Colors.YELLOW if grade == 'C' else (Colors.RED if grade == 'D' else Colors.GRAY)))
|
|
391
|
+
print(f"{code:<12} {name:<35} {cred_reg:<5} {cred_earned:<5} {gpa:<5} {grade_color}{grade}{Colors.END}")
|
|
392
|
+
print()
|
|
393
|
+
|
|
394
|
+
async def async_main():
|
|
395
|
+
print_header()
|
|
396
|
+
|
|
397
|
+
config = load_config()
|
|
398
|
+
if not config:
|
|
399
|
+
print(f"\n{Colors.BOLD}{Colors.CYAN}--- First Time Setup ---{Colors.END}")
|
|
400
|
+
usn = input(f"[?] Enter USN: ").strip().upper()
|
|
401
|
+
print(f"[i] Enter Date of Birth (numerical):")
|
|
402
|
+
day = input(f"[?] Day (DD): ").strip().zfill(2)
|
|
403
|
+
month = input(f"[?] Month (MM): ").strip().zfill(2)
|
|
404
|
+
year = input(f"[?] Year (YYYY): ").strip()
|
|
405
|
+
dob = f"{day}-{month}-{year}"
|
|
406
|
+
|
|
407
|
+
save_config(usn, dob)
|
|
408
|
+
config = {'usn': usn, 'dob': dob}
|
|
409
|
+
print(f"{Colors.GREEN}✓ Credentials saved securely.{Colors.END}\n")
|
|
410
|
+
|
|
411
|
+
scraper = KLEScraper()
|
|
412
|
+
scraper.usn = config['usn']
|
|
413
|
+
scraper.dob = config['dob']
|
|
414
|
+
|
|
415
|
+
print(f"\n{Colors.GRAY}[*] Connecting to KLE Portal in background...{Colors.END}")
|
|
416
|
+
|
|
417
|
+
await scraper.start_browser()
|
|
418
|
+
|
|
419
|
+
site_up = False
|
|
420
|
+
logged_in = False
|
|
421
|
+
|
|
422
|
+
async def check_and_login():
|
|
423
|
+
nonlocal site_up, logged_in
|
|
424
|
+
if await scraper.check_site_status():
|
|
425
|
+
site_up = True
|
|
426
|
+
if await scraper.login(scraper.usn, scraper.dob):
|
|
427
|
+
logged_in = True
|
|
428
|
+
|
|
429
|
+
status_task = asyncio.create_task(check_and_login())
|
|
430
|
+
|
|
431
|
+
chars = "/-\\|"
|
|
432
|
+
i = 0
|
|
433
|
+
while not status_task.done():
|
|
434
|
+
sys.stdout.write(f"\r{Colors.CYAN}Setting up session... {chars[i % 4]}{Colors.END}")
|
|
435
|
+
sys.stdout.flush()
|
|
436
|
+
await asyncio.sleep(0.1)
|
|
437
|
+
i += 1
|
|
438
|
+
sys.stdout.write("\r" + " " * 40 + "\r")
|
|
439
|
+
|
|
440
|
+
if not site_up:
|
|
441
|
+
print(f"{Colors.BG_RED}{Colors.WHITE} ⚠️ SERVER DOWN {Colors.END}")
|
|
442
|
+
print(f"{Colors.YELLOW}The KLE portal is currently unavailable.{Colors.END}")
|
|
443
|
+
elif not logged_in:
|
|
444
|
+
print(f"{Colors.RED}✗ Login failed! Invalid credentials or site changed.{Colors.END}")
|
|
445
|
+
delete_config()
|
|
446
|
+
print(f"{Colors.YELLOW}Cache cleared. Please restart the app.{Colors.END}")
|
|
447
|
+
await scraper.close_browser()
|
|
448
|
+
return
|
|
449
|
+
else:
|
|
450
|
+
print(f"{Colors.GREEN}✓ Session Established successfully!{Colors.END}")
|
|
451
|
+
|
|
452
|
+
# Main menu loop
|
|
453
|
+
while True:
|
|
454
|
+
if site_up and logged_in:
|
|
455
|
+
print(f"\n{Colors.BOLD}{Colors.GREEN}✓ Welcome {scraper.student_name} ({scraper.usn}){Colors.END}\n")
|
|
456
|
+
print(f"{Colors.BOLD}Select an option:{Colors.END}")
|
|
457
|
+
print(f" {Colors.CYAN}1{Colors.END}. View Attendance")
|
|
458
|
+
print(f" {Colors.CYAN}2{Colors.END}. View Exam Results")
|
|
459
|
+
print(f" {Colors.CYAN}3{Colors.END}. Logout (Clear Cache)")
|
|
460
|
+
print(f" {Colors.CYAN}4{Colors.END}. Exit")
|
|
461
|
+
else:
|
|
462
|
+
print(f"\n{Colors.BOLD}Select an option:{Colors.END}")
|
|
463
|
+
print(f" {Colors.GRAY}1. View Attendance (Unavailable){Colors.END}")
|
|
464
|
+
print(f" {Colors.GRAY}2. View Exam Results (Unavailable){Colors.END}")
|
|
465
|
+
print(f" {Colors.CYAN}3{Colors.END}. Logout (Clear Cache)")
|
|
466
|
+
print(f" {Colors.CYAN}4{Colors.END}. Exit")
|
|
467
|
+
|
|
468
|
+
print("-" * 80)
|
|
469
|
+
|
|
470
|
+
choice = input(f"\n[?] Enter your choice (1-4): ").strip()
|
|
471
|
+
|
|
472
|
+
if choice == '1':
|
|
473
|
+
if not site_up or not logged_in:
|
|
474
|
+
print(f"{Colors.RED}Option unavailable.{Colors.END}")
|
|
475
|
+
continue
|
|
476
|
+
print(f"\n{Colors.GRAY}[*] Fetching attendance data...{Colors.END}")
|
|
477
|
+
result = await scraper.fetch_attendance()
|
|
478
|
+
print_attendance(result)
|
|
479
|
+
|
|
480
|
+
elif choice == '2':
|
|
481
|
+
if not site_up or not logged_in:
|
|
482
|
+
print(f"{Colors.RED}Option unavailable.{Colors.END}")
|
|
483
|
+
continue
|
|
484
|
+
print(f"\n{Colors.GRAY}[*] Fetching exam results...{Colors.END}")
|
|
485
|
+
result = await scraper.fetch_results()
|
|
486
|
+
print_results(result)
|
|
487
|
+
|
|
488
|
+
elif choice == '3':
|
|
489
|
+
delete_config()
|
|
490
|
+
print(f"\n{Colors.GREEN}✓ Logged out successfully.{Colors.END}")
|
|
491
|
+
print(f"{Colors.YELLOW}Credentials have been removed from cache.{Colors.END}")
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
elif choice == '4':
|
|
495
|
+
print(f"\n{Colors.GREEN}✓ Thank you for using KLE Academic Portal!{Colors.END}")
|
|
496
|
+
print("=" * 80 + "\n")
|
|
497
|
+
break
|
|
498
|
+
|
|
499
|
+
else:
|
|
500
|
+
print(f"{Colors.RED}✗ Invalid choice.{Colors.END}")
|
|
501
|
+
|
|
502
|
+
print("\n")
|
|
503
|
+
|
|
504
|
+
await scraper.close_browser()
|
|
505
|
+
|
|
506
|
+
def run():
|
|
507
|
+
if sys.platform == 'win32':
|
|
508
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
509
|
+
try:
|
|
510
|
+
asyncio.run(async_main())
|
|
511
|
+
except KeyboardInterrupt:
|
|
512
|
+
print(f"\n\033[93mOperation cancelled\033[0m\n")
|
|
513
|
+
sys.exit(0)
|
|
514
|
+
except Exception as e:
|
|
515
|
+
print(f"An error occurred: {e}")
|
|
516
|
+
sys.exit(1)
|
|
517
|
+
|
|
518
|
+
if __name__ == "__main__":
|
|
519
|
+
run()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smartbvb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A fast terminal application for viewing KLE Tech attendance and results.
|
|
5
|
+
Author-email: Mohammad Sadiq Lakkundi <author@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: playwright>=1.30.0
|
|
14
|
+
Requires-Dist: beautifulsoup4>=4.11.0
|
|
15
|
+
|
|
16
|
+
# smartbvb
|
|
17
|
+
|
|
18
|
+
A fast, beautifully styled terminal application for viewing KLE Tech attendance and results natively from your command line!
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Navigate to this folder in your terminal and run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
*Note: Playwright requires a browser to be installed to work in the background. If you haven't installed it before, run:*
|
|
29
|
+
```bash
|
|
30
|
+
playwright install chromium
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Once installed, you can launch the app from anywhere on your terminal just by typing:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
smartbvb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Features
|
|
42
|
+
- **Global `smartbvb` Command:** Run it from any directory on your computer.
|
|
43
|
+
- **Credential Caching:** First-time setup asks for your USN and DOB and securely caches it in `~/.smartbvb_config.json`. You won't be asked again unless you log out!
|
|
44
|
+
- **Persistent Session Manager:** The CLI uses a single, persistent browser session in the background. It will not restart the browser when switching between "View Attendance" and "View Results".
|
|
45
|
+
- **Dynamic Site Status Check:** It validates if the server is up *synchronously* during the "Welcome" loading screen. If the site is down, the viewing options will be grayed out to save you time.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
smartbvb/__init__.py
|
|
4
|
+
smartbvb/main.py
|
|
5
|
+
smartbvb.egg-info/PKG-INFO
|
|
6
|
+
smartbvb.egg-info/SOURCES.txt
|
|
7
|
+
smartbvb.egg-info/dependency_links.txt
|
|
8
|
+
smartbvb.egg-info/entry_points.txt
|
|
9
|
+
smartbvb.egg-info/requires.txt
|
|
10
|
+
smartbvb.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
smartbvb
|