4747from requests .adapters import HTTPAdapter
4848from rich .console import Console
4949from rich .logging import RichHandler
50- from selenium import webdriver
50+ from selenium .webdriver .chrome .options import Options as ChromeOptions
51+ from selenium .webdriver .chrome .webdriver import WebDriver as ChromeDriver
5152from selenium .webdriver .common .by import By
52- from selenium .webdriver .remote .webdriver import WebDriver
5353from selenium .webdriver .support import expected_conditions as EC
5454from selenium .webdriver .support .ui import WebDriverWait
5555from selenium_stealth import stealth
7979
8080DEFAULT_SLOT_SEARCH_TYPE : int = 0
8181
82+ # Load environment variables
83+ load_dotenv ()
84+
8285token_lock = FileLock (TOKEN_LOCK_PATH , timeout = 60 )
8386login_lock = FileLock (LOGIN_LOCK_PATH , timeout = 60 )
8487
8588# Setup logging
8689console = Console ()
8790
8891logging .basicConfig (
89- level = " INFO" ,
92+ level = os . environ . get ( "LOG_LEVEL" , " INFO"). upper () ,
9093 format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ,
9194 handlers = [
9295 RotatingFileHandler (
98101
99102log = logging .getLogger ("medichaser" )
100103
101- # Load environment variables
102- load_dotenv ()
103-
104104retry_strategy = Retry (
105105 total = 10 ,
106106 backoff_factor = 1 ,
@@ -152,7 +152,7 @@ def __init__(self, username: str, password: str) -> None:
152152 self .tokenA : str | None = None
153153 self .tokenR : str | None = None
154154 self .expires_at : int | None = None
155- self .driver : WebDriver | None = None
155+ self .driver : ChromeDriver | None = None
156156
157157 def _get_or_create_device_id (self ) -> str :
158158 """Gets the device ID from storage or creates a new one."""
@@ -261,7 +261,9 @@ def login_requests(self) -> None: # pragma: no cover
261261 allow_redirects = False ,
262262 )
263263 response .raise_for_status ()
264- next_url = response .headers ["Location" ]
264+ next_url = response .headers .get ("Location" )
265+ if not next_url :
266+ raise ValueError ("Missing Location header in authorize response." )
265267 time .sleep (2 )
266268
267269 response = self .session .get (
@@ -279,7 +281,10 @@ def login_requests(self) -> None: # pragma: no cover
279281 csrf_token = match .group (1 )
280282 parsed_url = urlparse (response .url )
281283 query_params = parse_qs (parsed_url .query )
282- return_url = query_params ["ReturnUrl" ][0 ]
284+ return_url_values = query_params .get ("ReturnUrl" )
285+ if not return_url_values :
286+ raise MFAError ("ReturnUrl not found in login page query string." )
287+ return_url = return_url_values [0 ]
283288 # Step 2: POST credentials
284289 login_data = {
285290 "Input.ReturnUrl" : return_url ,
@@ -308,7 +313,9 @@ def login_requests(self) -> None: # pragma: no cover
308313
309314 # Step 3: Handle redirects and potential MFA
310315 while response .status_code == 302 :
311- redirect_url = response .headers ["Location" ]
316+ redirect_url = response .headers .get ("Location" )
317+ if not redirect_url :
318+ raise ValueError ("Missing Location header in login redirect response." )
312319 if not redirect_url .startswith ("https://" ):
313320 redirect_url = MEDICOVER_LOGIN_URL + redirect_url
314321
@@ -399,7 +406,10 @@ def _handle_mfa(self, mfa_url: str) -> requests.Response: # pragma: no cover
399406
400407 parsed_url = urlparse (mfa_url )
401408 query_params = parse_qs (parsed_url .query )
402- return_url = query_params ["returnUrl" ][0 ]
409+ return_url_values = query_params .get ("ReturnUrl" )
410+ if not return_url_values :
411+ raise MFAError ("ReturnUrl not found in MFA URL query string." )
412+ return_url = return_url_values [0 ]
403413
404414 mfa_data = {
405415 "Input.MfaCodeId" : mfa_code_id ,
@@ -456,7 +466,8 @@ def _exchange_code_for_token(
456466
457467 data = response .json ()
458468 if "error" in data :
459- raise ValueError (f"Failed to get token: { data ['error_description' ]} " )
469+ error_description = data .get ("error_description" , data .get ("error" ))
470+ raise ValueError (f"Failed to get token: { error_description } " )
460471
461472 expires_in = data .get ("expires_in" )
462473 expires_at = int (time .time ()) + expires_in if expires_in else None
@@ -475,7 +486,7 @@ def _exchange_code_for_token(
475486 @tenacity .retry (
476487 stop = tenacity .stop_after_attempt (2 ),
477488 wait = tenacity .wait_fixed (10 ),
478- retry = tenacity .retry_if_not_exception_type (MFAError ),
489+ retry = tenacity .retry_if_not_exception_type (( MFAError , ValueError ) ),
479490 reraise = True ,
480491 )
481492 def login (self ) -> None :
@@ -500,16 +511,16 @@ def login(self) -> None:
500511 else :
501512 self .login_requests ()
502513
503- def _init_driver (self ) -> WebDriver :
504- """Initializes the Selenium WebDriver if it's not already running."""
514+ def _init_driver (self ) -> ChromeDriver :
515+ """Initializes the Selenium ChromeDriver if it's not already running."""
505516 if self .driver is None :
506- options = webdriver . ChromeOptions ()
517+ options = ChromeOptions ()
507518 options .add_argument ("--headless" ) # Run in headless mode
508519 options .add_argument ("--disable-gpu" )
509520 options .add_argument ("--no-sandbox" )
510521 options .add_argument ("--disable-dev-shm-usage" )
511522 options .add_argument (f"user-data-dir={ DATA_PATH / 'chrome_profile' } " )
512- self .driver = webdriver . Chrome (options = options )
523+ self .driver = ChromeDriver (options = options )
513524 stealth (
514525 self .driver ,
515526 languages = ["pl-PL" , "pl" ],
@@ -533,7 +544,7 @@ def _init_driver(self) -> WebDriver:
533544 return self .driver
534545
535546 def _quit_driver (self ) -> None :
536- """Quits the Selenium WebDriver if it's running."""
547+ """Quits the Selenium ChromeDriver if it's running."""
537548 if self .driver :
538549 self .driver .quit ()
539550 self .driver = None
@@ -574,7 +585,8 @@ def refresh_token(self) -> None:
574585
575586 data = response .json ()
576587 if "error" in data :
577- if data ["error" ] == "invalid_grant" :
588+ error = data .get ("error" )
589+ if error == "invalid_grant" :
578590 log .error (
579591 "Refresh token is invalid or expired. Deleting token file and re-authenticating."
580592 )
@@ -584,7 +596,7 @@ def refresh_token(self) -> None:
584596 "Invalid grant: refresh token is likely expired or revoked."
585597 )
586598 raise ValueError (
587- f"Failed to refresh token: { response .status_code } { response .text } "
599+ f"Failed to refresh token: { response .status_code } { data . get ( 'error_description' , response .text ) } "
588600 )
589601
590602 # manually set expires_at
@@ -833,7 +845,8 @@ def find_appointments(
833845 items = [
834846 x
835847 for x in items
836- if datetime .datetime .fromisoformat (x ["appointmentDate" ]).date ()
848+ if x .get ("appointmentDate" )
849+ and datetime .datetime .fromisoformat (x ["appointmentDate" ]).date ()
837850 <= end_date
838851 ]
839852
@@ -1222,8 +1235,8 @@ def main() -> None:
12221235 else :
12231236 filters = finder .find_filters ()
12241237
1225- for r in filters [ args .filter_type ] :
1226- log .info (f"{ r [ 'id' ] } - { r [ 'value' ] } " )
1238+ for r in filters . get ( args .filter_type , []) :
1239+ log .info (f"{ r . get ( 'id' , 'N/A' ) } - { r . get ( 'value' , 'N/A' ) } " )
12271240
12281241
12291242if __name__ == "__main__" :
0 commit comments