openenv 0.4.0__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. {openenv-0.4.0/src/openenv.egg-info → openenv-0.4.2}/PKG-INFO +6 -2
  2. {openenv-0.4.0 → openenv-0.4.2}/README.md +4 -0
  3. {openenv-0.4.0 → openenv-0.4.2}/pyproject.toml +3 -2
  4. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/__main__.py +4 -0
  5. openenv-0.4.2/src/openenv/cli/commands/__init__.py +16 -0
  6. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/build.py +26 -9
  7. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/collect.py +22 -21
  8. openenv-0.4.2/src/openenv/cli/commands/import_env.py +145 -0
  9. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/init.py +16 -10
  10. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/push.py +66 -51
  11. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/serve.py +1 -1
  12. openenv-0.4.2/src/openenv/cli/importers/__init__.py +16 -0
  13. openenv-0.4.2/src/openenv/cli/importers/base.py +344 -0
  14. openenv-0.4.2/src/openenv/cli/importers/ors.py +229 -0
  15. openenv-0.4.2/src/openenv/cli/importers/templates/__init__.py +1 -0
  16. openenv-0.4.2/src/openenv/cli/importers/templates/ors_app.py.tpl +30 -0
  17. openenv-0.4.2/src/openenv/cli/importers/templates/ors_environment.py.tpl +283 -0
  18. openenv-0.4.2/src/openenv/cli/importers/templates/verifiers_app.py.tpl +30 -0
  19. openenv-0.4.2/src/openenv/cli/importers/templates/verifiers_environment.py.tpl +404 -0
  20. openenv-0.4.2/src/openenv/cli/importers/verifiers.py +128 -0
  21. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/README.md +2 -2
  22. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/client.py +2 -2
  23. openenv-0.4.2/src/openenv/core/containers/runtime/_server_config.py +53 -0
  24. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/daytona_provider.py +3 -42
  25. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/modal_provider.py +3 -41
  26. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_client.py +249 -66
  27. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/__init__.py +10 -1
  28. openenv-0.4.2/src/openenv/core/env_server/_utils.py +10 -0
  29. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/http_server.py +195 -15
  30. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/interfaces.py +111 -0
  31. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/types.py +27 -0
  32. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/web_interface.py +78 -40
  33. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/evals/inspect_harness.py +32 -40
  34. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/generic_client.py +3 -3
  35. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/harness/__init__.py +13 -4
  36. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/llm_client.py +14 -14
  37. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/mcp_client.py +21 -3
  38. {openenv-0.4.0 → openenv-0.4.2/src/openenv.egg-info}/PKG-INFO +6 -2
  39. {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/SOURCES.txt +12 -0
  40. {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/requires.txt +1 -1
  41. openenv-0.4.0/src/openenv/cli/commands/__init__.py +0 -7
  42. {openenv-0.4.0 → openenv-0.4.2}/LICENSE +0 -0
  43. {openenv-0.4.0 → openenv-0.4.2}/MANIFEST.in +0 -0
  44. {openenv-0.4.0 → openenv-0.4.2}/setup.cfg +0 -0
  45. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/__init__.py +0 -0
  46. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/auto/__init__.py +0 -0
  47. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/auto/_discovery.py +0 -0
  48. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/auto/auto_action.py +0 -0
  49. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/auto/auto_env.py +0 -0
  50. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/__init__.py +0 -0
  51. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/_cli_utils.py +0 -0
  52. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/_validation.py +0 -0
  53. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/fork.py +0 -0
  54. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/skills.py +0 -0
  55. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/validate.py +0 -0
  56. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/__init__.py +0 -0
  57. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/__init__.py +0 -0
  58. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/models.py +0 -0
  59. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/openenv.yaml +0 -0
  60. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/pyproject.toml +0 -0
  61. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/Dockerfile +0 -0
  62. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/__ENV_NAME___environment.py +0 -0
  63. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/__init__.py +0 -0
  64. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/app.py +0 -0
  65. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/requirements.txt +0 -0
  66. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/__init__.py +0 -0
  67. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/client_types.py +0 -0
  68. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/__init__.py +0 -0
  69. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/__init__.py +0 -0
  70. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/aca_provider.py +0 -0
  71. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/hf_sandbox_provider.py +0 -0
  72. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/providers.py +0 -0
  73. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/uv_provider.py +0 -0
  74. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/test_local_docker_provider.py +0 -0
  75. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/base_transforms.py +0 -0
  76. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/exceptions.py +0 -0
  77. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/gradio_theme.py +0 -0
  78. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/gradio_ui.py +0 -0
  79. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/mcp_environment.py +0 -0
  80. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/mcp_types.py +0 -0
  81. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/route_config.py +0 -0
  82. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/serialization.py +0 -0
  83. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/evals/__init__.py +0 -0
  84. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/evals/base.py +0 -0
  85. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/evals/types.py +0 -0
  86. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/harness/collect.py +0 -0
  87. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/__init__.py +0 -0
  88. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/base.py +0 -0
  89. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/containers.py +0 -0
  90. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/llm_judge.py +0 -0
  91. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/trajectory.py +0 -0
  92. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/sync_client.py +0 -0
  93. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/tools/__init__.py +0 -0
  94. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/tools/git_server_client.py +0 -0
  95. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/tools/local_python_executor.py +0 -0
  96. {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/utils.py +0 -0
  97. {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/dependency_links.txt +0 -0
  98. {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/entry_points.txt +0 -0
  99. {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/top_level.txt +0 -0
  100. {openenv-0.4.0 → openenv-0.4.2}/src/openenv_core/__init__.py +0 -0
  101. {openenv-0.4.0 → openenv-0.4.2}/tests/test_line_endings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openenv
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: A unified framework for reinforcement learning environments
5
5
  License-Expression: BSD-3-Clause
6
6
  Requires-Python: >=3.10
@@ -19,7 +19,7 @@ Requires-Dist: openai>=2.7.2
19
19
  Requires-Dist: tomli>=2.3.0
20
20
  Requires-Dist: tomli-w>=1.2.0
21
21
  Requires-Dist: websockets>=15.0.1
22
- Requires-Dist: fastmcp>=3.0.0
22
+ Requires-Dist: fastmcp<4.0.0,>=3.0.0
23
23
  Requires-Dist: gradio>=4.0.0
24
24
  Requires-Dist: httpx>=0.28.1
25
25
  Provides-Extra: daytona
@@ -305,6 +305,7 @@ See example scripts in `examples/` directory.
305
305
  The OpenEnv CLI provides commands to manage environments:
306
306
 
307
307
  - **`openenv init <env_name>`** - Initialize a new environment from template
308
+ - **`openenv import <source> --name <env_name> --output-dir <dir>`** - Wrap a supported third-party source environment, including ORS/OpenReward and Verifiers, as OpenEnv
308
309
  - **`openenv push [--repo-id <repo>] [--private]`** - Deploy environment to Hugging Face Spaces
309
310
  - **`openenv serve`** - Serve an environment locally with optional auto-reload
310
311
  - **`openenv build`** - Build the Docker image for an environment
@@ -317,6 +318,9 @@ The OpenEnv CLI provides commands to manage environments:
317
318
  # Create a new environment
318
319
  openenv init my_game_env
319
320
 
321
+ # Or import an ORS/OpenReward or Verifiers source environment
322
+ openenv import path/to/source --name my_game_env --output-dir .
323
+
320
324
  # Deploy to Hugging Face (will prompt for login if needed)
321
325
  cd my_game_env
322
326
  openenv push
@@ -268,6 +268,7 @@ See example scripts in `examples/` directory.
268
268
  The OpenEnv CLI provides commands to manage environments:
269
269
 
270
270
  - **`openenv init <env_name>`** - Initialize a new environment from template
271
+ - **`openenv import <source> --name <env_name> --output-dir <dir>`** - Wrap a supported third-party source environment, including ORS/OpenReward and Verifiers, as OpenEnv
271
272
  - **`openenv push [--repo-id <repo>] [--private]`** - Deploy environment to Hugging Face Spaces
272
273
  - **`openenv serve`** - Serve an environment locally with optional auto-reload
273
274
  - **`openenv build`** - Build the Docker image for an environment
@@ -280,6 +281,9 @@ The OpenEnv CLI provides commands to manage environments:
280
281
  # Create a new environment
281
282
  openenv init my_game_env
282
283
 
284
+ # Or import an ORS/OpenReward or Verifiers source environment
285
+ openenv import path/to/source --name my_game_env --output-dir .
286
+
283
287
  # Deploy to Hugging Face (will prompt for login if needed)
284
288
  cd my_game_env
285
289
  openenv push
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openenv"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "A unified framework for reinforcement learning environments"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -29,7 +29,7 @@ dependencies = [
29
29
  "tomli-w>=1.2.0",
30
30
  "websockets>=15.0.1",
31
31
  # MCP support
32
- "fastmcp>=3.0.0",
32
+ "fastmcp>=3.0.0,<4.0.0",
33
33
  # Web UI dependencies
34
34
  "gradio>=4.0.0",
35
35
  "httpx>=0.28.1",
@@ -65,6 +65,7 @@ include-package-data = true
65
65
 
66
66
  [tool.setuptools.package-data]
67
67
  "openenv.cli" = ["templates/**/*"]
68
+ "openenv.cli.importers" = ["templates/**/*"]
68
69
 
69
70
  [tool.setuptools.exclude-package-data]
70
71
  "*" = ["*.pyc", "*.pyo", "__pycache__/*"]
@@ -14,6 +14,7 @@ from openenv.cli.commands import (
14
14
  build,
15
15
  collect,
16
16
  fork,
17
+ import_env,
17
18
  init,
18
19
  push,
19
20
  serve,
@@ -30,6 +31,9 @@ app = typer.Typer(
30
31
 
31
32
  # Register commands
32
33
  app.command(name="init", help="Initialize a new OpenEnv environment")(init.init)
34
+ app.command(name="import", help="Import a third-party environment into OpenEnv")(
35
+ import_env.import_env
36
+ )
33
37
  app.command(name="build", help="Build Docker images for OpenEnv environments")(
34
38
  build.build
35
39
  )
@@ -0,0 +1,16 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+
3
+ """OpenEnv CLI commands."""
4
+
5
+ from . import build, fork, import_env, init, push, serve, skills, validate
6
+
7
+ __all__ = [
8
+ "build",
9
+ "fork",
10
+ "import_env",
11
+ "init",
12
+ "push",
13
+ "serve",
14
+ "skills",
15
+ "validate",
16
+ ]
@@ -293,19 +293,36 @@ def _build_docker_image(
293
293
  build_args["BUILD_MODE"] = build_mode
294
294
  build_args["ENV_NAME"] = env_path.name.replace("_env", "")
295
295
 
296
- # Build Docker command
297
- cmd = ["docker", "build", "-t", tag, "-f", str(dockerfile)]
296
+ cmd = _docker_build_command(
297
+ tag=tag,
298
+ dockerfile=dockerfile,
299
+ build_dir=build_dir,
300
+ build_args=build_args,
301
+ no_cache=no_cache,
302
+ )
298
303
 
299
- if no_cache:
300
- cmd.append("--no-cache")
304
+ result = _run_command(cmd, check=False)
305
+ return result.returncode == 0
301
306
 
302
- for key, value in build_args.items():
303
- cmd.extend(["--build-arg", f"{key}={value}"])
304
307
 
305
- cmd.append(str(build_dir))
308
+ def _docker_build_command(
309
+ tag: str,
310
+ dockerfile: Path,
311
+ build_dir: Path,
312
+ build_args: dict[str, str],
313
+ no_cache: bool,
314
+ ) -> list[str]:
315
+ """Build the docker build command for an OpenEnv environment."""
316
+ cmd = ["docker", "build", "-t", tag, "-f", str(dockerfile)]
306
317
 
307
- result = _run_command(cmd, check=False)
308
- return result.returncode == 0
318
+ if no_cache:
319
+ cmd.append("--no-cache")
320
+
321
+ for key, value in build_args.items():
322
+ cmd.extend(["--build-arg", f"{key}={value}"])
323
+
324
+ cmd.append(str(build_dir))
325
+ return cmd
309
326
 
310
327
 
311
328
  def _push_docker_image(tag: str, registry: str | None = None) -> bool:
@@ -86,6 +86,28 @@ def _resolve_api_key(provider: str) -> str:
86
86
  )
87
87
 
88
88
 
89
+ def _extract_legal_actions(messages: list[dict[str, Any]]) -> list[int]:
90
+ for message in reversed(messages):
91
+ content = message.get("content") or ""
92
+ if not isinstance(content, str):
93
+ continue
94
+ try:
95
+ payload = json.loads(content)
96
+ except (json.JSONDecodeError, ValueError):
97
+ payload = None
98
+ if isinstance(payload, dict) and "legal_actions" in payload:
99
+ legal = payload["legal_actions"]
100
+ if isinstance(legal, list):
101
+ return [int(a) for a in legal]
102
+ match = re.search(r"Legal actions:\s*\[([^\]]*)\]", content)
103
+ if match:
104
+ raw = match.group(1).strip()
105
+ if not raw:
106
+ return []
107
+ return [int(x) for x in raw.split(",")]
108
+ return []
109
+
110
+
89
111
  def _build_scripted_model_step():
90
112
  """Default teacher: pick the first legal action from the latest observation.
91
113
 
@@ -93,27 +115,6 @@ def _build_scripted_model_step():
93
115
  users smoke-test the CLI without any API key configured.
94
116
  """
95
117
 
96
- def _extract_legal_actions(messages: list[dict[str, Any]]) -> list[int]:
97
- for message in reversed(messages):
98
- content = message.get("content") or ""
99
- if not isinstance(content, str):
100
- continue
101
- try:
102
- payload = json.loads(content)
103
- except (json.JSONDecodeError, ValueError):
104
- payload = None
105
- if isinstance(payload, dict) and "legal_actions" in payload:
106
- legal = payload["legal_actions"]
107
- if isinstance(legal, list):
108
- return [int(a) for a in legal]
109
- match = re.search(r"Legal actions:\s*\[([^\]]*)\]", content)
110
- if match:
111
- raw = match.group(1).strip()
112
- if not raw:
113
- return []
114
- return [int(x) for x in raw.split(",")]
115
- return []
116
-
117
118
  def model_step(messages, tools, sampling):
118
119
  del sampling
119
120
  legal = _extract_legal_actions(messages)
@@ -0,0 +1,145 @@
1
+ """Import third-party environments into OpenEnv wrappers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+ from openenv.cli.importers import DEFAULT_IMPORTERS, ImporterRegistry
11
+
12
+ from .._cli_utils import console
13
+ from .init import _generate_uv_lock, _validate_env_name
14
+
15
+
16
+ def _select_match(
17
+ registry: ImporterRegistry,
18
+ source: Path,
19
+ source_type: str | None,
20
+ env_class: str | None,
21
+ ):
22
+ try:
23
+ matches = registry.detect(source, source_type=source_type)
24
+ except ValueError as e:
25
+ raise typer.BadParameter(str(e)) from e
26
+ if not matches:
27
+ supported = ", ".join(registry.supported_types)
28
+ raise typer.BadParameter(
29
+ f"No supported environment found in {source}. Supported source types: {supported}."
30
+ )
31
+
32
+ if env_class:
33
+ matches = [
34
+ match
35
+ for match in matches
36
+ if match[1].class_name == env_class or match[1].qualified_name == env_class
37
+ ]
38
+ if not matches:
39
+ raise typer.BadParameter(f"No detected environment matched {env_class!r}.")
40
+
41
+ detected_types = {importer.source_type for importer, _ in matches}
42
+ if source_type is None and len(detected_types) > 1:
43
+ raise typer.BadParameter(
44
+ "Multiple source formats were detected. Re-run with --type "
45
+ f"({'/'.join(sorted(detected_types))})."
46
+ )
47
+
48
+ if len(matches) > 1:
49
+ choices = ", ".join(detected.qualified_name for _, detected in matches)
50
+ raise typer.BadParameter(
51
+ f"Multiple environment entrypoints were detected ({choices}). "
52
+ "Re-run with --env-class."
53
+ )
54
+
55
+ return matches[0]
56
+
57
+
58
+ def import_env(
59
+ source: Annotated[
60
+ str,
61
+ typer.Argument(help="Local source repository or directory to import"),
62
+ ],
63
+ name: Annotated[
64
+ str,
65
+ typer.Option("--name", "-n", help="Name for the generated OpenEnv package"),
66
+ ],
67
+ output_dir: Annotated[
68
+ str,
69
+ typer.Option(
70
+ "--output-dir",
71
+ "-o",
72
+ help="Directory where the generated package will be created",
73
+ ),
74
+ ],
75
+ env_class: Annotated[
76
+ str | None,
77
+ typer.Option(
78
+ "--env-class",
79
+ help="Environment class name or module:Class when detection is ambiguous",
80
+ ),
81
+ ] = None,
82
+ source_type: Annotated[
83
+ str | None,
84
+ typer.Option(
85
+ "--type",
86
+ help="Optional source type override, such as 'ors'",
87
+ ),
88
+ ] = None,
89
+ ) -> None:
90
+ """Deterministically import a third-party environment into OpenEnv."""
91
+ env_name = _validate_env_name(name)
92
+ source_path = Path(source).expanduser().resolve()
93
+ if not source_path.exists() or not source_path.is_dir():
94
+ raise typer.BadParameter(f"Source must be an existing directory: {source_path}")
95
+
96
+ base_dir = Path(output_dir).expanduser().resolve()
97
+ env_dir = base_dir / env_name
98
+ try:
99
+ env_dir.relative_to(source_path)
100
+ except ValueError:
101
+ pass
102
+ else:
103
+ raise typer.BadParameter("Output directory must not be inside the source tree")
104
+
105
+ if env_dir.exists():
106
+ if env_dir.is_file():
107
+ raise typer.BadParameter(f"Path '{env_dir}' exists and is a file")
108
+ if any(env_dir.iterdir()):
109
+ raise typer.BadParameter(
110
+ f"Directory '{env_dir}' already exists and is not empty."
111
+ )
112
+
113
+ registry = ImporterRegistry(DEFAULT_IMPORTERS)
114
+ importer, detected = _select_match(
115
+ registry,
116
+ source_path,
117
+ source_type=source_type,
118
+ env_class=env_class,
119
+ )
120
+
121
+ try:
122
+ env_dir.mkdir(parents=True, exist_ok=True)
123
+ console.print(
124
+ "[bold cyan]Importing environment[/bold cyan] "
125
+ f"{detected.qualified_name} as '{env_name}' ({importer.source_type})"
126
+ )
127
+ importer.generate(
128
+ source=source_path,
129
+ destination=env_dir,
130
+ env_name=env_name,
131
+ detected=detected,
132
+ )
133
+
134
+ console.print("[bold green]OK[/bold green] Generated OpenEnv wrapper")
135
+ if _generate_uv_lock(env_dir):
136
+ console.print("[green]OK[/green] Generated uv.lock")
137
+ else:
138
+ console.print("[yellow]Warning:[/yellow] Could not generate uv.lock")
139
+
140
+ console.print(f"[bold green]Environment created at: {env_dir}[/bold green]")
141
+ except Exception as e:
142
+ if env_dir.exists() and env_dir.is_dir():
143
+ shutil.rmtree(env_dir, ignore_errors=True)
144
+ console.print(f"[bold red]Error:[/bold red] {e}")
145
+ raise typer.Exit(1) from e
@@ -298,16 +298,8 @@ def _copy_and_template_file(
298
298
  ) from e
299
299
 
300
300
 
301
- def _copy_template_directory(
302
- template_pkg: str,
303
- template_dir: str,
304
- dest_dir: Path,
305
- replacements: Dict[str, str],
306
- env_name: str,
307
- ) -> List[Path]:
308
- """Recursively copy template directory and apply replacements."""
309
- created_files: List[Path] = []
310
-
301
+ def _resolve_template_path(template_pkg: str, template_dir: str) -> Path:
302
+ """Resolve the template package directory used by `openenv init`."""
311
303
  # Get the package path using importlib.resources but avoid importing the template package
312
304
  # We'll use the package's __file__ to get the directory path
313
305
  import importlib
@@ -341,6 +333,20 @@ def _copy_template_directory(
341
333
  f"Template directory not found: {template_pkg}.{template_dir}"
342
334
  )
343
335
 
336
+ return template_path
337
+
338
+
339
+ def _copy_template_directory(
340
+ template_pkg: str,
341
+ template_dir: str,
342
+ dest_dir: Path,
343
+ replacements: Dict[str, str],
344
+ env_name: str,
345
+ ) -> List[Path]:
346
+ """Recursively copy template directory and apply replacements."""
347
+ created_files: List[Path] = []
348
+ template_path = _resolve_template_path(template_pkg, template_dir)
349
+
344
350
  # Walk through all files in template directory using Path
345
351
  for item in template_path.rglob("*"):
346
352
  if item.is_file():
@@ -239,6 +239,64 @@ def _validate_openenv_directory(directory: Path) -> tuple[str, dict]:
239
239
  return env_name, manifest
240
240
 
241
241
 
242
+ def _rewrite_dockerfile_for_space(
243
+ dockerfile_content: str,
244
+ *,
245
+ base_image: str | None,
246
+ enable_interface: bool,
247
+ ) -> tuple[str, list[str]]:
248
+ """Rewrite deployment Dockerfile content and return printed change labels."""
249
+ lines = dockerfile_content.split("\n")
250
+ new_lines = []
251
+ cmd_found = False
252
+ base_image_updated = False
253
+ web_interface_env_exists = "ENABLE_WEB_INTERFACE" in dockerfile_content
254
+ last_instruction = None
255
+
256
+ for line in lines:
257
+ stripped = line.strip()
258
+ token = stripped.split(maxsplit=1)[0] if stripped else ""
259
+ current_instruction = token.upper()
260
+
261
+ is_healthcheck_continuation = last_instruction == "HEALTHCHECK"
262
+
263
+ # Update base image if specified
264
+ if base_image and stripped.startswith("FROM") and not base_image_updated:
265
+ new_lines.append(f"FROM {base_image}")
266
+ base_image_updated = True
267
+ last_instruction = "FROM"
268
+ continue
269
+
270
+ if (
271
+ stripped.startswith("CMD")
272
+ and not cmd_found
273
+ and not web_interface_env_exists
274
+ and enable_interface
275
+ and not is_healthcheck_continuation
276
+ ):
277
+ new_lines.append("ENV ENABLE_WEB_INTERFACE=true")
278
+ cmd_found = True
279
+
280
+ new_lines.append(line)
281
+
282
+ if current_instruction:
283
+ last_instruction = current_instruction
284
+
285
+ if not cmd_found and not web_interface_env_exists and enable_interface:
286
+ new_lines.append("ENV ENABLE_WEB_INTERFACE=true")
287
+
288
+ if base_image and not base_image_updated:
289
+ new_lines.insert(0, f"FROM {base_image}")
290
+
291
+ changes = []
292
+ if base_image and base_image_updated:
293
+ changes.append("updated base image")
294
+ if enable_interface and not web_interface_env_exists:
295
+ changes.append("enabled web interface")
296
+
297
+ return "\n".join(new_lines), changes
298
+
299
+
242
300
  def _get_hf_username() -> str:
243
301
  """Return the authenticated Hugging Face username from whoami()."""
244
302
  username = _extract_hf_username(whoami())
@@ -323,55 +381,12 @@ def _prepare_staging_directory(
323
381
  # Modify Dockerfile to optionally enable web interface and update base image
324
382
  if dockerfile_path and dockerfile_path.exists():
325
383
  dockerfile_content = dockerfile_path.read_text()
326
- lines = dockerfile_content.split("\n")
327
- new_lines = []
328
- cmd_found = False
329
- base_image_updated = False
330
- web_interface_env_exists = "ENABLE_WEB_INTERFACE" in dockerfile_content
331
- last_instruction = None
332
-
333
- for line in lines:
334
- stripped = line.strip()
335
- token = stripped.split(maxsplit=1)[0] if stripped else ""
336
- current_instruction = token.upper()
337
-
338
- is_healthcheck_continuation = last_instruction == "HEALTHCHECK"
339
-
340
- # Update base image if specified
341
- if base_image and stripped.startswith("FROM") and not base_image_updated:
342
- new_lines.append(f"FROM {base_image}")
343
- base_image_updated = True
344
- last_instruction = "FROM"
345
- continue
346
-
347
- if (
348
- stripped.startswith("CMD")
349
- and not cmd_found
350
- and not web_interface_env_exists
351
- and enable_interface
352
- and not is_healthcheck_continuation
353
- ):
354
- new_lines.append("ENV ENABLE_WEB_INTERFACE=true")
355
- cmd_found = True
356
-
357
- new_lines.append(line)
358
-
359
- if current_instruction:
360
- last_instruction = current_instruction
361
-
362
- if not cmd_found and not web_interface_env_exists and enable_interface:
363
- new_lines.append("ENV ENABLE_WEB_INTERFACE=true")
364
-
365
- if base_image and not base_image_updated:
366
- new_lines.insert(0, f"FROM {base_image}")
367
-
368
- dockerfile_path.write_text("\n".join(new_lines))
369
-
370
- changes = []
371
- if base_image and base_image_updated:
372
- changes.append("updated base image")
373
- if enable_interface and not web_interface_env_exists:
374
- changes.append("enabled web interface")
384
+ dockerfile_content, changes = _rewrite_dockerfile_for_space(
385
+ dockerfile_content,
386
+ base_image=base_image,
387
+ enable_interface=enable_interface,
388
+ )
389
+ dockerfile_path.write_text(dockerfile_content)
375
390
  if changes:
376
391
  console.print(
377
392
  f"[bold green]✓[/bold green] Updated Dockerfile: {', '.join(changes)}"
@@ -406,8 +421,8 @@ def _prepare_staging_directory(
406
421
  frontmatter = f"""---
407
422
  title: {env_name.replace("_", " ").title()} Environment Server
408
423
  emoji: 🔊
409
- colorFrom: '#00C9FF'
410
- colorTo: '#1B2845'
424
+ colorFrom: blue
425
+ colorTo: indigo
411
426
  sdk: docker
412
427
  pinned: false
413
428
  app_port: 8000
@@ -87,4 +87,4 @@ def serve(
87
87
  console.print(" From an environment directory with pyproject.toml:")
88
88
  console.print(f" [dim]$ uv run --project . server --port {port}[/dim]\n")
89
89
 
90
- raise typer.Exit(0)
90
+ raise typer.Exit(1)
@@ -0,0 +1,16 @@
1
+ """Deterministic source importers for OpenEnv environments."""
2
+
3
+ from .base import DetectedEnvironment, EnvironmentImporter, ImporterRegistry
4
+ from .ors import ORSImporter
5
+ from .verifiers import VerifiersImporter
6
+
7
+ DEFAULT_IMPORTERS = [ORSImporter(), VerifiersImporter()]
8
+
9
+ __all__ = [
10
+ "DEFAULT_IMPORTERS",
11
+ "DetectedEnvironment",
12
+ "EnvironmentImporter",
13
+ "ImporterRegistry",
14
+ "ORSImporter",
15
+ "VerifiersImporter",
16
+ ]