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.
@@ -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,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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,2 @@
1
+ [console_scripts]
2
+ smartbvb = smartbvb.main:run
@@ -0,0 +1,2 @@
1
+ playwright>=1.30.0
2
+ beautifulsoup4>=4.11.0
@@ -0,0 +1 @@
1
+ smartbvb