plato-sdk-v2 2.7.6__py3-none-any.whl → 2.7.8__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 +1204 -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 +1462 -0
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.8.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.8.dist-info}/RECORD +21 -9
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.8.dist-info}/entry_points.txt +1 -1
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.8.dist-info}/WHEEL +0 -0
plato/cli/utils.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Shared utilities for Plato CLI commands."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import typer
|
|
12
|
+
import yaml
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
# Initialize Rich console - shared across all CLI modules
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
# Workspace-based paths
|
|
19
|
+
PLATO_DIR = ".plato"
|
|
20
|
+
STATE_FILE = ".plato/state.yaml"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_sandbox_state(working_dir: Path | str | None = None) -> dict | None:
|
|
24
|
+
"""Read sandbox state from .plato/state.yaml.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
working_dir: Directory containing .plato/. If None, uses cwd.
|
|
28
|
+
"""
|
|
29
|
+
base_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
30
|
+
state_file = base_dir / STATE_FILE
|
|
31
|
+
if not state_file.exists():
|
|
32
|
+
return None
|
|
33
|
+
with open(state_file) as f:
|
|
34
|
+
return yaml.safe_load(f)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def save_sandbox_state(state: dict, working_dir: Path | str | None = None) -> None:
|
|
38
|
+
"""Save sandbox state to .plato/state.yaml.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
state: State dict to save.
|
|
42
|
+
working_dir: Directory to save .plato/ in. If None, uses cwd.
|
|
43
|
+
"""
|
|
44
|
+
base_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
45
|
+
plato_dir = base_dir / PLATO_DIR
|
|
46
|
+
plato_dir.mkdir(mode=0o700, exist_ok=True)
|
|
47
|
+
state_file = base_dir / STATE_FILE
|
|
48
|
+
with open(state_file, "w") as f:
|
|
49
|
+
yaml.dump(state, f, default_flow_style=False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def remove_sandbox_state(working_dir: Path | str | None = None, remove_all: bool = True) -> None:
|
|
53
|
+
"""Remove sandbox state and optionally the entire .plato/ directory.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
working_dir: Directory containing .plato/. If None, uses cwd.
|
|
57
|
+
remove_all: If True, removes entire .plato/ directory. If False, only removes state.yaml.
|
|
58
|
+
"""
|
|
59
|
+
base_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
60
|
+
if remove_all:
|
|
61
|
+
plato_dir = base_dir / PLATO_DIR
|
|
62
|
+
if plato_dir.exists():
|
|
63
|
+
shutil.rmtree(plato_dir)
|
|
64
|
+
else:
|
|
65
|
+
state_file = base_dir / STATE_FILE
|
|
66
|
+
if state_file.exists():
|
|
67
|
+
state_file.unlink()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def require_sandbox_state(working_dir: Path | str | None = None) -> dict:
|
|
71
|
+
"""Get sandbox state or exit with error.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
working_dir: Directory containing .plato/. If None, uses cwd.
|
|
75
|
+
"""
|
|
76
|
+
state = get_sandbox_state(working_dir)
|
|
77
|
+
if not state:
|
|
78
|
+
console.print("[red]No sandbox found (.plato/state.yaml missing)[/red]")
|
|
79
|
+
console.print("\n[yellow]Start a sandbox with:[/yellow]")
|
|
80
|
+
console.print(" plato sandbox start --from-config")
|
|
81
|
+
console.print(" plato sandbox start --simulator <name>")
|
|
82
|
+
console.print(" plato sandbox start --artifact-id <id>")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
return state
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def require_sandbox_field(state: dict, field: str, hint: str | None = None) -> str:
|
|
88
|
+
"""Get a required field from sandbox state or exit with error."""
|
|
89
|
+
value = state.get(field)
|
|
90
|
+
if not value:
|
|
91
|
+
console.print(f"[red]❌ No {field} found in .plato/state.yaml[/red]")
|
|
92
|
+
if hint:
|
|
93
|
+
console.print(f"\n[yellow]{hint}[/yellow]")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def with_sandbox_state(*required_fields: str):
|
|
99
|
+
"""Decorator that fills args from .plato/state.yaml and validates required fields.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
required_fields: Field names that must exist (from arg or state).
|
|
103
|
+
|
|
104
|
+
Fails with helpful message if:
|
|
105
|
+
- State file missing and required fields not provided as args
|
|
106
|
+
- Required field is None after checking both arg and state
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
@with_sandbox_state("job_id", "session_id")
|
|
110
|
+
def stop(job_id: str | None = None, session_id: str | None = None, working_dir: Path | None = None):
|
|
111
|
+
# job_id and session_id guaranteed filled or already exited
|
|
112
|
+
...
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def decorator(fn):
|
|
116
|
+
@wraps(fn)
|
|
117
|
+
def wrapper(*args, **kwargs):
|
|
118
|
+
working_dir = kwargs.get("working_dir")
|
|
119
|
+
state = get_sandbox_state(working_dir)
|
|
120
|
+
|
|
121
|
+
sig = inspect.signature(fn)
|
|
122
|
+
bound = sig.bind(*args, **kwargs)
|
|
123
|
+
bound.apply_defaults()
|
|
124
|
+
|
|
125
|
+
missing = []
|
|
126
|
+
for field in required_fields:
|
|
127
|
+
if bound.arguments.get(field) is None:
|
|
128
|
+
if state and field in state:
|
|
129
|
+
bound.arguments[field] = state[field]
|
|
130
|
+
else:
|
|
131
|
+
missing.append(field)
|
|
132
|
+
|
|
133
|
+
if missing:
|
|
134
|
+
if state is None:
|
|
135
|
+
console.print("[red]No .plato/state.yaml found[/red]")
|
|
136
|
+
console.print("[dim]Run from workspace or use -w /path/to/workspace[/dim]")
|
|
137
|
+
else:
|
|
138
|
+
console.print(f"[red]Missing: {', '.join(missing)}[/red]")
|
|
139
|
+
console.print(f"[dim]Provide via --{missing[0].replace('_', '-')} or add to state[/dim]")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
return fn(*bound.args, **bound.kwargs)
|
|
143
|
+
|
|
144
|
+
return wrapper
|
|
145
|
+
|
|
146
|
+
return decorator
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def read_plato_config(config_path: str | Path) -> dict:
|
|
150
|
+
"""Read plato-config.yml file or exit with error."""
|
|
151
|
+
try:
|
|
152
|
+
with open(config_path) as f:
|
|
153
|
+
return yaml.safe_load(f)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
console.print(f"[red]❌ Error reading plato-config.yml: {e}[/red]")
|
|
156
|
+
raise typer.Exit(1) from e
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def require_plato_config_field(config: dict, field: str) -> str:
|
|
160
|
+
"""Get a required field from plato config or exit with error."""
|
|
161
|
+
value = config.get(field)
|
|
162
|
+
if not value:
|
|
163
|
+
console.print(f"[red]❌ No {field} in plato-config.yml[/red]")
|
|
164
|
+
raise typer.Exit(1)
|
|
165
|
+
return value
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def require_api_key() -> str:
|
|
169
|
+
"""Get API key or exit with error."""
|
|
170
|
+
api_key = os.getenv("PLATO_API_KEY")
|
|
171
|
+
if not api_key:
|
|
172
|
+
console.print("[red]PLATO_API_KEY environment variable not set[/red]")
|
|
173
|
+
console.print("\n[yellow]Set your API key:[/yellow]")
|
|
174
|
+
console.print(" export PLATO_API_KEY='your-api-key-here'")
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
return api_key
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_http_client() -> httpx.Client:
|
|
180
|
+
"""Get configured httpx client."""
|
|
181
|
+
base_url = os.getenv("PLATO_BASE_URL", "https://plato.so")
|
|
182
|
+
# Strip trailing /api if present (to match v2 SDK behavior)
|
|
183
|
+
if base_url.endswith("/api"):
|
|
184
|
+
base_url = base_url[:-4]
|
|
185
|
+
base_url = base_url.rstrip("/")
|
|
186
|
+
return httpx.Client(base_url=base_url, timeout=httpx.Timeout(600.0))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def handle_async(coro):
|
|
190
|
+
"""Helper to run async functions with proper error handling."""
|
|
191
|
+
try:
|
|
192
|
+
return asyncio.run(coro)
|
|
193
|
+
except KeyboardInterrupt:
|
|
194
|
+
console.print("\n[red]🛑 Operation cancelled by user.[/red]")
|
|
195
|
+
raise typer.Exit(1) from None
|
|
196
|
+
except Exception as e:
|
|
197
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
198
|
+
if "401" in str(e) or "Unauthorized" in str(e):
|
|
199
|
+
console.print("💡 [yellow]Hint: Make sure PLATO_API_KEY is set in your environment[/yellow]")
|
|
200
|
+
raise typer.Exit(1) from e
|