From 5767ee8f561d9c27b34c47e84af27a9fb8012300 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 23 Mar 2026 06:39:05 +0000 Subject: [PATCH 1/6] Fix issue #114: Merge nested if conditions in _load_credentials Refactored the nested if conditions to reduce nesting depth as suggested by the merge-nested-ifs rule. The logic now checks if credentials exist and are either valid or expired with refresh token available, then builds the gmail service. If credentials are expired, they are refreshed first. Co-authored-by: openhands --- src/backend/python_nlp/gmail_integration.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/backend/python_nlp/gmail_integration.py b/src/backend/python_nlp/gmail_integration.py index 365315943..6884eb1b2 100644 --- a/src/backend/python_nlp/gmail_integration.py +++ b/src/backend/python_nlp/gmail_integration.py @@ -275,15 +275,15 @@ def _load_credentials(self): creds = Credentials.from_authorized_user_file(TOKEN_JSON_PATH, SCOPES) except Exception as e: self.logger.error(f"Error loading credentials from {TOKEN_JSON_PATH}: {e}") - if creds and creds.valid: - self.gmail_service = build("gmail", "v1", credentials=creds) - elif creds and creds.expired and creds.refresh_token: + + if creds and (creds.valid or (creds.expired and creds.refresh_token)): try: - creds.refresh(Request()) + if creds.expired and creds.refresh_token: + creds.refresh(Request()) + self._store_credentials(creds) self.gmail_service = build("gmail", "v1", credentials=creds) - self._store_credentials(creds) except Exception as e: - self.logger.error(f"Error refreshing credentials: {e}") + self.logger.error(f"Error with credentials: {e}") def _store_credentials(self, creds): """Stores the API credentials to a token file.""" From 380a6a038e56817df0c42f3cdba038eb02f5095f Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 01:29:23 +0000 Subject: [PATCH 2/6] Improve Dependabot auto-merge workflow - Add pull_request_target trigger for better reliability - Check mergeable state before attempting to merge - Wait for CI checks if PR is not in mergeable state - Re-check mergeable state after waiting - Only attempt merge when PR is actually mergeable Co-authored-by: openhands --- .github/workflows/dependabot-auto-merge.yml | 63 ++++++++++++++------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 32d7ef94f..9bdb8976d 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -3,6 +3,8 @@ name: Dependabot Auto-Merge on: pull_request: types: [opened, synchronize, reopened] + pull_request_target: + types: [opened, synchronize, reopened] permissions: contents: write @@ -12,10 +14,28 @@ permissions: jobs: auto-merge: runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' && github.event.pull_request.mergeable_state == 'clean' && !github.event.pull_request.draft + if: github.actor == 'dependabot[bot]' && !github.event.pull_request.draft steps: - - name: Wait for CI checks to complete + - name: Check if PR is mergeable + id: check-mergeable + run: | + echo "Checking PR #${{ github.event.pull_request.number }} mergeable state..." + echo "Current mergeable state: ${{ github.event.pull_request.mergeable_state }}" + echo "Draft: ${{ github.event.pull_request.draft }}" + + # Get the PR mergeable state + PR_STATE="${{ github.event.pull_request.mergeable_state }}" + if [[ "$PR_STATE" == "clean" || "$PR_STATE" == "unstable" || "$PR_STATE" == "has_hooks" ]]; then + echo "PR is in mergeable state: $PR_STATE" + echo "mergeable=true" >> $GITHUB_OUTPUT + else + echo "PR is not in mergeable state yet: $PR_STATE" + echo "mergeable=false" >> $GITHUB_OUTPUT + fi + + - name: Wait for required checks if not mergeable + if: steps.check-mergeable.outputs.mergeable == 'false' uses: fountainhead/action-wait-for-check@v1.2.0 id: wait-for-ci with: @@ -25,30 +45,35 @@ jobs: timeoutSeconds: 600 intervalSeconds: 10 + - name: Re-check mergeable state after waiting + if: steps.check-mergeable.outputs.mergeable == 'false' + id: recheck-mergeable + run: | + echo "Re-checking mergeable state after CI..." + PR_STATE="${{ github.event.pull_request.mergeable_state }}" + echo "Current mergeable state: $PR_STATE" + if [[ "$PR_STATE" == "clean" || "$PR_STATE" == "unstable" || "$PR_STATE" == "has_hooks" ]]; then + echo "mergeable=true" >> $GITHUB_OUTPUT + else + echo "mergeable=false" >> $GITHUB_OUTPUT + fi + - name: Auto-merge Dependabot PR - if: steps.wait-for-ci.outputs.conclusion == 'success' + if: steps.check-mergeable.outputs.mergeable == 'true' || steps.recheck-mergeable.outputs.mergeable == 'true' run: | set -e - echo "CI checks passed. Auto-merging Dependabot PR #${{ github.event.pull_request.number }}" + PR_NUM=${{ github.event.pull_request.number }} + echo "Processing Dependabot PR #$PR_NUM" + + # Get current status + gh pr view $PR_NUM --json state,mergeable,mergeableState # Approve the PR first - if ! gh pr review ${{ github.event.pull_request.number }} --approve --body "✅ CI checks passed! Auto-merging this Dependabot PR."; then - echo "Failed to approve PR. Exiting." - exit 1 - fi + gh pr review $PR_NUM --approve --body "✅ CI checks passed! Auto-merging this Dependabot PR." || echo "PR already approved or review failed" # Enable auto-merge - if ! gh pr merge --auto --merge ${{ github.event.pull_request.number }}; then - echo "Failed to enable auto-merge. Checking if already enabled..." - # Check if auto-merge is already enabled - if gh pr view ${{ github.event.pull_request.number }} --json autoMergeRequest --jq '.autoMergeRequest' | grep -q "null"; then - echo "Auto-merge failed and is not enabled. Manual intervention required." - exit 1 - else - echo "Auto-merge is already enabled." - fi - fi + gh pr merge --auto --merge $PR_NUM || echo "Auto-merge failed or already enabled" - echo "Auto-merge successfully enabled for PR #${{ github.event.pull_request.number }}" + echo "Auto-merge processing complete for PR #$PR_NUM" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 9d6d2d3570cc13cd06ffc5fe62fb4bf2e5685e00 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 03:25:30 +0000 Subject: [PATCH 3/6] Apply security review suggestions: fork check and API re-check - Remove pull_request_target trigger (not needed with fork check) - Add fork detection step to prevent untrusted fork abuse - Use gh pr view API to get fresh mergeable state instead of event payload - Add security guards to all steps Co-authored-by: openhands --- .github/workflows/dependabot-auto-merge.yml | 33 ++++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 9bdb8976d..a5c5dbdd1 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -3,8 +3,6 @@ name: Dependabot Auto-Merge on: pull_request: types: [opened, synchronize, reopened] - pull_request_target: - types: [opened, synchronize, reopened] permissions: contents: write @@ -14,11 +12,28 @@ permissions: jobs: auto-merge: runs-on: ubuntu-latest + # Only run for Dependabot PRs that are not drafts if: github.actor == 'dependabot[bot]' && !github.event.pull_request.draft steps: + - name: Check if PR is from a fork + id: check-fork + run: | + echo "PR head repo: ${{ github.event.pull_request.head.repo.full_name }}" + echo "Base repo: ${{ github.repository }}" + + # Check if PR is from the same repository (not a fork) + if [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then + echo "PR is from the same repository - safe to proceed" + echo "is_fork=false" >> $GITHUB_OUTPUT + else + echo "PR is from a fork - skipping for security" + echo "is_fork=true" >> $GITHUB_OUTPUT + fi + - name: Check if PR is mergeable id: check-mergeable + if: steps.check-fork.outputs.is_fork == 'false' run: | echo "Checking PR #${{ github.event.pull_request.number }} mergeable state..." echo "Current mergeable state: ${{ github.event.pull_request.mergeable_state }}" @@ -35,7 +50,7 @@ jobs: fi - name: Wait for required checks if not mergeable - if: steps.check-mergeable.outputs.mergeable == 'false' + if: steps.check-fork.outputs.is_fork == 'false' && steps.check-mergeable.outputs.mergeable == 'false' uses: fountainhead/action-wait-for-check@v1.2.0 id: wait-for-ci with: @@ -46,12 +61,14 @@ jobs: intervalSeconds: 10 - name: Re-check mergeable state after waiting - if: steps.check-mergeable.outputs.mergeable == 'false' + if: steps.check-fork.outputs.is_fork == 'false' && steps.check-mergeable.outputs.mergeable == 'false' id: recheck-mergeable run: | - echo "Re-checking mergeable state after CI..." - PR_STATE="${{ github.event.pull_request.mergeable_state }}" - echo "Current mergeable state: $PR_STATE" + echo "Re-checking mergeable state via API..." + # Use gh pr view to get fresh mergeable state + PR_STATE=$(gh pr view ${{ github.event.pull_request.number }} --json mergeableState -q '.mergeableState') + echo "Current mergeable state from API: $PR_STATE" + if [[ "$PR_STATE" == "clean" || "$PR_STATE" == "unstable" || "$PR_STATE" == "has_hooks" ]]; then echo "mergeable=true" >> $GITHUB_OUTPUT else @@ -59,7 +76,7 @@ jobs: fi - name: Auto-merge Dependabot PR - if: steps.check-mergeable.outputs.mergeable == 'true' || steps.recheck-mergeable.outputs.mergeable == 'true' + if: steps.check-fork.outputs.is_fork == 'false' && (steps.check-mergeable.outputs.mergeable == 'true' || steps.recheck-mergeable.outputs.mergeable == 'true') run: | set -e PR_NUM=${{ github.event.pull_request.number }} From 4e6f610fc6ec6e9af259944ffcaafdb50045d436 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 03:28:36 +0000 Subject: [PATCH 4/6] Apply remaining review fixes Workflow fixes: - Use github.event.pull_request.user.login for PR author check - Use mergeStateStatus field instead of mergeableState - Check reviewDecision before approving - Verify autoMergeRequest after enabling merge - Use live API calls for all mergeable state checks Gmail integration fixes: - Add retry with exponential backoff for credential refresh - Add specific exception handling with informative logging - Only store credentials after successful refresh Co-authored-by: openhands --- .github/workflows/dependabot-auto-merge.yml | 42 ++++++++++++++------- src/backend/python_nlp/gmail_integration.py | 30 +++++++++++++-- 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index a5c5dbdd1..3f29d431c 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -13,7 +13,8 @@ jobs: auto-merge: runs-on: ubuntu-latest # Only run for Dependabot PRs that are not drafts - if: github.actor == 'dependabot[bot]' && !github.event.pull_request.draft + # Check PR author identity to handle reopened PRs + if: github.event.pull_request.user.login == 'dependabot[bot]' && !github.event.pull_request.draft steps: - name: Check if PR is from a fork @@ -31,16 +32,18 @@ jobs: echo "is_fork=true" >> $GITHUB_OUTPUT fi - - name: Check if PR is mergeable + - name: Check if PR is mergeable via API id: check-mergeable if: steps.check-fork.outputs.is_fork == 'false' run: | - echo "Checking PR #${{ github.event.pull_request.number }} mergeable state..." - echo "Current mergeable state: ${{ github.event.pull_request.mergeable_state }}" - echo "Draft: ${{ github.event.pull_request.draft }}" + PR_NUM=${{ github.event.pull_request.number }} + echo "Checking PR #$PR_NUM mergeable state via API..." + + # Query API for fresh mergeable state + PR_STATE=$(gh pr view $PR_NUM --json mergeStateStatus -q '.mergeStateStatus') + echo "Current mergeable state from API: $PR_STATE" - # Get the PR mergeable state - PR_STATE="${{ github.event.pull_request.mergeable_state }}" + # Check if PR is in a mergeable state if [[ "$PR_STATE" == "clean" || "$PR_STATE" == "unstable" || "$PR_STATE" == "has_hooks" ]]; then echo "PR is in mergeable state: $PR_STATE" echo "mergeable=true" >> $GITHUB_OUTPUT @@ -65,8 +68,9 @@ jobs: id: recheck-mergeable run: | echo "Re-checking mergeable state via API..." + PR_NUM=${{ github.event.pull_request.number }} # Use gh pr view to get fresh mergeable state - PR_STATE=$(gh pr view ${{ github.event.pull_request.number }} --json mergeableState -q '.mergeableState') + PR_STATE=$(gh pr view $PR_NUM --json mergeStateStatus -q '.mergeStateStatus') echo "Current mergeable state from API: $PR_STATE" if [[ "$PR_STATE" == "clean" || "$PR_STATE" == "unstable" || "$PR_STATE" == "has_hooks" ]]; then @@ -82,14 +86,26 @@ jobs: PR_NUM=${{ github.event.pull_request.number }} echo "Processing Dependabot PR #$PR_NUM" - # Get current status - gh pr view $PR_NUM --json state,mergeable,mergeableState + # Check current review status + REVIEW_DECISION=$(gh pr view $PR_NUM --json reviewDecision -q '.reviewDecision') + echo "Current review decision: $REVIEW_DECISION" - # Approve the PR first - gh pr review $PR_NUM --approve --body "✅ CI checks passed! Auto-merging this Dependabot PR." || echo "PR already approved or review failed" + # Approve the PR only if not already approved + if [[ "$REVIEW_DECISION" != "APPROVED" ]]; then + gh pr review $PR_NUM --approve --body "✅ CI checks passed! Auto-merging this Dependabot PR." + else + echo "PR already approved" + fi # Enable auto-merge - gh pr merge --auto --merge $PR_NUM || echo "Auto-merge failed or already enabled" + gh pr merge --auto --merge $PR_NUM + + # Verify auto-merge was enabled + AUTO_MERGE=$(gh pr view $PR_NUM --json autoMergeRequest -q '.autoMergeRequest') + if [[ "$AUTO_MERGE" == "null" || -z "$AUTO_MERGE" ]]; then + echo "ERROR: Auto-merge was not enabled properly" + exit 1 + fi echo "Auto-merge processing complete for PR #$PR_NUM" env: diff --git a/src/backend/python_nlp/gmail_integration.py b/src/backend/python_nlp/gmail_integration.py index 6884eb1b2..2029b0014 100644 --- a/src/backend/python_nlp/gmail_integration.py +++ b/src/backend/python_nlp/gmail_integration.py @@ -23,6 +23,7 @@ from typing import Any, Dict, List, Optional from dotenv import load_dotenv +from google.auth.exceptions import RefreshError from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow @@ -279,11 +280,34 @@ def _load_credentials(self): if creds and (creds.valid or (creds.expired and creds.refresh_token)): try: if creds.expired and creds.refresh_token: - creds.refresh(Request()) - self._store_credentials(creds) + # Retry refresh with exponential backoff for transient failures + max_retries = 3 + base_delay = 1 + for attempt in range(max_retries): + try: + creds.refresh(Request()) + self._store_credentials(creds) + break + except (RefreshError, HttpError) as e: + if attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + self.logger.warning( + f"Credential refresh attempt {attempt + 1} failed: {e}. " + f"Retrying in {delay}s..." + ) + time.sleep(delay) + else: + self.logger.error( + f"Credential refresh failed after {max_retries} attempts: {e}" + ) + raise self.gmail_service = build("gmail", "v1", credentials=creds) + except RefreshError as e: + self.logger.error(f"Error refreshing credentials: {e}") + except HttpError as e: + self.logger.error(f"Error building Gmail service: {e}") except Exception as e: - self.logger.error(f"Error with credentials: {e}") + self.logger.error(f"Unexpected error with credentials: {e}") def _store_credentials(self, creds): """Stores the API credentials to a token file.""" From 4cf416271fbdf9278f823e4c10541bab20642178 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 03:33:54 +0000 Subject: [PATCH 5/6] Apply final review fixes: GH_TOKEN env and case normalization - Add GH_TOKEN environment variable to check-mergeable and recheck-mergeable steps - Add case normalization (tr '[:upper:]' '[:lower:]') for mergeStateStatus comparison - This ensures gh CLI can authenticate properly and handles case variations Co-authored-by: openhands --- .github/workflows/dependabot-auto-merge.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 3f29d431c..18abadc2e 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -35,12 +35,14 @@ jobs: - name: Check if PR is mergeable via API id: check-mergeable if: steps.check-fork.outputs.is_fork == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | PR_NUM=${{ github.event.pull_request.number }} echo "Checking PR #$PR_NUM mergeable state via API..." - # Query API for fresh mergeable state - PR_STATE=$(gh pr view $PR_NUM --json mergeStateStatus -q '.mergeStateStatus') + # Query API for fresh mergeable state (normalize to lowercase for comparison) + PR_STATE=$(gh pr view "$PR_NUM" --json mergeStateStatus -q '.mergeStateStatus' | tr '[:upper:]' '[:lower:]') echo "Current mergeable state from API: $PR_STATE" # Check if PR is in a mergeable state @@ -66,11 +68,13 @@ jobs: - name: Re-check mergeable state after waiting if: steps.check-fork.outputs.is_fork == 'false' && steps.check-mergeable.outputs.mergeable == 'false' id: recheck-mergeable + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Re-checking mergeable state via API..." PR_NUM=${{ github.event.pull_request.number }} - # Use gh pr view to get fresh mergeable state - PR_STATE=$(gh pr view $PR_NUM --json mergeStateStatus -q '.mergeStateStatus') + # Use gh pr view to get fresh mergeable state (normalize to lowercase) + PR_STATE=$(gh pr view "$PR_NUM" --json mergeStateStatus -q '.mergeStateStatus' | tr '[:upper:]' '[:lower:]') echo "Current mergeable state from API: $PR_STATE" if [[ "$PR_STATE" == "clean" || "$PR_STATE" == "unstable" || "$PR_STATE" == "has_hooks" ]]; then From 487f0497e6638bc15a4b8b01bda43c861e8f79fe Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 25 Mar 2026 03:38:53 +0000 Subject: [PATCH 6/6] Fix syntax error in setup/launch.py The file had unresolved merge conflict markers causing a SyntaxError. Restored clean version from parent commit. Co-authored-by: openhands --- setup/launch.py | 337 ------------------------------------------------ 1 file changed, 337 deletions(-) diff --git a/setup/launch.py b/setup/launch.py index 09f724015..97849ca7d 100644 --- a/setup/launch.py +++ b/setup/launch.py @@ -2,20 +2,6 @@ """ EmailIntelligence Unified Launcher -<<<<<<< HEAD -This script provides a single, unified way to set up, manage, and run all -components of the EmailIntelligence application, including the Python backend, -Gradio UI, and Node.js services. It uses 'uv' for Python dependency management -based on pyproject.toml. - -Usage: - python launch.py [arguments] -""" - -import argparse -import atexit -import logging -======= This script provides a unified entry point for setting up and running the EmailIntelligence application. It supports both legacy arguments for backward compatibility and modern command-based interface. @@ -47,31 +33,17 @@ # Standard library imports import argparse import atexit ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 import os import platform import shutil import subprocess import sys -<<<<<<< HEAD -import time -import threading -======= import threading import time ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 import venv from pathlib import Path from typing import List -<<<<<<< HEAD -# Add project root to sys.path for imports -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - -# Command pattern not available in this branch - -from deployment.test_stages import test_stages -======= # Import project configuration from setup.project_config import get_project_config @@ -84,7 +56,6 @@ get_command_factory = None get_container = None initialize_all_services = None ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 try: from dotenv import load_dotenv @@ -95,10 +66,7 @@ load_dotenv = None # Will be loaded later if needed # Configure logging -<<<<<<< HEAD -======= import logging ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) @@ -106,63 +74,10 @@ # --- Global state --- -<<<<<<< HEAD -def find_project_root() -> Path: - """Find the project root directory by looking for key files.""" - current = Path(__file__).resolve().parent - if (current / "pyproject.toml").exists() and current.name != "setup": - return current - for parent in current.parents: - if (parent / "pyproject.toml").exists(): - return parent - return current - - -ROOT_DIR = find_project_root() - - -class ProcessManager: - """Manages child processes for the application.""" - - def __init__(self): - self.processes = [] - self.lock = threading.Lock() # Add lock for thread safety - - def add_process(self, process): - with self.lock: - self.processes.append(process) - - def cleanup(self): - logger.info("Performing explicit resource cleanup...") - # Create a copy of the list to avoid modifying while iterating - with self.lock: - processes_copy = self.processes[:] - - for p in processes_copy: - if p.poll() is None: - logger.info(f"Terminating process {p.pid}...") - p.terminate() - try: - p.wait(timeout=10) - except subprocess.TimeoutExpired: - logger.warning(f"Process {p.pid} did not terminate gracefully, killing.") - p.kill() - logger.info("Resource cleanup completed.") - - def shutdown(self): - logger.info("Received shutdown signal, cleaning up processes...") - self.cleanup() - sys.exit(0) - - -process_manager = ProcessManager() -atexit.register(process_manager.cleanup) -======= ROOT_DIR = get_project_config().root_dir # Import process manager from utils from setup.utils import process_manager ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 # --- Constants --- PYTHON_MIN_VERSION = (3, 12) @@ -171,18 +86,6 @@ def shutdown(self): CONDA_ENV_NAME = os.getenv("CONDA_ENV_NAME", "base") -<<<<<<< HEAD -# --- WSL Support --- -def is_wsl(): - """Check if running in WSL environment""" - try: - with open("/proc/version", "r") as f: - content = f.read().lower() - return "microsoft" in content or "wsl" in content - except Exception: - return False -======= ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 def setup_wsl_environment(): @@ -227,21 +130,16 @@ def check_python_version(): """Check if the current Python version is compatible.""" current_version = sys.version_info[:2] if not (PYTHON_MIN_VERSION <= current_version <= PYTHON_MAX_VERSION): -<<<<<<< HEAD - logger.error(f"Python version {platform.python_version()} is not compatible.") -======= logger.error( f"Python version {platform.python_version()} is not compatible. " f"Please use Python version {PYTHON_MIN_VERSION[0]}.{PYTHON_MIN_VERSION[1]} " f"to {PYTHON_MAX_VERSION[0]}.{PYTHON_MAX_VERSION[1]}." ) ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 sys.exit(1) logger.info(f"Python version {platform.python_version()} is compatible.") # --- Environment Validation --- -<<<<<<< HEAD def check_for_merge_conflicts() -> bool: """Check for unresolved merge conflict markers in critical files.""" conflict_markers = ["<<<<<<< ", "======= ", ">>>>>>> "] @@ -290,9 +188,6 @@ def check_for_merge_conflicts() -> bool: logger.info("No unresolved merge conflicts detected in critical files.") return True -======= -# check_for_merge_conflicts is imported from setup.validation ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 def check_required_components() -> bool: @@ -352,64 +247,6 @@ def validate_environment() -> bool: return True -<<<<<<< HEAD -# --- Input Validation --- -def validate_port(port: int) -> int: - """Validate port number is within valid range.""" - if not 1 <= port <= 65535: - raise ValueError(f"Invalid port: {port}. Port must be between 1 and 65535.") - return port - - -def validate_host(host: str) -> str: - """Validate host name/address format.""" - import re - - if not re.match(r"^[a-zA-Z0-9.-]+$", host): - raise ValueError(f"Invalid host: {host}") - return host - - -# --- Conda Environment Support --- -def is_conda_available() -> bool: - """Check if conda is available on the system.""" - try: - subprocess.run(["conda", "--version"], capture_output=True, text=True, check=True) - return True - except (subprocess.CalledProcessError, FileNotFoundError): - return False - - -def get_conda_env_info(): - """Get information about the current conda environment.""" - env_vars = os.environ - conda_default_env = env_vars.get("CONDA_DEFAULT_ENV") - conda_prefix = env_vars.get("CONDA_PREFIX") - - return { - "is_active": conda_default_env is not None, - "env_name": conda_default_env, - "prefix": conda_prefix, - "python_exe": env_vars.get("CONDA_PREFIX", "") + "/python" if conda_prefix else None, - } - - -def activate_conda_env(env_name: str = None) -> bool: - """Activate a conda environment.""" - env_name = env_name or CONDA_ENV_NAME - - # Validate environment name to prevent command injection - import re - - if not re.match(r"^[a-zA-Z0-9_-]+$", env_name): - logger.error( - f"Invalid conda environment name: {env_name}. Only alphanumeric characters, hyphens, and underscores are allowed." - ) - return False - - if not is_conda_available(): - logger.debug("Conda not available, skipping environment activation.") -======= def check_critical_files() -> bool: """Check for critical files that must exist in the orchestration-tools branch.""" # Critical files that are essential for orchestration @@ -553,60 +390,10 @@ def validate_orchestration_environment() -> bool: logger.warning(f"Conda not available, cannot activate environment '{env_name}'. Please install Conda.") else: logger.debug("Conda not available, skipping environment activation.") ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 return False conda_info = get_conda_env_info() if conda_info["is_active"]: -<<<<<<< HEAD - logger.info(f"Already in conda environment: {conda_info['env_name']}") - return True - - # Check if the requested environment exists - try: - result = subprocess.run( - ["conda", "info", "--envs"], capture_output=True, text=True, check=True - ) - envs = result.stdout.strip().split("\n") - env_names = [line.split()[0] for line in envs if line.strip() and not line.startswith("#")] - if env_name not in env_names: - logger.warning(f"Conda environment '{env_name}' not found.") - return False - except subprocess.CalledProcessError as e: - logger.warning(f"Failed to list conda environments: {e}") - return False - - logger.info(f"Will use conda environment: {env_name}") - # We don't actually activate the environment here since subprocess.run - # cannot persist environment changes. Instead, we rely on get_python_executable() - # to find the correct Python executable for the environment. - return True - - -def get_python_executable() -> str: - """Get the appropriate Python executable (conda > venv > system).""" - # Check if we're in a conda environment - conda_info = get_conda_env_info() - if conda_info["is_active"] and conda_info["python_exe"]: - python_exe = conda_info["python_exe"] - if platform.system() == "Windows": - python_exe += ".exe" - if os.path.exists(python_exe): - logger.info(f"Using conda Python: {python_exe}") - return python_exe - - # Check for venv - venv_path = ROOT_DIR / VENV_DIR - if venv_path.exists(): - python_exe = get_venv_executable(venv_path, "python") - if python_exe.exists(): - logger.info(f"Using venv Python: {python_exe}") - return str(python_exe) - - # Fall back to system Python - logger.info("Using system Python") - return sys.executable -======= if conda_info["env_name"] == env_name: logger.info(f"Already in specified conda environment: {conda_info['env_name']}") return True @@ -624,7 +411,6 @@ def get_python_executable() -> str: ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 # --- Helper Functions --- @@ -674,11 +460,8 @@ def setup_dependencies(venv_path: Path, use_poetry: bool = False): python_exe = get_python_executable() if use_poetry: -<<<<<<< HEAD -======= # Ensure pip is up-to-date before installing other packages run_command([python_exe, "-m", "pip", "install", "--upgrade", "pip"], "Upgrading pip") ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 # For poetry, we need to install it first if not available try: subprocess.run([python_exe, "-c", "import poetry"], check=True, capture_output=True) @@ -691,40 +474,20 @@ def setup_dependencies(venv_path: Path, use_poetry: bool = False): cwd=ROOT_DIR, ) else: -<<<<<<< HEAD -======= # Ensure pip is up-to-date before installing other packages run_command([python_exe, "-m", "pip", "install", "--upgrade", "pip"], "Upgrading pip") ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 # For uv, install if not available try: subprocess.run([python_exe, "-c", "import uv"], check=True, capture_output=True) except subprocess.CalledProcessError: run_command([python_exe, "-m", "pip", "install", "uv"], "Installing uv") -<<<<<<< HEAD - # Install CPU-only PyTorch first to prevent CUDA package installation - logger.info("Installing CPU-only PyTorch packages...") - run_command( - [python_exe, "-m", "pip", "install", "torch", "torchvision", "torchaudio", "--index-url", "https://download.pytorch.org/whl/cpu"], - "Installing CPU-only PyTorch packages", - cwd=ROOT_DIR, - ) - - run_command( - [python_exe, "-m", "uv", "pip", "install", "-e", "."], - "Installing remaining dependencies with uv", - cwd=ROOT_DIR, - ) - -======= run_command( [python_exe, "-m", "uv", "pip", "install", "-e", ".[dev]", "--exclude", "notmuch"], "Installing dependencies with uv (excluding notmuch)", cwd=ROOT_DIR, ) ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 # Install notmuch with version matching system install_notmuch_matching_system() @@ -739,23 +502,10 @@ def install_notmuch_matching_system(): version = version_line.split()[1] major_minor = ".".join(version.split(".")[:2]) # e.g., 0.38 python_exe = get_python_executable() -<<<<<<< HEAD - # Try to install matching version, fallback to latest if not available - if not run_command( - [python_exe, "-m", "pip", "install", f"notmuch=={major_minor}"], - f"Installing notmuch {major_minor} to match system", - ): - logger.warning(f"Could not install notmuch {major_minor}, trying latest version") - run_command( - [python_exe, "-m", "pip", "install", "notmuch"], - "Installing latest notmuch version", - ) -======= run_command( [python_exe, "-m", "pip", "install", f"notmuch=={major_minor}"], f"Installing notmuch {major_minor} to match system", ) ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 except (subprocess.CalledProcessError, FileNotFoundError): logger.warning("notmuch not found on system, skipping version-specific install") @@ -1089,12 +839,6 @@ def print_system_info(): def main(): -<<<<<<< HEAD - # Services initialization not available in this branch - - # Parse command line arguments - parser = argparse.ArgumentParser(description="EmailIntelligence Unified Launcher") -======= # Initialize services if command pattern is available if COMMAND_PATTERN_AVAILABLE and initialize_all_services and get_container: initialize_all_services(get_container()) @@ -1134,7 +878,6 @@ def main(): check_parser.add_argument("--env", action="store_true", help="Check orchestration environment") # Legacy argument parsing for backward compatibility ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 parser.add_argument("--setup", action="store_true", help="Set up the environment (legacy)") parser.add_argument( "--stage", choices=["dev", "test"], default="dev", help="Application mode (legacy)" @@ -1199,37 +942,6 @@ def main(): ) parser.add_argument("--debug", action="store_true", help="Enable debug mode.") -<<<<<<< HEAD - # Testing Options - parser.add_argument( - "--coverage", action="store_true", help="Generate coverage report when running tests." - ) - parser.add_argument("--unit", action="store_true", help="Run unit tests.") - parser.add_argument("--integration", action="store_true", help="Run integration tests.") - parser.add_argument("--e2e", action="store_true", help="Run end-to-end tests.") - parser.add_argument("--performance", action="store_true", help="Run performance tests.") - parser.add_argument("--security", action="store_true", help="Run security tests.") - - # Extensions and Models - parser.add_argument("--skip-extensions", action="store_true", help="Skip loading extensions.") - parser.add_argument("--skip-models", action="store_true", help="Skip downloading models.") - - # Advanced Options - parser.add_argument( - "--system-info", action="store_true", help="Print system information then exit." - ) - parser.add_argument("--share", action="store_true", help="Create a public URL.") - parser.add_argument("--listen", action="store_true", help="Make the server listen on network.") - parser.add_argument( - "--ngrok", type=str, help="Use ngrok to create a tunnel, specify ngrok region." - ) - parser.add_argument("--env-file", type=str, help="Specify environment file to load.") - - args = parser.parse_args() - - # Handle legacy arguments - return _handle_legacy_args(args) -======= args = parser.parse_args() # Handle command pattern vs legacy arguments @@ -1239,7 +951,6 @@ def main(): else: # Handle legacy arguments return _handle_legacy_args(args) ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 def _add_common_args(parser): @@ -1256,10 +967,6 @@ def _add_common_args(parser): def _add_legacy_args(parser): """Add legacy arguments for backward compatibility.""" # Environment Setup -<<<<<<< HEAD - parser.add_argument("--setup", action="store_true", help="Run environment setup.") -======= ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 parser.add_argument( "--force-recreate-venv", action="store_true", help="Force recreation of the venv." ) @@ -1326,10 +1033,7 @@ def _add_legacy_args(parser): parser.add_argument( "--system-info", action="store_true", help="Print system information then exit." ) -<<<<<<< HEAD -======= parser.add_argument("--env-file", type=str, help="Specify environment file to load.") ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 parser.add_argument("--share", action="store_true", help="Create a public URL.") parser.add_argument("--listen", action="store_true", help="Make the server listen on network.") parser.add_argument( @@ -1337,21 +1041,6 @@ def _add_legacy_args(parser): ) -<<<<<<< HEAD -def _execute_command(command_name: str, args) -> int: - """Execute a command using the command pattern.""" - factory = get_command_factory() - command = factory.create_command(command_name, args) - - if command is None: - logger.error(f"Unknown command: {command_name}") - return 1 - - try: - return command.execute() - finally: - command.cleanup() -======= def main(): # Check for common setup issues before proceeding _check_setup_warnings() @@ -1459,27 +1148,19 @@ def _execute_check_command(args) -> int: else: logger.error("Orchestration checks failed!") return 1 ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 def _handle_legacy_args(args) -> int: """Handle legacy argument parsing for backward compatibility.""" # Setup WSL environment if applicable (early setup) -<<<<<<< HEAD -======= from setup.environment import setup_wsl_environment, check_wsl_requirements ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 setup_wsl_environment() check_wsl_requirements() if not args.skip_python_version_check: check_python_version() -<<<<<<< HEAD - logging.getLogger().setLevel(args.loglevel) -======= logging.getLogger().setLevel(getattr(args, 'loglevel', 'INFO')) ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 if DOTENV_AVAILABLE: # Load user customizations from launch-user.env if it exists @@ -1503,14 +1184,11 @@ def _handle_legacy_args(args) -> int: args.use_conda = True # Set flag when conda env is specified # args.use_conda remains as set by command line argument -<<<<<<< HEAD -======= # Check for system info first (doesn't need validation) if args.system_info: print_system_info() return 0 ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 # Validate environment if not skipping preparation if not args.skip_prepare and not validate_environment(): return 1 @@ -1531,10 +1209,7 @@ def _handle_legacy_args(args) -> int: return 0 # Handle Conda environment if requested -<<<<<<< HEAD -======= from setup.environment import is_conda_available, get_conda_env_info, activate_conda_env ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 if args.use_conda: if not is_conda_available(): logger.error("Conda is not available. Please install Conda or use venv.") @@ -1549,19 +1224,13 @@ def _handle_legacy_args(args) -> int: prepare_environment(args) if args.system_info: -<<<<<<< HEAD -======= print("DEBUG: system_info flag detected") ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 print_system_info() return 0 # Handle test stage if hasattr(args, "stage") and args.stage == "test": -<<<<<<< HEAD -======= from setup.test_stages import handle_test_stage ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 handle_test_stage(args) return 0 @@ -1571,10 +1240,7 @@ def _handle_legacy_args(args) -> int: or getattr(args, "integration", False) or getattr(args, "coverage", False) ): -<<<<<<< HEAD -======= from setup.test_stages import handle_test_stage ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 handle_test_stage(args) return 0 @@ -1593,8 +1259,6 @@ def _handle_legacy_args(args) -> int: return 0 -<<<<<<< HEAD -======= def _check_setup_warnings(): """Check for common setup issues and warn users.""" import sys @@ -1620,6 +1284,5 @@ def _check_setup_warnings(): logger.info("💡 Virtual environment exists. Activate it with: source venv/bin/activate") ->>>>>>> a7da61cf1f697de3c8c81f536bf579d36d88e613 if __name__ == "__main__": main()