openenv/coding_env
21
1# Copyright (c) Meta Platforms, Inc. and affiliates.2# All rights reserved.3#4# This source code is licensed under the BSD-style license found in the5# LICENSE file in the root directory of this source tree.6 7"""8AutoEnv - Automatic Environment Selection9==========================================10 11AutoEnv provides a HuggingFace-style API for automatically selecting and12instantiating the correct environment client from installed packages or13HuggingFace Hub.14 15This module simplifies environment creation by automatically detecting the16environment type from the name and instantiating the appropriate client class.17 18Example:19 >>> from openenv import AutoEnv, AutoAction20 >>>21 >>> # From installed package22 >>> env = AutoEnv.from_env("coding-env")23 >>>24 >>> # From HuggingFace Hub25 >>> env = AutoEnv.from_env("meta-pytorch/coding-env")26 >>>27 >>> # With configuration28 >>> env = AutoEnv.from_env("coding", env_vars={"DEBUG": "1"})29"""30 31from __future__ import annotations32 33import importlib34import logging35import os36import shutil37import subprocess38import sys39from typing import Any, Dict, Optional, TYPE_CHECKING40 41import requests42from openenv.core.utils import run_async_safely43 44from ._discovery import _is_hub_url, get_discovery45 46 47if TYPE_CHECKING:48 from openenv.core.containers.runtime import ContainerProvider49 from openenv.core.env_client import EnvClient50 51logger = logging.getLogger(__name__)52 53# Cache for repo ID → env_name mapping to avoid redundant downloads54_hub_env_name_cache: Dict[str, str] = {}55 56# Environment variable to skip user confirmation for remote installs57OPENENV_TRUST_REMOTE_CODE = "OPENENV_TRUST_REMOTE_CODE"58 59 60def _has_uv() -> bool:61 """Check if uv is available in the system."""62 return shutil.which("uv") is not None63 64 65def _get_pip_command() -> list[str]:66 """67 Get the appropriate pip command (uv pip or pip).68 69 Returns:70 List of command parts for pip installation71 """72 if _has_uv():73 return ["uv", "pip"]74 return [sys.executable, "-m", "pip"]75 76 77def _confirm_remote_install(repo_id: str) -> bool:78 """79 Ask user for confirmation before installing remote code.80 81 This is a security measure since we're executing code from the internet.82 83 Args:84 repo_id: The HuggingFace repo ID being installed85 86 Returns:87 True if user confirms, False otherwise88 """89 # Check environment variable for automated/CI environments90 if os.environ.get(OPENENV_TRUST_REMOTE_CODE, "").lower() in ("1", "true", "yes"):91 logger.info("Skipping confirmation (OPENENV_TRUST_REMOTE_CODE is set)")92 return True93 94 # Check if we're in an interactive terminal95 if not sys.stdin.isatty():96 logger.warning(97 "Cannot prompt for confirmation in non-interactive mode. "98 "Set OPENENV_TRUST_REMOTE_CODE=1 to allow remote installs."99 )100 return False101 102 print(f"\n{'=' * 60}")103 print("⚠️ SECURITY WARNING: Remote Code Installation")104 print(f"{'=' * 60}")105 print("You are about to install code from a remote repository:")106 print(f" Repository: {repo_id}")107 print(f" Source: https://huggingface.co/spaces/{repo_id}")108 print("\nThis will execute code from the internet on your machine.")109 print("Only proceed if you trust the source.")110 print(f"{'=' * 60}\n")111 112 try:113 response = input("Do you want to proceed? [y/N]: ").strip().lower()114 return response in ("y", "yes")115 except (EOFError, KeyboardInterrupt):116 print("\nInstallation cancelled.")117 return False118 119 120class AutoEnv:121 """122 AutoEnv automatically selects and instantiates the correct environment client123 based on environment names or HuggingFace Hub repositories.124 125 This class follows the HuggingFace AutoModel pattern, making it easy to work126 with different environments without needing to import specific client classes.127 128 The class provides factory methods that:129 1. Check if name is a HuggingFace Hub URL/repo ID130 2. If Hub: download and install the environment package131 3. If local: look up the installed openenv-* package132 4. Import and instantiate the client class133 134 Example:135 >>> # From installed package136 >>> env = AutoEnv.from_env("coding-env")137 >>>138 >>> # From HuggingFace Hub139 >>> env = AutoEnv.from_env("meta-pytorch/coding-env")140 >>>141 >>> # List available environments142 >>> AutoEnv.list_environments()143 144 Note:145 AutoEnv is not meant to be instantiated directly. Use the class method146 from_env() instead.147 """148 149 def __init__(self):150 """AutoEnv should not be instantiated directly. Use class methods instead."""151 raise TypeError(152 "AutoEnv is a factory class and should not be instantiated directly. "153 "Use AutoEnv.from_hub() or AutoEnv.from_env() instead."154 )155 156 @classmethod157 def _resolve_space_url(cls, repo_id: str) -> str:158 """159 Resolve HuggingFace Space repo ID to Space URL.160 161 Args:162 repo_id: HuggingFace repo ID (e.g., "wukaixingxp/coding-env-test")163 164 Returns:165 Space URL (e.g., "https://wukaixingxp-coding-env-test.hf.space")166 167 Examples:168 >>> AutoEnv._resolve_space_url("wukaixingxp/coding-env-test")169 'https://wukaixingxp-coding-env-test.hf.space'170 """171 # Clean up repo_id if it's a full URL172 if "huggingface.co" in repo_id:173 # Extract org/repo from URL174 # https://huggingface.co/wukaixingxp/coding-env-test -> wukaixingxp/coding-env-test175 parts = repo_id.split("/")176 if len(parts) >= 2:177 repo_id = f"{parts[-2]}/{parts[-1]}"178 179 # Convert user/space-name to user-space-name.hf.space180 space_slug = repo_id.replace("/", "-")181 return f"https://{space_slug}.hf.space"182 183 @classmethod184 def _is_local_url(cls, url: str) -> bool:185 """186 Check if a URL points to a local server.187 188 Args:189 url: URL to check190 191 Returns:192 True if URL is localhost or 127.0.0.1, False otherwise193 194 Examples:195 >>> AutoEnv._is_local_url("http://localhost:8000")196 True197 >>> AutoEnv._is_local_url("http://127.0.0.1:8000")198 True199 >>> AutoEnv._is_local_url("https://example.com")200 False201 """202 url_lower = url.lower()203 return "localhost" in url_lower or "127.0.0.1" in url_lower204 205 @classmethod206 def _check_server_availability(cls, base_url: str, timeout: float = 2.0) -> bool:207 """208 Check if a server at the given URL is running and accessible.209 210 Args:211 base_url: Server base URL to check212 timeout: Request timeout in seconds213 214 Returns:215 True if server is accessible, False otherwise216 217 Examples:218 >>> AutoEnv._check_server_availability("http://localhost:8000")219 True # if server is running220 """221 try:222 # Bypass proxy for localhost to avoid proxy issues223 proxies = None224 if cls._is_local_url(base_url):225 proxies = {"http": None, "https": None}226 227 # Try to access the health endpoint228 response = requests.get(229 f"{base_url}/health", timeout=timeout, proxies=proxies230 )231 if response.status_code == 200:232 return True233 234 # If health endpoint doesn't exist, try root endpoint235 response = requests.get(base_url, timeout=timeout, proxies=proxies)236 return response.status_code == 200237 except (requests.RequestException, Exception) as e:238 logger.debug(f"Server {base_url} not accessible: {e}")239 return False240 241 @classmethod242 def _check_space_availability(cls, space_url: str, timeout: float = 5.0) -> bool:243 """244 Check if HuggingFace Space is running and accessible.245 246 Args:247 space_url: Space URL to check248 timeout: Request timeout in seconds249 250 Returns:251 True if Space is accessible, False otherwise252 253 Examples:254 >>> AutoEnv._check_space_availability("https://wukaixingxp-coding-env-test.hf.space")255 True256 """257 try:258 # Try to access the health endpoint259 response = requests.get(f"{space_url}/health", timeout=timeout)260 if response.status_code == 200:261 return True262 263 # If health endpoint doesn't exist, try root endpoint264 response = requests.get(space_url, timeout=timeout)265 return response.status_code == 200266 except (requests.RequestException, Exception) as e:267 logger.debug(f"Space {space_url} not accessible: {e}")268 return False269 270 @classmethod271 def _get_hub_git_url(cls, repo_id: str) -> str:272 """273 Get the git URL for a HuggingFace Space.274 275 Args:276 repo_id: HuggingFace repo ID (e.g., "wukaixingxp/coding-env-test")277 278 Returns:279 Git URL for pip installation (e.g., "git+https://huggingface.co/spaces/wukaixingxp/coding-env-test")280 """281 # Clean up repo_id if it's a full URL282 if "huggingface.co" in repo_id:283 parts = repo_id.split("/")284 if len(parts) >= 2:285 repo_id = f"{parts[-2]}/{parts[-1]}"286 287 return f"git+https://huggingface.co/spaces/{repo_id}"288 289 @classmethod290 def _install_from_hub(cls, repo_id: str, trust_remote_code: bool = False) -> str:291 """292 Install environment package directly from HuggingFace Hub using git+.293 294 This is the preferred method as it avoids downloading the entire repo295 and uses pip/uv's native git support.296 297 Args:298 repo_id: HuggingFace repo ID (e.g., "wukaixingxp/coding-env-test")299 trust_remote_code: If True, skip user confirmation300 301 Returns:302 Package name that was installed303 304 Raises:305 ValueError: If installation fails or user declines306 """307 # Security check - confirm with user before installing remote code308 if not trust_remote_code and not _confirm_remote_install(repo_id):309 raise ValueError(310 "Installation cancelled by user.\n"311 "To allow remote installs without prompting, set OPENENV_TRUST_REMOTE_CODE=1"312 )313 314 git_url = cls._get_hub_git_url(repo_id)315 pip_cmd = _get_pip_command()316 pip_name = "uv pip" if pip_cmd[0] == "uv" else "pip"317 318 logger.info(f"Installing from HuggingFace Space using {pip_name}: {repo_id}")319 logger.info(f"Command: {' '.join(pip_cmd)} install {git_url}")320 321 try:322 result = subprocess.run(323 [*pip_cmd, "install", git_url],324 check=True,325 capture_output=True,326 text=True,327 )328 329 # Try to extract package name from pip output330 # Look for "Successfully installed <package-name>-<version>"331 for line in result.stdout.split("\n"):332 if "Successfully installed" in line:333 # Parse package name from the line334 parts = line.replace("Successfully installed", "").strip().split()335 for part in parts:336 if part.startswith("openenv-"):337 # Remove version suffix (e.g., "openenv-coding_env-0.1.0" -> "openenv-coding_env")338 # Check if last segment looks like a version number339 last_segment = part.rsplit("-", 1)[-1]340 if last_segment.replace(".", "").isdigit():341 package_name = "-".join(part.rsplit("-", 1)[:-1])342 else:343 package_name = part344 logger.info(f"Successfully installed: {package_name}")345 return package_name346 347 # Fallback: try to determine package name from repo_id348 # Convention: repo name like "coding-env-test" -> package "openenv-coding_env"349 env_name = repo_id.split("/")[-1] # Get repo name from "user/repo"350 env_name = env_name.replace("-", "_")351 if not env_name.endswith("_env"):352 env_name = f"{env_name}_env"353 package_name = f"openenv-{env_name}"354 355 logger.info(f"Installed (inferred package name): {package_name}")356 return package_name357 358 except subprocess.CalledProcessError as e:359 error_msg = e.stderr or e.stdout or str(e)360 raise ValueError(361 f"Failed to install environment from HuggingFace Space: {repo_id}\n"362 f"Command: {' '.join(pip_cmd)} install {git_url}\n"363 f"Error: {error_msg}\n"364 f"Make sure the repository exists and contains a valid Python package."365 ) from e366 367 @classmethod368 def _is_package_installed(cls, package_name: str) -> bool:369 """370 Check if a package is already installed.371 372 Args:373 package_name: Package name (e.g., "openenv-coding_env")374 375 Returns:376 True if installed, False otherwise377 """378 try:379 import importlib.metadata380 381 importlib.metadata.distribution(package_name)382 return True383 except importlib.metadata.PackageNotFoundError:384 return False385 386 @classmethod387 def _ensure_package_from_hub(388 cls, name: str, trust_remote_code: bool = False389 ) -> str:390 """391 Ensure package from HuggingFace Hub is installed.392 393 Uses git+ URLs for direct installation without downloading the entire repo.394 Prompts user for confirmation before installing remote code.395 396 Args:397 name: HuggingFace repo ID (e.g., "wukaixingxp/coding-env-test")398 trust_remote_code: If True, skip user confirmation399 400 Returns:401 Environment name (e.g., "coding_env")402 """403 global _hub_env_name_cache404 405 # Check if we already resolved this repo ID406 if name in _hub_env_name_cache:407 env_name = _hub_env_name_cache[name]408 logger.debug(f"Using cached env name for {name}: {env_name}")409 return env_name410 411 # Try to infer expected package name from repo ID412 # Convention: repo "user/coding-env" -> package "openenv-coding_env"413 repo_name = name.split("/")[-1] if "/" in name else name414 expected_env_name = repo_name.replace("-", "_")415 if not expected_env_name.endswith("_env"):416 expected_env_name = f"{expected_env_name}_env"417 expected_package_name = f"openenv-{expected_env_name}"418 419 # Check if already installed420 if cls._is_package_installed(expected_package_name):421 logger.info(f"Package already installed: {expected_package_name}")422 # Clear and refresh discovery cache to make sure it's detected423 get_discovery().clear_cache()424 get_discovery().discover(use_cache=False)425 # Cache the result426 _hub_env_name_cache[name] = expected_env_name427 return expected_env_name428 429 # Not installed, install using git+ URL430 logger.info(f"Package not found locally, installing from Hub: {name}")431 432 # Track existing packages before installation433 get_discovery().clear_cache()434 existing_envs = set(get_discovery().discover(use_cache=False).keys())435 436 # Install the package437 cls._install_from_hub(name, trust_remote_code=trust_remote_code)438 439 # Clear discovery cache to pick up the newly installed package440 try:441 importlib.invalidate_caches()442 except Exception:443 pass444 get_discovery().clear_cache()445 discovered_envs = get_discovery().discover(use_cache=False)446 447 # Find the newly installed environment by comparing before/after448 new_envs = set(discovered_envs.keys()) - existing_envs449 450 if new_envs:451 # Use the first newly discovered environment452 env_name = next(iter(new_envs))453 logger.info(f"Found newly installed environment: '{env_name}'")454 else:455 # Fallback: try to find by matching module patterns456 # Look for any env that might match the repo name pattern457 repo_name = name.split("/")[-1] if "/" in name else name458 repo_base = (459 repo_name.replace("-", "_").replace("_env", "").replace("_test", "")460 )461 462 env_name = None463 for env_key, env_info in discovered_envs.items():464 # Check if env_key is a prefix/substring match465 if env_key in repo_base or repo_base.startswith(env_key):466 env_name = env_key467 logger.info(468 f"Found matching environment '{env_name}' for repo '{name}'"469 )470 break471 472 if env_name is None:473 # Last resort: use inferred name from repo474 env_name = repo_name.replace("-", "_")475 if not env_name.endswith("_env"):476 env_name = f"{env_name}_env"477 # Strip to get env_key478 env_name = env_name.replace("_env", "")479 logger.warning(480 f"Could not find newly installed environment for repo '{name}', "481 f"using inferred name: {env_name}"482 )483 484 # Cache the result to avoid redundant installs485 _hub_env_name_cache[name] = env_name486 487 return env_name488 489 @classmethod490 def from_env(491 cls,492 name: str,493 base_url: Optional[str] = None,494 docker_image: Optional[str] = None,495 container_provider: Optional[ContainerProvider] = None,496 wait_timeout: float = 30.0,497 env_vars: Optional[Dict[str, str]] = None,498 trust_remote_code: bool = False,499 skip_install: bool = False,500 **kwargs: Any,501 ) -> "EnvClient":502 """503 Create an environment client from a name or HuggingFace Hub repository.504 505 This method automatically:506 1. Checks if the name is a HuggingFace Hub URL/repo ID507 2. If Hub: installs the environment package using git+ URL508 3. If local: looks up the installed openenv-* package509 4. Imports the client class and instantiates it510 511 Args:512 name: Environment name or HuggingFace Hub repo ID513 Examples:514 - "coding" / "coding-env" / "coding_env"515 - "meta-pytorch/coding-env" (Hub repo ID)516 - "https://huggingface.co/meta-pytorch/coding-env" (Hub URL)517 base_url: Optional base URL for HTTP connection518 docker_image: Optional Docker image name (overrides default)519 container_provider: Optional container provider520 wait_timeout: Timeout for container startup (seconds)521 env_vars: Optional environment variables for the container522 trust_remote_code: If True, skip user confirmation when installing523 from HuggingFace Hub. Can also be set via OPENENV_TRUST_REMOTE_CODE524 environment variable.525 skip_install: If True, skip package installation and return a526 GenericEnvClient for remote environments. Useful when you only527 want to connect to a running server without installing any528 remote code. When True:529 - If base_url is provided: connects directly using GenericEnvClient530 - If HF Space is running: connects to Space using GenericEnvClient531 - If HF Space is not running: uses Docker from HF registry532 **kwargs: Additional arguments passed to the client class533 534 Returns:535 Instance of the environment client class536 537 Raises:538 ValueError: If environment not found or cannot be loaded539 ImportError: If environment package is not installed540 541 Examples:542 >>> # From installed package543 >>> env = AutoEnv.from_env("coding-env")544 >>>545 >>> # From HuggingFace Hub546 >>> env = AutoEnv.from_env("meta-pytorch/coding-env")547 >>>548 >>> # With custom Docker image549 >>> env = AutoEnv.from_env("coding", docker_image="my-coding-env:v2")550 >>>551 >>> # With environment variables552 >>> env = AutoEnv.from_env(553 ... "dipg",554 ... env_vars={"DIPG_DATASET_PATH": "/data/dipg"}555 ... )556 >>>557 >>> # Skip package installation, use GenericEnvClient558 >>> env = AutoEnv.from_env(559 ... "user/my-env",560 ... skip_install=True561 ... )562 """563 from openenv.core import GenericEnvClient564 565 # Handle skip_install mode - return GenericEnvClient without package installation566 if skip_install:567 # If base_url is provided, connect directly568 if base_url:569 if cls._check_server_availability(base_url):570 logger.info(571 f"Using GenericEnvClient for {base_url} (skip_install=True)"572 )573 return GenericEnvClient(base_url=base_url, **kwargs)574 else:575 raise ConnectionError(576 f"Server not available at {base_url}. "577 f"Please ensure the server is running."578 )579 580 # If it's a Hub URL, try to connect to Space or use Docker581 if _is_hub_url(name):582 space_url = cls._resolve_space_url(name)583 logger.info(f"Checking if HuggingFace Space is accessible: {space_url}")584 585 if cls._check_space_availability(space_url):586 logger.info(587 f"Using GenericEnvClient for Space {space_url} (skip_install=True)"588 )589 return GenericEnvClient(base_url=space_url, **kwargs)590 else:591 # Space not running, use Docker from HF registry592 logger.info(593 f"Space not running at {space_url}, "594 f"using GenericEnvClient with HF Docker registry"595 )596 return run_async_safely(597 GenericEnvClient.from_env(598 name,599 use_docker=True,600 provider=container_provider,601 env_vars=env_vars or {},602 **kwargs,603 )604 )605 606 # For local environments with skip_install, we need docker_image607 if docker_image:608 logger.info(609 f"Using GenericEnvClient with Docker image {docker_image} "610 f"(skip_install=True)"611 )612 return run_async_safely(613 GenericEnvClient.from_docker_image(614 image=docker_image,615 provider=container_provider,616 wait_timeout=wait_timeout,617 env_vars=env_vars or {},618 **kwargs,619 )620 )621 else:622 raise ValueError(623 f"Cannot use skip_install=True for local environment '{name}' "624 f"without providing base_url or docker_image. "625 f"For local environments, either:\n"626 f" 1. Provide base_url to connect to a running server\n"627 f" 2. Provide docker_image to start a container\n"628 f" 3. Set skip_install=False to use the installed package"629 )630 631 # Check if it's a HuggingFace Hub URL or repo ID632 if _is_hub_url(name):633 # Try to connect to Space directly first634 space_url = cls._resolve_space_url(name)635 logger.info(f"Checking if HuggingFace Space is accessible: {space_url}")636 637 space_is_available = cls._check_space_availability(space_url)638 639 if space_is_available and base_url is None:640 # Space is accessible! We'll connect directly without Docker641 logger.info(f"Space is accessible at: {space_url}")642 logger.info("Installing package for client code (no Docker needed)...")643 644 # Ensure package is installed (uses git+ URL)645 env_name = cls._ensure_package_from_hub(646 name, trust_remote_code=trust_remote_code647 )648 649 # Set base_url to connect to remote Space650 base_url = space_url651 logger.info("Will connect to remote Space (no local Docker)")652 else:653 # Space not accessible or user provided explicit base_url654 if not space_is_available:655 logger.info(f"Space not accessible at {space_url}")656 logger.info("Falling back to local Docker mode...")657 658 # Ensure package is installed (uses git+ URL)659 env_name = cls._ensure_package_from_hub(660 name, trust_remote_code=trust_remote_code661 )662 else:663 env_name = name664 665 # Get environment info from discovery666 discovery = get_discovery()667 env_info = discovery.get_environment_by_name(env_name)668 669 if not env_info:670 # Environment not found - provide helpful error message671 available_envs = discovery.discover()672 673 if not available_envs:674 raise ValueError(675 "No OpenEnv environments found.\n"676 "Install an environment with: pip install openenv-<env-name>\n"677 "Or specify a HuggingFace Hub repository: AutoEnv.from_env('openenv/echo_env')"678 )679 680 # Try to suggest similar environment names681 from difflib import get_close_matches682 683 env_keys = list(available_envs.keys())684 suggestions = get_close_matches(env_name, env_keys, n=3, cutoff=0.6)685 686 error_msg = f"Unknown environment '{env_name}'.\n"687 if suggestions:688 error_msg += f"Did you mean: {', '.join(suggestions)}?\n"689 error_msg += f"Available environments: {', '.join(sorted(env_keys))}"690 691 raise ValueError(error_msg)692 693 # Get the client class694 try:695 client_class = env_info.get_client_class()696 except ImportError as e:697 raise ImportError(698 f"Failed to import environment client for '{env_name}'.\n"699 f"Package '{env_info.package_name}' appears to be installed but the module cannot be imported.\n"700 f"Try reinstalling: pip install --force-reinstall {env_info.package_name}\n"701 f"Original error: {e}"702 ) from e703 704 # Determine Docker image to use705 if docker_image is None:706 docker_image = env_info.default_image707 708 # Create client instance709 try:710 if base_url:711 # Check if the server at base_url is available712 is_local = cls._is_local_url(base_url)713 server_available = cls._check_server_availability(base_url)714 715 if server_available:716 # Server is running, connect directly717 logger.info(718 f"✅ Server available at {base_url}, connecting directly"719 )720 return client_class(base_url=base_url, provider=None, **kwargs)721 elif is_local:722 # Local server not running, auto-start Docker container723 logger.info(f"❌ Server not available at {base_url}")724 logger.info(f"🐳 Auto-starting Docker container: {docker_image}")725 return run_async_safely(726 client_class.from_docker_image(727 image=docker_image,728 provider=container_provider,729 wait_timeout=wait_timeout,730 env_vars=env_vars or {},731 **kwargs,732 )733 )734 else:735 # Remote server not available, cannot auto-start736 raise ConnectionError(737 f"Remote server not available at {base_url}. "738 f"Please ensure the server is running."739 )740 else:741 # No base_url provided, start new Docker container742 return run_async_safely(743 client_class.from_docker_image(744 image=docker_image,745 provider=container_provider,746 wait_timeout=wait_timeout,747 env_vars=env_vars or {},748 **kwargs,749 )750 )751 except Exception as e:752 raise ValueError(753 f"Failed to create environment client for '{env_name}'.\n"754 f"Client class: {client_class.__name__}\n"755 f"Docker image: {docker_image}\n"756 f"Error: {e}"757 ) from e758 759 @classmethod760 def from_hub(761 cls,762 name: str,763 base_url: Optional[str] = None,764 docker_image: Optional[str] = None,765 container_provider: Optional["ContainerProvider"] = None,766 wait_timeout: float = 30.0,767 env_vars: Optional[Dict[str, str]] = None,768 trust_remote_code: bool = False,769 skip_install: bool = False,770 **kwargs: Any,771 ) -> "EnvClient":772 """773 Create an environment client from a name or HuggingFace Hub repository.774 775 This is an alias for from_env() for backward compatibility.776 777 Args:778 name: Environment name or HuggingFace Hub repo ID779 base_url: Optional base URL for HTTP connection780 docker_image: Optional Docker image name (overrides default)781 container_provider: Optional container provider782 wait_timeout: Timeout for container startup (seconds)783 env_vars: Optional environment variables for the container784 trust_remote_code: If True, skip user confirmation when installing785 from HuggingFace Hub786 skip_install: If True, skip package installation and return a787 GenericEnvClient for remote environments788 **kwargs: Additional arguments passed to the client class789 790 Returns:791 Instance of the environment client class792 793 Examples:794 >>> env = AutoEnv.from_hub("coding-env")795 >>> env = AutoEnv.from_hub("meta-pytorch/coding-env")796 """797 return cls.from_env(798 name=name,799 base_url=base_url,800 docker_image=docker_image,801 container_provider=container_provider,802 wait_timeout=wait_timeout,803 env_vars=env_vars,804 trust_remote_code=trust_remote_code,805 skip_install=skip_install,806 **kwargs,807 )808 809 @classmethod810 def get_env_class(cls, name: str):811 """812 Get the environment client class without instantiating it.813 814 Args:815 name: Environment name816 817 Returns:818 The environment client class819 820 Raises:821 ValueError: If environment not found822 823 Examples:824 >>> CodingEnv = AutoEnv.get_env_class("coding")825 >>> # Now you can instantiate it yourself826 >>> env = CodingEnv(base_url="http://localhost:8000")827 """828 discovery = get_discovery()829 env_info = discovery.get_environment_by_name(name)830 831 if not env_info:832 raise ValueError(f"Unknown environment: {name}")833 834 return env_info.get_client_class()835 836 @classmethod837 def get_env_info(cls, name: str) -> Dict[str, Any]:838 """839 Get detailed information about an environment.840 841 Args:842 name: Environment name843 844 Returns:845 Dictionary with environment metadata846 847 Raises:848 ValueError: If environment not found849 850 Examples:851 >>> info = AutoEnv.get_env_info("coding")852 >>> print(info['description'])853 'Coding environment for OpenEnv'854 >>> print(info['default_image'])855 'coding-env:latest'856 """857 discovery = get_discovery()858 env_info = discovery.get_environment_by_name(name)859 860 if not env_info:861 raise ValueError(f"Unknown environment: {name}")862 863 return {864 "env_key": env_info.env_key,865 "name": env_info.name,866 "package": env_info.package_name,867 "version": env_info.version,868 "description": env_info.description,869 "env_class": env_info.client_class_name,870 "action_class": env_info.action_class_name,871 "observation_class": env_info.observation_class_name,872 "module": env_info.client_module_path,873 "default_image": env_info.default_image,874 "spec_version": env_info.spec_version,875 }876 877 @classmethod878 def list_environments(cls) -> None:879 """880 Print a formatted list of all available environments.881 882 This discovers all installed openenv-* packages and displays883 their metadata in a user-friendly format.884 885 Examples:886 >>> AutoEnv.list_environments()887 Available OpenEnv Environments:888 ----------------------------------------------------------------------889 echo : Echo Environment (v0.1.0)890 Package: openenv-echo-env891 coding : Coding Environment (v0.1.0)892 Package: openenv-coding_env893 ----------------------------------------------------------------------894 Total: 2 environments895 """896 discovery = get_discovery()897 discovery.list_environments()898 