|
35 | 35 | import os |
36 | 36 | import subprocess |
37 | 37 | import tempfile |
| 38 | +import requests |
38 | 39 | from pathlib import Path |
39 | 40 | from typing import Any, Optional, TYPE_CHECKING, Dict |
40 | 41 |
|
@@ -83,6 +84,62 @@ def __init__(self): |
83 | 84 | "Use AutoEnv.from_name() instead." |
84 | 85 | ) |
85 | 86 |
|
| 87 | + @classmethod |
| 88 | + def _resolve_space_url(cls, repo_id: str) -> str: |
| 89 | + """ |
| 90 | + Resolve HuggingFace Space repo ID to Space URL. |
| 91 | +
|
| 92 | + Args: |
| 93 | + repo_id: HuggingFace repo ID (e.g., "wukaixingxp/coding-env-test") |
| 94 | +
|
| 95 | + Returns: |
| 96 | + Space URL (e.g., "https://wukaixingxp-coding-env-test.hf.space") |
| 97 | +
|
| 98 | + Examples: |
| 99 | + >>> AutoEnv._resolve_space_url("wukaixingxp/coding-env-test") |
| 100 | + 'https://wukaixingxp-coding-env-test.hf.space' |
| 101 | + """ |
| 102 | + # Clean up repo_id if it's a full URL |
| 103 | + if "huggingface.co" in repo_id: |
| 104 | + # Extract org/repo from URL |
| 105 | + # https://huggingface.co/wukaixingxp/coding-env-test -> wukaixingxp/coding-env-test |
| 106 | + parts = repo_id.split("/") |
| 107 | + if len(parts) >= 2: |
| 108 | + repo_id = f"{parts[-2]}/{parts[-1]}" |
| 109 | + |
| 110 | + # Convert user/space-name to user-space-name.hf.space |
| 111 | + space_slug = repo_id.replace("/", "-") |
| 112 | + return f"https://{space_slug}.hf.space" |
| 113 | + |
| 114 | + @classmethod |
| 115 | + def _check_space_availability(cls, space_url: str, timeout: float = 5.0) -> bool: |
| 116 | + """ |
| 117 | + Check if HuggingFace Space is running and accessible. |
| 118 | +
|
| 119 | + Args: |
| 120 | + space_url: Space URL to check |
| 121 | + timeout: Request timeout in seconds |
| 122 | +
|
| 123 | + Returns: |
| 124 | + True if Space is accessible, False otherwise |
| 125 | +
|
| 126 | + Examples: |
| 127 | + >>> AutoEnv._check_space_availability("https://wukaixingxp-coding-env-test.hf.space") |
| 128 | + True |
| 129 | + """ |
| 130 | + try: |
| 131 | + # Try to access the health endpoint |
| 132 | + response = requests.get(f"{space_url}/health", timeout=timeout) |
| 133 | + if response.status_code == 200: |
| 134 | + return True |
| 135 | + |
| 136 | + # If health endpoint doesn't exist, try root endpoint |
| 137 | + response = requests.get(space_url, timeout=timeout) |
| 138 | + return response.status_code == 200 |
| 139 | + except (requests.RequestException, Exception) as e: |
| 140 | + logger.debug(f"Space {space_url} not accessible: {e}") |
| 141 | + return False |
| 142 | + |
86 | 143 | @classmethod |
87 | 144 | def _download_from_hub( |
88 | 145 | cls, repo_id: str, cache_dir: Optional[Path] = None |
@@ -185,6 +242,94 @@ def _install_from_path(cls, env_path: Path) -> str: |
185 | 242 | except Exception as e: |
186 | 243 | raise ValueError(f"Failed to install environment package: {e}") from e |
187 | 244 |
|
| 245 | + @classmethod |
| 246 | + def _get_package_name_from_hub(cls, name: str) -> tuple[str, Path]: |
| 247 | + """ |
| 248 | + Download Space and get the package name from pyproject.toml. |
| 249 | + |
| 250 | + Args: |
| 251 | + name: HuggingFace repo ID (e.g., "wukaixingxp/coding-env-test") |
| 252 | + |
| 253 | + Returns: |
| 254 | + Tuple of (package_name, env_path) |
| 255 | + Example: ("openenv-coding_env", Path("/tmp/...")) |
| 256 | + """ |
| 257 | + # Download from Hub |
| 258 | + env_path = cls._download_from_hub(name) |
| 259 | + |
| 260 | + # Read package name from pyproject.toml |
| 261 | + import toml |
| 262 | + |
| 263 | + pyproject_path = env_path / "pyproject.toml" |
| 264 | + if not pyproject_path.exists(): |
| 265 | + raise ValueError( |
| 266 | + f"Environment directory does not contain pyproject.toml: {env_path}" |
| 267 | + ) |
| 268 | + |
| 269 | + with open(pyproject_path, "r") as f: |
| 270 | + pyproject = toml.load(f) |
| 271 | + |
| 272 | + package_name = pyproject.get("project", {}).get("name") |
| 273 | + if not package_name: |
| 274 | + raise ValueError( |
| 275 | + f"Could not determine package name from pyproject.toml at {pyproject_path}" |
| 276 | + ) |
| 277 | + |
| 278 | + return package_name, env_path |
| 279 | + |
| 280 | + @classmethod |
| 281 | + def _is_package_installed(cls, package_name: str) -> bool: |
| 282 | + """ |
| 283 | + Check if a package is already installed. |
| 284 | + |
| 285 | + Args: |
| 286 | + package_name: Package name (e.g., "openenv-coding_env") |
| 287 | + |
| 288 | + Returns: |
| 289 | + True if installed, False otherwise |
| 290 | + """ |
| 291 | + try: |
| 292 | + import importlib.metadata |
| 293 | + importlib.metadata.distribution(package_name) |
| 294 | + return True |
| 295 | + except importlib.metadata.PackageNotFoundError: |
| 296 | + return False |
| 297 | + |
| 298 | + @classmethod |
| 299 | + def _ensure_package_from_hub(cls, name: str) -> str: |
| 300 | + """ |
| 301 | + Ensure package from HuggingFace Hub is installed. |
| 302 | + |
| 303 | + Only downloads and installs if not already installed. |
| 304 | + |
| 305 | + Args: |
| 306 | + name: HuggingFace repo ID (e.g., "wukaixingxp/coding-env-test") |
| 307 | + |
| 308 | + Returns: |
| 309 | + Environment name (e.g., "coding_env") |
| 310 | + """ |
| 311 | + # Download and get actual package name from pyproject.toml |
| 312 | + logger.info(f"📦 Checking package from HuggingFace Space...") |
| 313 | + package_name, env_path = cls._get_package_name_from_hub(name) |
| 314 | + |
| 315 | + # Check if already installed |
| 316 | + if cls._is_package_installed(package_name): |
| 317 | + logger.info(f"✅ Package already installed: {package_name}") |
| 318 | + # Clear and refresh discovery cache to make sure it's detected |
| 319 | + get_discovery().clear_cache() |
| 320 | + get_discovery().discover(use_cache=False) |
| 321 | + else: |
| 322 | + # Not installed, install it now |
| 323 | + logger.info(f"📦 Package not found, installing: {package_name}") |
| 324 | + cls._install_from_path(env_path) |
| 325 | + # Clear discovery cache to pick up the newly installed package |
| 326 | + get_discovery().clear_cache() |
| 327 | + |
| 328 | + # Extract environment name from package name |
| 329 | + # "openenv-coding_env" -> "coding_env" |
| 330 | + env_name = package_name.replace("openenv-", "").replace("-", "_") |
| 331 | + return env_name |
| 332 | + |
188 | 333 | @classmethod |
189 | 334 | def from_name( |
190 | 335 | cls, |
@@ -243,16 +388,31 @@ def from_name( |
243 | 388 | """ |
244 | 389 | # Check if it's a HuggingFace Hub URL or repo ID |
245 | 390 | if _is_hub_url(name): |
246 | | - # Download from Hub and install |
247 | | - env_path = cls._download_from_hub(name) |
248 | | - package_name = cls._install_from_path(env_path) |
249 | | - |
250 | | - # Clear discovery cache to pick up the newly installed package |
251 | | - get_discovery().clear_cache() |
252 | | - |
253 | | - # Extract environment name from package name |
254 | | - # "openenv-coding_env" -> "coding_env" |
255 | | - env_name = package_name.replace("openenv-", "").replace("-", "_") |
| 391 | + # Try to connect to Space directly first |
| 392 | + space_url = cls._resolve_space_url(name) |
| 393 | + logger.info(f"Checking if HuggingFace Space is accessible: {space_url}") |
| 394 | + |
| 395 | + space_is_available = cls._check_space_availability(space_url) |
| 396 | + |
| 397 | + if space_is_available and base_url is None: |
| 398 | + # Space is accessible! We'll connect directly without Docker |
| 399 | + logger.info(f"✅ Space is accessible at: {space_url}") |
| 400 | + logger.info("📦 Installing package for client code (no Docker needed)...") |
| 401 | + |
| 402 | + # Ensure package is installed (downloads only if needed) |
| 403 | + env_name = cls._ensure_package_from_hub(name) |
| 404 | + |
| 405 | + # Set base_url to connect to remote Space |
| 406 | + base_url = space_url |
| 407 | + logger.info(f"🚀 Will connect to remote Space (no local Docker)") |
| 408 | + else: |
| 409 | + # Space not accessible or user provided explicit base_url |
| 410 | + if not space_is_available: |
| 411 | + logger.info(f"❌ Space not accessible at {space_url}") |
| 412 | + logger.info("📦 Falling back to local Docker mode...") |
| 413 | + |
| 414 | + # Ensure package is installed (downloads only if needed) |
| 415 | + env_name = cls._ensure_package_from_hub(name) |
256 | 416 | else: |
257 | 417 | env_name = name |
258 | 418 |
|
|
0 commit comments