plato-sdk-v2 2.7.6__py3-none-any.whl → 2.7.7__py3-none-any.whl
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.
- plato/cli/__init__.py +5 -0
- plato/cli/agent.py +1209 -0
- plato/cli/audit_ui.py +316 -0
- plato/cli/chronos.py +817 -0
- plato/cli/main.py +193 -0
- plato/cli/pm.py +1206 -0
- plato/cli/proxy.py +222 -0
- plato/cli/sandbox.py +808 -0
- plato/cli/utils.py +200 -0
- plato/cli/verify.py +690 -0
- plato/cli/world.py +250 -0
- plato/v1/cli/pm.py +4 -1
- plato/v2/__init__.py +2 -0
- plato/v2/models.py +42 -0
- plato/v2/sync/__init__.py +6 -0
- plato/v2/sync/client.py +6 -3
- plato/v2/sync/sandbox.py +1461 -0
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.7.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.7.dist-info}/RECORD +21 -9
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.7.dist-info}/entry_points.txt +1 -1
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.7.dist-info}/WHEEL +0 -0
plato/cli/world.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""World CLI commands for Plato."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from plato.cli.utils import console, require_api_key
|
|
12
|
+
|
|
13
|
+
world_app = typer.Typer(help="Manage and deploy worlds")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_module_name(pkg_path: Path, package_name: str) -> str:
|
|
17
|
+
"""Get the actual importable module name from pyproject.toml or package name."""
|
|
18
|
+
try:
|
|
19
|
+
import tomli
|
|
20
|
+
|
|
21
|
+
pyproject_file = pkg_path / "pyproject.toml"
|
|
22
|
+
if pyproject_file.exists():
|
|
23
|
+
with open(pyproject_file, "rb") as f:
|
|
24
|
+
pyproject = tomli.load(f)
|
|
25
|
+
|
|
26
|
+
# Check hatch config for packages
|
|
27
|
+
packages = (
|
|
28
|
+
pyproject.get("tool", {})
|
|
29
|
+
.get("hatch", {})
|
|
30
|
+
.get("build", {})
|
|
31
|
+
.get("targets", {})
|
|
32
|
+
.get("wheel", {})
|
|
33
|
+
.get("packages", [])
|
|
34
|
+
)
|
|
35
|
+
if packages:
|
|
36
|
+
# Extract module name from path like "src/code_world"
|
|
37
|
+
module_path = packages[0]
|
|
38
|
+
return module_path.split("/")[-1]
|
|
39
|
+
|
|
40
|
+
# Check setuptools config
|
|
41
|
+
packages = pyproject.get("tool", {}).get("setuptools", {}).get("packages", [])
|
|
42
|
+
if packages:
|
|
43
|
+
return packages[0]
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# Fall back to normalized package name
|
|
48
|
+
return package_name.replace("-", "_")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _extract_schema_from_wheel(wheel_path: Path, module_name: str) -> dict | None:
|
|
52
|
+
"""Extract schema.json from a built wheel file."""
|
|
53
|
+
try:
|
|
54
|
+
with zipfile.ZipFile(wheel_path, "r") as zf:
|
|
55
|
+
# Look for schema.json in the module directory
|
|
56
|
+
schema_path = f"{module_name}/schema.json"
|
|
57
|
+
if schema_path in zf.namelist():
|
|
58
|
+
with zf.open(schema_path) as f:
|
|
59
|
+
return json.load(f)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
console.print(f"[yellow]Warning: Could not read schema from wheel: {e}[/yellow]")
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@world_app.command(name="publish")
|
|
66
|
+
def world_publish(
|
|
67
|
+
path: str = typer.Argument(".", help="Path to the world package directory (default: current directory)"),
|
|
68
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Build without uploading"),
|
|
69
|
+
):
|
|
70
|
+
"""Build and publish a world package to the Plato worlds repository.
|
|
71
|
+
|
|
72
|
+
Reads pyproject.toml for package info, builds with 'uv build', extracts the
|
|
73
|
+
config schema from schema.json in the wheel, and uploads to the Plato worlds
|
|
74
|
+
repository via uv publish.
|
|
75
|
+
|
|
76
|
+
The schema.json is automatically generated during build by a hatch build hook
|
|
77
|
+
that calls the world's get_schema() method.
|
|
78
|
+
|
|
79
|
+
Arguments:
|
|
80
|
+
path: Path to the world package directory containing pyproject.toml
|
|
81
|
+
(default: current directory)
|
|
82
|
+
|
|
83
|
+
Options:
|
|
84
|
+
--dry-run: Build the package and show schema without uploading
|
|
85
|
+
|
|
86
|
+
Requires PLATO_API_KEY environment variable for upload.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
import tomli
|
|
90
|
+
except ImportError:
|
|
91
|
+
console.print("[red]Error: tomli is not installed[/red]")
|
|
92
|
+
console.print("\n[yellow]Install with:[/yellow]")
|
|
93
|
+
console.print(" pip install tomli")
|
|
94
|
+
raise typer.Exit(1) from None
|
|
95
|
+
|
|
96
|
+
# Get API key (skip check for dry_run)
|
|
97
|
+
api_key = None
|
|
98
|
+
if not dry_run:
|
|
99
|
+
api_key = require_api_key()
|
|
100
|
+
|
|
101
|
+
# Get base URL (default to production)
|
|
102
|
+
base_url = os.getenv("PLATO_BASE_URL", "https://plato.so")
|
|
103
|
+
base_url = base_url.rstrip("/")
|
|
104
|
+
if base_url.endswith("/api"):
|
|
105
|
+
base_url = base_url[:-4]
|
|
106
|
+
api_url = f"{base_url}/api"
|
|
107
|
+
|
|
108
|
+
# Resolve package path
|
|
109
|
+
pkg_path = Path(path).resolve()
|
|
110
|
+
if not pkg_path.exists():
|
|
111
|
+
console.print(f"[red]Error: Path does not exist: {pkg_path}[/red]")
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
|
|
114
|
+
# Load pyproject.toml
|
|
115
|
+
pyproject_file = pkg_path / "pyproject.toml"
|
|
116
|
+
if not pyproject_file.exists():
|
|
117
|
+
console.print(f"[red]Error: No pyproject.toml found at {pkg_path}[/red]")
|
|
118
|
+
raise typer.Exit(1)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
with open(pyproject_file, "rb") as f:
|
|
122
|
+
pyproject = tomli.load(f)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
console.print(f"[red]Error reading pyproject.toml: {e}[/red]")
|
|
125
|
+
raise typer.Exit(1) from e
|
|
126
|
+
|
|
127
|
+
# Extract package info
|
|
128
|
+
project = pyproject.get("project", {})
|
|
129
|
+
package_name = project.get("name")
|
|
130
|
+
version = project.get("version")
|
|
131
|
+
|
|
132
|
+
if not package_name:
|
|
133
|
+
console.print("[red]Error: No package name in pyproject.toml[/red]")
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
if not version:
|
|
136
|
+
console.print("[red]Error: No version in pyproject.toml[/red]")
|
|
137
|
+
raise typer.Exit(1)
|
|
138
|
+
|
|
139
|
+
console.print(f"[cyan]Package:[/cyan] {package_name}")
|
|
140
|
+
console.print(f"[cyan]Version:[/cyan] {version}")
|
|
141
|
+
console.print("[cyan]Repository:[/cyan] worlds")
|
|
142
|
+
console.print(f"[cyan]Path:[/cyan] {pkg_path}")
|
|
143
|
+
console.print()
|
|
144
|
+
|
|
145
|
+
# Build package (this will trigger the schema generation hook)
|
|
146
|
+
console.print("[cyan]Building package...[/cyan]")
|
|
147
|
+
try:
|
|
148
|
+
result = subprocess.run(
|
|
149
|
+
["uv", "build"],
|
|
150
|
+
cwd=pkg_path,
|
|
151
|
+
capture_output=True,
|
|
152
|
+
text=True,
|
|
153
|
+
)
|
|
154
|
+
if result.returncode != 0:
|
|
155
|
+
console.print("[red]Build failed:[/red]")
|
|
156
|
+
console.print(result.stderr)
|
|
157
|
+
raise typer.Exit(1)
|
|
158
|
+
console.print("[green]Build successful[/green]")
|
|
159
|
+
except FileNotFoundError:
|
|
160
|
+
console.print("[red]Error: uv not found. Install with: pip install uv[/red]")
|
|
161
|
+
raise typer.Exit(1) from None
|
|
162
|
+
|
|
163
|
+
# Find built wheel
|
|
164
|
+
dist_dir = pkg_path / "dist"
|
|
165
|
+
if not dist_dir.exists():
|
|
166
|
+
console.print("[red]Error: dist/ directory not found after build[/red]")
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
169
|
+
normalized_name = package_name.replace("-", "_")
|
|
170
|
+
wheel_files = list(dist_dir.glob(f"{normalized_name}-{version}-*.whl"))
|
|
171
|
+
|
|
172
|
+
if not wheel_files:
|
|
173
|
+
wheel_files = list(dist_dir.glob("*.whl"))
|
|
174
|
+
|
|
175
|
+
if not wheel_files:
|
|
176
|
+
console.print(f"[red]Error: No wheel file found in {dist_dir}[/red]")
|
|
177
|
+
raise typer.Exit(1)
|
|
178
|
+
|
|
179
|
+
wheel_file = wheel_files[0]
|
|
180
|
+
console.print(f"[cyan]Built:[/cyan] {wheel_file.name}")
|
|
181
|
+
|
|
182
|
+
# Extract schema from the wheel
|
|
183
|
+
module_name = _get_module_name(pkg_path, package_name)
|
|
184
|
+
schema_data = _extract_schema_from_wheel(wheel_file, module_name)
|
|
185
|
+
if schema_data:
|
|
186
|
+
props = schema_data.get("properties", {})
|
|
187
|
+
agents = schema_data.get("agents", [])
|
|
188
|
+
secrets = schema_data.get("secrets", [])
|
|
189
|
+
console.print(
|
|
190
|
+
f"[green]Schema found:[/green] {len(props)} properties, {len(agents)} agents, {len(secrets)} secrets"
|
|
191
|
+
)
|
|
192
|
+
console.print(f"[dim] Properties: {', '.join(props.keys()) if props else 'none'}[/dim]")
|
|
193
|
+
if agents:
|
|
194
|
+
console.print(f"[dim] Agents: {', '.join(a.get('name', '?') for a in agents)}[/dim]")
|
|
195
|
+
else:
|
|
196
|
+
console.print("[red]Error: No schema.json found in wheel[/red]")
|
|
197
|
+
console.print("[dim] Add a hatch build hook to generate schema.json from get_schema()[/dim]")
|
|
198
|
+
console.print("[dim] See: https://docs.plato.so/worlds/publishing#schema-generation[/dim]")
|
|
199
|
+
raise SystemExit(1)
|
|
200
|
+
|
|
201
|
+
if dry_run:
|
|
202
|
+
console.print("\n[yellow]Dry run - skipping upload[/yellow]")
|
|
203
|
+
if schema_data:
|
|
204
|
+
console.print("\n[bold]Schema:[/bold]")
|
|
205
|
+
console.print(json.dumps(schema_data, indent=2))
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Upload wheel using uv publish
|
|
209
|
+
upload_url = f"{api_url}/v2/pypi/worlds/"
|
|
210
|
+
console.print(f"\n[cyan]Uploading to {upload_url}...[/cyan]")
|
|
211
|
+
|
|
212
|
+
# api_key is guaranteed to be set (checked earlier when not dry_run)
|
|
213
|
+
assert api_key is not None, "api_key must be set when not in dry_run mode"
|
|
214
|
+
try:
|
|
215
|
+
result = subprocess.run(
|
|
216
|
+
[
|
|
217
|
+
"uv",
|
|
218
|
+
"publish",
|
|
219
|
+
"--publish-url",
|
|
220
|
+
upload_url,
|
|
221
|
+
"--username",
|
|
222
|
+
"__token__",
|
|
223
|
+
"--password",
|
|
224
|
+
api_key,
|
|
225
|
+
str(wheel_file),
|
|
226
|
+
],
|
|
227
|
+
capture_output=True,
|
|
228
|
+
text=True,
|
|
229
|
+
check=False,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if result.returncode == 0:
|
|
233
|
+
console.print("[green]Upload successful![/green]")
|
|
234
|
+
else:
|
|
235
|
+
console.print("[red]Upload failed:[/red]")
|
|
236
|
+
if result.stdout:
|
|
237
|
+
console.print(result.stdout)
|
|
238
|
+
if result.stderr:
|
|
239
|
+
console.print(result.stderr)
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
|
|
242
|
+
except FileNotFoundError:
|
|
243
|
+
console.print("[red]Error: uv not found[/red]")
|
|
244
|
+
raise typer.Exit(1) from None
|
|
245
|
+
except Exception as e:
|
|
246
|
+
console.print(f"[red]Upload error: {e}[/red]")
|
|
247
|
+
raise typer.Exit(1) from e
|
|
248
|
+
|
|
249
|
+
console.print("\n[bold]Install with:[/bold]")
|
|
250
|
+
console.print(f" uv add {package_name} --index-url {api_url}/v2/pypi/worlds/simple/")
|
plato/v1/cli/pm.py
CHANGED
|
@@ -389,7 +389,7 @@ def review_base(
|
|
|
389
389
|
browser = await playwright.chromium.launch(headless=False)
|
|
390
390
|
|
|
391
391
|
# Install fake clock if requested
|
|
392
|
-
fake_time = None
|
|
392
|
+
fake_time: datetime | None = None
|
|
393
393
|
if clock:
|
|
394
394
|
# Parse clock option: ISO format or offset like '-30d'
|
|
395
395
|
if clock.startswith("-") and clock[-1] in "dhms":
|
|
@@ -404,10 +404,13 @@ def review_base(
|
|
|
404
404
|
fake_time = datetime.now() - timedelta(minutes=amount)
|
|
405
405
|
elif unit == "s":
|
|
406
406
|
fake_time = datetime.now() - timedelta(seconds=amount)
|
|
407
|
+
else:
|
|
408
|
+
raise ValueError(f"Invalid clock offset unit: {unit}")
|
|
407
409
|
else:
|
|
408
410
|
# ISO format
|
|
409
411
|
fake_time = datetime.fromisoformat(clock)
|
|
410
412
|
|
|
413
|
+
assert fake_time is not None, f"Failed to parse clock value: {clock}"
|
|
411
414
|
console.print(f"[cyan]Setting fake browser time to:[/cyan] {fake_time.isoformat()}")
|
|
412
415
|
|
|
413
416
|
if local:
|
plato/v2/__init__.py
CHANGED
|
@@ -24,6 +24,7 @@ from plato.v2.sync.chronos import Chronos, ChronosSession
|
|
|
24
24
|
from plato.v2.sync.client import Plato
|
|
25
25
|
from plato.v2.sync.environment import Environment
|
|
26
26
|
from plato.v2.sync.flow_executor import FlowExecutionError, FlowExecutor
|
|
27
|
+
from plato.v2.sync.sandbox import SandboxClient
|
|
27
28
|
from plato.v2.sync.session import LoginResult, Session
|
|
28
29
|
|
|
29
30
|
# Helper types
|
|
@@ -46,6 +47,7 @@ __all__ = [
|
|
|
46
47
|
"ArtifactInfoResponse",
|
|
47
48
|
"Chronos",
|
|
48
49
|
"ChronosSession",
|
|
50
|
+
"SandboxClient",
|
|
49
51
|
# Async
|
|
50
52
|
"AsyncPlato",
|
|
51
53
|
"AsyncSession",
|
plato/v2/models.py
CHANGED
|
@@ -63,3 +63,45 @@ class EnvOption(BaseModel):
|
|
|
63
63
|
alias=alias,
|
|
64
64
|
sim_config=SimConfig(cpus=cpus, memory=memory, disk=disk),
|
|
65
65
|
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SandboxState(BaseModel):
|
|
69
|
+
"""Schema for the sandbox state file (.plato/state.yaml).
|
|
70
|
+
|
|
71
|
+
All fields that can be persisted in the state file.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Core identifiers
|
|
75
|
+
session_id: str
|
|
76
|
+
job_id: str
|
|
77
|
+
public_url: str | None = None
|
|
78
|
+
|
|
79
|
+
# Mode and service
|
|
80
|
+
mode: str # "blank", "config", "artifact", "simulator"
|
|
81
|
+
|
|
82
|
+
# Blank mode fields
|
|
83
|
+
dataset: str | None = None
|
|
84
|
+
cpus: int | None = None
|
|
85
|
+
memory: int | None = None
|
|
86
|
+
disk: int | None = None
|
|
87
|
+
app_port: int | None = None
|
|
88
|
+
messaging_port: int | None = None
|
|
89
|
+
|
|
90
|
+
# Config mode fields
|
|
91
|
+
plato_config_path: str | None = None
|
|
92
|
+
|
|
93
|
+
# Simulator/artifact mode fields
|
|
94
|
+
simulator_name: str | None = None
|
|
95
|
+
artifact_id: str | None = None
|
|
96
|
+
tag: str | None = None
|
|
97
|
+
|
|
98
|
+
# SSH configuration
|
|
99
|
+
ssh_config_path: str | None = None
|
|
100
|
+
ssh_host: str | None = None
|
|
101
|
+
ssh_command: str | None = None # Full SSH command for copy-paste
|
|
102
|
+
|
|
103
|
+
# Process management
|
|
104
|
+
heartbeat_pid: int | None = None
|
|
105
|
+
|
|
106
|
+
# Network
|
|
107
|
+
network_connected: bool = False
|
plato/v2/sync/__init__.py
CHANGED
|
@@ -4,6 +4,10 @@ from plato._generated.models import ArtifactInfoResponse
|
|
|
4
4
|
from plato.v2.sync.chronos import Chronos, ChronosSession
|
|
5
5
|
from plato.v2.sync.client import Plato
|
|
6
6
|
from plato.v2.sync.environment import Environment
|
|
7
|
+
from plato.v2.sync.sandbox import (
|
|
8
|
+
SandboxClient,
|
|
9
|
+
SandboxState,
|
|
10
|
+
)
|
|
7
11
|
from plato.v2.sync.session import Session
|
|
8
12
|
|
|
9
13
|
__all__ = [
|
|
@@ -13,4 +17,6 @@ __all__ = [
|
|
|
13
17
|
"Environment",
|
|
14
18
|
"Chronos",
|
|
15
19
|
"ChronosSession",
|
|
20
|
+
"SandboxClient",
|
|
21
|
+
"SandboxState",
|
|
16
22
|
]
|
plato/v2/sync/client.py
CHANGED
|
@@ -126,6 +126,7 @@ class Plato:
|
|
|
126
126
|
api_key: str | None = None,
|
|
127
127
|
base_url: str | None = None,
|
|
128
128
|
timeout: float = DEFAULT_TIMEOUT,
|
|
129
|
+
http_client: httpx.Client | None = None,
|
|
129
130
|
):
|
|
130
131
|
self.api_key = api_key or os.environ.get("PLATO_API_KEY")
|
|
131
132
|
if not self.api_key:
|
|
@@ -138,7 +139,8 @@ class Plato:
|
|
|
138
139
|
self.base_url = url.rstrip("/")
|
|
139
140
|
self.timeout = timeout
|
|
140
141
|
|
|
141
|
-
self.
|
|
142
|
+
self._owns_http = http_client is None
|
|
143
|
+
self._http = http_client or httpx.Client(
|
|
142
144
|
base_url=self.base_url,
|
|
143
145
|
timeout=httpx.Timeout(timeout),
|
|
144
146
|
)
|
|
@@ -147,5 +149,6 @@ class Plato:
|
|
|
147
149
|
self.artifacts = ArtifactManager(self._http, self.api_key)
|
|
148
150
|
|
|
149
151
|
def close(self) -> None:
|
|
150
|
-
"""Close the underlying HTTP client."""
|
|
151
|
-
self.
|
|
152
|
+
"""Close the underlying HTTP client (if we own it)."""
|
|
153
|
+
if self._owns_http:
|
|
154
|
+
self._http.close()
|