Team Ai
Apppublic

openenv/atari_env

sourceHugging Faceupdated 6mo agoView on Hugging Face
3likes
auto_env.py898 linesDownload Raw Back to auto
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