diff --git a/examples/share_run.py b/examples/share_run.py new file mode 100644 index 0000000..15c6255 --- /dev/null +++ b/examples/share_run.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Example demonstrating automatic run sharing with p95. + +When share=True, a public share link (https://p95.run/{slug}) is printed +after the run completes. Anyone with the link can view the run without +needing an account. + +Usage: + export P95_URL=https://p.ninetyfive.gg + export P95_API_KEY=ss67_your_key_here + + python examples/share_run.py +""" + +import math +import os +import random +import time + +from p95 import Run, configure + +configure( + base_url=os.environ.get("P95_URL", "https://p.ninetyfive.gg"), +) + + +def main(): + config = { + "learning_rate": 0.01, + "batch_size": 64, + "epochs": 10, + "optimizer": "sgd", + } + + with Run( + project="peepo/peepo", + name=f"shared-run-{int(time.time())}", + tags=["example", "shared"], + config=config, + share=True, + ) as run: + print(f"Run ID: {run.id}") + print() + + for epoch in range(config["epochs"]): + loss = math.exp(-0.3 * epoch) + random.gauss(0, 0.02) + 0.05 + accuracy = 1.0 - loss * 0.7 + random.gauss(0, 0.01) + + run.log_metrics( + { + "train/loss": max(0.01, loss), + "train/accuracy": max(0.0, min(1.0, accuracy)), + }, + step=epoch, + ) + + print( + f"Epoch {epoch + 1}/{config['epochs']} loss={loss:.4f} acc={accuracy:.4f}" + ) + time.sleep(0.1) + + +if __name__ == "__main__": + main() diff --git a/mise.toml b/mise.toml index 6e144db..5a6500f 100644 --- a/mise.toml +++ b/mise.toml @@ -81,6 +81,11 @@ depends = ["build-release"] env = { P95_LOGDIR = "./logs", P95_BINARY = "./bin/pnf" } run = "python examples/multi_stage_training.py" +[tasks.demo-share] +description = "Run shared run example (remote mode, prints public share link on completion)" +depends = ["build-sdk"] +run = "python examples/share_run.py" + # =================== # Development tasks # =================== diff --git a/sdk/python/src/p95/client.py b/sdk/python/src/p95/client.py index 8b1c33f..93d0567 100644 --- a/sdk/python/src/p95/client.py +++ b/sdk/python/src/p95/client.py @@ -277,3 +277,7 @@ def link_run_to_job(self, job_id: str, run_id: str) -> Dict[str, Any]: return self._request( "POST", f"/jobs/{job_id}/link-run", data={"run_id": run_id} ) + + def share_run(self, run_id: str) -> Dict[str, Any]: + """Create a public share link for a run.""" + return self._request("POST", f"/runs/{run_id}/share") diff --git a/sdk/python/src/p95/run.py b/sdk/python/src/p95/run.py index 5a348cd..e519f5d 100644 --- a/sdk/python/src/p95/run.py +++ b/sdk/python/src/p95/run.py @@ -71,6 +71,8 @@ def __init__( # Server option (local mode only) start_server: bool = False, start_tui: bool = False, + # Sharing option (remote mode only) + share: bool = False, ): """ Initialize a new run. @@ -93,6 +95,9 @@ def __init__( browser (local mode only). The server stops when the run ends. start_tui: Automatically open the p95 TUI in a new terminal window (local mode only). The TUI manages its own internal server. + share: Automatically create a public share link when the run finishes + (remote mode only). The link is printed to stdout in the form + https://p95.run/{slug}. Raises: ValidationError: If project format is invalid (remote mode) @@ -137,6 +142,7 @@ def __init__( self._server_manager: Optional["ServerManager"] = None self._start_server = start_server self._start_tui = start_tui + self._share = share # Capture info before creating run self._git_info = None @@ -522,12 +528,22 @@ def _finalize(self, status: str, error: Optional[str] = None) -> None: # Launch TUI after the script fully exits if self._server_manager is not None and self._start_tui: atexit.register(self._server_manager.start) + if self._share: + print("p95: Warning: share=True is only available in remote mode.") else: # Flush and stop remote batcher self._remote_batcher.flush() self._remote_batcher.stop() # Update status on server self._remote_client.update_run_status(self._run_id, status, error=error) + if self._share: + try: + share_response = self._remote_client.share_run(self._run_id) + slug = share_response.get("slug") + if slug: + print(f"p95: Share your run at https://p95.run/{slug}") + except Exception as e: + print(f"p95: Warning: Failed to create share link: {e}") def __enter__(self) -> "Run": """Enter context manager."""