From 00efc7afe630b40d017271e6ab0150ec596da0ea Mon Sep 17 00:00:00 2001 From: Matanga1-2 Date: Fri, 31 Jan 2025 11:42:04 +0200 Subject: [PATCH 1/2] add email refresh --- src/app.py | 27 +++++++++--- src/mail_sender/init_credentials.py | 64 +++++++++++++++-------------- src/mail_sender/sender.py | 16 +++++--- src/yad2/browser.py | 8 ++-- tests/email/test_sender.py | 15 +++++++ tests/test_app.py | 3 +- 6 files changed, 86 insertions(+), 47 deletions(-) diff --git a/src/app.py b/src/app.py index 4aa8574..f2ee454 100644 --- a/src/app.py +++ b/src/app.py @@ -4,6 +4,7 @@ from src.cli.input_handler import display_feed_stats, get_valid_url from src.db.database import Database from src.db.saved_items_repository import SavedItemsRepository +from src.mail_sender.init_credentials import init_gmail_credentials from src.processor.feed_processor import categorize_feed_items, process_feed_items from src.utils.console import prompt_yes_no from src.utils.text_formatter import format_hebrew @@ -47,9 +48,10 @@ def _process_menu_choice(self) -> bool: print("3. Process current feed") print("4. Store saved items") print("5. Go to all URLs") - print("6. Exit") + print("6. Refresh Gmail credentials") + print("7. Exit") - choice = input("\nEnter your choice (1-6): ").strip() + choice = input("\nEnter your choice (1-7): ").strip() if choice == '1': self._handle_new_url() @@ -62,10 +64,12 @@ def _process_menu_choice(self) -> bool: elif choice == '5': self._handle_go_to_all_urls() elif choice == '6': + self._handle_refresh_credentials() + elif choice == '7': print("Goodbye!") return False else: - print("Invalid choice. Please enter a number between 1 and 5.") + print("Invalid choice. Please enter a number between 1 and 7.") return True @@ -135,6 +139,19 @@ def _handle_process_feed(self) -> None: return items_to_process = [item for item in self.feed_items if not item.is_saved] + process_feed_items(items_to_process, self.address_matcher, self.client, self.saved_items_repo) + + def _handle_refresh_credentials(self) -> None: + """Handle refreshing Gmail credentials.""" + print("\nRefreshing Gmail credentials...") + force_new = prompt_yes_no("Do you want to force creation of new credentials?") - if prompt_yes_no("\nProceed with processing these items?"): - process_feed_items(items_to_process, self.address_matcher, self.client, self.saved_items_repo) \ No newline at end of file + try: + success = init_gmail_credentials(force_new=force_new) + if success: + print("Successfully refreshed Gmail credentials!") + else: + print("Failed to refresh Gmail credentials. Check the logs for more details.") + except Exception as e: + logging.error(f"Failed to refresh credentials: {str(e)}", exc_info=True) + print(f"Failed to refresh credentials: {str(e)}") \ No newline at end of file diff --git a/src/mail_sender/init_credentials.py b/src/mail_sender/init_credentials.py index 57367a3..06f2c0a 100644 --- a/src/mail_sender/init_credentials.py +++ b/src/mail_sender/init_credentials.py @@ -7,47 +7,46 @@ from src.utils.console import prompt_yes_no -def _setup_logging(): - logger = logging.getLogger(__name__) - logger.setLevel(logging.INFO) - - if not logger.handlers: - # Get the directory where this script is located - current_dir = os.path.dirname(os.path.abspath(__file__)) - log_path = os.path.join(current_dir, 'gmail_auth.log') - - handlers = [ - logging.FileHandler(log_path), - logging.StreamHandler() - ] - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - - for handler in handlers: - handler.setFormatter(formatter) - handler.setLevel(logging.INFO) - logger.addHandler(handler) - - return logger - -logger = _setup_logging() +def get_credentials_dir(): + """Get the directory where credentials should be stored""" + home = os.path.expanduser('~') + credentials_dir = os.path.join(home, '.Yad2Scraper', 'credentials') + os.makedirs(credentials_dir, exist_ok=True) + return credentials_dir -def init_gmail_credentials(): +def init_gmail_credentials(force_new=False): """ Initialize Gmail API credentials. This script should be run once before using the main application. It will: 1. Check for existing token.pickle and client_secret.json 2. Start the OAuth2 flow if needed 3. Save the credentials to token.pickle + + Args: + force_new (bool): If True, force creation of new credentials regardless of existing ones """ + logger = logging.getLogger(__name__) SCOPES = ['https://www.googleapis.com/auth/gmail.send'] - # Get the directory where this script is located - current_dir = os.path.dirname(os.path.abspath(__file__)) - client_secret_path = os.path.join(current_dir, 'client_secret.json') - token_path = os.path.join(current_dir, 'token.pickle') + # Get the credentials directory in user's home + credentials_dir = get_credentials_dir() + + # For client_secret.json, first check in the credentials dir, then fall back to package dir + client_secret_name = 'client_secret.json' + token_path = os.path.join(credentials_dir, 'token.pickle') + client_secret_path = os.path.join(credentials_dir, client_secret_name) + + if not os.path.exists(client_secret_path): + # Fall back to checking in the package directory + package_dir = os.path.dirname(os.path.abspath(__file__)) + package_client_secret = os.path.join(package_dir, client_secret_name) + if os.path.exists(package_client_secret): + # Copy to user directory + import shutil + shutil.copy2(package_client_secret, client_secret_path) # Check if token.pickle already exists - if os.path.exists(token_path): + if os.path.exists(token_path) and not force_new: logger.info(f"Existing credentials found at {token_path}") if not prompt_yes_no("Credentials already exist. Do you want to create new ones?"): logger.info("Using existing credentials") @@ -61,7 +60,7 @@ def init_gmail_credentials(): logger.info("2. Create a project or select existing one") logger.info("3. Enable Gmail API") logger.info("4. Create OAuth 2.0 credentials") - logger.info("5. Download client configuration and save as 'client_secret.json'") + logger.info(f"5. Download client configuration and save as '{client_secret_name}' in {credentials_dir}") return False try: @@ -81,4 +80,9 @@ def init_gmail_credentials(): return False if __name__ == "__main__": + # Setup basic logging for when script is run directly + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) init_gmail_credentials() \ No newline at end of file diff --git a/src/mail_sender/sender.py b/src/mail_sender/sender.py index 5bfe321..df31593 100644 --- a/src/mail_sender/sender.py +++ b/src/mail_sender/sender.py @@ -7,7 +7,7 @@ from googleapiclient.discovery import build -from src.mail_sender.init_credentials import init_gmail_credentials +from src.mail_sender.init_credentials import get_credentials_dir, init_gmail_credentials class EmailSender: @@ -26,9 +26,9 @@ def __init__(self): def initialize_credentials(self): """Initialize or load credentials for Gmail API using init_credentials logic.""" - # Get the directory where this script is located - current_dir = os.path.dirname(os.path.abspath(__file__)) - token_path = os.path.join(current_dir, 'token.pickle') + # Get the credentials directory + credentials_dir = get_credentials_dir() + token_path = os.path.join(credentials_dir, 'token.pickle') # Initialize credentials using the init_credentials function if not os.path.exists(token_path): @@ -38,8 +38,12 @@ def initialize_credentials(self): raise RuntimeError("Failed to initialize Gmail credentials.") # Load the credentials from token.pickle - with open(token_path, 'rb') as token: - self.creds = pickle.load(token) + try: + with open(token_path, 'rb') as token: + self.creds = pickle.load(token) + except Exception as err: + logging.error(f"Failed to load credentials from {token_path}: {str(err)}") + raise RuntimeError(f"Failed to load Gmail credentials: {str(err)}") from err def create_message(self, subject: str, body: str) -> dict: """Create a message for an email.""" diff --git a/src/yad2/browser.py b/src/yad2/browser.py index eb9dae9..041a370 100644 --- a/src/yad2/browser.py +++ b/src/yad2/browser.py @@ -49,10 +49,10 @@ def wait_for_element(self, by: By, value: str, timeout: int = 10): ) self.logger.debug("Element found through wait_for_element") return element - except Exception as e: - self.logger.error(f"Element not found: {by}={value}, {str(e)}") - self.logger.error(f"Current URL: {self.driver.current_url}") - self.logger.error(f"Page source snippet: {self.driver.page_source[:500]}") + except Exception: + self.logger.error(f"Element not found: {by}={value}") + # self.logger.error(f"Current URL: {self.driver.current_url}") + # self.logger.error(f"Page source snippet: {self.driver.page_source[:500]}") raise @staticmethod diff --git a/tests/email/test_sender.py b/tests/email/test_sender.py index e89f1a0..3d2ffb5 100644 --- a/tests/email/test_sender.py +++ b/tests/email/test_sender.py @@ -1,5 +1,7 @@ import pytest +from google.auth.exceptions import RefreshError +from src.mail_sender.init_credentials import init_gmail_credentials from src.mail_sender.sender import EmailSender @@ -25,5 +27,18 @@ def test_send_email(setup_env_vars): try: sender.send_email(subject, body) print("Test email sent successfully.") + except RefreshError: + # If token is expired, try to refresh credentials and retry + print("Token expired, attempting to refresh credentials...") + if init_gmail_credentials(force_new=True): + # Retry with new credentials + try: + sender = EmailSender() # Create new sender with fresh credentials + sender.send_email(subject, body) + print("Test email sent successfully after refreshing credentials.") + except Exception as e: + pytest.fail(f"Sending test email failed even after refreshing credentials: {e}") + else: + pytest.fail("Failed to refresh Gmail credentials") except Exception as e: pytest.fail(f"Sending test email failed: {e}") \ No newline at end of file diff --git a/tests/test_app.py b/tests/test_app.py index e92c045..d3d7ebb 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,5 +1,4 @@ -from unittest.mock import MagicMock, patch -import json +from unittest.mock import MagicMock import pytest From 1d582e00c15f0c30af3f22ed360fe1f50c0e1b24 Mon Sep 17 00:00:00 2001 From: Matanga1-2 Date: Fri, 31 Jan 2025 11:46:01 +0200 Subject: [PATCH 2/2] update exe --- build_exe.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/build_exe.py b/build_exe.py index eb6a83a..c115930 100644 --- a/build_exe.py +++ b/build_exe.py @@ -42,6 +42,19 @@ def main(): if src_path not in sys.path: sys.path.insert(0, src_path) + # Create Yad2Scraper directory in user's home if it doesn't exist + home_dir = os.path.expanduser('~') + app_dir = os.path.join(home_dir, '.Yad2Scraper') + credentials_dir = os.path.join(app_dir, 'credentials') + os.makedirs(credentials_dir, exist_ok=True) + + # Copy client_secret.json to credentials directory if it exists in the package + client_secret_src = os.path.join(base_path, 'src', 'mail_sender', 'client_secret.json') + client_secret_dst = os.path.join(credentials_dir, 'client_secret.json') + if os.path.exists(client_secret_src) and not os.path.exists(client_secret_dst): + import shutil + shutil.copy2(client_secret_src, client_secret_dst) + # Now import and run the actual main function from src.main import main sys.exit(main()) @@ -74,7 +87,7 @@ def main(): # Include all data files '--add-data=consts/*:consts', - '--add-data=src/mail_sender/client_secret.json:src/mail_sender', + '--add-data=src/mail_sender/client_secret.json:src/mail_sender', # Still include it in the package '--add-data=.env:.', '--add-data=src:src', # Add the entire src directory