umpaper-fetch 1.0.0__py3-none-any.whl

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.
auth/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Authentication package
auth/chrome_fix.py ADDED
@@ -0,0 +1,119 @@
1
+ """
2
+ Chrome Driver Fix Module
3
+
4
+ Handles Chrome driver setup issues, particularly the Win32 application error
5
+ that occurs due to architecture mismatches.
6
+ """
7
+
8
+ import os
9
+ import platform
10
+ import logging
11
+ from webdriver_manager.chrome import ChromeDriverManager
12
+
13
+
14
+ def get_chrome_driver_path():
15
+ """
16
+ Get Chrome driver path with proper architecture handling.
17
+
18
+ Returns:
19
+ str: Path to the Chrome driver executable
20
+ """
21
+ logger = logging.getLogger(__name__)
22
+
23
+ try:
24
+ # Determine system architecture
25
+ is_64bit = platform.machine().endswith('64')
26
+ system = platform.system()
27
+
28
+ logger.info(f"System: {system}, 64-bit: {is_64bit}")
29
+
30
+ # Force specific Chrome driver version/architecture if needed
31
+ if system == "Windows" and is_64bit:
32
+ # Try to get the latest driver for Windows 64-bit
33
+ driver_manager = ChromeDriverManager()
34
+ driver_path = driver_manager.install()
35
+
36
+ # Verify the driver is executable
37
+ if os.path.exists(driver_path) and os.access(driver_path, os.X_OK):
38
+ logger.info(f"Chrome driver ready: {driver_path}")
39
+ return driver_path
40
+ else:
41
+ logger.warning(f"Chrome driver not executable: {driver_path}")
42
+ # Try to fix permissions
43
+ try:
44
+ os.chmod(driver_path, 0o755)
45
+ if os.access(driver_path, os.X_OK):
46
+ logger.info("Fixed Chrome driver permissions")
47
+ return driver_path
48
+ except Exception as perm_error:
49
+ logger.error(f"Could not fix permissions: {perm_error}")
50
+ else:
51
+ # For other systems, use default behavior
52
+ driver_path = ChromeDriverManager().install()
53
+ return driver_path
54
+
55
+ except Exception as e:
56
+ logger.error(f"Chrome driver setup failed: {e}")
57
+ raise
58
+
59
+ raise Exception("Could not setup Chrome driver")
60
+
61
+
62
+ def test_chrome_driver(driver_path):
63
+ """
64
+ Test if the Chrome driver is working properly.
65
+
66
+ Args:
67
+ driver_path (str): Path to Chrome driver
68
+
69
+ Returns:
70
+ bool: True if driver works, False otherwise
71
+ """
72
+ logger = logging.getLogger(__name__)
73
+
74
+ try:
75
+ import subprocess
76
+
77
+ # Test if the driver can start
78
+ result = subprocess.run(
79
+ [driver_path, '--version'],
80
+ capture_output=True,
81
+ text=True,
82
+ timeout=10
83
+ )
84
+
85
+ if result.returncode == 0:
86
+ logger.info(f"Chrome driver test passed: {result.stdout.strip()}")
87
+ return True
88
+ else:
89
+ logger.error(f"Chrome driver test failed: {result.stderr}")
90
+ return False
91
+
92
+ except Exception as e:
93
+ logger.error(f"Chrome driver test error: {e}")
94
+ return False
95
+
96
+
97
+ def cleanup_chrome_cache():
98
+ """Clean up problematic Chrome driver cache."""
99
+ logger = logging.getLogger(__name__)
100
+
101
+ try:
102
+ import shutil
103
+ from pathlib import Path
104
+
105
+ # Get the cache directory
106
+ cache_dir = Path.home() / '.wdm' / 'drivers' / 'chromedriver'
107
+
108
+ if cache_dir.exists():
109
+ logger.info(f"Cleaning Chrome driver cache: {cache_dir}")
110
+ shutil.rmtree(cache_dir)
111
+ logger.info("Chrome driver cache cleaned")
112
+ return True
113
+ else:
114
+ logger.info("No Chrome driver cache to clean")
115
+ return True
116
+
117
+ except Exception as e:
118
+ logger.error(f"Could not clean Chrome driver cache: {e}")
119
+ return False
@@ -0,0 +1,521 @@
1
+ """
2
+ UM Authentication Handler
3
+
4
+ Handles the complex University Malaya authentication flow through OpenAthens
5
+ and SAML authentication systems.
6
+ """
7
+
8
+ import logging
9
+ import time
10
+ import platform
11
+ import subprocess
12
+ from selenium import webdriver
13
+ from selenium.webdriver.common.by import By
14
+ from selenium.webdriver.support.wait import WebDriverWait
15
+ from selenium.webdriver.support.select import Select
16
+ from selenium.webdriver.support import expected_conditions as EC
17
+ from selenium.webdriver.chrome.options import Options as ChromeOptions
18
+ from selenium.webdriver.chrome.service import Service as ChromeService
19
+ from selenium.webdriver.edge.options import Options as EdgeOptions
20
+ from selenium.webdriver.edge.service import Service as EdgeService
21
+ from selenium.common.exceptions import TimeoutException, WebDriverException
22
+ from webdriver_manager.chrome import ChromeDriverManager
23
+ from webdriver_manager.microsoft import EdgeChromiumDriverManager
24
+ from .chrome_fix import get_chrome_driver_path, cleanup_chrome_cache
25
+ import requests
26
+
27
+
28
+ class UMAuthenticator:
29
+ """Handles UM authentication through OpenAthens proxy."""
30
+
31
+ # UM authentication URLs
32
+ OPENATHENS_URL = "https://proxy.openathens.net/login?qurl=https%3A%2F%2Fexampaper.um.edu.my%2F&entityID=https%3A%2F%2Fidp.um.edu.my%2Fentity"
33
+ EXAM_PAPER_BASE_URL = "https://exampaper-um-edu-my.eu1.proxy.openathens.net"
34
+
35
+ def __init__(self, headless=False, timeout=30, browser='auto'):
36
+ """Initialize the authenticator."""
37
+ self.headless = headless
38
+ self.timeout = timeout
39
+ self.browser = browser
40
+ self.driver = None
41
+ self.session = None
42
+ self.logger = logging.getLogger(__name__)
43
+
44
+ def _setup_driver(self):
45
+ """Setup WebDriver with appropriate options for the selected browser."""
46
+ browser_type = self._detect_browser() if self.browser == 'auto' else self.browser
47
+
48
+ try:
49
+ if browser_type == 'edge':
50
+ self.driver = self._setup_edge_driver()
51
+ else: # Default to Chrome
52
+ self.driver = self._setup_chrome_driver()
53
+
54
+ self.driver.set_page_load_timeout(self.timeout)
55
+ self.logger.info(f"{browser_type.title()} WebDriver initialized successfully")
56
+
57
+ except Exception as e:
58
+ self.logger.error(f"Failed to initialize {browser_type} WebDriver: {e}")
59
+
60
+ # Try fallback browsers if auto-detection was used
61
+ if self.browser == 'auto' and browser_type != 'chrome':
62
+ self.logger.info("Trying Chrome as fallback...")
63
+ try:
64
+ self.driver = self._setup_chrome_driver()
65
+ self.driver.set_page_load_timeout(self.timeout)
66
+ self.logger.info("Chrome WebDriver initialized as fallback")
67
+ return
68
+ except Exception as chrome_error:
69
+ self.logger.error(f"Chrome fallback also failed: {chrome_error}")
70
+
71
+ raise Exception(f"Failed to initialize any WebDriver: {e}")
72
+
73
+ def _detect_browser(self):
74
+ """Detect the best available browser."""
75
+ import platform
76
+
77
+ # On Windows, strongly prefer Edge since Chrome has driver issues
78
+ if platform.system() == "Windows":
79
+ try:
80
+ import subprocess
81
+ # Check if Edge is installed
82
+ subprocess.run(['where', 'msedge'], check=True, capture_output=True)
83
+ self.logger.info("Detected Edge browser on Windows")
84
+ return 'edge'
85
+ except (subprocess.CalledProcessError, FileNotFoundError):
86
+ self.logger.warning("Edge not found, will try Chrome")
87
+
88
+ # Default to Chrome for non-Windows or if Edge not found
89
+ self.logger.info("Defaulting to Chrome browser")
90
+ return 'chrome'
91
+
92
+ def _setup_chrome_driver(self):
93
+ """Setup Chrome WebDriver with proper architecture detection."""
94
+ options = ChromeOptions()
95
+ self._add_common_options(options)
96
+
97
+ # Fix for Win32 application error - use our custom Chrome driver setup
98
+ try:
99
+ # First, try our custom driver setup
100
+ driver_path = get_chrome_driver_path()
101
+
102
+ return webdriver.Chrome(
103
+ service=ChromeService(driver_path),
104
+ options=options
105
+ )
106
+ except Exception as e:
107
+ self.logger.error(f"Chrome driver setup failed: {e}")
108
+
109
+ # Try cleaning cache and retry
110
+ self.logger.info("Attempting to clean Chrome driver cache and retry...")
111
+ try:
112
+ cleanup_chrome_cache()
113
+ driver_path = get_chrome_driver_path()
114
+
115
+ return webdriver.Chrome(
116
+ service=ChromeService(driver_path),
117
+ options=options
118
+ )
119
+ except Exception as retry_error:
120
+ self.logger.error(f"Chrome driver retry also failed: {retry_error}")
121
+ raise Exception(f"Chrome driver setup failed even after cache cleanup: {retry_error}")
122
+
123
+ def _setup_edge_driver(self):
124
+ """Setup Edge WebDriver."""
125
+ options = EdgeOptions()
126
+ self._add_common_options(options)
127
+
128
+ try:
129
+ driver_path = EdgeChromiumDriverManager().install()
130
+
131
+ return webdriver.Edge(
132
+ service=EdgeService(driver_path),
133
+ options=options
134
+ )
135
+ except Exception as e:
136
+ self.logger.error(f"Edge driver setup failed: {e}")
137
+ raise
138
+
139
+ def _add_common_options(self, options):
140
+ """Add common options for Chrome and Edge."""
141
+ if self.headless:
142
+ options.add_argument('--headless')
143
+
144
+ # Security and performance options
145
+ options.add_argument('--no-sandbox')
146
+ options.add_argument('--disable-dev-shm-usage')
147
+ options.add_argument('--disable-gpu')
148
+ options.add_argument('--disable-web-security')
149
+ options.add_argument('--disable-features=VizDisplayCompositor')
150
+ options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
151
+
152
+ def _wait_for_element(self, by, value, timeout=None):
153
+ """Wait for an element to be present and return it."""
154
+ if timeout is None:
155
+ timeout = self.timeout
156
+
157
+ if not self.driver:
158
+ raise Exception("WebDriver not initialized")
159
+
160
+ try:
161
+ element = WebDriverWait(self.driver, timeout).until(
162
+ EC.presence_of_element_located((by, value))
163
+ )
164
+ return element
165
+ except TimeoutException:
166
+ self.logger.error(f"Timeout waiting for element: {by}={value}")
167
+ raise
168
+
169
+ def _wait_for_clickable(self, by, value, timeout=None):
170
+ """Wait for an element to be clickable and return it."""
171
+ if timeout is None:
172
+ timeout = self.timeout
173
+
174
+ if not self.driver:
175
+ raise Exception("WebDriver not initialized")
176
+
177
+ try:
178
+ element = WebDriverWait(self.driver, timeout).until(
179
+ EC.element_to_be_clickable((by, value))
180
+ )
181
+ return element
182
+ except TimeoutException:
183
+ self.logger.error(f"Timeout waiting for clickable element: {by}={value}")
184
+ raise
185
+
186
+ def login(self, username, password):
187
+ """
188
+ Perform the complete UM login flow.
189
+
190
+ Args:
191
+ username (str): UM username (without @siswa.um.edu.my)
192
+ password (str): UM password
193
+
194
+ Returns:
195
+ requests.Session: Authenticated session for making requests
196
+ """
197
+ try:
198
+ self._setup_driver()
199
+
200
+ if not self.driver:
201
+ raise Exception("Failed to initialize WebDriver")
202
+
203
+ # Step 1: Navigate to OpenAthens proxy
204
+ self.logger.info("Navigating to OpenAthens proxy...")
205
+ self.driver.get(self.OPENATHENS_URL)
206
+
207
+ # Step 2: Select "UM Staff and Students" option
208
+ self.logger.info("Selecting UM Staff and Students option...")
209
+ um_option = self._wait_for_clickable(
210
+ By.XPATH,
211
+ "//div[contains(text(), 'UM Staff and Students')]"
212
+ )
213
+ um_option.click()
214
+
215
+ # Step 3: Fill in credentials
216
+ self.logger.info("Entering credentials...")
217
+
218
+ # Wait for username field and enter username
219
+ username_field = self._wait_for_element(By.NAME, "username")
220
+ username_field.clear()
221
+ username_field.send_keys(username)
222
+
223
+ # Enter password
224
+ password_field = self._wait_for_element(By.NAME, "password")
225
+ password_field.clear()
226
+ password_field.send_keys(password)
227
+
228
+ # Give the page a moment to process the entered credentials and load the status dropdown
229
+ self.logger.info("Waiting for page to fully load after entering credentials...")
230
+ time.sleep(3)
231
+
232
+ # Select "Student" status
233
+ self.logger.info("Selecting 'Student' status...")
234
+
235
+ # Wait for the status dropdown to be available with longer timeout
236
+ status_dropdown = None
237
+
238
+ # First, wait for any select element to appear
239
+ try:
240
+ WebDriverWait(self.driver, 15).until(
241
+ EC.presence_of_element_located((By.TAG_NAME, "select"))
242
+ )
243
+ self.logger.info("Select elements are now available on page")
244
+ except TimeoutException:
245
+ self.logger.warning("No select elements found - the form might have a different structure")
246
+
247
+ # Start with the method that actually works - searching through select elements
248
+ self.logger.info("Looking for status dropdown through select elements...")
249
+ try:
250
+ select_elements = self.driver.find_elements(By.TAG_NAME, "select")
251
+ self.logger.info(f"Found {len(select_elements)} select elements")
252
+ for i, select_elem in enumerate(select_elements):
253
+ try:
254
+ options = select_elem.find_elements(By.TAG_NAME, "option")
255
+ option_texts = [opt.text for opt in options if opt.text.strip()]
256
+ self.logger.info(f"Select {i} options: {option_texts}")
257
+
258
+ # Check if this select has Student/Staff options
259
+ has_student = any("student" in text.lower() for text in option_texts)
260
+ has_staff = any("staff" in text.lower() for text in option_texts)
261
+
262
+ if has_student and has_staff:
263
+ status_dropdown = select_elem
264
+ self.logger.info(f"✅ Found status dropdown in select {i}")
265
+ break
266
+ except Exception as e:
267
+ self.logger.warning(f"Error checking select {i}: {e}")
268
+ continue
269
+ except Exception as e:
270
+ self.logger.error(f"Error searching for select elements: {e}")
271
+
272
+ # Only try name/ID if the working method failed
273
+ if not status_dropdown:
274
+ self.logger.info("Trying backup methods for status dropdown...")
275
+ try:
276
+ status_dropdown = self._wait_for_element(By.NAME, "status", timeout=10)
277
+ self.logger.info("✅ Found status dropdown by name")
278
+ except Exception as e1:
279
+ self.logger.warning(f"Could not find status dropdown by name: {e1}")
280
+ try:
281
+ status_dropdown = self._wait_for_element(By.ID, "status", timeout=5)
282
+ self.logger.info("✅ Found status dropdown by ID")
283
+ except Exception as e2:
284
+ self.logger.warning(f"Could not find status dropdown by ID: {e2}")
285
+
286
+ if not status_dropdown:
287
+ raise Exception("Could not find status dropdown - check if page loaded correctly")
288
+
289
+ # Wait a moment for the dropdown to be fully interactive
290
+ time.sleep(1)
291
+
292
+ select = Select(status_dropdown)
293
+
294
+ # Debug: Log available options first
295
+ options = select.options
296
+ option_texts = [opt.text for opt in options if opt.text.strip()]
297
+ self.logger.info(f"Available status options: {option_texts}")
298
+
299
+ # Try multiple ways to select Student with better error handling
300
+ selection_successful = False
301
+
302
+ # Method 1: Try by visible text (most reliable)
303
+ try:
304
+ select.select_by_visible_text("Student")
305
+ self.logger.info("✅ Selected Student by visible text")
306
+ selection_successful = True
307
+ except Exception as e1:
308
+ self.logger.warning(f"Could not select by visible text 'Student': {e1}")
309
+
310
+ # Method 2: Try by value
311
+ try:
312
+ select.select_by_value("Student")
313
+ self.logger.info("✅ Selected Student by value")
314
+ selection_successful = True
315
+ except Exception as e2:
316
+ self.logger.warning(f"Could not select by value 'Student': {e2}")
317
+
318
+ # Method 3: Try variations of text
319
+ for option in options:
320
+ if option.text.strip() and "student" in option.text.lower():
321
+ try:
322
+ select.select_by_visible_text(option.text)
323
+ self.logger.info(f"✅ Selected Student by text variation: '{option.text}'")
324
+ selection_successful = True
325
+ break
326
+ except Exception as e3:
327
+ continue
328
+
329
+ # Method 4: Try by index if nothing else worked
330
+ if not selection_successful:
331
+ try:
332
+ # Find the index of the Student option
333
+ for i, option in enumerate(options):
334
+ if option.text.strip() and "student" in option.text.lower():
335
+ select.select_by_index(i)
336
+ self.logger.info(f"✅ Selected Student by index {i}")
337
+ selection_successful = True
338
+ break
339
+ except Exception as e4:
340
+ self.logger.error(f"Could not select by index: {e4}")
341
+
342
+ if not selection_successful:
343
+ raise Exception("Failed to select Student status using any method")
344
+
345
+ # Verify selection and wait for it to be processed
346
+ selected_option = select.first_selected_option
347
+ self.logger.info(f"Status dropdown selected: {selected_option.text}")
348
+
349
+ # Give the form time to process the selection
350
+ time.sleep(3)
351
+
352
+ # Final verification before submitting
353
+ final_selection = select.first_selected_option
354
+ self.logger.info(f"Final status before submit: {final_selection.text}")
355
+
356
+ # Submit the form
357
+ self.logger.info("Looking for sign-in button...")
358
+
359
+ # Wait for submit button to be available
360
+ try:
361
+ WebDriverWait(self.driver, 10).until(
362
+ lambda driver: driver.find_elements(By.XPATH, "//button[@type='submit']") or
363
+ driver.find_elements(By.XPATH, "//input[@type='submit']") or
364
+ driver.find_elements(By.XPATH, "//input[contains(@value, 'Sign')]")
365
+ )
366
+ self.logger.info("Submit button is now available")
367
+ except TimeoutException:
368
+ self.logger.warning("No submit button found with standard selectors")
369
+
370
+ # Try multiple ways to find the sign-in button, starting with the method that works
371
+ sign_in_button = None
372
+ button_selectors = [
373
+ (By.XPATH, "//button[@type='submit']", "submit button"),
374
+ (By.XPATH, "//input[@type='submit']", "submit input"),
375
+ (By.XPATH, "//input[@value='Sign in']", "exact Sign in value"),
376
+ (By.XPATH, "//input[contains(@value, 'Sign')]", "contains Sign value"),
377
+ (By.XPATH, "//button[contains(text(), 'Sign')]", "button with Sign text"),
378
+ (By.XPATH, "//input[contains(@value, 'Login')]", "contains Login value"),
379
+ (By.XPATH, "//button[contains(text(), 'Login')]", "button with Login text"),
380
+ (By.CSS_SELECTOR, "button[type='submit']", "CSS submit button"),
381
+ (By.CSS_SELECTOR, "input[type='submit']", "CSS submit input")
382
+ ]
383
+
384
+ for i, (by, selector, description) in enumerate(button_selectors):
385
+ try:
386
+ self.logger.info(f"Trying method {i+1}: {description}")
387
+ sign_in_button = self._wait_for_clickable(by, selector, timeout=8)
388
+ self.logger.info(f"✅ Found sign-in button using: {description}")
389
+ break
390
+ except Exception as e:
391
+ self.logger.warning(f"Method {i+1} ({description}) failed: {e}")
392
+ continue
393
+
394
+ if not sign_in_button:
395
+ # Last resort: look for any clickable element that might be the button
396
+ self.logger.info("Looking for any submit-like elements...")
397
+ try:
398
+ all_inputs = self.driver.find_elements(By.TAG_NAME, "input")
399
+ all_buttons = self.driver.find_elements(By.TAG_NAME, "button")
400
+
401
+ self.logger.info(f"Found {len(all_inputs)} input elements and {len(all_buttons)} button elements")
402
+
403
+ for elem in all_inputs + all_buttons:
404
+ try:
405
+ elem_type = elem.get_attribute("type")
406
+ elem_value = elem.get_attribute("value")
407
+ elem_text = elem.text
408
+ self.logger.info(f"Element: type='{elem_type}', value='{elem_value}', text='{elem_text}'")
409
+ except:
410
+ pass
411
+
412
+ except Exception as e:
413
+ self.logger.error(f"Error examining form elements: {e}")
414
+
415
+ raise Exception("Could not find sign-in button. Check if form structure changed.")
416
+
417
+ self.logger.info("Clicking sign-in button...")
418
+ sign_in_button.click()
419
+
420
+ # Step 4: Wait for redirect to exam paper repository
421
+ self.logger.info("Waiting for authentication to complete...")
422
+
423
+ # Give some time for form submission
424
+ time.sleep(3)
425
+
426
+ # Check current URL and wait for redirect
427
+ current_url = self.driver.current_url
428
+ self.logger.info(f"Current URL after clicking sign-in: {current_url}")
429
+
430
+ # Wait for successful redirect to exam paper site with increased timeout
431
+ try:
432
+ WebDriverWait(self.driver, self.timeout + 15).until(
433
+ lambda driver: self.EXAM_PAPER_BASE_URL in driver.current_url
434
+ )
435
+ self.logger.info(f"Successfully redirected to: {self.driver.current_url}")
436
+ except TimeoutException:
437
+ # Log current state for debugging
438
+ current_url = self.driver.current_url
439
+ page_title = self.driver.title
440
+ self.logger.error(f"Timeout waiting for redirect. Current URL: {current_url}")
441
+ self.logger.error(f"Page title: {page_title}")
442
+
443
+ # Check if we're still on login page (indicates failed login)
444
+ if "login" in current_url.lower() or "auth" in current_url.lower():
445
+ raise Exception("Login failed - still on authentication page. Check credentials.")
446
+ else:
447
+ # We might be on a different page - let's continue and see
448
+ self.logger.warning("Redirect timeout but not on login page - continuing...")
449
+
450
+ self.logger.info("Authentication successful, creating session...")
451
+
452
+ # Step 5: Extract cookies and create requests session
453
+ self.session = self._create_authenticated_session()
454
+
455
+ return self.session
456
+
457
+ except TimeoutException:
458
+ self.logger.error("Authentication timeout - check credentials and network")
459
+ raise Exception("Authentication failed: Timeout")
460
+ except Exception as e:
461
+ self.logger.error(f"Authentication failed: {e}")
462
+ raise Exception(f"Authentication failed: {e}")
463
+
464
+ def _create_authenticated_session(self):
465
+ """Create a requests session with authentication cookies."""
466
+ session = requests.Session()
467
+
468
+ if not self.driver:
469
+ raise Exception("WebDriver not initialized")
470
+
471
+ # Copy cookies from Selenium to requests session
472
+ for cookie in self.driver.get_cookies():
473
+ session.cookies.set(
474
+ cookie['name'],
475
+ cookie['value'],
476
+ domain=cookie.get('domain'),
477
+ path=cookie.get('path')
478
+ )
479
+
480
+ # Set common headers
481
+ session.headers.update({
482
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
483
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
484
+ 'Accept-Language': 'en-US,en;q=0.5',
485
+ 'Accept-Encoding': 'gzip, deflate',
486
+ 'Connection': 'keep-alive',
487
+ 'Upgrade-Insecure-Requests': '1'
488
+ })
489
+
490
+ self.logger.info("Authenticated session created successfully")
491
+ return session
492
+
493
+ def test_session(self):
494
+ """Test if the session is still valid."""
495
+ if not self.session:
496
+ return False
497
+
498
+ try:
499
+ response = self.session.get(
500
+ f"{self.EXAM_PAPER_BASE_URL}/",
501
+ timeout=10
502
+ )
503
+ return response.status_code == 200
504
+ except:
505
+ return False
506
+
507
+ def cleanup(self):
508
+ """Clean up resources."""
509
+ if self.driver:
510
+ try:
511
+ self.driver.quit()
512
+ self.logger.info("WebDriver cleaned up")
513
+ except:
514
+ pass
515
+
516
+ if self.session:
517
+ try:
518
+ self.session.close()
519
+ self.logger.info("Session cleaned up")
520
+ except:
521
+ pass
downloader/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Downloader package