Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ extend-exclude = [
"rerun_cpp/src/rerun/archetypes/image.hpp", # TODO(emilk): remove once we remove from_greyscale8
"rerun_cpp/src/rerun/archetypes/image_ext.cpp", # TODO(emilk): remove once we remove from_greyscale8
"rerun_cpp/src/rerun/third_party/cxxopts.hpp",
"*.png",
"*.mp4",
"*.rrd",
]


[default.extend-words]
lod = "lod" # level-of-detail
ND = "ND" # np.NDArray
Expand Down
2 changes: 1 addition & 1 deletion crates/build/re_types_builder/src/codegen/cpp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ impl QuotedObject {
// -> this means that there's no non-move constructors/assignments
// * we really want to make sure that the object is movable, therefore creating a move ctor
// -> this means that there's no implicit move assignment.
// Therefore, we have to define all five move/copy constructors/assignments.
// Therefore, we have to define all five move/copy constructors/assignments.
let hpp = quote! {
#hpp_includes

Expand Down
2 changes: 1 addition & 1 deletion rerun_js/web-viewer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ export class WebViewer {
// NOTE: Callbacks passed to this function must NOT invoke any viewer methods!
// The `setTimeout` is omitted to avoid the 1-tick delay, as it is unnecessary,
// because this is only meant to be used for sending events to Jupyter/Gradio.
//
//
// Do not change this without searching for grepping for usage!
private _on_raw_event(callback: (event: string) => void): () => void {
this.#_raw_events.add(callback);
Expand Down
22 changes: 13 additions & 9 deletions rerun_notebook/src/js/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ interface WidgetModel {

_url?: string;
_panel_states?: PanelStates;
_time_ctrl: [timeline: string | null, time: number | null, play: boolean];
_recording_id?: string;

_fallback_token?: string;
}
Expand All @@ -38,7 +36,6 @@ class ViewerWidget {

constructor(model: AnyModel<WidgetModel>) {
this.url = model.get("_url");
model.on("change:_url", this.on_change_url);

this.panel_states = model.get("_panel_states");
model.on("change:_panel_states", this.on_change_panel_states);
Expand Down Expand Up @@ -104,12 +101,6 @@ class ViewerWidget {
}
};

on_change_url = (_: unknown, new_url?: Opt<string>) => {
if (this.url) this.viewer.close(this.url);
if (new_url) this.viewer.open(new_url);
this.url = new_url;
};

on_change_panel_states = (
_: unknown,
new_panel_states?: Opt<PanelStates>,
Expand Down Expand Up @@ -143,6 +134,10 @@ class ViewerWidget {
this.set_recording_id(msg.recording_id ?? null)
break;
}
case "partition_url": {
this.set_partition_url(msg.partition_url ?? null)
break;
}
default: {
console.error("received unknown message type", msg, buffers);
throw new Error(`unknown message type ${msg}, check console for more details`);
Expand Down Expand Up @@ -188,8 +183,16 @@ class ViewerWidget {

this.viewer.set_active_recording_id(recording_id);
};

set_partition_url(partition_url: string | null){
if (this.url) this.viewer.close(this.url);
if (partition_url) this.viewer.open(partition_url);
this.url = partition_url;
};
}



const render: Render<WidgetModel> = ({ model, el }) => {
el.classList.add("rerun_notebook");

Expand All @@ -213,4 +216,5 @@ function error_boundary<Fn extends (...args: any[]) => any>(f: Fn): Fn {
return wrapper as any;
}


export default { render: error_boundary(render) };
5 changes: 5 additions & 0 deletions rerun_notebook/src/rerun_notebook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ class Viewer(anywidget.AnyWidget): # type: ignore[misc]
_width = traitlets.Int(allow_none=True).tag(sync=True)
_height = traitlets.Int(allow_none=True).tag(sync=True)

# TODO(nick): This traitlet is only used for initialization
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could achieve this by calling open_url in the constructor. We'd need to ensure that events sent before the Viewer is ready are buffered like data with _data_queue.

At that point having another queue doesn't seem great, because data, tables, and other events all go through the same interface. We should have a single _event_queue and a helper (_send_buffered or something) to push data into it, or send it directly depending on if the Viewer is ready yet.

# we should figure out how to pass directly and remove it
_url = traitlets.Unicode(allow_none=True).tag(sync=True)

_panel_states = traitlets.Dict(
Expand Down Expand Up @@ -261,6 +263,9 @@ def set_time_ctrl(self, timeline: str | None, time: int | None, play: bool) -> N
def set_active_recording(self, recording_id: str) -> None:
self.send({"type": "recording_id", "recording_id": recording_id})

def set_active_partition_url(self, url: str) -> None:
self.send({"type": "partition_url", "partition_url": url})

def _on_raw_event(self, callback: Callable[[str], None]) -> None:
"""Register a set of callbacks with this instance of the Viewer."""
# TODO(jan): maybe allow unregister by making this a map instead
Expand Down
48 changes: 44 additions & 4 deletions rerun_py/rerun_bindings/rerun_bindings.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -606,8 +606,7 @@ def load_archive(path_to_rrd: str | os.PathLike[str]) -> RRDArchive:
"""

# AI generated stubs for `PyRecordingStream` related class and functions
# TODO(#9187): this will be entirely replaced with `RecordingStream` is itself written in Rust

# TODO(#9187): this will be entirely replaced with `RecordingStream` is qitself written in Rust
Comment thread
ntjohnson1 marked this conversation as resolved.
Outdated
class PyRecordingStream:
def is_forked_child(self) -> bool:
"""
Expand Down Expand Up @@ -1190,8 +1189,49 @@ class DatasetEntry(Entry):
def partition_table(self) -> DataFusionTable:
"""Return the partition table as a Datafusion table provider."""

def partition_url(self, partition_id: str) -> str:
"""Return the URL for the given partition."""
def partition_url(
self,
partition_id: str,
timeline: str | None = None,
start: datetime | int | None = None,
end: datetime | int | None = None,
) -> str:
"""
Return the URL for the given partition.

Parameters
----------
partition_id: str
The ID of the partition to get the URL for.

timeline: str | None
The name of the timeline to display.

start: int | datetime | None
The start time for the partition.
Integer for ticks, or datetime/nanoseconds for timestamps.


end: int | datetime | None
The end time for the partition.
Integer for ticks, or datetime/nanoseconds for timestamps.

Examples
--------
# With ticks
>>> start_tick, end_time = 0, 10
>>> dataset.partition_url("some_id", "log_tick", start_tick, end_time)

# With timestamps
>>> start_time, end_time = datetime.now() - timedelta(seconds=4), datetime.now()
>>> dataset.partition_url("some_id", "real_time", start_time, end_time)

Returns
-------
str
The URL for the given partition.

"""

def register(self, recording_uri: str, timeout_secs: int = 60) -> str:
"""
Expand Down
19 changes: 19 additions & 0 deletions rerun_py/rerun_sdk/rerun/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,25 @@ def set_active_recording(

self._viewer.set_active_recording(recording_id)

def set_active_partition_url(
self,
*,
url: str,
) -> None:
"""
Set the active partition url for the viewer.

This is equivalent to clicking on the partition in the dataset table.
Comment thread
ntjohnson1 marked this conversation as resolved.
Outdated

Parameters
----------
url: str
The URL of the partition to set the viewer to.

"""

self._viewer.set_active_partition_url(url)

@deprecated_param("nanoseconds", use_instead="duration or timestamp", since="0.23.0")
@deprecated_param("seconds", use_instead="duration or timestamp", since="0.23.0")
def set_time_ctrl(
Expand Down
89 changes: 83 additions & 6 deletions rerun_py/src/catalog/dataset_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ use std::sync::Arc;
use arrow::array::{RecordBatch, StringArray};
use arrow::datatypes::{Field, Schema as ArrowSchema};
use arrow::pyarrow::PyArrowType;
use pyo3::Bound;
use pyo3::types::PyAnyMethods;
use pyo3::{
Py, PyAny, PyRef, PyRefMut, PyResult, Python, exceptions::PyRuntimeError, pyclass, pymethods,
Py, PyAny, PyRef, PyRefMut, PyResult, Python, exceptions::PyRuntimeError,
exceptions::PyValueError, pyclass, pymethods,
};
use tokio_stream::StreamExt as _;
use tracing::instrument;
Expand Down Expand Up @@ -161,20 +164,55 @@ impl PyDatasetEntry {
}

/// Return the URL for the given partition.
fn partition_url(self_: PyRef<'_, Self>, partition_id: String) -> String {
#[pyo3(signature = (partition_id, timeline=None, start=None, end=None))]
fn partition_url(
self_: PyRef<'_, Self>,
py: Python<'_>,
partition_id: String,
timeline: Option<&str>,
start: Option<Bound<'_, PyAny>>,
end: Option<Bound<'_, PyAny>>,
) -> PyResult<String> {
let super_ = self_.as_super();
let connection = super_.client.borrow(self_.py()).connection().clone();

re_uri::DatasetDataUri {
// Timeline with default name and no limits overrides blueprint timeline settings
// only override if timeline is selected
if timeline.is_none() && (start.is_some() || end.is_some()) {
return Err(PyValueError::new_err(
"If `start` or `end` is specified, `timeline` must also be specified.",
));
}

// Convert Python objects to i64
let start_i64 = start
.as_ref()
.map(|s| py_object_to_i64(py, s))
.transpose()?;
let end_i64 = end.as_ref().map(|e| py_object_to_i64(py, e)).transpose()?;

let time_range: Option<re_uri::TimeRange> = match timeline {
Some(name) => Some(re_uri::TimeRange {
timeline: re_chunk::Timeline::new_timestamp(name),
min: start_i64
.map(|start| start.try_into().expect("start time must be valid"))
.unwrap_or(re_log_types::NonMinI64::MIN),
max: end_i64
.map(|end| end.try_into().expect("end time must be valid"))
.unwrap_or(re_log_types::NonMinI64::MAX),
}),
None => None,
};
Ok(re_uri::DatasetDataUri {
origin: connection.origin().clone(),
dataset_id: super_.details.id.id,
partition_id,

//TODO(ab): add support for these two
time_range: None,
time_range: time_range,
//TODO(ab): add support for this
fragment: Default::default(),
}
.to_string()
.to_string())
}

/// Register a RRD URI to the dataset and wait for completion.
Expand Down Expand Up @@ -667,3 +705,42 @@ impl PyDatasetEntry {
})
}
}

/// Helper function to convert a Python object to i64.
///
/// This function attempts to convert various Python types to i64, including:
/// - Python int
/// - numpy datetime64 (via timestamp conversion)
/// - Any object with an `__int__` method
/// - Any object that can be converted to int via Python's int() function
fn py_object_to_i64(py: Python<'_>, obj: &Bound<'_, PyAny>) -> PyResult<i64> {
// First try direct extraction as i64
if let Ok(value) = obj.extract::<i64>() {
return Ok(value);
}

// Try to extract as Python int first
if let Ok(value) = obj.extract::<i32>() {
return Ok(value as i64);
}

// Check if it's a numpy datetime64 and try to get timestamp
if obj.hasattr("timestamp")? {
let timestamp = obj.call_method0("timestamp")?;
if let Ok(ts_float) = timestamp.extract::<f64>() {
// Convert seconds to nanoseconds (assuming timestamp is in seconds)
return Ok((ts_float * 1_000_000_000.0) as i64);
}
}

// Try calling __int__ method if it exists
if obj.hasattr("__int__")? {
let int_result = obj.call_method0("__int__")?;
return int_result.extract::<i64>();
}

// As a last resort, try to convert via Python's int() function
let int_builtin = py.import("builtins")?.getattr("int")?;
let converted = int_builtin.call1((obj,))?;
converted.extract::<i64>()
}
25 changes: 20 additions & 5 deletions scripts/fast_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import concurrent.futures
import logging
import os
import re
import subprocess
import sys
import time
Expand All @@ -27,17 +28,25 @@ def changed_files() -> list[str]:

@dataclass
class LintJob:
command: str
command: str | list[str]
extensions: list[str] | None = None
accepts_files: bool = True
no_filter_args: list[str] = field(default_factory=list)
no_filter_cmd: str | None = None
allow_no_filter: bool = True
filter_files: str | None = None
_commands: list[str] = field(init=False, repr=False)

def __post_init__(self) -> None:
if isinstance(self.command, str):
self._commands = self.command.split()
else:
self._commands = self.command

def run_cmd(self, files: list[str], skip_list: list[str], no_change_filter: bool) -> bool:
start = time.time()

cmd = self.command
cmd = self._commands

if self.extensions is not None:
files = [f for f in files if any(f.endswith(e) for e in self.extensions)]
Expand All @@ -58,9 +67,14 @@ def run_cmd(self, files: list[str], skip_list: list[str], no_change_filter: bool
return True
files = self.no_filter_args
if self.no_filter_cmd is not None:
cmd = self.no_filter_cmd
cmd = self.no_filter_cmd.split()
else:
# Apply regex filtering if filter_files is specified
if self.filter_files is not None:
pattern = re.compile(self.filter_files)
files = [f for f in files if not pattern.match(f)]

cmd_arr = ["pixi", "run", cmd]
cmd_arr = ["pixi", "run"] + cmd

cmd_preview = subprocess.list2cmdline(cmd_arr + ["<FILES>"]) if files else subprocess.list2cmdline(cmd_arr)

Expand Down Expand Up @@ -150,13 +164,14 @@ def main() -> None:
"lint-rs-files",
extensions=[".rs"],
no_filter_cmd="lint-rs-all",
filter_files=r"^crates/store/re_types(_core)?/",
),
LintJob("py-fmt-check", extensions=[".py"], no_filter_args=PY_FOLDERS),
# Even though mypy will accept a list of files, the results it generates are inconsistent
# with running on the full project.
LintJob("py-lint", extensions=[".py"], accepts_files=False),
LintJob("toml-fmt-check", extensions=[".toml"]),
LintJob("lint-typos"),
LintJob("lint-typos --force-exclude"),
]

for command in skip:
Expand Down
2 changes: 1 addition & 1 deletion scripts/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,7 @@ def main() -> None:
num_errors += lint_file(filepath, args)
else:
for root, dirs, files in os.walk(".", topdown=True):
dirs[:] = [d for d in dirs if not should_ignore(d)]
dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]

for filename in files:
extension = filename.split(".")[-1]
Expand Down
Loading