diff --git a/docs/content/reference/sdk-operating-modes.md b/docs/content/reference/sdk-operating-modes.md index b8f543b7162c..9c8bcfa3d1c5 100644 --- a/docs/content/reference/sdk-operating-modes.md +++ b/docs/content/reference/sdk-operating-modes.md @@ -80,6 +80,12 @@ Use [`RecordingStream::save`](https://docs.rs/rerun/latest/rerun/struct.Recordin Streams all logging data to standard output, which can then be loaded by the Rerun Viewer by streaming it from standard input. +#### Python + +Use [`rr.stdout`](https://ref.rerun.io/docs/python/stable/common/initialization_functions/#rerun.stdout?speculative-link). + +Check out our [dedicated example](https://github.com/rerun-io/rerun/tree/latest/examples/python/stdio/main.py?speculative-link). + #### Rust Use [`RecordingStream::stdout`](https://docs.rs/rerun/latest/rerun/struct.RecordingStream.html#method.stdout?speculative-link). diff --git a/examples/python/requirements.txt b/examples/python/requirements.txt index 5be4a3853cc8..2cad455095bc 100644 --- a/examples/python/requirements.txt +++ b/examples/python/requirements.txt @@ -28,5 +28,6 @@ -r segment_anything_model/requirements.txt -r shared_recording/requirements.txt -r signed_distance_fields/requirements.txt +-r stdio/requirements.txt -r structure_from_motion/requirements.txt -r template/requirements.txt diff --git a/examples/python/stdio/README.md b/examples/python/stdio/README.md new file mode 100644 index 000000000000..c4bf386fea89 --- /dev/null +++ b/examples/python/stdio/README.md @@ -0,0 +1,22 @@ +--- +title: Standard Input/Output example +python: https://github.com/rerun-io/rerun/tree/latest/examples/python/stdio/main.py?speculative-link +rust: https://github.com/rerun-io/rerun/tree/latest/examples/rust/stdio/src/main.rs?speculative-link +cpp: https://github.com/rerun-io/rerun/tree/latest/examples/cpp/stdio/main.cpp?speculative-link +thumbnail: https://static.rerun.io/stdio/25c5aba992d4c8b3861386d8d9539a4823dca117/480w.png +thumbnail_dimensions: [480, 298] +--- + + + + + + + + + +Demonstrates how to log data to standard output with the Rerun SDK, and then visualize it from standard input with the Rerun Viewer. + +```bash +echo 'hello from stdin!' | python main.py | rerun - +``` diff --git a/examples/python/stdio/main.py b/examples/python/stdio/main.py new file mode 100755 index 000000000000..9a9e57cbd342 --- /dev/null +++ b/examples/python/stdio/main.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +""" +Demonstrates how to use standard input/output with the Rerun SDK/Viewer. + +Usage: `echo 'hello from stdin!' | python main.py | rerun -` +""" +from __future__ import annotations + +import sys + +import rerun as rr # pip install rerun-sdk + +# sanity-check since all other example scripts take arguments: +assert len(sys.argv) == 1, f"{sys.argv[0]} does not take any arguments" + +rr.init("rerun_example_stdio") +rr.stdout() + +input = sys.stdin.buffer.read() + +rr.log("stdin", rr.TextDocument(input.decode("utf-8"))) diff --git a/examples/python/stdio/requirements.txt b/examples/python/stdio/requirements.txt new file mode 100644 index 000000000000..ebb847ff0d2d --- /dev/null +++ b/examples/python/stdio/requirements.txt @@ -0,0 +1 @@ +rerun-sdk diff --git a/rerun_py/rerun_sdk/rerun/__init__.py b/rerun_py/rerun_sdk/rerun/__init__.py index 138f25eeb83b..c055123c6613 100644 --- a/rerun_py/rerun_sdk/rerun/__init__.py +++ b/rerun_py/rerun_sdk/rerun/__init__.py @@ -153,7 +153,7 @@ set_thread_local_data_recording, ) from .script_helpers import script_add_args, script_setup, script_teardown -from .sinks import connect, disconnect, memory_recording, save, serve, spawn +from .sinks import connect, disconnect, memory_recording, save, serve, spawn, stdout from .time import ( disable_timeline, reset_time, @@ -192,7 +192,7 @@ def _init_recording_stream() -> None: from rerun.recording_stream import _patch as recording_stream_patch recording_stream_patch( - [connect, save, disconnect, memory_recording, serve, spawn] + [connect, save, stdout, disconnect, memory_recording, serve, spawn] + [ set_time_sequence, set_time_seconds, diff --git a/rerun_py/rerun_sdk/rerun/sinks.py b/rerun_py/rerun_sdk/rerun/sinks.py index ff5d6aedb233..63554653a2cd 100644 --- a/rerun_py/rerun_sdk/rerun/sinks.py +++ b/rerun_py/rerun_sdk/rerun/sinks.py @@ -68,6 +68,34 @@ def save(path: str | pathlib.Path, recording: RecordingStream | None = None) -> bindings.save(path=str(path), recording=recording) +def stdout(recording: RecordingStream | None = None) -> None: + """ + Stream all log-data to stdout. + + Pipe it into a Rerun Viewer to visualize it. + + Call this _before_ you log any data! + + If there isn't any listener at the other end of the pipe, the `RecordingStream` will + default back to `buffered` mode, in order not to break the user's terminal. + + Parameters + ---------- + recording: + Specifies the [`rerun.RecordingStream`][] to use. + If left unspecified, defaults to the current active data recording, if there is one. + See also: [`rerun.init`][], [`rerun.set_global_data_recording`][]. + + """ + + if not bindings.is_enabled(): + logging.warning("Rerun is disabled - save() call ignored. You must call rerun.init before saving a recording.") + return + + recording = RecordingStream.to_native(recording) + bindings.stdout(recording=recording) + + def disconnect(recording: RecordingStream | None = None) -> None: """ Closes all TCP connections, servers, and files. diff --git a/rerun_py/src/python_bridge.rs b/rerun_py/src/python_bridge.rs index 719c950372ca..5410c0382851 100644 --- a/rerun_py/src/python_bridge.rs +++ b/rerun_py/src/python_bridge.rs @@ -132,6 +132,7 @@ fn rerun_bindings(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(is_enabled, m)?)?; m.add_function(wrap_pyfunction!(connect, m)?)?; m.add_function(wrap_pyfunction!(save, m)?)?; + m.add_function(wrap_pyfunction!(stdout, m)?)?; m.add_function(wrap_pyfunction!(memory_recording, m)?)?; m.add_function(wrap_pyfunction!(serve, m)?)?; m.add_function(wrap_pyfunction!(disconnect, m)?)?; @@ -525,6 +526,22 @@ fn save(path: &str, recording: Option<&PyRecordingStream>, py: Python<'_>) -> Py }) } +#[pyfunction] +#[pyo3(signature = (recording = None))] +fn stdout(recording: Option<&PyRecordingStream>, py: Python<'_>) -> PyResult<()> { + let Some(recording) = get_data_recording(recording) else { + return Ok(()); + }; + + // The call to stdout may internally flush. + // Release the GIL in case any flushing behavior needs to cleanup a python object. + py.allow_threads(|| { + recording + .stdout() + .map_err(|err| PyRuntimeError::new_err(err.to_string())) + }) +} + /// Create an in-memory rrd file #[pyfunction] #[pyo3(signature = (recording = None))]