plato-sdk-v2 2.8.0__py3-none-any.whl → 2.8.2__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/pm.py +143 -152
- plato/cli/sandbox.py +9 -2
- plato/v1/cli/pm.py +10 -1252
- plato/v2/sync/sandbox.py +47 -30
- {plato_sdk_v2-2.8.0.dist-info → plato_sdk_v2-2.8.2.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.8.0.dist-info → plato_sdk_v2-2.8.2.dist-info}/RECORD +8 -8
- {plato_sdk_v2-2.8.0.dist-info → plato_sdk_v2-2.8.2.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.8.0.dist-info → plato_sdk_v2-2.8.2.dist-info}/entry_points.txt +0 -0
plato/cli/pm.py
CHANGED
|
@@ -22,12 +22,10 @@ from plato._generated.api.v1.simulator import (
|
|
|
22
22
|
update_simulator_status,
|
|
23
23
|
update_tag,
|
|
24
24
|
)
|
|
25
|
-
from plato._generated.api.v2.sessions import state as sessions_state
|
|
26
25
|
from plato._generated.models import (
|
|
27
26
|
AddReviewRequest,
|
|
28
27
|
AppApiV1SimulatorRoutesUpdateSimulatorRequest,
|
|
29
28
|
Authentication,
|
|
30
|
-
Flow,
|
|
31
29
|
Outcome,
|
|
32
30
|
ReviewType,
|
|
33
31
|
UpdateStatusRequest,
|
|
@@ -42,9 +40,9 @@ from plato.cli.utils import (
|
|
|
42
40
|
require_sandbox_field,
|
|
43
41
|
require_sandbox_state,
|
|
44
42
|
)
|
|
45
|
-
from plato.
|
|
46
|
-
from plato.
|
|
47
|
-
from plato.
|
|
43
|
+
from plato.v1.flow_executor import FlowExecutor
|
|
44
|
+
from plato.v1.models.flow import Flow
|
|
45
|
+
from plato.v1.sdk import Plato
|
|
48
46
|
|
|
49
47
|
# =============================================================================
|
|
50
48
|
# CONSTANTS
|
|
@@ -318,25 +316,30 @@ def review_base(
|
|
|
318
316
|
)
|
|
319
317
|
|
|
320
318
|
async def _review_base():
|
|
319
|
+
import warnings
|
|
320
|
+
|
|
321
321
|
base_url = _get_base_url()
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
# v1 SDK expects base_url to include /api suffix
|
|
323
|
+
v1_base_url = f"{base_url}/api"
|
|
324
|
+
# Suppress the deprecation warning from v1 Plato
|
|
325
|
+
with warnings.catch_warnings():
|
|
326
|
+
warnings.simplefilter("ignore", DeprecationWarning)
|
|
327
|
+
plato = Plato(api_key=api_key, base_url=v1_base_url)
|
|
328
|
+
env = None
|
|
324
329
|
playwright = None
|
|
325
330
|
browser = None
|
|
326
|
-
login_result = None
|
|
327
331
|
|
|
328
332
|
try:
|
|
329
|
-
http_client = plato._http
|
|
330
|
-
|
|
331
333
|
# simulator_name is guaranteed set by parse_simulator_artifact (or we exit)
|
|
332
334
|
assert simulator_name is not None, "simulator_name must be set"
|
|
333
335
|
|
|
334
|
-
# Get simulator by name
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
# Get simulator by name using httpx for API calls
|
|
337
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as http_client:
|
|
338
|
+
sim = await get_simulator_by_name.asyncio(
|
|
339
|
+
client=http_client,
|
|
340
|
+
name=simulator_name,
|
|
341
|
+
x_api_key=api_key,
|
|
342
|
+
)
|
|
340
343
|
simulator_id = sim.id
|
|
341
344
|
current_config = sim.config or {}
|
|
342
345
|
current_status = current_config.get("status", "not_started")
|
|
@@ -357,26 +360,27 @@ def review_base(
|
|
|
357
360
|
|
|
358
361
|
console.print(f"[cyan]Using artifact:[/cyan] {artifact_id}")
|
|
359
362
|
|
|
360
|
-
# Try to create
|
|
363
|
+
# Try to create environment from artifact using v1 API
|
|
361
364
|
try:
|
|
362
365
|
console.print(f"[cyan]Creating {simulator_name} environment with artifact {artifact_id}...[/cyan]")
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
+
env = await plato.make_environment(
|
|
367
|
+
env_id=simulator_name,
|
|
368
|
+
artifact_id=artifact_id,
|
|
366
369
|
)
|
|
367
|
-
console.print(f"[green]✅
|
|
370
|
+
console.print(f"[green]✅ Environment created: {env.id}[/green]")
|
|
371
|
+
|
|
372
|
+
# Wait for environment to be ready
|
|
373
|
+
console.print("[cyan]Waiting for environment to be ready...[/cyan]")
|
|
374
|
+
await env.wait_for_ready(timeout=300)
|
|
375
|
+
console.print("[green]✅ Environment ready![/green]")
|
|
368
376
|
|
|
369
377
|
# Reset
|
|
370
378
|
console.print("[cyan]Resetting environment...[/cyan]")
|
|
371
|
-
await
|
|
379
|
+
await env.reset()
|
|
372
380
|
console.print("[green]✅ Environment reset complete![/green]")
|
|
373
381
|
|
|
374
|
-
# Get public URL
|
|
375
|
-
|
|
376
|
-
first_alias = session.envs[0].alias if session.envs else None
|
|
377
|
-
public_url = public_urls.get(first_alias) if first_alias else None
|
|
378
|
-
if not public_url and public_urls:
|
|
379
|
-
public_url = list(public_urls.values())[0]
|
|
382
|
+
# Get public URL (v1 returns string directly)
|
|
383
|
+
public_url = await env.get_public_url()
|
|
380
384
|
console.print(f"[cyan]Public URL:[/cyan] {public_url}")
|
|
381
385
|
|
|
382
386
|
# Launch Playwright browser and login
|
|
@@ -452,70 +456,66 @@ def review_base(
|
|
|
452
456
|
except Exception as e:
|
|
453
457
|
console.print(f"[yellow]⚠️ Flow execution error: {e}[/yellow]")
|
|
454
458
|
else:
|
|
455
|
-
# Use default login via
|
|
459
|
+
# Use default login via env.login() (v1 API takes Page, not Browser)
|
|
456
460
|
if fake_time:
|
|
457
461
|
console.print("[yellow]⚠️ --clock with default login may not work correctly.[/yellow]")
|
|
458
462
|
console.print("[yellow] Use --local with a flow file for reliable clock testing.[/yellow]")
|
|
459
463
|
try:
|
|
460
|
-
|
|
461
|
-
page = list(login_result.pages.values())[0] if login_result.pages else None
|
|
462
|
-
console.print("[green]✅ Logged into environment[/green]")
|
|
463
|
-
except Exception as e:
|
|
464
|
-
console.print(f"[yellow]⚠️ Login error: {e}[/yellow]")
|
|
464
|
+
# Create page and navigate to public URL first
|
|
465
465
|
page = await browser.new_page()
|
|
466
|
-
# Install fake clock on fallback page
|
|
467
466
|
if fake_time:
|
|
468
467
|
await page.clock.install(time=fake_time)
|
|
469
468
|
console.print(f"[green]✅ Fake clock installed: {fake_time.isoformat()}[/green]")
|
|
470
469
|
if public_url:
|
|
471
470
|
await page.goto(public_url)
|
|
471
|
+
# v1 login takes a Page and uses from_api=True to fetch flows from server
|
|
472
|
+
await env.login(page, dataset="base", from_api=True)
|
|
473
|
+
console.print("[green]✅ Logged into environment[/green]")
|
|
474
|
+
except Exception as e:
|
|
475
|
+
console.print(f"[yellow]⚠️ Login error: {e}[/yellow]")
|
|
476
|
+
# Page already created above, just navigate if not already done
|
|
477
|
+
if public_url and page:
|
|
478
|
+
try:
|
|
479
|
+
await page.goto(public_url)
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
472
482
|
|
|
473
483
|
# ALWAYS check state after login to verify no mutations
|
|
474
484
|
console.print("\n[cyan]Checking environment state after login...[/cyan]")
|
|
475
485
|
has_mutations = False
|
|
476
486
|
has_errors = False
|
|
477
487
|
try:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
console.print(f"
|
|
488
|
-
|
|
489
|
-
if isinstance(state_data, dict):
|
|
490
|
-
# Check for error in state response
|
|
491
|
-
if "error" in state_data:
|
|
492
|
-
has_errors = True
|
|
493
|
-
console.print("\n[bold red]❌ State API Error:[/bold red]")
|
|
494
|
-
console.print(f"[red]{state_data['error']}[/red]")
|
|
495
|
-
continue
|
|
496
|
-
|
|
497
|
-
mutations = state_data.pop("mutations", [])
|
|
498
|
-
console.print("\n[bold]State:[/bold]")
|
|
499
|
-
console.print(json.dumps(state_data, indent=2, default=str))
|
|
500
|
-
if mutations:
|
|
501
|
-
has_mutations = True
|
|
502
|
-
console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
|
|
503
|
-
console.print(json.dumps(mutations, indent=2, default=str))
|
|
504
|
-
else:
|
|
505
|
-
console.print("\n[green]No mutations recorded[/green]")
|
|
506
|
-
else:
|
|
507
|
-
console.print(f"[yellow]Unexpected state format: {type(state_data)}[/yellow]")
|
|
508
|
-
|
|
509
|
-
if has_errors:
|
|
510
|
-
console.print("\n[bold red]❌ State check failed due to errors![/bold red]")
|
|
511
|
-
console.print("[yellow]The worker may not be properly connected.[/yellow]")
|
|
512
|
-
elif has_mutations:
|
|
513
|
-
console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
|
|
514
|
-
console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
|
|
488
|
+
# v1 API: env.get_state() returns state dict directly
|
|
489
|
+
state_data = await env.get_state(merge_mutations=True)
|
|
490
|
+
console.print(f"\n[bold cyan]Environment {env.id}:[/bold cyan]")
|
|
491
|
+
|
|
492
|
+
if isinstance(state_data, dict):
|
|
493
|
+
# Check for error in state response (only if error has a truthy value)
|
|
494
|
+
if state_data.get("error"):
|
|
495
|
+
has_errors = True
|
|
496
|
+
console.print("\n[bold red]❌ State API Error:[/bold red]")
|
|
497
|
+
console.print(f"[red]{state_data['error']}[/red]")
|
|
515
498
|
else:
|
|
516
|
-
|
|
499
|
+
mutations = state_data.pop("mutations", [])
|
|
500
|
+
console.print("\n[bold]State:[/bold]")
|
|
501
|
+
console.print(json.dumps(state_data, indent=2, default=str))
|
|
502
|
+
if mutations:
|
|
503
|
+
has_mutations = True
|
|
504
|
+
console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
|
|
505
|
+
console.print(json.dumps(mutations, indent=2, default=str))
|
|
506
|
+
else:
|
|
507
|
+
console.print("\n[green]No mutations recorded[/green]")
|
|
517
508
|
else:
|
|
518
|
-
console.print("[yellow]
|
|
509
|
+
console.print(f"[yellow]Unexpected state format: {type(state_data)}[/yellow]")
|
|
510
|
+
|
|
511
|
+
if has_errors:
|
|
512
|
+
console.print("\n[bold red]❌ State check failed due to errors![/bold red]")
|
|
513
|
+
console.print("[yellow]The worker may not be properly connected.[/yellow]")
|
|
514
|
+
elif has_mutations:
|
|
515
|
+
console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
|
|
516
|
+
console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
|
|
517
|
+
else:
|
|
518
|
+
console.print("\n[bold green]✅ Login flow verified - no mutations created[/bold green]")
|
|
519
519
|
except Exception as e:
|
|
520
520
|
console.print(f"[red]❌ Error getting state: {e}[/red]")
|
|
521
521
|
|
|
@@ -562,37 +562,26 @@ def review_base(
|
|
|
562
562
|
elif command in ["state", "s"]:
|
|
563
563
|
console.print("\n[cyan]Getting environment state with mutations...[/cyan]")
|
|
564
564
|
try:
|
|
565
|
-
#
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
console.print(f"[red]{state_data['error']}[/red]")
|
|
582
|
-
continue
|
|
583
|
-
|
|
584
|
-
mutations = state_data.pop("mutations", [])
|
|
585
|
-
console.print("\n[bold]State:[/bold]")
|
|
586
|
-
console.print(json.dumps(state_data, indent=2, default=str))
|
|
587
|
-
if mutations:
|
|
588
|
-
console.print(f"\n[bold]Mutations ({len(mutations)}):[/bold]")
|
|
589
|
-
console.print(json.dumps(mutations, indent=2, default=str))
|
|
590
|
-
else:
|
|
591
|
-
console.print("\n[yellow]No mutations recorded[/yellow]")
|
|
565
|
+
# v1 API: env.get_state() returns state dict directly
|
|
566
|
+
state_data = await env.get_state(merge_mutations=True)
|
|
567
|
+
console.print(f"\n[bold cyan]Environment {env.id}:[/bold cyan]")
|
|
568
|
+
|
|
569
|
+
if isinstance(state_data, dict):
|
|
570
|
+
# Check for error in state response (only if error has a truthy value)
|
|
571
|
+
if state_data.get("error"):
|
|
572
|
+
console.print("\n[bold red]❌ State API Error:[/bold red]")
|
|
573
|
+
console.print(f"[red]{state_data['error']}[/red]")
|
|
574
|
+
else:
|
|
575
|
+
mutations = state_data.pop("mutations", [])
|
|
576
|
+
console.print("\n[bold]State:[/bold]")
|
|
577
|
+
console.print(json.dumps(state_data, indent=2, default=str))
|
|
578
|
+
if mutations:
|
|
579
|
+
console.print(f"\n[bold]Mutations ({len(mutations)}):[/bold]")
|
|
580
|
+
console.print(json.dumps(mutations, indent=2, default=str))
|
|
592
581
|
else:
|
|
593
|
-
console.print(
|
|
582
|
+
console.print("\n[yellow]No mutations recorded[/yellow]")
|
|
594
583
|
else:
|
|
595
|
-
console.print(
|
|
584
|
+
console.print(json.dumps(state_data, indent=2, default=str))
|
|
596
585
|
console.print()
|
|
597
586
|
except Exception as e:
|
|
598
587
|
console.print(f"[red]❌ Error getting state: {e}[/red]")
|
|
@@ -633,56 +622,60 @@ def review_base(
|
|
|
633
622
|
validate_status_transition(current_status, "env_review_requested", "review base reject")
|
|
634
623
|
new_status = "env_in_progress"
|
|
635
624
|
|
|
636
|
-
#
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
x_api_key=api_key,
|
|
642
|
-
)
|
|
643
|
-
|
|
644
|
-
# Add review if rejecting
|
|
645
|
-
if outcome == "reject":
|
|
646
|
-
comments = ""
|
|
647
|
-
while not comments:
|
|
648
|
-
comments = typer.prompt("Comments (required for reject)").strip()
|
|
649
|
-
if not comments:
|
|
650
|
-
console.print("[yellow]Comments are required when rejecting. Please provide feedback.[/yellow]")
|
|
651
|
-
|
|
652
|
-
await add_simulator_review.asyncio(
|
|
653
|
-
client=http_client,
|
|
625
|
+
# Create httpx client for API calls
|
|
626
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as api_client:
|
|
627
|
+
# Update status
|
|
628
|
+
await update_simulator_status.asyncio(
|
|
629
|
+
client=api_client,
|
|
654
630
|
simulator_id=simulator_id,
|
|
655
|
-
body=
|
|
656
|
-
review_type=ReviewType.env,
|
|
657
|
-
outcome=Outcome.reject,
|
|
658
|
-
artifact_id=artifact_id,
|
|
659
|
-
comments=comments,
|
|
660
|
-
),
|
|
631
|
+
body=UpdateStatusRequest(status=new_status),
|
|
661
632
|
x_api_key=api_key,
|
|
662
633
|
)
|
|
663
634
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
635
|
+
# Add review if rejecting
|
|
636
|
+
if outcome == "reject":
|
|
637
|
+
comments = ""
|
|
638
|
+
while not comments:
|
|
639
|
+
comments = typer.prompt("Comments (required for reject)").strip()
|
|
640
|
+
if not comments:
|
|
641
|
+
console.print(
|
|
642
|
+
"[yellow]Comments are required when rejecting. Please provide feedback.[/yellow]"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
await add_simulator_review.asyncio(
|
|
646
|
+
client=api_client,
|
|
647
|
+
simulator_id=simulator_id,
|
|
648
|
+
body=AddReviewRequest(
|
|
649
|
+
review_type=ReviewType.env,
|
|
650
|
+
outcome=Outcome.reject,
|
|
677
651
|
artifact_id=artifact_id,
|
|
678
|
-
|
|
679
|
-
dataset="base",
|
|
652
|
+
comments=comments,
|
|
680
653
|
),
|
|
681
654
|
x_api_key=api_key,
|
|
682
655
|
)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
656
|
+
|
|
657
|
+
console.print(f"[green]✅ Review submitted: {outcome}[/green]")
|
|
658
|
+
console.print(f"[cyan]Status:[/cyan] {current_status} → {new_status}")
|
|
659
|
+
|
|
660
|
+
# If passed, automatically tag artifact as prod-latest
|
|
661
|
+
if outcome == "pass" and artifact_id:
|
|
662
|
+
console.print("\n[cyan]Tagging artifact as prod-latest...[/cyan]")
|
|
663
|
+
try:
|
|
664
|
+
# simulator_name and artifact_id are guaranteed to be set at this point
|
|
665
|
+
assert simulator_name is not None
|
|
666
|
+
await update_tag.asyncio(
|
|
667
|
+
client=api_client,
|
|
668
|
+
body=UpdateTagRequest(
|
|
669
|
+
simulator_name=simulator_name,
|
|
670
|
+
artifact_id=artifact_id,
|
|
671
|
+
tag_name="prod-latest",
|
|
672
|
+
dataset="base",
|
|
673
|
+
),
|
|
674
|
+
x_api_key=api_key,
|
|
675
|
+
)
|
|
676
|
+
console.print(f"[green]✅ Tagged {artifact_id[:8]}... as prod-latest[/green]")
|
|
677
|
+
except Exception as e:
|
|
678
|
+
console.print(f"[yellow]⚠️ Could not tag as prod-latest: {e}[/yellow]")
|
|
686
679
|
|
|
687
680
|
except typer.Exit:
|
|
688
681
|
raise
|
|
@@ -693,8 +686,6 @@ def review_base(
|
|
|
693
686
|
finally:
|
|
694
687
|
# Cleanup
|
|
695
688
|
try:
|
|
696
|
-
if login_result and login_result.context:
|
|
697
|
-
await login_result.context.close()
|
|
698
689
|
if browser:
|
|
699
690
|
await browser.close()
|
|
700
691
|
if playwright:
|
|
@@ -702,13 +693,13 @@ def review_base(
|
|
|
702
693
|
except Exception as e:
|
|
703
694
|
console.print(f"[yellow]⚠️ Browser cleanup error: {e}[/yellow]")
|
|
704
695
|
|
|
705
|
-
if
|
|
696
|
+
if env:
|
|
706
697
|
try:
|
|
707
|
-
console.print("[cyan]Shutting down
|
|
708
|
-
await
|
|
709
|
-
console.print("[green]✅
|
|
698
|
+
console.print("[cyan]Shutting down environment...[/cyan]")
|
|
699
|
+
await env.close()
|
|
700
|
+
console.print("[green]✅ Environment shut down[/green]")
|
|
710
701
|
except Exception as e:
|
|
711
|
-
console.print(f"[yellow]⚠️
|
|
702
|
+
console.print(f"[yellow]⚠️ Environment cleanup error: {e}[/yellow]")
|
|
712
703
|
|
|
713
704
|
try:
|
|
714
705
|
await plato.close()
|
plato/cli/sandbox.py
CHANGED
|
@@ -407,8 +407,9 @@ def sandbox_snapshot(
|
|
|
407
407
|
|
|
408
408
|
Captures VM state and database for later restoration.
|
|
409
409
|
|
|
410
|
-
|
|
411
|
-
plato sandbox snapshot
|
|
410
|
+
Examples:
|
|
411
|
+
plato sandbox snapshot # Uses mode from state.json
|
|
412
|
+
plato sandbox snapshot --mode config # Override to pass local plato-config.yml and flows to artifact
|
|
412
413
|
"""
|
|
413
414
|
with sandbox_context(working_dir, json_output, verbose) as (client, out):
|
|
414
415
|
out.console.print("Creating snapshot...")
|
|
@@ -747,6 +748,9 @@ def sandbox_ssh(
|
|
|
747
748
|
|
|
748
749
|
Uses .plato/ssh_config from 'start'. Extra args after -- are passed to ssh.
|
|
749
750
|
|
|
751
|
+
NOTE FOR AGENTS: Do not use this command. Instead, use the raw SSH command
|
|
752
|
+
from 'plato sandbox status' which shows: ssh -F .plato/ssh_config sandbox
|
|
753
|
+
|
|
750
754
|
Examples:
|
|
751
755
|
plato sandbox ssh
|
|
752
756
|
plato sandbox ssh -- -L 8080:localhost:8080
|
|
@@ -781,6 +785,9 @@ def sandbox_tunnel(
|
|
|
781
785
|
|
|
782
786
|
Creates a TCP tunnel through the TLS gateway. Useful for database access.
|
|
783
787
|
|
|
788
|
+
NOTE FOR AGENTS: Do not use this command. Use raw SSH port forwarding instead:
|
|
789
|
+
ssh -F .plato/ssh_config sandbox -L <local_port>:127.0.0.1:<remote_port>
|
|
790
|
+
|
|
784
791
|
Examples:
|
|
785
792
|
plato sandbox tunnel 5432 # Forward PostgreSQL
|
|
786
793
|
plato sandbox tunnel 3306 # Forward MySQL
|