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.
- {openenv-0.4.0/src/openenv.egg-info → openenv-0.4.2}/PKG-INFO +6 -2
- {openenv-0.4.0 → openenv-0.4.2}/README.md +4 -0
- {openenv-0.4.0 → openenv-0.4.2}/pyproject.toml +3 -2
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/__main__.py +4 -0
- openenv-0.4.2/src/openenv/cli/commands/__init__.py +16 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/build.py +26 -9
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/collect.py +22 -21
- openenv-0.4.2/src/openenv/cli/commands/import_env.py +145 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/init.py +16 -10
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/push.py +66 -51
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/serve.py +1 -1
- openenv-0.4.2/src/openenv/cli/importers/__init__.py +16 -0
- openenv-0.4.2/src/openenv/cli/importers/base.py +344 -0
- openenv-0.4.2/src/openenv/cli/importers/ors.py +229 -0
- openenv-0.4.2/src/openenv/cli/importers/templates/__init__.py +1 -0
- openenv-0.4.2/src/openenv/cli/importers/templates/ors_app.py.tpl +30 -0
- openenv-0.4.2/src/openenv/cli/importers/templates/ors_environment.py.tpl +283 -0
- openenv-0.4.2/src/openenv/cli/importers/templates/verifiers_app.py.tpl +30 -0
- openenv-0.4.2/src/openenv/cli/importers/templates/verifiers_environment.py.tpl +404 -0
- openenv-0.4.2/src/openenv/cli/importers/verifiers.py +128 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/README.md +2 -2
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/client.py +2 -2
- openenv-0.4.2/src/openenv/core/containers/runtime/_server_config.py +53 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/daytona_provider.py +3 -42
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/modal_provider.py +3 -41
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_client.py +249 -66
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/__init__.py +10 -1
- openenv-0.4.2/src/openenv/core/env_server/_utils.py +10 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/http_server.py +195 -15
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/interfaces.py +111 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/types.py +27 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/web_interface.py +78 -40
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/evals/inspect_harness.py +32 -40
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/generic_client.py +3 -3
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/harness/__init__.py +13 -4
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/llm_client.py +14 -14
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/mcp_client.py +21 -3
- {openenv-0.4.0 → openenv-0.4.2/src/openenv.egg-info}/PKG-INFO +6 -2
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/SOURCES.txt +12 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/requires.txt +1 -1
- openenv-0.4.0/src/openenv/cli/commands/__init__.py +0 -7
- {openenv-0.4.0 → openenv-0.4.2}/LICENSE +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/MANIFEST.in +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/setup.cfg +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/auto/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/auto/_discovery.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/auto/auto_action.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/auto/auto_env.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/_cli_utils.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/_validation.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/fork.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/skills.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/commands/validate.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/models.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/openenv.yaml +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/pyproject.toml +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/Dockerfile +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/__ENV_NAME___environment.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/app.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/requirements.txt +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/client_types.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/aca_provider.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/hf_sandbox_provider.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/providers.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/runtime/uv_provider.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/containers/test_local_docker_provider.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/base_transforms.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/exceptions.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/gradio_theme.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/gradio_ui.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/mcp_environment.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/mcp_types.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/route_config.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/env_server/serialization.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/evals/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/evals/base.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/evals/types.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/harness/collect.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/base.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/containers.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/llm_judge.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/rubrics/trajectory.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/sync_client.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/tools/__init__.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/tools/git_server_client.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/tools/local_python_executor.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv/core/utils.py +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/dependency_links.txt +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/entry_points.txt +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv.egg-info/top_level.txt +0 -0
- {openenv-0.4.0 → openenv-0.4.2}/src/openenv_core/__init__.py +0 -0
- {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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
|
302
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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:
|
|
410
|
-
colorTo:
|
|
424
|
+
colorFrom: blue
|
|
425
|
+
colorTo: indigo
|
|
411
426
|
sdk: docker
|
|
412
427
|
pinned: false
|
|
413
428
|
app_port: 8000
|
|
@@ -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
|
+
]
|