openenv 0.4.1__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 (100) hide show
  1. {openenv-0.4.1/src/openenv.egg-info → openenv-0.4.2}/PKG-INFO +2 -2
  2. {openenv-0.4.1 → openenv-0.4.2}/pyproject.toml +2 -2
  3. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/build.py +26 -9
  4. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/collect.py +22 -21
  5. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/init.py +16 -10
  6. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/push.py +66 -51
  7. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/base.py +36 -0
  8. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/ors.py +5 -26
  9. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/verifiers.py +5 -26
  10. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/README.md +2 -2
  11. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/client.py +2 -2
  12. openenv-0.4.2/src/openenv/core/containers/runtime/_server_config.py +53 -0
  13. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/daytona_provider.py +3 -42
  14. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/modal_provider.py +3 -41
  15. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_client.py +249 -66
  16. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/interfaces.py +82 -6
  17. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/web_interface.py +69 -38
  18. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/generic_client.py +3 -3
  19. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/harness/__init__.py +13 -4
  20. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/llm_client.py +14 -14
  21. {openenv-0.4.1 → openenv-0.4.2/src/openenv.egg-info}/PKG-INFO +2 -2
  22. {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/SOURCES.txt +1 -0
  23. {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/requires.txt +1 -1
  24. {openenv-0.4.1 → openenv-0.4.2}/LICENSE +0 -0
  25. {openenv-0.4.1 → openenv-0.4.2}/MANIFEST.in +0 -0
  26. {openenv-0.4.1 → openenv-0.4.2}/README.md +0 -0
  27. {openenv-0.4.1 → openenv-0.4.2}/setup.cfg +0 -0
  28. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/__init__.py +0 -0
  29. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/auto/__init__.py +0 -0
  30. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/auto/_discovery.py +0 -0
  31. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/auto/auto_action.py +0 -0
  32. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/auto/auto_env.py +0 -0
  33. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/__init__.py +0 -0
  34. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/__main__.py +0 -0
  35. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/_cli_utils.py +0 -0
  36. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/_validation.py +0 -0
  37. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/__init__.py +0 -0
  38. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/fork.py +0 -0
  39. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/import_env.py +0 -0
  40. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/serve.py +0 -0
  41. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/skills.py +0 -0
  42. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/commands/validate.py +0 -0
  43. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/__init__.py +0 -0
  44. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/__init__.py +0 -0
  45. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/ors_app.py.tpl +0 -0
  46. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/ors_environment.py.tpl +0 -0
  47. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/verifiers_app.py.tpl +0 -0
  48. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/importers/templates/verifiers_environment.py.tpl +0 -0
  49. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/__init__.py +0 -0
  50. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/__init__.py +0 -0
  51. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/models.py +0 -0
  52. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/openenv.yaml +0 -0
  53. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/pyproject.toml +0 -0
  54. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/Dockerfile +0 -0
  55. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/__ENV_NAME___environment.py +0 -0
  56. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/__init__.py +0 -0
  57. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/app.py +0 -0
  58. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/cli/templates/openenv_env/server/requirements.txt +0 -0
  59. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/__init__.py +0 -0
  60. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/client_types.py +0 -0
  61. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/__init__.py +0 -0
  62. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/__init__.py +0 -0
  63. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/aca_provider.py +0 -0
  64. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/hf_sandbox_provider.py +0 -0
  65. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/providers.py +0 -0
  66. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/runtime/uv_provider.py +0 -0
  67. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/containers/test_local_docker_provider.py +0 -0
  68. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/__init__.py +0 -0
  69. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/_utils.py +0 -0
  70. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/base_transforms.py +0 -0
  71. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/exceptions.py +0 -0
  72. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/gradio_theme.py +0 -0
  73. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/gradio_ui.py +0 -0
  74. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/http_server.py +0 -0
  75. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/mcp_environment.py +0 -0
  76. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/mcp_types.py +0 -0
  77. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/route_config.py +0 -0
  78. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/serialization.py +0 -0
  79. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/env_server/types.py +0 -0
  80. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/evals/__init__.py +0 -0
  81. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/evals/base.py +0 -0
  82. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/evals/inspect_harness.py +0 -0
  83. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/evals/types.py +0 -0
  84. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/harness/collect.py +0 -0
  85. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/mcp_client.py +0 -0
  86. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/__init__.py +0 -0
  87. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/base.py +0 -0
  88. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/containers.py +0 -0
  89. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/llm_judge.py +0 -0
  90. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/rubrics/trajectory.py +0 -0
  91. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/sync_client.py +0 -0
  92. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/tools/__init__.py +0 -0
  93. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/tools/git_server_client.py +0 -0
  94. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/tools/local_python_executor.py +0 -0
  95. {openenv-0.4.1 → openenv-0.4.2}/src/openenv/core/utils.py +0 -0
  96. {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/dependency_links.txt +0 -0
  97. {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/entry_points.txt +0 -0
  98. {openenv-0.4.1 → openenv-0.4.2}/src/openenv.egg-info/top_level.txt +0 -0
  99. {openenv-0.4.1 → openenv-0.4.2}/src/openenv_core/__init__.py +0 -0
  100. {openenv-0.4.1 → 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.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openenv"
7
- version = "0.4.1"
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",
@@ -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)
@@ -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
@@ -257,6 +257,42 @@ class DetectedEnvironment:
257
257
  return f"{self.module_path}:{self.class_name}"
258
258
 
259
259
 
260
+ @dataclass(frozen=True)
261
+ class ImportPackageContext:
262
+ """Shared details produced while scaffolding an imported environment package."""
263
+
264
+ class_name_prefix: str
265
+ vendor_dir: str
266
+
267
+
268
+ def prepare_import_package(
269
+ source: Path, destination: Path, env_name: str
270
+ ) -> ImportPackageContext:
271
+ from openenv.cli.commands.init import (
272
+ _copy_template_directory,
273
+ _create_template_replacements,
274
+ )
275
+
276
+ replacements = _create_template_replacements(env_name)
277
+ _copy_template_directory(
278
+ "openenv.cli.templates.openenv_env",
279
+ "",
280
+ destination,
281
+ replacements,
282
+ env_name,
283
+ )
284
+
285
+ vendor_dir = safe_vendor_dir_name(source)
286
+ vendor_path = destination / "vendor" / vendor_dir
287
+ copy_source_tree(source, vendor_path)
288
+ ensure_vendor_package(vendor_path)
289
+
290
+ return ImportPackageContext(
291
+ class_name_prefix=replacements["__ENV_CLASS_NAME__"],
292
+ vendor_dir=vendor_dir,
293
+ )
294
+
295
+
260
296
  class EnvironmentImporter(Protocol):
261
297
  source_type: str
262
298
 
@@ -8,13 +8,11 @@ from pathlib import Path
8
8
  from .base import (
9
9
  append_dependency_files,
10
10
  collect_source_dependencies,
11
- copy_source_tree,
12
11
  DetectedEnvironment,
13
- ensure_vendor_package,
14
12
  iter_python_files,
15
13
  module_path,
14
+ prepare_import_package,
16
15
  render_importer_template,
17
- safe_vendor_dir_name,
18
16
  write_text,
19
17
  )
20
18
 
@@ -209,39 +207,20 @@ class ORSImporter:
209
207
  env_name: str,
210
208
  detected: DetectedEnvironment,
211
209
  ) -> None:
212
- from openenv.cli.commands.init import (
213
- _copy_template_directory,
214
- _create_template_replacements,
215
- )
216
-
217
- replacements = _create_template_replacements(env_name)
218
- _copy_template_directory(
219
- "openenv.cli.templates.openenv_env",
220
- "",
221
- destination,
222
- replacements,
223
- env_name,
224
- )
225
-
226
- vendor_dir = safe_vendor_dir_name(source)
227
- vendor_path = destination / "vendor" / vendor_dir
228
- copy_source_tree(source, vendor_path)
229
- ensure_vendor_package(vendor_path)
230
-
231
- prefix = replacements["__ENV_CLASS_NAME__"]
210
+ package = prepare_import_package(source, destination, env_name)
232
211
  write_text(
233
212
  destination / "server" / f"{env_name}_environment.py",
234
213
  _wrapper_source(
235
214
  env_name=env_name,
236
- class_name_prefix=prefix,
215
+ class_name_prefix=package.class_name_prefix,
237
216
  source_module=detected.module_path,
238
217
  source_class=detected.class_name,
239
- vendor_dir=vendor_dir,
218
+ vendor_dir=package.vendor_dir,
240
219
  ),
241
220
  )
242
221
  write_text(
243
222
  destination / "server" / "app.py",
244
- _app_source(env_name=env_name, class_name_prefix=prefix),
223
+ _app_source(env_name=env_name, class_name_prefix=package.class_name_prefix),
245
224
  )
246
225
  dependencies = collect_source_dependencies(source)
247
226
  for dependency in detect_ors_dependencies(source):
@@ -8,13 +8,11 @@ from pathlib import Path
8
8
  from .base import (
9
9
  append_dependency_files,
10
10
  collect_source_dependencies,
11
- copy_source_tree,
12
11
  DetectedEnvironment,
13
- ensure_vendor_package,
14
12
  iter_python_files,
15
13
  module_path,
14
+ prepare_import_package,
16
15
  render_importer_template,
17
- safe_vendor_dir_name,
18
16
  write_text,
19
17
  )
20
18
 
@@ -110,38 +108,19 @@ class VerifiersImporter:
110
108
  env_name: str,
111
109
  detected: DetectedEnvironment,
112
110
  ) -> None:
113
- from openenv.cli.commands.init import (
114
- _copy_template_directory,
115
- _create_template_replacements,
116
- )
117
-
118
- replacements = _create_template_replacements(env_name)
119
- _copy_template_directory(
120
- "openenv.cli.templates.openenv_env",
121
- "",
122
- destination,
123
- replacements,
124
- env_name,
125
- )
126
-
127
- vendor_dir = safe_vendor_dir_name(source)
128
- vendor_path = destination / "vendor" / vendor_dir
129
- copy_source_tree(source, vendor_path)
130
- ensure_vendor_package(vendor_path)
131
-
132
- prefix = replacements["__ENV_CLASS_NAME__"]
111
+ package = prepare_import_package(source, destination, env_name)
133
112
  write_text(
134
113
  destination / "server" / f"{env_name}_environment.py",
135
114
  _wrapper_source(
136
115
  env_name=env_name,
137
- class_name_prefix=prefix,
116
+ class_name_prefix=package.class_name_prefix,
138
117
  source_module=detected.module_path,
139
- vendor_dir=vendor_dir,
118
+ vendor_dir=package.vendor_dir,
140
119
  ),
141
120
  )
142
121
  write_text(
143
122
  destination / "server" / "app.py",
144
- _app_source(env_name=env_name, class_name_prefix=prefix),
123
+ _app_source(env_name=env_name, class_name_prefix=package.class_name_prefix),
145
124
  )
146
125
  dependencies = collect_source_dependencies(source)
147
126
  if "verifiers>=0.1.14" not in dependencies:
@@ -23,8 +23,8 @@ The simplest way to use the __ENV_TITLE_NAME__ environment is through the `__ENV
23
23
  from __ENV_NAME__ import __ENV_CLASS_NAME__Action, __ENV_CLASS_NAME__Env
24
24
 
25
25
  try:
26
- # Create environment from Docker image
27
- __ENV_NAME__env = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest")
26
+ # Create environment from Docker image (.sync() for synchronous use)
27
+ __ENV_NAME__env = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest").sync()
28
28
 
29
29
  # Reset
30
30
  result = __ENV_NAME__env.reset()
@@ -31,8 +31,8 @@ class __ENV_CLASS_NAME__Env(
31
31
  ... print(result.observation.echoed_message)
32
32
 
33
33
  Example with Docker:
34
- >>> # Automatically start container and connect
35
- >>> client = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest")
34
+ >>> # Automatically start container and connect (.sync() for sync use)
35
+ >>> client = __ENV_CLASS_NAME__Env.from_docker_image("__ENV_NAME__-env:latest").sync()
36
36
  >>> try:
37
37
  ... result = client.reset()
38
38
  ... result = client.step(__ENV_CLASS_NAME__Action(message="Test"))
@@ -0,0 +1,53 @@
1
+ # SPDX-License-Identifier: BSD-3-Clause
2
+
3
+ """Shared helpers for reading environment server configuration."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import re
9
+
10
+ import yaml
11
+
12
+
13
+ def parse_openenv_app_field(yaml_content: str) -> str | None:
14
+ """Extract the top-level ``app`` value from raw ``openenv.yaml`` content."""
15
+ try:
16
+ data = yaml.safe_load(yaml_content) or {}
17
+ except Exception:
18
+ return None
19
+
20
+ if not isinstance(data, dict):
21
+ return None
22
+
23
+ value = data.get("app")
24
+ if isinstance(value, str):
25
+ value = value.strip()
26
+ return value if value else None
27
+ return None
28
+
29
+
30
+ def parse_dockerfile_cmd(dockerfile_content: str) -> str | None:
31
+ """Extract the server command from the last Dockerfile ``CMD``."""
32
+ last_cmd: str | None = None
33
+ for line in dockerfile_content.splitlines():
34
+ stripped = line.strip()
35
+ if stripped.startswith("#"):
36
+ continue
37
+ match = re.match(r"CMD\s+(.+)", stripped, flags=re.IGNORECASE)
38
+ if match:
39
+ last_cmd = match.group(1).strip()
40
+
41
+ if last_cmd is None:
42
+ return None
43
+
44
+ if last_cmd.startswith("["):
45
+ try:
46
+ parts = json.loads(last_cmd)
47
+ if isinstance(parts, list) and all(isinstance(p, str) for p in parts):
48
+ return " ".join(parts)
49
+ except (json.JSONDecodeError, TypeError):
50
+ # Invalid exec-form JSON falls back to the raw CMD below.
51
+ pass
52
+
53
+ return last_cmd if last_cmd else None
@@ -8,14 +8,12 @@ Requires the ``daytona`` SDK: ``pip install daytona>=0.10``
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import json
12
11
  import os
13
12
  import shlex
14
13
  import time
15
14
  from typing import Any, Callable, Dict, Optional
16
15
 
17
- import yaml
18
-
16
+ from ._server_config import parse_dockerfile_cmd, parse_openenv_app_field
19
17
  from .providers import ContainerProvider
20
18
 
21
19
 
@@ -149,19 +147,7 @@ class DaytonaProvider(ContainerProvider):
149
147
 
150
148
  Uses PyYAML to handle comments, quotes, and nested keys correctly.
151
149
  """
152
- try:
153
- data = yaml.safe_load(yaml_content) or {}
154
- except Exception:
155
- return None
156
-
157
- if not isinstance(data, dict):
158
- return None
159
-
160
- value = data.get("app")
161
- if isinstance(value, str):
162
- value = value.strip()
163
- return value if value else None
164
- return None
150
+ return parse_openenv_app_field(yaml_content)
165
151
 
166
152
  @staticmethod
167
153
  def _parse_dockerfile_cmd(dockerfile_content: str) -> Optional[str]:
@@ -176,32 +162,7 @@ class DaytonaProvider(ContainerProvider):
176
162
  Returns:
177
163
  The command as a single string, or ``None`` if no ``CMD`` found.
178
164
  """
179
- import re
180
-
181
- last_cmd: Optional[str] = None
182
- for line in dockerfile_content.splitlines():
183
- stripped = line.strip()
184
- # Skip comments
185
- if stripped.startswith("#"):
186
- continue
187
- match = re.match(r"CMD\s+(.+)", stripped, flags=re.IGNORECASE)
188
- if match:
189
- last_cmd = match.group(1).strip()
190
-
191
- if last_cmd is None:
192
- return None
193
-
194
- # Exec form: CMD ["executable", "param1", ...]
195
- if last_cmd.startswith("["):
196
- try:
197
- parts = json.loads(last_cmd)
198
- if isinstance(parts, list) and all(isinstance(p, str) for p in parts):
199
- return " ".join(parts)
200
- except (json.JSONDecodeError, TypeError):
201
- pass
202
-
203
- # Shell form: CMD executable param1 ...
204
- return last_cmd if last_cmd else None
165
+ return parse_dockerfile_cmd(dockerfile_content)
205
166
 
206
167
  @staticmethod
207
168
  def strip_buildkit_syntax(dockerfile_content: str) -> str: