diff --git a/.github/workflows/test_lemonade_client.yml b/.github/workflows/test_lemonade_client.yml index 1c9e18d..6cf70e1 100644 --- a/.github/workflows/test_lemonade_client.yml +++ b/.github/workflows/test_lemonade_client.yml @@ -46,6 +46,38 @@ jobs: run: | python test/lemonade_client_unit.py + - name: Download and install lemonade-server (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + Write-Host "Downloading lemonade-server MSI installer..." + Invoke-WebRequest -Uri "https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi" -OutFile "lemonade-server-minimal.msi" + Write-Host "Installing lemonade-server to C:\lemonade-server..." + Start-Process msiexec.exe -ArgumentList "/i", "lemonade-server-minimal.msi", "/qn", "/norestart", "INSTALLDIR=C:\lemonade-server" -Wait + Write-Host "Adding lemonade-server to PATH..." + Add-Content -Path $env:GITHUB_PATH -Value "C:\lemonade-server\bin" + Write-Host "lemonade-server installation complete" + + - name: Download and install lemonade-server (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + echo "Getting latest release info..." + # Get the latest release tag and find the .deb asset + LATEST_RELEASE=$(curl -s https://api.github.com/repos/lemonade-sdk/lemonade/releases/latest) + DEB_URL=$(echo "$LATEST_RELEASE" | grep -o 'https://github.com/lemonade-sdk/lemonade/releases/download/[^"]*\.deb' | head -1) + + if [ -z "$DEB_URL" ]; then + echo "Error: Could not find .deb package in latest release" + echo "Skipping lemonade-server installation for Linux tests" + else + echo "Downloading lemonade-server .deb package from: $DEB_URL" + curl -L -o lemonade-server.deb "$DEB_URL" + echo "Installing lemonade-server..." + sudo dpkg -i lemonade-server.deb || sudo apt-get install -f -y + echo "lemonade-server installation complete" + fi + - name: Run integration tests run: | python test/lemonade_client_integration.py diff --git a/docs/lemonade_client_api.md b/docs/lemonade_client_api.md index 5f4582d..fc72a80 100644 --- a/docs/lemonade_client_api.md +++ b/docs/lemonade_client_api.md @@ -11,13 +11,10 @@ Here's the typical sequence of API calls for setting up a lemonade-server-based from lemonade_client import LemonadeClient # Create client with minimum version requirement -client = LemonadeClient(minimum_version="8.1.9") +client = LemonadeClient(minimum_version="9.0.3") # Check deployment environment is_pyinstaller = client.is_pyinstaller_environment() - -# Check if SDK is available (for development environments) -sdk_available = await client.check_lemonade_sdk_available() ``` ### 2. Installation Status Check @@ -83,11 +80,11 @@ Once the above steps complete successfully, your application can make inference ## Constructor -#### `LemonadeClient(minimum_version: str = "8.1.0", logger=None)` +#### `LemonadeClient(minimum_version: str = "9.0.3", logger=None)` Initialize a new LemonadeClient instance. **Parameters:** -- `minimum_version` (str, optional): Minimum required version of lemonade-server. Defaults to "8.1.0". The client will check server compatibility against this version. +- `minimum_version` (str, optional): Minimum required version of lemonade-server. Defaults to "9.0.3". The client will check server compatibility against this version. - `logger` (logging.Logger, optional): Logger instance to use for logging. If None, creates a default logger named "lemonade_client". **When to use:** Create a client instance at the start of your application. Specify the minimum version your application requires to ensure compatibility. @@ -96,15 +93,15 @@ Initialize a new LemonadeClient instance. ```python import logging -# Use default minimum version (8.1.0) and default logger +# Use default minimum version (9.0.3) and default logger client = LemonadeClient() # Specify custom minimum version -client = LemonadeClient(minimum_version="8.1.9") +client = LemonadeClient(minimum_version="9.0.5") # Use custom logger custom_logger = logging.getLogger("my_app") -client = LemonadeClient(minimum_version="8.1.9", logger=custom_logger) +client = LemonadeClient(minimum_version="9.0.5", logger=custom_logger) # Version checking will use your specified minimum version_info = await client.check_lemonade_server_version() @@ -121,7 +118,7 @@ print(f"Compatible: {version_info['compatible']}") # True if server >= mini #### `is_pyinstaller_environment()` Check if the application is running in a PyInstaller bundle environment. -**When to use:** Determine installation method preferences or adjust behavior based on deployment type. PyInstaller environments typically prefer installer-based server installation over pip. +**When to use:** Determine installation method preferences or adjust behavior based on deployment type. **Returns:** `bool` - True if running in PyInstaller bundle, False otherwise @@ -129,10 +126,8 @@ Check if the application is running in a PyInstaller bundle environment. ```python if client.is_pyinstaller_environment(): print("Running as packaged executable") - # Prefer installer-based installation else: - print("Running in development environment") - # Can use pip installation + print("Running in development environment") ``` --- @@ -185,7 +180,7 @@ client.reset_server_state() #### `execute_lemonade_server_command(args, timeout=10, use_popen=False, stdout_file=None, stderr_file=None)` Execute lemonade-server commands using the best available method for the system. -**When to use:** As the primary interface for running any lemonade-server command. The method automatically tries different installation methods (pip, installer, dev) and caches the successful command for future use. Essential for cross-platform compatibility. +**When to use:** As the primary interface for running any lemonade-server command. The method automatically discovers the lemonade-server installation and caches the successful command for future use. Essential for cross-platform compatibility. **Parameters:** - `args: List[str]` - Command arguments to pass to lemonade-server (e.g., `["--version"]`, `["serve"]`) @@ -214,23 +209,6 @@ process = await client.execute_lemonade_server_command( ### Installation and Setup -#### `check_lemonade_sdk_available()` -Check if the lemonade-sdk Python package is installed and importable. - -**When to use:** Determine if pip-based installation is available before attempting SDK-based operations. Helpful for showing installation options to users or choosing between different installation methods. - -**Returns:** `bool` - True if lemonade-sdk package can be imported, False otherwise - -**Example:** -```python -if await client.check_lemonade_sdk_available(): - print("Can use lemonade-server-dev command") -else: - print("Need to install via pip or use installer") -``` - ---- - #### `check_lemonade_server_version()` Check lemonade-server installation status and version compatibility. @@ -253,37 +231,20 @@ else: --- -#### `install_lemonade_sdk_package()` -Install the lemonade-sdk Python package using pip. - -**When to use:** Install lemonade-server via pip when in development environments or when the SDK approach is preferred. Provides access to lemonade-server-dev command after successful installation. - -**Returns:** `dict` with keys: -- `success: bool` - Whether installation succeeded -- `message: str` - Success message or error details - -**Example:** -```python -result = await client.install_lemonade_sdk_package() -if result["success"]: - print("SDK installed successfully") - client.refresh_environment() -else: - print(f"Installation failed: {result['message']}") -``` - ---- - #### `download_and_install_lemonade_server()` -Download and install lemonade-server using the best method for the environment. +Download and install lemonade-server using the platform-specific installer. + +**When to use:** As the primary installation method. On Windows, downloads and launches the MSI installer. On Linux, directs users to the installation options page for manual .deb package installation. -**When to use:** As the primary installation method. Automatically chooses between pip installation (development environments) or executable installer (PyInstaller bundles). Handles the complete installation process including download and setup. +**Platform Behavior:** +- **Windows**: Downloads `lemonade-server-minimal.msi` and launches it with `msiexec` +- **Linux**: Returns instructions to visit `https://lemonade-server.ai/install_options.html` for .deb package download **Returns:** `dict` with keys: -- `success: bool` - Whether installation succeeded +- `success: bool` - Whether installation succeeded or launched successfully - `message: str` - Status message or error details -- `interactive: bool` (optional) - Whether installer requires user interaction -- `github_link: str` (optional) - Link for manual installation if automated fails +- `interactive: bool` (optional) - Whether installer requires user interaction (Windows only) +- `install_link: str` (optional) - Link for manual installation if automated fails **Example:** ```python @@ -296,8 +257,8 @@ if result["success"]: client.reset_server_state() else: print(f"Installation failed: {result['message']}") - if "github_link" in result: - print(f"Manual installation: {result['github_link']}") + if "install_link" in result: + print(f"Manual installation: {result['install_link']}") ``` --- diff --git a/src/infinity_arcade/main.py b/src/infinity_arcade/main.py index 8fb3215..b3fd9d3 100644 --- a/src/infinity_arcade/main.py +++ b/src/infinity_arcade/main.py @@ -32,7 +32,7 @@ from infinity_arcade.llm_service import LLMService # Minimum required version of lemonade-server -LEMONADE_MINIMUM_VERSION = "8.1.12" +LEMONADE_MINIMUM_VERSION = "9.0.3" # Pygame will be imported on-demand to avoid early DLL loading issues @@ -239,16 +239,16 @@ async def model_loading_status(): async def installation_environment(): logger.info("Installation environment endpoint called") is_pyinstaller = self.lemonade_handle.is_pyinstaller_environment() - sdk_available = ( - await self.lemonade_handle.check_lemonade_sdk_available() - if not is_pyinstaller - else False - ) + # Determine preferred installation method based on platform + if sys.platform == "win32": + preferred_method = "installer" + else: + preferred_method = "manual" + result = { "is_pyinstaller": is_pyinstaller, - "sdk_available": sdk_available, "platform": sys.platform, - "preferred_method": "pip" if not is_pyinstaller else "installer", + "preferred_method": preferred_method, } logger.info(f"Returning installation environment: {result}") return JSONResponse(result) diff --git a/src/infinity_arcade/version.py b/src/infinity_arcade/version.py index b5fdc75..493f741 100644 --- a/src/infinity_arcade/version.py +++ b/src/infinity_arcade/version.py @@ -1 +1 @@ -__version__ = "0.2.2" +__version__ = "0.3.0" diff --git a/src/lemonade_client/lemonade_client.py b/src/lemonade_client/lemonade_client.py index 47b356a..a68387b 100644 --- a/src/lemonade_client/lemonade_client.py +++ b/src/lemonade_client/lemonade_client.py @@ -27,7 +27,7 @@ class LemonadeClient: by automating many common tasks. """ - def __init__(self, minimum_version: str = "8.1.0", logger=None): + def __init__(self, minimum_version: str = "9.0.3", logger=None): # Track which command is used for this server instance self.server_command = None # Track the server process to avoid starting multiple instances @@ -46,8 +46,7 @@ def is_pyinstaller_environment(self): Check if the application is running in a PyInstaller bundle environment. Use this when your app needs to determine installation method preferences - or adjust behavior based on deployment type. PyInstaller environments - typically prefer installer-based server installation over pip. + or adjust behavior based on deployment type. Returns: bool: True if running in PyInstaller bundle, False otherwise @@ -136,15 +135,6 @@ def refresh_environment(self): if user_path: new_path = user_path + ";" + system_path - # Also add common Python Scripts directories that pip might use - python_scripts_paths = self._discover_python_scripts_paths() - - # Add these paths to the PATH if they're not already there - for scripts_path in python_scripts_paths: - if scripts_path.lower() not in new_path.lower(): - new_path = scripts_path + ";" + new_path - self.logger.info(f"Added {scripts_path} to PATH") - os.environ["PATH"] = new_path self.logger.info( f"Updated PATH: {new_path[:200]}..." @@ -157,32 +147,6 @@ def refresh_environment(self): except Exception as e: self.logger.warning(f"Failed to refresh environment: {e}") - def _discover_python_scripts_paths(self): - """Discover Python Scripts directories where pip installs console scripts.""" - python_scripts_paths = [] - - # Add Python Scripts directory (where pip installs console scripts) - python_base = os.path.dirname(sys.executable) - scripts_dir = os.path.join(python_base, "Scripts") - if os.path.exists(scripts_dir): - python_scripts_paths.append(scripts_dir) - self.logger.info(f"Found Python Scripts directory: {scripts_dir}") - - # Add user site-packages Scripts directory - try: - import site - - user_site = site.getusersitepackages() - if user_site: - user_scripts = os.path.join(os.path.dirname(user_site), "Scripts") - if os.path.exists(user_scripts): - python_scripts_paths.append(user_scripts) - self.logger.info(f"Found user Scripts directory: {user_scripts}") - except Exception: - pass - - return python_scripts_paths - async def execute_lemonade_server_command( self, args: List[str], @@ -195,8 +159,8 @@ async def execute_lemonade_server_command( Execute lemonade-server commands using the best available method for the system. Use this as the primary interface for running any lemonade-server command. The method - automatically tries different installation methods (pip, installer, dev) and caches - the successful command for future use. Essential for cross-platform compatibility. + automatically tries different installation methods and caches the successful command + for future use. Essential for cross-platform compatibility. Args: args: Command arguments to pass to lemonade-server (e.g., ["--version"], ["serve"]) @@ -221,15 +185,11 @@ async def execute_lemonade_server_command( commands_to_try = [] if sys.platform == "win32": - # Windows: Try traditional commands first, then Python module fallback - if not self.is_pyinstaller_environment(): - commands_to_try.append(["lemonade-server-dev"] + args) - # Windows traditional commands commands_to_try.extend( [ ["lemonade-server"] + args, - ["lemonade-server.bat"] + args, + ["lemonade-server.exe"] + args, ] ) @@ -238,24 +198,11 @@ async def execute_lemonade_server_command( commands_to_try.extend( [ [os.path.join(bin_path, "lemonade-server.exe")] + args, - [os.path.join(bin_path, "lemonade-server.bat")] + args, ] ) - - # Python module fallback (most reliable after pip install) - # Only use sys.executable with -m flag in non-frozen environments - if not self.is_pyinstaller_environment(): - commands_to_try.append( - [sys.executable, "-m", "lemonade_server"] + args - ) else: - # Linux/Unix: Try lemonade-server-dev first, then Python module fallback - commands_to_try.append(["lemonade-server-dev"] + args) - # Only use sys.executable with -m flag in non-frozen environments - if not self.is_pyinstaller_environment(): - commands_to_try.append( - [sys.executable, "-m", "lemonade_server"] + args - ) + # Linux/Unix: Try lemonade-server command + commands_to_try.append(["lemonade-server"] + args) for i, cmd in enumerate(commands_to_try): try: @@ -338,58 +285,6 @@ async def execute_lemonade_server_command( self.logger.error("All lemonade-server commands failed") return None - async def check_lemonade_sdk_available(self): - """ - Check if the lemonade-sdk Python package is installed and importable. - - Use this to determine if pip-based installation is available before attempting - SDK-based operations. Helpful for showing installation options to users or - choosing between different installation methods. - - Returns: - bool: True if lemonade-sdk package can be imported, False otherwise - """ - self.logger.info("Checking for lemonade-sdk package...") - try: - # Handle Windows vs Unix path quoting differently - cmd = [sys.executable, "-c", "import lemonade_server; print('available')"] - - if sys.platform == "win32": - # On Windows, quote paths with spaces using double quotes - quoted_args = [] - for arg in cmd: - if " " in arg: - quoted_args.append(f'"{arg}"') - else: - quoted_args.append(arg) - cmd_str = " ".join(quoted_args) - else: - # On Unix systems, use shlex.quote - import shlex - - cmd_str = " ".join(shlex.quote(arg) for arg in cmd) - - self.logger.debug(f"Executing command: {cmd_str}") - result = subprocess.run( - cmd_str, - capture_output=True, - text=True, - timeout=10, - shell=True, # Keep shell=True for environment handling - check=False, # Don't raise exception on non-zero exit - ) - - self.logger.debug( - f"Command result: returncode={result.returncode}, " - f"stdout='{result.stdout.strip()}', stderr='{result.stderr.strip()}'" - ) - is_available = result.returncode == 0 and "available" in result.stdout - self.logger.info(f"lemonade-sdk package available: {is_available}") - return is_available - except Exception as e: - self.logger.info(f"lemonade-sdk package check failed: {e}") - return False - async def check_lemonade_server_version(self): """ Check lemonade-server installation status and version compatibility. @@ -418,7 +313,7 @@ async def check_lemonade_server_version(self): version_line = result.stdout.strip() self.logger.info(f"Raw version output: '{version_line}'") - # Extract version number (format might be "lemonade-server 8.1.3" or just "8.1.3") + # Extract version number (format might be "lemonade-server 9.0.0" or just "9.0.0") version_match = re.search(r"(\d+\.\d+\.\d+)", version_line) if version_match: version = version_match.group(1) @@ -573,161 +468,105 @@ async def start_lemonade_server(self): return {"success": False, "message": "Server process died immediately"} - async def install_lemonade_sdk_package(self): - """ - Install the lemonade-sdk Python package using pip. - - Use this to install lemonade-server via pip when in development environments - or when the SDK approach is preferred. Provides access to lemonade-server-dev - command after successful installation. - - Returns: - dict: Contains 'success' (bool) and 'message' (str) keys indicating - installation result and any error details - """ - try: - self.logger.info("Installing lemonade-sdk package using pip...") - - # Install the package - result = subprocess.run( - [sys.executable, "-m", "pip", "install", "lemonade-sdk"], - capture_output=True, - text=True, - timeout=300, # 5 minutes timeout - check=True, - ) - - if result.returncode == 0: - self.logger.info("lemonade-sdk package installed successfully") - return { - "success": True, - "message": "lemonade-sdk package installed successfully. " - "You can now use 'lemonade-server-dev' command.", - } - else: - error_msg = ( - result.stderr or result.stdout or "Unknown installation error" - ) - self.logger.error(f"pip install failed: {error_msg}") - return {"success": False, "message": f"pip install failed: {error_msg}"} - - except Exception as e: - self.logger.error(f"Failed to install lemonade-sdk package: {e}") - return {"success": False, "message": f"Failed to install: {e}"} - async def download_and_install_lemonade_server(self): """ - Download and install lemonade-server using the best method for the environment. + Download and install lemonade-server using the platform-specific installer. - Use this as the primary installation method. Automatically chooses between pip - installation (development environments) or executable installer (PyInstaller bundles). - Handles the complete installation process including download and setup. + Use this as the primary installation method. On Windows, downloads and launches the + MSI installer. On Linux, directs users to the installation options page. Returns: dict: Contains 'success' (bool), 'message' (str), and optionally 'interactive' (bool) - or 'github_link' (str) keys with installation results and next steps + or 'install_link' (str) keys with installation results and next steps """ # Reset server state since we're installing/updating self.reset_server_state() - # If not in PyInstaller environment, prefer pip installation - if not self.is_pyinstaller_environment(): - self.logger.info( - "Development environment detected, attempting pip installation first..." - ) - pip_result = await self.install_lemonade_sdk_package() - if pip_result["success"]: - return pip_result - else: - self.logger.info( - "pip installation failed, falling back to GitHub instructions..." - ) - return { - "success": False, - "message": "Could not install lemonade-sdk package. " - "Please visit https://github.com/lemonade-sdk/lemonade for " - "installation instructions.", - "github_link": "https://github.com/lemonade-sdk/lemonade", - } - - # PyInstaller environment or fallback - use installer for Windows - try: - # Download the installer - # pylint: disable=line-too-long - installer_url = "https://github.com/lemonade-sdk/lemonade/releases/latest/download/Lemonade_Server_Installer.exe" + if sys.platform == "win32": + # Windows: Use MSI installer + try: + # Download the MSI installer + # pylint: disable=line-too-long + installer_url = "https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi" - # Create temp directory for installer - temp_dir = tempfile.mkdtemp() - installer_path = os.path.join(temp_dir, "Lemonade_Server_Installer.exe") + # Create temp directory for installer + temp_dir = tempfile.mkdtemp() + installer_path = os.path.join(temp_dir, "lemonade-server-minimal.msi") - self.logger.info(f"Downloading installer from {installer_url}") + self.logger.info(f"Downloading MSI installer from {installer_url}") - # Download with progress tracking - async with httpx.AsyncClient( - timeout=300.0, follow_redirects=True - ) as client: - async with client.stream("GET", installer_url) as response: - if response.status_code != 200: - return { - "success": False, - "message": f"Failed to download installer: HTTP {response.status_code}", - } + # Download with progress tracking + async with httpx.AsyncClient( + timeout=300.0, follow_redirects=True + ) as client: + async with client.stream("GET", installer_url) as response: + if response.status_code != 200: + return { + "success": False, + "message": f"Failed to download installer: HTTP {response.status_code}", + } - with open(installer_path, "wb") as f: - async for chunk in response.aiter_bytes(8192): - f.write(chunk) + with open(installer_path, "wb") as f: + async for chunk in response.aiter_bytes(8192): + f.write(chunk) - self.logger.info(f"Downloaded installer to {installer_path}") + self.logger.info(f"Downloaded installer to {installer_path}") - # Run interactive installation (not silent) - install_cmd = [installer_path] + # Run MSI installer with msiexec + install_cmd = ["msiexec", "/i", installer_path] - self.logger.info( - f"Running interactive installation: {' '.join(install_cmd)}" - ) + self.logger.info(f"Running MSI installation: {' '.join(install_cmd)}") - # Start the installer but don't wait for it to complete - # This allows the user to see the installation UI - try: - process = subprocess.Popen(install_cmd) + # Start the installer but don't wait for it to complete + # This allows the user to see the installation UI + try: + process = subprocess.Popen(install_cmd) - # Wait a moment to see if the process stays alive - time.sleep(1) + # Wait a moment to see if the process stays alive + time.sleep(1) - # Check if the process is still running - if process.poll() is None: - # Process is still running, installer likely opened successfully - self.logger.info( - f"Installer launched successfully with PID: {process.pid}" - ) - return { - "success": True, - "message": "Installer launched. Please complete the installation.", - "interactive": True, - } - else: - # Process died immediately - self.logger.error( - f"Installer process died immediately with return code: {process.returncode}" - ) + # Check if the process is still running + if process.poll() is None: + # Process is still running, installer likely opened successfully + self.logger.info( + f"MSI Installer launched successfully with PID: {process.pid}" + ) + return { + "success": True, + "message": "MSI Installer launched. Please complete the installation.", + "interactive": True, + } + else: + # Process died immediately + self.logger.error( + f"Installer process died immediately with return code: {process.returncode}" + ) + return { + "success": False, + "message": "Failed to launch installer. Please download and install manually from: https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi", + "install_link": "https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi", + } + + except Exception as launch_error: + self.logger.error(f"Failed to launch installer: {launch_error}") return { "success": False, - "message": "Failed to launch installer. Please download and install manually from: https://github.com/lemonade-sdk/lemonade/releases/latest/download/Lemonade_Server_Installer.exe", - "github_link": "https://github.com/lemonade-sdk/lemonade/releases/latest/download/Lemonade_Server_Installer.exe", + "message": f"Failed to launch installer: {launch_error}. Please download and install manually from: https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi", + "install_link": "https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade-server-minimal.msi", } - except Exception as launch_error: - self.logger.error(f"Failed to launch installer: {launch_error}") - return { - "success": False, - "message": f"Failed to launch installer: {launch_error}. Please download and install manually from: https://github.com/lemonade-sdk/lemonade/releases/latest/download/Lemonade_Server_Installer.exe", - "github_link": "https://github.com/lemonade-sdk/lemonade/releases/latest/download/Lemonade_Server_Installer.exe", - } - - except Exception as e: - self.logger.error(f"Failed to download/install lemonade-server: {e}") - return {"success": False, "message": f"Failed to install: {e}"} + except Exception as e: + self.logger.error(f"Failed to download/install lemonade-server: {e}") + return {"success": False, "message": f"Failed to install: {e}"} + else: + # Linux: Direct users to installation options page + self.logger.info("Linux detected - directing user to installation page") + return { + "success": False, + "message": "Please visit https://lemonade-server.ai/install_options.html to download and install the latest .deb package for your system.", + "install_link": "https://lemonade-server.ai/install_options.html", + } async def check_lemonade_server_api(self): """ diff --git a/test/lemonade_client_integration.py b/test/lemonade_client_integration.py index fb8cc48..a2c3daa 100644 --- a/test/lemonade_client_integration.py +++ b/test/lemonade_client_integration.py @@ -84,52 +84,6 @@ def run_async(self, coro, timeout=None): timeout = self.test_timeout return asyncio.wait_for(coro, timeout=timeout) - def test_01_install_and_check_lemonade_sdk(self): - """Test installing lemonade-sdk if needed and checking if it's available.""" - # First check if it's already available - result = self.loop.run_until_complete( - self.run_async(self.client.check_lemonade_sdk_available()) - ) - - if not result: - print("Installing lemonade-sdk...") - # Install lemonade-sdk using pip - install_result = self.loop.run_until_complete( - self.run_async( - self.client.install_lemonade_sdk_package(), - timeout=self.setup_timeout, - ) - ) - - self.assertTrue( - install_result["success"], - f"lemonade-sdk installation should succeed: {install_result.get('message', '')}", - ) - - # Reset server state and refresh environment after installation - print("Refreshing environment after installation...") - self.client.reset_server_state() - self.client.refresh_environment() - - # Wait a moment for environment changes to take effect - import time - - time.sleep(2) - - # Verify it's now available - result_after = self.loop.run_until_complete( - self.run_async(self.client.check_lemonade_sdk_available()) - ) - self.assertTrue( - result_after, "lemonade-sdk should be available after installation" - ) - else: - print("lemonade-sdk already available") - - # The final check - either it was already available or we installed it successfully - final_result = result or result_after if not result else True - self.assertTrue(final_result, "lemonade-sdk should be available") - def test_02_check_lemonade_server_version(self): """Test checking lemonade server version.""" result = self.loop.run_until_complete( diff --git a/test/lemonade_client_unit.py b/test/lemonade_client_unit.py index 8ca2179..ff1215f 100644 --- a/test/lemonade_client_unit.py +++ b/test/lemonade_client_unit.py @@ -28,7 +28,7 @@ def test_init(self): self.assertIsNone(client.server_command) self.assertIsNone(client.server_process) self.assertEqual(client.url, "http://localhost:8000") - self.assertEqual(client.minimum_version, "8.1.0") # Default value + self.assertEqual(client.minimum_version, "9.0.3") # Default value # Test with custom minimum version custom_client = LemonadeClient(minimum_version="8.2.0") @@ -151,11 +151,7 @@ def test_refresh_environment_windows_success( ("C:\\Users\\User\\bin", 1), # User PATH ] - # Mock Python Scripts discovery to return empty list - with patch.object( - self.client, "_discover_python_scripts_paths", return_value=[] - ): - self.client.refresh_environment() + self.client.refresh_environment() # Check that PATH was updated expected_path = "C:\\Users\\User\\bin;C:\\Windows\\System32;C:\\Program Files" @@ -174,11 +170,7 @@ def test_refresh_environment_windows_no_user_path(self, mock_query, mock_open_ke ] with patch("os.environ") as mock_environ: - # Mock Python Scripts discovery to return empty list - with patch.object( - self.client, "_discover_python_scripts_paths", return_value=[] - ): - self.client.refresh_environment() + self.client.refresh_environment() # Should still set PATH to system PATH only mock_environ.__setitem__.assert_called_with("PATH", "C:\\Windows\\System32") @@ -230,7 +222,7 @@ async def test_execute_lemonade_server_command_windows_success(self): result = await self.client.execute_lemonade_server_command(["--version"]) self.assertEqual(result, mock_result) - self.assertEqual(self.client.server_command, ["lemonade-server-dev"]) + self.assertEqual(self.client.server_command, ["lemonade-server"]) async def test_execute_lemonade_server_command_linux_success(self): """Test executing command on Linux with successful result.""" @@ -245,7 +237,7 @@ async def test_execute_lemonade_server_command_linux_success(self): result = await self.client.execute_lemonade_server_command(["--version"]) self.assertEqual(result, mock_result) - self.assertEqual(self.client.server_command, ["lemonade-server-dev"]) + self.assertEqual(self.client.server_command, ["lemonade-server"]) async def test_execute_lemonade_server_command_popen_mode(self): """Test executing command with use_popen=True.""" @@ -280,36 +272,6 @@ async def test_execute_lemonade_server_command_timeout(self): self.assertIsNone(result) - async def test_check_lemonade_sdk_available_true(self): - """Test checking lemonade-sdk availability when available.""" - with patch("subprocess.run") as mock_run: - mock_result = MagicMock() - mock_result.returncode = 0 - mock_result.stdout = "available" - mock_run.return_value = mock_result - - result = await self.client.check_lemonade_sdk_available() - - self.assertTrue(result) - - async def test_check_lemonade_sdk_available_false(self): - """Test checking lemonade-sdk availability when not available.""" - with patch("subprocess.run") as mock_run: - mock_result = MagicMock() - mock_result.returncode = 1 - mock_result.stdout = "error" - mock_run.return_value = mock_result - - result = await self.client.check_lemonade_sdk_available() - - self.assertFalse(result) - - async def test_check_lemonade_sdk_available_exception(self): - """Test checking lemonade-sdk availability when exception occurs.""" - with patch("subprocess.run", side_effect=Exception("Test error")): - result = await self.client.check_lemonade_sdk_available() - self.assertFalse(result) - async def test_check_lemonade_server_version_success(self): """Test checking lemonade server version successfully.""" with patch.object(self.client, "execute_lemonade_server_command") as mock_exec: @@ -495,79 +457,13 @@ async def test_start_lemonade_server_process_dies( expected = {"success": False, "message": "Server process died immediately"} self.assertEqual(result, expected) - async def test_install_lemonade_sdk_package_success(self): - """Test installing lemonade-sdk package successfully.""" - with patch("subprocess.run") as mock_run: - mock_result = MagicMock() - mock_result.returncode = 0 - mock_run.return_value = mock_result - - result = await self.client.install_lemonade_sdk_package() - - expected = { - "success": True, - "message": "lemonade-sdk package installed successfully. You can now use 'lemonade-server-dev' command.", - } - self.assertEqual(result, expected) - - async def test_install_lemonade_sdk_package_failure(self): - """Test installing lemonade-sdk package with failure.""" - with patch("subprocess.run") as mock_run: - mock_result = MagicMock() - mock_result.returncode = 1 - mock_result.stderr = "Installation failed" - mock_run.return_value = mock_result - - result = await self.client.install_lemonade_sdk_package() - - self.assertFalse(result["success"]) - self.assertIn("pip install failed", result["message"]) - - async def test_install_lemonade_sdk_package_exception(self): - """Test installing lemonade-sdk package with exception.""" - with patch("subprocess.run", side_effect=Exception("Test error")): - result = await self.client.install_lemonade_sdk_package() - - self.assertFalse(result["success"]) - self.assertIn("Failed to install", result["message"]) - - @patch.object(LemonadeClient, "reset_server_state") - @patch.object(LemonadeClient, "is_pyinstaller_environment") - @patch.object(LemonadeClient, "install_lemonade_sdk_package") - async def test_download_and_install_lemonade_server_pip_success( - self, mock_install, mock_pyinstaller, mock_reset - ): - """Test downloading and installing lemonade server via pip successfully.""" - mock_pyinstaller.return_value = False - mock_install.return_value = {"success": True, "message": "Success"} - - result = await self.client.download_and_install_lemonade_server() - - self.assertTrue(result["success"]) - mock_reset.assert_called_once() - mock_install.assert_called_once() - - @patch.object(LemonadeClient, "reset_server_state") - @patch.object(LemonadeClient, "is_pyinstaller_environment") - @patch.object(LemonadeClient, "install_lemonade_sdk_package") - async def test_download_and_install_lemonade_server_pip_failure( - self, mock_install, mock_pyinstaller, mock_reset - ): - """Test downloading and installing lemonade server when pip fails.""" - mock_pyinstaller.return_value = False - mock_install.return_value = {"success": False, "message": "Pip failed"} - - result = await self.client.download_and_install_lemonade_server() - - self.assertFalse(result["success"]) - self.assertIn("github.com", result["message"]) - @patch("tempfile.mkdtemp") @patch("subprocess.Popen") @patch("httpx.AsyncClient") @patch("time.sleep") @patch.object(LemonadeClient, "reset_server_state") @patch.object(LemonadeClient, "is_pyinstaller_environment") + @patch("sys.platform", "win32") async def test_download_and_install_lemonade_server_installer_success( self, mock_pyinstaller, @@ -626,6 +522,7 @@ async def mock_aiter_bytes(chunk_size=8192): @patch("time.sleep") @patch.object(LemonadeClient, "reset_server_state") @patch.object(LemonadeClient, "is_pyinstaller_environment") + @patch("sys.platform", "win32") async def test_download_and_install_lemonade_server_installer_process_dies( self, mock_pyinstaller, @@ -673,8 +570,8 @@ async def mock_aiter_bytes(chunk_size=8192): self.assertFalse(result["success"]) self.assertIn("Failed to launch installer", result["message"]) - self.assertIn("github_link", result) - self.assertIn("Lemonade_Server_Installer.exe", result["github_link"]) + self.assertIn("install_link", result) + self.assertIn("lemonade-server-minimal.msi", result["install_link"]) mock_reset.assert_called_once() mock_sleep.assert_called_once_with(1) # Verify 1 second wait mock_process.poll.assert_called_once() # Verify process status check @@ -685,6 +582,7 @@ async def mock_aiter_bytes(chunk_size=8192): @patch("time.sleep") @patch.object(LemonadeClient, "reset_server_state") @patch.object(LemonadeClient, "is_pyinstaller_environment") + @patch("sys.platform", "win32") async def test_download_and_install_lemonade_server_installer_launch_exception( self, mock_pyinstaller, @@ -731,8 +629,8 @@ async def mock_aiter_bytes(chunk_size=8192): self.assertIn( "Failed to launch installer: Permission denied", result["message"] ) - self.assertIn("github_link", result) - self.assertIn("Lemonade_Server_Installer.exe", result["github_link"]) + self.assertIn("install_link", result) + self.assertIn("lemonade-server-minimal.msi", result["install_link"]) mock_reset.assert_called_once() # time.sleep should not be called if Popen raises exception mock_sleep.assert_not_called()