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 +1 -0
- auth/chrome_fix.py +119 -0
- auth/um_authenticator.py +521 -0
- downloader/__init__.py +1 -0
- downloader/pdf_downloader.py +207 -0
- scraper/__init__.py +1 -0
- scraper/paper_scraper.py +316 -0
- umpaper_fetch/__init__.py +26 -0
- umpaper_fetch/auth/__init__.py +1 -0
- umpaper_fetch/auth/chrome_fix.py +119 -0
- umpaper_fetch/auth/um_authenticator.py +521 -0
- umpaper_fetch/cli.py +316 -0
- umpaper_fetch/downloader/__init__.py +1 -0
- umpaper_fetch/downloader/pdf_downloader.py +207 -0
- umpaper_fetch/scraper/__init__.py +1 -0
- umpaper_fetch/scraper/paper_scraper.py +316 -0
- umpaper_fetch/utils/__init__.py +1 -0
- umpaper_fetch/utils/logger.py +67 -0
- umpaper_fetch/utils/zip_creator.py +299 -0
- umpaper_fetch-1.0.0.dist-info/METADATA +462 -0
- umpaper_fetch-1.0.0.dist-info/RECORD +28 -0
- umpaper_fetch-1.0.0.dist-info/WHEEL +5 -0
- umpaper_fetch-1.0.0.dist-info/entry_points.txt +2 -0
- umpaper_fetch-1.0.0.dist-info/licenses/LICENSE +22 -0
- umpaper_fetch-1.0.0.dist-info/top_level.txt +5 -0
- utils/__init__.py +1 -0
- utils/logger.py +67 -0
- utils/zip_creator.py +299 -0
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
|
auth/um_authenticator.py
ADDED
@@ -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
|