plato-sdk-v2 2.0.50__py3-none-any.whl → 2.2.4__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/__init__.py +7 -6
- plato/_generated/__init__.py +1 -1
- plato/_generated/api/v1/env/evaluate_session.py +3 -3
- plato/_generated/api/v1/env/log_state_mutation.py +4 -4
- plato/_generated/api/v1/sandbox/checkpoint_vm.py +3 -3
- plato/_generated/api/v1/sandbox/save_vm_snapshot.py +3 -3
- plato/_generated/api/v1/sandbox/setup_sandbox.py +8 -8
- plato/_generated/api/v1/session/__init__.py +2 -0
- plato/_generated/api/v1/session/get_sessions_for_archival.py +100 -0
- plato/_generated/api/v1/testcases/__init__.py +6 -2
- plato/_generated/api/v1/testcases/get_mutation_groups_for_testcase.py +98 -0
- plato/_generated/api/v1/testcases/{get_next_output_testcase_for_scoring.py → get_next_testcase_for_scoring.py} +23 -10
- plato/_generated/api/v1/testcases/get_testcase_metadata_for_scoring.py +74 -0
- plato/_generated/api/v2/__init__.py +2 -1
- plato/_generated/api/v2/jobs/__init__.py +4 -0
- plato/_generated/api/v2/jobs/checkpoint.py +3 -3
- plato/_generated/api/v2/jobs/disk_snapshot.py +3 -3
- plato/_generated/api/v2/jobs/log_for_job.py +4 -39
- plato/_generated/api/v2/jobs/make.py +4 -4
- plato/_generated/api/v2/jobs/setup_sandbox.py +97 -0
- plato/_generated/api/v2/jobs/snapshot.py +3 -3
- plato/_generated/api/v2/jobs/snapshot_store.py +91 -0
- plato/_generated/api/v2/sessions/__init__.py +4 -0
- plato/_generated/api/v2/sessions/checkpoint.py +3 -3
- plato/_generated/api/v2/sessions/disk_snapshot.py +3 -3
- plato/_generated/api/v2/sessions/evaluate.py +3 -3
- plato/_generated/api/v2/sessions/log_job_mutation.py +4 -39
- plato/_generated/api/v2/sessions/make.py +4 -4
- plato/_generated/api/v2/sessions/setup_sandbox.py +98 -0
- plato/_generated/api/v2/sessions/snapshot.py +3 -3
- plato/_generated/api/v2/sessions/snapshot_store.py +94 -0
- plato/_generated/api/v2/user/__init__.py +7 -0
- plato/_generated/api/v2/user/get_current_user.py +76 -0
- plato/_generated/models/__init__.py +174 -23
- plato/_sims_generator/__init__.py +19 -4
- plato/_sims_generator/instruction.py +203 -0
- plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
- plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
- plato/agents/__init__.py +107 -517
- plato/agents/base.py +145 -0
- plato/agents/build.py +61 -0
- plato/agents/config.py +160 -0
- plato/agents/logging.py +401 -0
- plato/agents/runner.py +161 -0
- plato/agents/trajectory.py +266 -0
- plato/chronos/__init__.py +37 -0
- plato/chronos/api/__init__.py +3 -0
- plato/chronos/api/agents/__init__.py +13 -0
- plato/chronos/api/agents/create_agent.py +63 -0
- plato/chronos/api/agents/delete_agent.py +61 -0
- plato/chronos/api/agents/get_agent.py +62 -0
- plato/chronos/api/agents/get_agent_schema.py +72 -0
- plato/chronos/api/agents/get_agent_versions.py +62 -0
- plato/chronos/api/agents/list_agents.py +57 -0
- plato/chronos/api/agents/lookup_agent.py +74 -0
- plato/chronos/api/auth/__init__.py +9 -0
- plato/chronos/api/auth/debug_auth_api_auth_debug_get.py +43 -0
- plato/chronos/api/auth/get_auth_status_api_auth_status_get.py +61 -0
- plato/chronos/api/auth/get_current_user_route_api_auth_me_get.py +60 -0
- plato/chronos/api/callback/__init__.py +11 -0
- plato/chronos/api/callback/push_agent_logs.py +61 -0
- plato/chronos/api/callback/update_agent_status.py +57 -0
- plato/chronos/api/callback/upload_artifacts.py +59 -0
- plato/chronos/api/callback/upload_logs_zip.py +57 -0
- plato/chronos/api/callback/upload_trajectory.py +57 -0
- plato/chronos/api/default/__init__.py +7 -0
- plato/chronos/api/default/health.py +43 -0
- plato/chronos/api/jobs/__init__.py +7 -0
- plato/chronos/api/jobs/launch_job.py +63 -0
- plato/chronos/api/registry/__init__.py +19 -0
- plato/chronos/api/registry/get_agent_schema_api_registry_agents__agent_name__schema_get.py +62 -0
- plato/chronos/api/registry/get_agent_versions_api_registry_agents__agent_name__versions_get.py +52 -0
- plato/chronos/api/registry/get_world_schema_api_registry_worlds__package_name__schema_get.py +68 -0
- plato/chronos/api/registry/get_world_versions_api_registry_worlds__package_name__versions_get.py +52 -0
- plato/chronos/api/registry/list_registry_agents_api_registry_agents_get.py +44 -0
- plato/chronos/api/registry/list_registry_worlds_api_registry_worlds_get.py +44 -0
- plato/chronos/api/runtimes/__init__.py +11 -0
- plato/chronos/api/runtimes/create_runtime.py +63 -0
- plato/chronos/api/runtimes/delete_runtime.py +61 -0
- plato/chronos/api/runtimes/get_runtime.py +62 -0
- plato/chronos/api/runtimes/list_runtimes.py +57 -0
- plato/chronos/api/runtimes/test_runtime.py +67 -0
- plato/chronos/api/secrets/__init__.py +11 -0
- plato/chronos/api/secrets/create_secret.py +63 -0
- plato/chronos/api/secrets/delete_secret.py +61 -0
- plato/chronos/api/secrets/get_secret.py +62 -0
- plato/chronos/api/secrets/list_secrets.py +57 -0
- plato/chronos/api/secrets/update_secret.py +68 -0
- plato/chronos/api/sessions/__init__.py +10 -0
- plato/chronos/api/sessions/get_session.py +62 -0
- plato/chronos/api/sessions/get_session_logs.py +72 -0
- plato/chronos/api/sessions/get_session_logs_download.py +62 -0
- plato/chronos/api/sessions/list_sessions.py +57 -0
- plato/chronos/api/status/__init__.py +8 -0
- plato/chronos/api/status/get_status_api_status_get.py +44 -0
- plato/chronos/api/status/get_version_info_api_version_get.py +44 -0
- plato/chronos/api/templates/__init__.py +11 -0
- plato/chronos/api/templates/create_template.py +63 -0
- plato/chronos/api/templates/delete_template.py +61 -0
- plato/chronos/api/templates/get_template.py +62 -0
- plato/chronos/api/templates/list_templates.py +57 -0
- plato/chronos/api/templates/update_template.py +68 -0
- plato/chronos/api/trajectories/__init__.py +8 -0
- plato/chronos/api/trajectories/get_trajectory.py +62 -0
- plato/chronos/api/trajectories/list_trajectories.py +62 -0
- plato/chronos/api/worlds/__init__.py +10 -0
- plato/chronos/api/worlds/create_world.py +63 -0
- plato/chronos/api/worlds/delete_world.py +61 -0
- plato/chronos/api/worlds/get_world.py +62 -0
- plato/chronos/api/worlds/list_worlds.py +57 -0
- plato/chronos/client.py +171 -0
- plato/chronos/errors.py +141 -0
- plato/chronos/models/__init__.py +647 -0
- plato/chronos/py.typed +0 -0
- plato/sims/cli.py +299 -123
- plato/sims/registry.py +77 -4
- plato/v1/cli/agent.py +88 -84
- plato/v1/cli/main.py +2 -0
- plato/v1/cli/pm.py +441 -119
- plato/v1/cli/sandbox.py +747 -191
- plato/v1/cli/sim.py +11 -0
- plato/v1/cli/verify.py +1269 -0
- plato/v1/cli/world.py +3 -0
- plato/v1/flow_executor.py +21 -17
- plato/v1/models/env.py +11 -11
- plato/v1/sdk.py +2 -2
- plato/v1/sync_env.py +11 -11
- plato/v1/sync_flow_executor.py +21 -17
- plato/v1/sync_sdk.py +4 -2
- plato/v2/__init__.py +2 -0
- plato/v2/async_/environment.py +20 -1
- plato/v2/async_/session.py +54 -3
- plato/v2/sync/environment.py +2 -1
- plato/v2/sync/session.py +52 -2
- plato/worlds/README.md +218 -0
- plato/worlds/__init__.py +54 -18
- plato/worlds/base.py +304 -93
- plato/worlds/config.py +239 -73
- plato/worlds/runner.py +391 -80
- {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/METADATA +1 -3
- {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/RECORD +143 -68
- {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/entry_points.txt +1 -0
- plato/_generated/api/v2/interfaces/__init__.py +0 -27
- plato/_generated/api/v2/interfaces/v2_interface_browser_create.py +0 -68
- plato/_generated/api/v2/interfaces/v2_interface_cdp_url.py +0 -65
- plato/_generated/api/v2/interfaces/v2_interface_click.py +0 -64
- plato/_generated/api/v2/interfaces/v2_interface_close.py +0 -59
- plato/_generated/api/v2/interfaces/v2_interface_computer_create.py +0 -68
- plato/_generated/api/v2/interfaces/v2_interface_cursor.py +0 -64
- plato/_generated/api/v2/interfaces/v2_interface_key.py +0 -68
- plato/_generated/api/v2/interfaces/v2_interface_screenshot.py +0 -65
- plato/_generated/api/v2/interfaces/v2_interface_scroll.py +0 -70
- plato/_generated/api/v2/interfaces/v2_interface_type.py +0 -64
- plato/world/__init__.py +0 -44
- plato/world/base.py +0 -267
- plato/world/config.py +0 -139
- plato/world/types.py +0 -47
- {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/WHEEL +0 -0
plato/v1/cli/pm.py
CHANGED
|
@@ -3,24 +3,28 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import shutil
|
|
7
8
|
import tempfile
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
import typer
|
|
12
|
-
from playwright.async_api import async_playwright
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
15
|
from plato._generated.api.v1.env import get_simulator_by_name, get_simulators
|
|
16
|
+
from plato._generated.api.v1.organization import get_organization_members
|
|
16
17
|
from plato._generated.api.v1.simulator import (
|
|
17
18
|
add_simulator_review,
|
|
19
|
+
update_simulator,
|
|
18
20
|
update_simulator_status,
|
|
19
21
|
update_tag,
|
|
20
22
|
)
|
|
21
23
|
from plato._generated.api.v2.sessions import state as sessions_state
|
|
22
24
|
from plato._generated.models import (
|
|
23
25
|
AddReviewRequest,
|
|
26
|
+
AppApiV1SimulatorRoutesUpdateSimulatorRequest,
|
|
27
|
+
Authentication,
|
|
24
28
|
Outcome,
|
|
25
29
|
ReviewType,
|
|
26
30
|
UpdateStatusRequest,
|
|
@@ -35,9 +39,17 @@ from plato.v1.cli.utils import (
|
|
|
35
39
|
require_sandbox_field,
|
|
36
40
|
require_sandbox_state,
|
|
37
41
|
)
|
|
42
|
+
from plato.v1.cli.verify import pm_verify_app
|
|
38
43
|
from plato.v2.async_.client import AsyncPlato
|
|
39
44
|
from plato.v2.types import Env
|
|
40
45
|
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# CONSTANTS
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
# UUID pattern for detecting artifact IDs in sim:artifact notation
|
|
51
|
+
UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE)
|
|
52
|
+
|
|
41
53
|
# =============================================================================
|
|
42
54
|
# APP STRUCTURE
|
|
43
55
|
# =============================================================================
|
|
@@ -50,6 +62,7 @@ submit_app = typer.Typer(help="Submit simulator artifacts for review")
|
|
|
50
62
|
pm_app.add_typer(list_app, name="list")
|
|
51
63
|
pm_app.add_typer(review_app, name="review")
|
|
52
64
|
pm_app.add_typer(submit_app, name="submit")
|
|
65
|
+
pm_app.add_typer(pm_verify_app, name="verify")
|
|
53
66
|
|
|
54
67
|
|
|
55
68
|
# =============================================================================
|
|
@@ -57,6 +70,70 @@ pm_app.add_typer(submit_app, name="submit")
|
|
|
57
70
|
# =============================================================================
|
|
58
71
|
|
|
59
72
|
|
|
73
|
+
def parse_simulator_artifact(
|
|
74
|
+
simulator: str | None,
|
|
75
|
+
artifact: str | None,
|
|
76
|
+
require_artifact: bool = False,
|
|
77
|
+
command_name: str = "command",
|
|
78
|
+
) -> tuple[str | None, str | None]:
|
|
79
|
+
"""
|
|
80
|
+
Parse simulator and artifact from CLI args, supporting colon notation.
|
|
81
|
+
|
|
82
|
+
Supports:
|
|
83
|
+
-s simulator # Simulator only
|
|
84
|
+
-s simulator -a <artifact-uuid> # Explicit artifact
|
|
85
|
+
-s simulator:<artifact-uuid> # Colon notation
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
simulator: The -s/--simulator arg value
|
|
89
|
+
artifact: The -a/--artifact arg value
|
|
90
|
+
require_artifact: If True, artifact is required
|
|
91
|
+
command_name: Name of command for error messages
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
(simulator_name, artifact_id) tuple
|
|
95
|
+
"""
|
|
96
|
+
simulator_name = None
|
|
97
|
+
artifact_id = artifact or ""
|
|
98
|
+
|
|
99
|
+
if simulator:
|
|
100
|
+
# Check for colon notation: sim:artifact
|
|
101
|
+
if ":" in simulator:
|
|
102
|
+
sim_part, colon_part = simulator.split(":", 1)
|
|
103
|
+
simulator_name = sim_part
|
|
104
|
+
if UUID_PATTERN.match(colon_part):
|
|
105
|
+
artifact_id = colon_part
|
|
106
|
+
else:
|
|
107
|
+
console.print(f"[red]❌ Invalid artifact UUID after colon: '{colon_part}'[/red]")
|
|
108
|
+
console.print()
|
|
109
|
+
console.print("[yellow]Usage:[/yellow]")
|
|
110
|
+
console.print(f" plato pm {command_name} -s <simulator> # Simulator only")
|
|
111
|
+
console.print(f" plato pm {command_name} -s <simulator> -a <artifact-uuid> # With artifact")
|
|
112
|
+
console.print(f" plato pm {command_name} -s <simulator>:<artifact-uuid> # Colon notation")
|
|
113
|
+
raise typer.Exit(1)
|
|
114
|
+
else:
|
|
115
|
+
simulator_name = simulator
|
|
116
|
+
|
|
117
|
+
if not simulator_name:
|
|
118
|
+
console.print("[red]❌ Simulator name is required[/red]")
|
|
119
|
+
console.print()
|
|
120
|
+
console.print("[yellow]Usage:[/yellow]")
|
|
121
|
+
console.print(f" plato pm {command_name} -s <simulator> # Simulator only")
|
|
122
|
+
console.print(f" plato pm {command_name} -s <simulator> -a <artifact-uuid> # With artifact")
|
|
123
|
+
console.print(f" plato pm {command_name} -s <simulator>:<artifact-uuid> # Colon notation")
|
|
124
|
+
raise typer.Exit(1)
|
|
125
|
+
|
|
126
|
+
if require_artifact and not artifact_id:
|
|
127
|
+
console.print("[red]❌ Artifact ID is required[/red]")
|
|
128
|
+
console.print()
|
|
129
|
+
console.print("[yellow]Usage:[/yellow]")
|
|
130
|
+
console.print(f" plato pm {command_name} -s <simulator> -a <artifact-uuid> # With artifact flag")
|
|
131
|
+
console.print(f" plato pm {command_name} -s <simulator>:<artifact-uuid> # Colon notation")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
|
|
134
|
+
return simulator_name, artifact_id or None
|
|
135
|
+
|
|
136
|
+
|
|
60
137
|
def _get_base_url() -> str:
|
|
61
138
|
"""Get base URL with /api suffix stripped."""
|
|
62
139
|
base_url = os.getenv("PLATO_BASE_URL", "https://plato.so")
|
|
@@ -95,6 +172,21 @@ def _list_pending_reviews(review_type: str):
|
|
|
95
172
|
x_api_key=api_key,
|
|
96
173
|
)
|
|
97
174
|
|
|
175
|
+
# Fetch organization members to map user IDs to usernames
|
|
176
|
+
user_id_to_name: dict[int, str] = {}
|
|
177
|
+
try:
|
|
178
|
+
members = await get_organization_members.asyncio(
|
|
179
|
+
client=client,
|
|
180
|
+
x_api_key=api_key,
|
|
181
|
+
)
|
|
182
|
+
for member in members:
|
|
183
|
+
user_id = member.get("id")
|
|
184
|
+
username = member.get("username") or member.get("email", "")
|
|
185
|
+
if user_id is not None:
|
|
186
|
+
user_id_to_name[user_id] = username
|
|
187
|
+
except Exception:
|
|
188
|
+
pass # Continue without usernames if fetch fails
|
|
189
|
+
|
|
98
190
|
# Filter by target status
|
|
99
191
|
pending_review = []
|
|
100
192
|
for sim in simulators:
|
|
@@ -110,6 +202,7 @@ def _list_pending_reviews(review_type: str):
|
|
|
110
202
|
# Build table
|
|
111
203
|
table = Table(title=f"Simulators Pending {review_type.title()} Review")
|
|
112
204
|
table.add_column("Name", style="cyan", no_wrap=True)
|
|
205
|
+
table.add_column("Assignees", style="magenta", no_wrap=True)
|
|
113
206
|
table.add_column("Notes", style="white", max_width=40)
|
|
114
207
|
artifact_col_name = "base_artifact_id" if review_type == "base" else "data_artifact_id"
|
|
115
208
|
table.add_column(artifact_col_name, style="green", no_wrap=True)
|
|
@@ -128,7 +221,16 @@ def _list_pending_reviews(review_type: str):
|
|
|
128
221
|
artifact_id = config.get(artifact_key, "") if isinstance(config, dict) else ""
|
|
129
222
|
artifact_id = artifact_id or "-"
|
|
130
223
|
|
|
131
|
-
|
|
224
|
+
# Get assignees based on review type (env_assignees for base, data_assignees for data)
|
|
225
|
+
assignee_key = "env_assignees" if review_type == "base" else "data_assignees"
|
|
226
|
+
assignee_ids = config.get(assignee_key, []) if isinstance(config, dict) else []
|
|
227
|
+
assignee_names = []
|
|
228
|
+
if assignee_ids:
|
|
229
|
+
for uid in assignee_ids:
|
|
230
|
+
assignee_names.append(user_id_to_name.get(uid, str(uid)))
|
|
231
|
+
assignees_str = ", ".join(assignee_names) if assignee_names else "-"
|
|
232
|
+
|
|
233
|
+
table.add_row(name, assignees_str, notes, artifact_id)
|
|
132
234
|
|
|
133
235
|
console.print(table)
|
|
134
236
|
console.print(f"\n[cyan]Total: {len(pending_review)} simulator(s) pending {review_type} review[/cyan]")
|
|
@@ -141,10 +243,19 @@ def list_base():
|
|
|
141
243
|
"""
|
|
142
244
|
List simulators pending base/environment review.
|
|
143
245
|
|
|
144
|
-
Shows
|
|
246
|
+
Shows simulators waiting for environment review (status: env_review_requested).
|
|
247
|
+
Use this to see what needs reviewing before running 'plato pm review base'.
|
|
248
|
+
|
|
249
|
+
USAGE:
|
|
145
250
|
|
|
146
|
-
Example:
|
|
147
251
|
plato pm list base
|
|
252
|
+
|
|
253
|
+
OUTPUT COLUMNS:
|
|
254
|
+
|
|
255
|
+
- Name: Simulator name
|
|
256
|
+
- Assignees: Who's assigned to review
|
|
257
|
+
- Notes: Any notes about the simulator
|
|
258
|
+
- base_artifact_id: The artifact to review
|
|
148
259
|
"""
|
|
149
260
|
_list_pending_reviews("base")
|
|
150
261
|
|
|
@@ -154,10 +265,19 @@ def list_data():
|
|
|
154
265
|
"""
|
|
155
266
|
List simulators pending data review.
|
|
156
267
|
|
|
157
|
-
Shows
|
|
268
|
+
Shows simulators waiting for data review (status: data_review_requested).
|
|
269
|
+
Use this to see what needs reviewing before running 'plato pm review data'.
|
|
270
|
+
|
|
271
|
+
USAGE:
|
|
158
272
|
|
|
159
|
-
Example:
|
|
160
273
|
plato pm list data
|
|
274
|
+
|
|
275
|
+
OUTPUT COLUMNS:
|
|
276
|
+
|
|
277
|
+
- Name: Simulator name
|
|
278
|
+
- Assignees: Who's assigned to review
|
|
279
|
+
- Notes: Any notes about the simulator
|
|
280
|
+
- data_artifact_id: The artifact to review
|
|
161
281
|
"""
|
|
162
282
|
_list_pending_reviews("data")
|
|
163
283
|
|
|
@@ -169,40 +289,50 @@ def list_data():
|
|
|
169
289
|
|
|
170
290
|
@review_app.command(name="base")
|
|
171
291
|
def review_base(
|
|
172
|
-
simulator: str = typer.Option(
|
|
173
|
-
|
|
292
|
+
simulator: str = typer.Option(
|
|
293
|
+
None,
|
|
294
|
+
"--simulator",
|
|
295
|
+
"-s",
|
|
296
|
+
help="Simulator name. Supports colon notation: -s sim:<artifact-uuid>",
|
|
297
|
+
),
|
|
298
|
+
artifact: str = typer.Option(
|
|
299
|
+
None,
|
|
300
|
+
"--artifact",
|
|
301
|
+
"-a",
|
|
302
|
+
help="Artifact UUID to review. If not provided, uses server's base_artifact_id.",
|
|
303
|
+
),
|
|
304
|
+
skip_review: bool = typer.Option(
|
|
305
|
+
False,
|
|
306
|
+
"--skip-review",
|
|
307
|
+
help="Run login flow and check state, but skip interactive review. For automated verification.",
|
|
308
|
+
),
|
|
174
309
|
):
|
|
175
310
|
"""
|
|
176
|
-
Review base/environment artifact
|
|
311
|
+
Review base/environment artifact for a simulator.
|
|
177
312
|
|
|
178
|
-
Opens simulator
|
|
179
|
-
|
|
313
|
+
Opens the simulator in a browser for manual testing. After testing,
|
|
314
|
+
you can pass (→ env_approved) or reject (→ env_in_progress).
|
|
180
315
|
|
|
181
|
-
|
|
316
|
+
SPECIFYING SIMULATOR AND ARTIFACT:
|
|
182
317
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"""
|
|
187
|
-
api_key = require_api_key()
|
|
318
|
+
-s <simulator> Use server's base_artifact_id
|
|
319
|
+
-s <simulator> -a <artifact-uuid> Explicit artifact
|
|
320
|
+
-s <simulator>:<artifact-uuid> Colon notation (same as above)
|
|
188
321
|
|
|
189
|
-
|
|
190
|
-
simulator_provided_via_arg = simulator is not None
|
|
322
|
+
EXAMPLES:
|
|
191
323
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
simulator_name = typer.prompt("Enter simulator name").strip()
|
|
196
|
-
if not simulator_name:
|
|
197
|
-
console.print("[red]❌ Simulator name is required[/red]")
|
|
198
|
-
raise typer.Exit(1)
|
|
324
|
+
plato pm review base -s espocrm
|
|
325
|
+
plato pm review base -s espocrm -a e9c25ca5-1234-5678-9abc-def012345678
|
|
326
|
+
plato pm review base -s espocrm:e9c25ca5-1234-5678-9abc-def012345678
|
|
199
327
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
328
|
+
Requires simulator status: env_review_requested
|
|
329
|
+
"""
|
|
330
|
+
api_key = require_api_key()
|
|
331
|
+
|
|
332
|
+
# Parse simulator and artifact from args (artifact not required - falls back to server config)
|
|
333
|
+
simulator_name, artifact_id_input = parse_simulator_artifact(
|
|
334
|
+
simulator, artifact, require_artifact=False, command_name="review base"
|
|
335
|
+
)
|
|
206
336
|
|
|
207
337
|
async def _review_base():
|
|
208
338
|
base_url = _get_base_url()
|
|
@@ -215,6 +345,9 @@ def review_base(
|
|
|
215
345
|
try:
|
|
216
346
|
http_client = plato._http
|
|
217
347
|
|
|
348
|
+
# simulator_name is guaranteed set by parse_simulator_artifact (or we exit)
|
|
349
|
+
assert simulator_name is not None, "simulator_name must be set"
|
|
350
|
+
|
|
218
351
|
# Get simulator by name
|
|
219
352
|
sim = await get_simulator_by_name.asyncio(
|
|
220
353
|
client=http_client,
|
|
@@ -227,10 +360,16 @@ def review_base(
|
|
|
227
360
|
|
|
228
361
|
console.print(f"[cyan]Current status:[/cyan] {current_status}")
|
|
229
362
|
|
|
230
|
-
# Use provided artifact ID or fall back to base_artifact_id from config
|
|
231
|
-
artifact_id = artifact_id_input if artifact_id_input else current_config.get("base_artifact_id")
|
|
363
|
+
# Use provided artifact ID or fall back to base_artifact_id from server config
|
|
364
|
+
artifact_id: str | None = artifact_id_input if artifact_id_input else current_config.get("base_artifact_id")
|
|
232
365
|
if not artifact_id:
|
|
233
|
-
console.print("[red]❌ No artifact ID provided
|
|
366
|
+
console.print("[red]❌ No artifact ID provided.[/red]")
|
|
367
|
+
console.print(
|
|
368
|
+
"[yellow]This simulator hasn't been submitted yet, so there's no base artifact on record.[/yellow]"
|
|
369
|
+
)
|
|
370
|
+
console.print(
|
|
371
|
+
"[yellow]Specify the artifact ID from your snapshot using: plato pm review base --artifact <artifact_id>[/yellow]"
|
|
372
|
+
)
|
|
234
373
|
raise typer.Exit(1)
|
|
235
374
|
|
|
236
375
|
console.print(f"[cyan]Using artifact:[/cyan] {artifact_id}")
|
|
@@ -259,6 +398,8 @@ def review_base(
|
|
|
259
398
|
|
|
260
399
|
# Launch Playwright browser and login
|
|
261
400
|
console.print("[cyan]Launching browser and logging in...[/cyan]")
|
|
401
|
+
from playwright.async_api import async_playwright
|
|
402
|
+
|
|
262
403
|
playwright = await async_playwright().start()
|
|
263
404
|
browser = await playwright.chromium.launch(headless=False)
|
|
264
405
|
|
|
@@ -272,6 +413,60 @@ def review_base(
|
|
|
272
413
|
if public_url:
|
|
273
414
|
await page.goto(public_url)
|
|
274
415
|
|
|
416
|
+
# ALWAYS check state after login to verify no mutations
|
|
417
|
+
console.print("\n[cyan]Checking environment state after login...[/cyan]")
|
|
418
|
+
has_mutations = False
|
|
419
|
+
has_errors = False
|
|
420
|
+
try:
|
|
421
|
+
state_response = await sessions_state.asyncio(
|
|
422
|
+
client=http_client,
|
|
423
|
+
session_id=session.session_id,
|
|
424
|
+
merge_mutations=True,
|
|
425
|
+
x_api_key=api_key,
|
|
426
|
+
)
|
|
427
|
+
if state_response and state_response.results:
|
|
428
|
+
for jid, result in state_response.results.items():
|
|
429
|
+
state_data = result.state if hasattr(result, "state") else result
|
|
430
|
+
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
431
|
+
|
|
432
|
+
if isinstance(state_data, dict):
|
|
433
|
+
# Check for error in state response
|
|
434
|
+
if "error" in state_data:
|
|
435
|
+
has_errors = True
|
|
436
|
+
console.print("\n[bold red]❌ State API Error:[/bold red]")
|
|
437
|
+
console.print(f"[red]{state_data['error']}[/red]")
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
mutations = state_data.pop("mutations", [])
|
|
441
|
+
console.print("\n[bold]State:[/bold]")
|
|
442
|
+
console.print(json.dumps(state_data, indent=2, default=str))
|
|
443
|
+
if mutations:
|
|
444
|
+
has_mutations = True
|
|
445
|
+
console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
|
|
446
|
+
console.print(json.dumps(mutations, indent=2, default=str))
|
|
447
|
+
else:
|
|
448
|
+
console.print("\n[green]No mutations recorded[/green]")
|
|
449
|
+
else:
|
|
450
|
+
console.print(f"[yellow]Unexpected state format: {type(state_data)}[/yellow]")
|
|
451
|
+
|
|
452
|
+
if has_errors:
|
|
453
|
+
console.print("\n[bold red]❌ State check failed due to errors![/bold red]")
|
|
454
|
+
console.print("[yellow]The worker may not be properly connected.[/yellow]")
|
|
455
|
+
elif has_mutations:
|
|
456
|
+
console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
|
|
457
|
+
console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
|
|
458
|
+
else:
|
|
459
|
+
console.print("\n[bold green]✅ Login flow verified - no mutations created[/bold green]")
|
|
460
|
+
else:
|
|
461
|
+
console.print("[yellow]No state data available[/yellow]")
|
|
462
|
+
except Exception as e:
|
|
463
|
+
console.print(f"[red]❌ Error getting state: {e}[/red]")
|
|
464
|
+
|
|
465
|
+
# If skip_review, exit without interactive loop
|
|
466
|
+
if skip_review:
|
|
467
|
+
console.print("\n[cyan]Skipping interactive review (--skip-review)[/cyan]")
|
|
468
|
+
return
|
|
469
|
+
|
|
275
470
|
console.print("\n" + "=" * 60)
|
|
276
471
|
console.print("[bold green]Environment Review Session Active[/bold green]")
|
|
277
472
|
console.print("=" * 60)
|
|
@@ -281,7 +476,7 @@ def review_base(
|
|
|
281
476
|
console.print("=" * 60)
|
|
282
477
|
|
|
283
478
|
# Show recent env review if available
|
|
284
|
-
reviews = current_config.get("reviews"
|
|
479
|
+
reviews = current_config.get("reviews") or []
|
|
285
480
|
env_reviews = [r for r in reviews if r.get("review_type") == "env"]
|
|
286
481
|
if env_reviews:
|
|
287
482
|
env_reviews.sort(key=lambda r: r.get("timestamp_iso", ""), reverse=True)
|
|
@@ -320,9 +515,16 @@ def review_base(
|
|
|
320
515
|
if state_response and state_response.results:
|
|
321
516
|
for jid, result in state_response.results.items():
|
|
322
517
|
state_data = result.state if hasattr(result, "state") else result
|
|
518
|
+
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
519
|
+
|
|
323
520
|
if isinstance(state_data, dict):
|
|
521
|
+
# Check for error in state response
|
|
522
|
+
if "error" in state_data:
|
|
523
|
+
console.print("\n[bold red]❌ State API Error:[/bold red]")
|
|
524
|
+
console.print(f"[red]{state_data['error']}[/red]")
|
|
525
|
+
continue
|
|
526
|
+
|
|
324
527
|
mutations = state_data.pop("mutations", [])
|
|
325
|
-
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
326
528
|
console.print("\n[bold]State:[/bold]")
|
|
327
529
|
console.print(json.dumps(state_data, indent=2, default=str))
|
|
328
530
|
if mutations:
|
|
@@ -331,7 +533,6 @@ def review_base(
|
|
|
331
533
|
else:
|
|
332
534
|
console.print("\n[yellow]No mutations recorded[/yellow]")
|
|
333
535
|
else:
|
|
334
|
-
console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
|
|
335
536
|
console.print(json.dumps(state_data, indent=2, default=str))
|
|
336
537
|
else:
|
|
337
538
|
console.print("[yellow]No state data available[/yellow]")
|
|
@@ -407,9 +608,11 @@ def review_base(
|
|
|
407
608
|
console.print(f"[cyan]Status:[/cyan] {current_status} → {new_status}")
|
|
408
609
|
|
|
409
610
|
# If passed, automatically tag artifact as prod-latest
|
|
410
|
-
if outcome == "pass":
|
|
611
|
+
if outcome == "pass" and artifact_id:
|
|
411
612
|
console.print("\n[cyan]Tagging artifact as prod-latest...[/cyan]")
|
|
412
613
|
try:
|
|
614
|
+
# simulator_name and artifact_id are guaranteed to be set at this point
|
|
615
|
+
assert simulator_name is not None
|
|
413
616
|
await update_tag.asyncio(
|
|
414
617
|
client=http_client,
|
|
415
618
|
body=UpdateTagRequest(
|
|
@@ -461,57 +664,89 @@ def review_base(
|
|
|
461
664
|
@review_app.command(name="data")
|
|
462
665
|
def review_data(
|
|
463
666
|
simulator: str = typer.Option(
|
|
464
|
-
None,
|
|
667
|
+
None,
|
|
668
|
+
"--simulator",
|
|
669
|
+
"-s",
|
|
670
|
+
help="Simulator name. Supports colon notation: -s sim:<artifact-uuid>",
|
|
671
|
+
),
|
|
672
|
+
artifact: str = typer.Option(
|
|
673
|
+
None,
|
|
674
|
+
"--artifact",
|
|
675
|
+
"-a",
|
|
676
|
+
help="Artifact UUID to review. If not provided, uses server's data_artifact_id.",
|
|
465
677
|
),
|
|
466
678
|
):
|
|
467
679
|
"""
|
|
468
680
|
Launch browser with EnvGen Recorder extension for data review.
|
|
469
681
|
|
|
470
|
-
Opens Chrome with the extension installed
|
|
471
|
-
|
|
472
|
-
|
|
682
|
+
Opens Chrome with the EnvGen Recorder extension installed for reviewing
|
|
683
|
+
data artifacts. Close the browser when done.
|
|
684
|
+
|
|
685
|
+
SPECIFYING SIMULATOR AND ARTIFACT:
|
|
473
686
|
|
|
474
|
-
|
|
687
|
+
-s <simulator> Use server's data_artifact_id
|
|
688
|
+
-s <simulator> -a <artifact-uuid> Explicit artifact
|
|
689
|
+
-s <simulator>:<artifact-uuid> Colon notation (same as above)
|
|
690
|
+
|
|
691
|
+
EXAMPLES:
|
|
475
692
|
|
|
476
|
-
Example:
|
|
477
|
-
plato pm review data
|
|
478
693
|
plato pm review data -s fathom
|
|
694
|
+
plato pm review data -s fathom -a e9c25ca5-1234-5678-9abc-def012345678
|
|
695
|
+
plato pm review data -s fathom:e9c25ca5-1234-5678-9abc-def012345678
|
|
696
|
+
|
|
697
|
+
Requires simulator status: data_review_requested
|
|
479
698
|
"""
|
|
480
699
|
api_key = require_api_key()
|
|
481
700
|
|
|
701
|
+
# Parse simulator and artifact from args (artifact not required - falls back to server config)
|
|
702
|
+
simulator_name, artifact_id = parse_simulator_artifact(
|
|
703
|
+
simulator, artifact, require_artifact=False, command_name="review data"
|
|
704
|
+
)
|
|
705
|
+
|
|
482
706
|
# Determine target URL based on simulator
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
console.print(f"[cyan]Simulator:[/cyan] {simulator}")
|
|
486
|
-
else:
|
|
487
|
-
target_url = "https://sims.plato.so"
|
|
707
|
+
target_url = f"https://{simulator_name}.web.plato.so"
|
|
708
|
+
console.print(f"[cyan]Simulator:[/cyan] {simulator_name}")
|
|
488
709
|
|
|
489
|
-
#
|
|
710
|
+
# Fetch simulator config and get artifact ID if not provided
|
|
490
711
|
recent_review = None
|
|
491
|
-
if simulator:
|
|
492
712
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
713
|
+
async def _fetch_artifact_info():
|
|
714
|
+
nonlocal artifact_id
|
|
715
|
+
# simulator_name is guaranteed set by parse_simulator_artifact (or we exit)
|
|
716
|
+
assert simulator_name is not None, "simulator_name must be set"
|
|
717
|
+
|
|
718
|
+
base_url = _get_base_url()
|
|
719
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
|
|
720
|
+
try:
|
|
721
|
+
sim = await get_simulator_by_name.asyncio(
|
|
722
|
+
client=client,
|
|
723
|
+
name=simulator_name,
|
|
724
|
+
x_api_key=api_key,
|
|
725
|
+
)
|
|
726
|
+
config = sim.config or {}
|
|
727
|
+
|
|
728
|
+
# If no artifact provided, try to get data_artifact_id from server
|
|
729
|
+
if not artifact_id:
|
|
730
|
+
artifact_id = config.get("data_artifact_id")
|
|
731
|
+
if artifact_id:
|
|
732
|
+
console.print(f"[cyan]Using data_artifact_id from server:[/cyan] {artifact_id}")
|
|
733
|
+
else:
|
|
734
|
+
console.print("[yellow]No artifact specified and no data_artifact_id on server[/yellow]")
|
|
735
|
+
|
|
736
|
+
# Find most recent data review
|
|
737
|
+
reviews = config.get("reviews") or []
|
|
738
|
+
data_reviews = [r for r in reviews if r.get("review_type") == "data"]
|
|
739
|
+
if data_reviews:
|
|
740
|
+
data_reviews.sort(key=lambda r: r.get("timestamp_iso", ""), reverse=True)
|
|
741
|
+
return data_reviews[0]
|
|
742
|
+
except Exception as e:
|
|
743
|
+
console.print(f"[yellow]⚠️ Could not fetch simulator info: {e}[/yellow]")
|
|
744
|
+
return None
|
|
513
745
|
|
|
514
|
-
|
|
746
|
+
recent_review = handle_async(_fetch_artifact_info())
|
|
747
|
+
|
|
748
|
+
if artifact_id:
|
|
749
|
+
console.print(f"[cyan]Artifact:[/cyan] {artifact_id}")
|
|
515
750
|
|
|
516
751
|
# Find Chrome extension source
|
|
517
752
|
package_dir = Path(__file__).resolve().parent.parent # v1/
|
|
@@ -554,6 +789,8 @@ def review_data(
|
|
|
554
789
|
|
|
555
790
|
console.print("[cyan]Launching Chrome with EnvGen Recorder extension...[/cyan]")
|
|
556
791
|
|
|
792
|
+
from playwright.async_api import async_playwright
|
|
793
|
+
|
|
557
794
|
playwright = await async_playwright().start()
|
|
558
795
|
|
|
559
796
|
browser = await playwright.chromium.launch_persistent_context(
|
|
@@ -690,12 +927,24 @@ def submit_base():
|
|
|
690
927
|
"""
|
|
691
928
|
Submit base/environment artifact for review after snapshot.
|
|
692
929
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
930
|
+
Reads simulator name and artifact ID from .sandbox.yaml (created by
|
|
931
|
+
plato sandbox start). Run this from the simulator directory after
|
|
932
|
+
creating a snapshot.
|
|
933
|
+
|
|
934
|
+
Transitions simulator from env_in_progress → env_review_requested.
|
|
935
|
+
|
|
936
|
+
USAGE:
|
|
937
|
+
|
|
938
|
+
plato pm submit base # No args needed - reads from .sandbox.yaml
|
|
696
939
|
|
|
697
|
-
|
|
698
|
-
|
|
940
|
+
PREREQUISITES:
|
|
941
|
+
|
|
942
|
+
1. plato sandbox start --from-config
|
|
943
|
+
2. plato sandbox start-services
|
|
944
|
+
3. plato sandbox snapshot
|
|
945
|
+
4. plato pm submit base ← you are here
|
|
946
|
+
|
|
947
|
+
Requires simulator status: env_in_progress
|
|
699
948
|
"""
|
|
700
949
|
api_key = require_api_key()
|
|
701
950
|
|
|
@@ -706,10 +955,43 @@ def submit_base():
|
|
|
706
955
|
)
|
|
707
956
|
plato_config_path = require_sandbox_field(sandbox_data, "plato_config_path")
|
|
708
957
|
|
|
709
|
-
# Read plato-config.yml to get simulator name
|
|
958
|
+
# Read plato-config.yml to get simulator name and metadata
|
|
710
959
|
plato_config = read_plato_config(plato_config_path)
|
|
711
960
|
simulator_name = require_plato_config_field(plato_config, "service")
|
|
712
961
|
|
|
962
|
+
# Extract metadata from plato-config.yml
|
|
963
|
+
datasets = plato_config.get("datasets", {})
|
|
964
|
+
base_dataset = datasets.get("base", {})
|
|
965
|
+
metadata = base_dataset.get("metadata", {})
|
|
966
|
+
|
|
967
|
+
# Get metadata fields
|
|
968
|
+
config_description = metadata.get("description")
|
|
969
|
+
config_license = metadata.get("license")
|
|
970
|
+
config_source_code_url = metadata.get("source_code_url")
|
|
971
|
+
config_start_url = metadata.get("start_url")
|
|
972
|
+
config_favicon_url = metadata.get("favicon_url") # Explicit favicon URL
|
|
973
|
+
|
|
974
|
+
# Get authentication from variables
|
|
975
|
+
variables = metadata.get("variables", [])
|
|
976
|
+
username = None
|
|
977
|
+
password = None
|
|
978
|
+
for var in variables:
|
|
979
|
+
if isinstance(var, dict):
|
|
980
|
+
var_name = var.get("name", "").lower()
|
|
981
|
+
var_value = var.get("value")
|
|
982
|
+
if var_name in ("username", "user", "email", "admin_email", "adminmail"):
|
|
983
|
+
username = var_value
|
|
984
|
+
elif var_name in ("password", "pass", "admin_password", "adminpass"):
|
|
985
|
+
password = var_value
|
|
986
|
+
|
|
987
|
+
# Use explicit favicon_url from config, or warn if missing
|
|
988
|
+
favicon_url = config_favicon_url
|
|
989
|
+
if not favicon_url:
|
|
990
|
+
console.print("[yellow]⚠️ No favicon_url in plato-config.yml metadata - favicon will not be set[/yellow]")
|
|
991
|
+
console.print(
|
|
992
|
+
"[yellow] Add 'favicon_url: https://www.google.com/s2/favicons?domain=APPNAME.com&sz=32' to metadata[/yellow]"
|
|
993
|
+
)
|
|
994
|
+
|
|
713
995
|
async def _submit_base():
|
|
714
996
|
base_url = _get_base_url()
|
|
715
997
|
|
|
@@ -733,6 +1015,52 @@ def submit_base():
|
|
|
733
1015
|
console.print(f"[cyan]Current Status:[/cyan] {current_status}")
|
|
734
1016
|
console.print()
|
|
735
1017
|
|
|
1018
|
+
# Sync metadata from plato-config.yml to server
|
|
1019
|
+
console.print("[cyan]Syncing metadata to server...[/cyan]")
|
|
1020
|
+
|
|
1021
|
+
# Build update request with metadata from plato-config.yml
|
|
1022
|
+
update_fields: dict = {}
|
|
1023
|
+
|
|
1024
|
+
if config_description:
|
|
1025
|
+
update_fields["description"] = config_description
|
|
1026
|
+
console.print(f" [dim]description:[/dim] {config_description[:50]}...")
|
|
1027
|
+
|
|
1028
|
+
if favicon_url:
|
|
1029
|
+
update_fields["img_url"] = favicon_url
|
|
1030
|
+
console.print(f" [dim]img_url:[/dim] {favicon_url}")
|
|
1031
|
+
|
|
1032
|
+
if config_license:
|
|
1033
|
+
update_fields["license"] = config_license
|
|
1034
|
+
console.print(f" [dim]license:[/dim] {config_license}")
|
|
1035
|
+
|
|
1036
|
+
if config_source_code_url:
|
|
1037
|
+
update_fields["source_code_url"] = config_source_code_url
|
|
1038
|
+
console.print(f" [dim]source_code_url:[/dim] {config_source_code_url}")
|
|
1039
|
+
|
|
1040
|
+
if config_start_url:
|
|
1041
|
+
update_fields["start_url"] = config_start_url
|
|
1042
|
+
console.print(f" [dim]start_url:[/dim] {config_start_url}")
|
|
1043
|
+
|
|
1044
|
+
if username and password:
|
|
1045
|
+
update_fields["authentication"] = Authentication(user=username, password=password)
|
|
1046
|
+
console.print(f" [dim]authentication:[/dim] {username} / {'*' * len(password)}")
|
|
1047
|
+
|
|
1048
|
+
# Always include base_artifact_id
|
|
1049
|
+
update_fields["base_artifact_id"] = artifact_id
|
|
1050
|
+
|
|
1051
|
+
try:
|
|
1052
|
+
await update_simulator.asyncio(
|
|
1053
|
+
client=client,
|
|
1054
|
+
simulator_id=simulator_id,
|
|
1055
|
+
body=AppApiV1SimulatorRoutesUpdateSimulatorRequest(**update_fields),
|
|
1056
|
+
x_api_key=api_key,
|
|
1057
|
+
)
|
|
1058
|
+
console.print("[green]✅ Metadata synced to server[/green]")
|
|
1059
|
+
except Exception as e:
|
|
1060
|
+
console.print(f"[yellow]⚠️ Could not sync metadata: {e}[/yellow]")
|
|
1061
|
+
|
|
1062
|
+
console.print()
|
|
1063
|
+
|
|
736
1064
|
# Update simulator status
|
|
737
1065
|
await update_simulator_status.asyncio(
|
|
738
1066
|
client=client,
|
|
@@ -741,21 +1069,6 @@ def submit_base():
|
|
|
741
1069
|
x_api_key=api_key,
|
|
742
1070
|
)
|
|
743
1071
|
|
|
744
|
-
# Set base_artifact_id via tag update
|
|
745
|
-
try:
|
|
746
|
-
await update_tag.asyncio(
|
|
747
|
-
client=client,
|
|
748
|
-
body=UpdateTagRequest(
|
|
749
|
-
simulator_name=simulator_name,
|
|
750
|
-
artifact_id=artifact_id,
|
|
751
|
-
tag_name="base-pending-review",
|
|
752
|
-
dataset="base",
|
|
753
|
-
),
|
|
754
|
-
x_api_key=api_key,
|
|
755
|
-
)
|
|
756
|
-
except Exception as e:
|
|
757
|
-
console.print(f"[yellow]⚠️ Could not set artifact tag: {e}[/yellow]")
|
|
758
|
-
|
|
759
1072
|
console.print("[green]✅ Environment review requested successfully![/green]")
|
|
760
1073
|
console.print(f"[cyan]Status:[/cyan] {current_status} → env_review_requested")
|
|
761
1074
|
console.print(f"[cyan]Base Artifact:[/cyan] {artifact_id}")
|
|
@@ -765,39 +1078,48 @@ def submit_base():
|
|
|
765
1078
|
|
|
766
1079
|
@submit_app.command(name="data")
|
|
767
1080
|
def submit_data(
|
|
768
|
-
simulator: str = typer.Option(
|
|
769
|
-
|
|
1081
|
+
simulator: str = typer.Option(
|
|
1082
|
+
None,
|
|
1083
|
+
"--simulator",
|
|
1084
|
+
"-s",
|
|
1085
|
+
help="Simulator name. Supports colon notation: -s sim:<artifact-uuid>",
|
|
1086
|
+
),
|
|
1087
|
+
artifact: str = typer.Option(
|
|
1088
|
+
None,
|
|
1089
|
+
"--artifact",
|
|
1090
|
+
"-a",
|
|
1091
|
+
help="Artifact UUID to submit for data review (required).",
|
|
1092
|
+
),
|
|
770
1093
|
):
|
|
771
1094
|
"""
|
|
772
1095
|
Submit data artifact for review after data generation.
|
|
773
1096
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1097
|
+
Transitions simulator from data_in_progress → data_review_requested.
|
|
1098
|
+
|
|
1099
|
+
SPECIFYING SIMULATOR AND ARTIFACT (both required):
|
|
1100
|
+
|
|
1101
|
+
-s <simulator> -a <artifact-uuid> Explicit artifact
|
|
1102
|
+
-s <simulator>:<artifact-uuid> Colon notation (same as above)
|
|
1103
|
+
|
|
1104
|
+
EXAMPLES:
|
|
1105
|
+
|
|
1106
|
+
plato pm submit data -s espocrm -a e9c25ca5-1234-5678-9abc-def012345678
|
|
1107
|
+
plato pm submit data -s espocrm:e9c25ca5-1234-5678-9abc-def012345678
|
|
777
1108
|
|
|
778
|
-
|
|
779
|
-
plato pm submit data --simulator espocrm --artifact abc123
|
|
780
|
-
plato pm submit data -s espocrm -a abc123
|
|
1109
|
+
Requires simulator status: data_in_progress
|
|
781
1110
|
"""
|
|
782
1111
|
api_key = require_api_key()
|
|
783
1112
|
|
|
784
|
-
#
|
|
785
|
-
simulator_name =
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if not simulator_name:
|
|
789
|
-
console.print("[red]❌ Simulator name is required[/red]")
|
|
790
|
-
raise typer.Exit(1)
|
|
791
|
-
|
|
792
|
-
# Artifact ID is required
|
|
793
|
-
artifact_id = artifact
|
|
794
|
-
if not artifact_id:
|
|
795
|
-
artifact_id = typer.prompt("Enter artifact ID").strip()
|
|
796
|
-
if not artifact_id:
|
|
797
|
-
console.print("[red]❌ Artifact ID is required[/red]")
|
|
798
|
-
raise typer.Exit(1)
|
|
1113
|
+
# Parse simulator and artifact from args (artifact IS required for data submit)
|
|
1114
|
+
simulator_name, artifact_id = parse_simulator_artifact(
|
|
1115
|
+
simulator, artifact, require_artifact=True, command_name="submit data"
|
|
1116
|
+
)
|
|
799
1117
|
|
|
800
1118
|
async def _submit_data():
|
|
1119
|
+
# simulator_name and artifact_id are guaranteed set by parse_simulator_artifact with require_artifact=True
|
|
1120
|
+
assert simulator_name is not None, "simulator_name must be set"
|
|
1121
|
+
assert artifact_id is not None, "artifact_id must be set"
|
|
1122
|
+
|
|
801
1123
|
base_url = _get_base_url()
|
|
802
1124
|
|
|
803
1125
|
async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
|
|
@@ -828,7 +1150,7 @@ def submit_data(
|
|
|
828
1150
|
x_api_key=api_key,
|
|
829
1151
|
)
|
|
830
1152
|
|
|
831
|
-
# Set data_artifact_id via tag update
|
|
1153
|
+
# Set data_artifact_id via tag update (simulator_name and artifact_id already asserted above)
|
|
832
1154
|
try:
|
|
833
1155
|
await update_tag.asyncio(
|
|
834
1156
|
client=client,
|