foxglove-sdk 0.14.2__cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl → 0.16.1__cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.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.

Potentially problematic release.


This version of foxglove-sdk might be problematic. Click here for more details.

foxglove/__init__.py CHANGED
@@ -9,20 +9,34 @@ from __future__ import annotations
9
9
 
10
10
  import atexit
11
11
  import logging
12
+ from typing import TYPE_CHECKING
12
13
 
13
14
  from . import _foxglove_py as _foxglove
14
15
 
15
16
  # Re-export these imports
16
- from ._foxglove_py import Context, Schema, open_mcap
17
+ from ._foxglove_py import (
18
+ ChannelDescriptor,
19
+ Context,
20
+ Schema,
21
+ SinkChannelFilter,
22
+ open_mcap,
23
+ )
17
24
  from .channel import Channel, log
18
25
 
19
26
  # Deprecated. Use foxglove.mcap.MCAPWriter instead.
20
27
  from .mcap import MCAPWriter
21
28
 
29
+ if TYPE_CHECKING:
30
+ from .notebook.notebook_buffer import NotebookBuffer
31
+
22
32
  atexit.register(_foxglove.shutdown)
23
33
 
24
34
 
25
35
  try:
36
+ from ._foxglove_py.cloud import CloudSink
37
+
38
+ # from ._foxglove_py.cloud import start_cloud_sink as _start_cloud_sink
39
+ from .cloud import CloudSinkListener
26
40
  from .websocket import (
27
41
  AssetHandler,
28
42
  Capability,
@@ -43,6 +57,7 @@ try:
43
57
  asset_handler: AssetHandler | None = None,
44
58
  context: Context | None = None,
45
59
  session_id: str | None = None,
60
+ channel_filter: SinkChannelFilter | None = None,
46
61
  ) -> WebSocketServer:
47
62
  """
48
63
  Start a websocket server for live visualization.
@@ -61,6 +76,9 @@ try:
61
76
  :param session_id: An ID which allows the client to understand if the connection is a
62
77
  re-connection or a new server instance. If None, then an ID is generated based on the
63
78
  current time.
79
+ :param channel_filter: A `Callable` that determines whether a channel should be logged to.
80
+ Return `True` to log the channel, or `False` to skip it. By default, all channels
81
+ will be logged.
64
82
  """
65
83
  return _foxglove.start_server(
66
84
  name=name,
@@ -73,6 +91,35 @@ try:
73
91
  asset_handler=asset_handler,
74
92
  context=context,
75
93
  session_id=session_id,
94
+ channel_filter=channel_filter,
95
+ )
96
+
97
+ def start_cloud_sink(
98
+ *,
99
+ listener: CloudSinkListener | None = None,
100
+ supported_encodings: list[str] | None = None,
101
+ context: Context | None = None,
102
+ session_id: str | None = None,
103
+ ) -> CloudSink:
104
+ """
105
+ Connect to Foxglove Agent for live visualization and teleop.
106
+
107
+ Foxglove Agent must be running on the same host for this to work.
108
+
109
+ :param capabilities: A list of capabilities to advertise to the agent.
110
+ :param listener: A Python object that implements the
111
+ :py:class:`cloud.CloudSinkListener` protocol.
112
+ :param supported_encodings: A list of encodings to advertise to the agent.
113
+ :param context: The context to use for logging. If None, the global context is used.
114
+ :param session_id: An ID which allows the agent to understand if the connection is a
115
+ re-connection or a new connection instance. If None, then an ID is generated based on
116
+ the current time.
117
+ """
118
+ return _foxglove.start_cloud_sink(
119
+ listener=listener,
120
+ supported_encodings=supported_encodings,
121
+ context=context,
122
+ session_id=session_id,
76
123
  )
77
124
 
78
125
  except ImportError:
@@ -123,13 +170,72 @@ def _level_names() -> dict[str, int]:
123
170
  }
124
171
 
125
172
 
173
+ def init_notebook_buffer(context: Context | None = None) -> NotebookBuffer:
174
+ """
175
+ Create a NotebookBuffer object to manage data buffering and visualization in Jupyter
176
+ notebooks.
177
+
178
+ The NotebookBuffer object will buffer all data logged to the provided context. When you
179
+ are ready to visualize the data, you can call the :meth:`show` method to display an embedded
180
+ Foxglove visualization widget. The widget provides a fully-featured Foxglove interface
181
+ directly within your Jupyter notebook, allowing you to explore multi-modal robotics data
182
+ including 3D scenes, plots, images, and more.
183
+
184
+ Args:
185
+ context: The Context used to log the messages. If no Context is provided, the global
186
+ context will be used. Logged messages will be buffered.
187
+
188
+ Returns:
189
+ NotebookBuffer: A NotebookBuffer object that can be used to manage the data buffering
190
+ and visualization.
191
+
192
+ Raises:
193
+ Exception: If the notebook extra package is not installed. Install it
194
+ with `pip install foxglove-sdk[notebook]`.
195
+
196
+ Note:
197
+ This function is only available when the `notebook` extra package
198
+ is installed. Install it with `pip install foxglove-sdk[notebook]`.
199
+
200
+ Example:
201
+ >>> import foxglove
202
+ >>>
203
+ >>> # Create a basic viewer using the default context
204
+ >>> nb_buffer = foxglove.init_notebook_buffer()
205
+ >>>
206
+ >>> # Or use a specific context
207
+ >>> nb_buffer = foxglove.init_notebook_buffer(context=my_ctx)
208
+ >>>
209
+ >>> # ... log data as usual ...
210
+ >>>
211
+ >>> # Display the widget in the notebook
212
+ >>> nb_buffer.show()
213
+ """
214
+ try:
215
+ from .notebook.notebook_buffer import NotebookBuffer
216
+
217
+ except ImportError:
218
+ raise Exception(
219
+ "NotebookBuffer is not installed. "
220
+ 'Please install it with `pip install "foxglove-sdk[notebook]"`'
221
+ )
222
+
223
+ return NotebookBuffer(context=context)
224
+
225
+
126
226
  __all__ = [
127
227
  "Channel",
228
+ "ChannelDescriptor",
128
229
  "Context",
129
230
  "MCAPWriter",
130
231
  "Schema",
232
+ "SinkChannelFilter",
233
+ "CloudSink",
234
+ "CloudSinkListener",
235
+ "start_cloud_sink",
131
236
  "log",
132
237
  "open_mcap",
133
238
  "set_log_level",
134
239
  "start_server",
240
+ "init_notebook_buffer",
135
241
  ]
@@ -1,8 +1,9 @@
1
1
  from pathlib import Path
2
- from typing import Any
2
+ from typing import Any, Callable
3
3
 
4
4
  from foxglove.websocket import AssetHandler
5
5
 
6
+ from .cloud import CloudSink
6
7
  from .mcap import MCAPWriteOptions, MCAPWriter
7
8
  from .websocket import Capability, Service, WebSocketServer
8
9
 
@@ -15,7 +16,7 @@ class BaseChannel:
15
16
  self,
16
17
  topic: str,
17
18
  message_encoding: str,
18
- schema: Schema | None = None,
19
+ schema: "Schema" | None = None,
19
20
  metadata: dict[str, str] | None = None,
20
21
  ) -> None: ...
21
22
  def id(self) -> int:
@@ -115,6 +116,26 @@ class Context:
115
116
  """
116
117
  ...
117
118
 
119
+ @staticmethod
120
+ def default() -> "Context":
121
+ """
122
+ Returns the default context.
123
+ """
124
+ ...
125
+
126
+ class ChannelDescriptor:
127
+ """
128
+ Information about a channel
129
+ """
130
+
131
+ id: int
132
+ topic: str
133
+ message_encoding: str
134
+ metadata: dict[str, str]
135
+ schema: "Schema" | None
136
+
137
+ SinkChannelFilter = Callable[[ChannelDescriptor], bool]
138
+
118
139
  def start_server(
119
140
  *,
120
141
  name: str | None = None,
@@ -127,12 +148,25 @@ def start_server(
127
148
  asset_handler: AssetHandler | None = None,
128
149
  context: Context | None = None,
129
150
  session_id: str | None = None,
151
+ channel_filter: SinkChannelFilter | None = None,
130
152
  ) -> WebSocketServer:
131
153
  """
132
154
  Start a websocket server for live visualization.
133
155
  """
134
156
  ...
135
157
 
158
+ def start_cloud_sink(
159
+ *,
160
+ listener: Any = None,
161
+ supported_encodings: list[str] | None = None,
162
+ context: Context | None = None,
163
+ session_id: str | None = None,
164
+ ) -> CloudSink:
165
+ """
166
+ Connect to Foxglove Agent for remote visualization and teleop.
167
+ """
168
+ ...
169
+
136
170
  def enable_logging(level: int) -> None:
137
171
  """
138
172
  Forward SDK logs to python's logging facility.
@@ -156,6 +190,7 @@ def open_mcap(
156
190
  *,
157
191
  allow_overwrite: bool = False,
158
192
  context: Context | None = None,
193
+ channel_filter: SinkChannelFilter | None = None,
159
194
  writer_options: MCAPWriteOptions | None = None,
160
195
  ) -> MCAPWriter:
161
196
  """
@@ -163,6 +198,8 @@ def open_mcap(
163
198
 
164
199
  If a context is provided, the MCAP file will be associated with that context. Otherwise, the
165
200
  global context will be used.
201
+
202
+ You must close the writer with close() or the with statement to ensure the file is correctly finished.
166
203
  """
167
204
  ...
168
205
 
@@ -0,0 +1,9 @@
1
+ class CloudSink:
2
+ """
3
+ A cloud sink for remote visualization and teleop.
4
+ """
5
+
6
+ def __init__(self) -> None: ...
7
+ def stop(self) -> None:
8
+ """Gracefully disconnect from the cloud sink, waiting for shutdown to complete."""
9
+ ...
@@ -82,3 +82,15 @@ class MCAPWriter:
82
82
  exiting the context manager.
83
83
  """
84
84
  ...
85
+
86
+ def write_metadata(self, name: str, metadata: dict[str, str]) -> None:
87
+ """
88
+ Write metadata to the MCAP file.
89
+
90
+ Metadata consists of key-value string pairs associated with a name.
91
+ If the metadata dictionary is empty, this method does nothing.
92
+
93
+ :param name: Name identifier for this metadata record
94
+ :param metadata: Dictionary of key-value pairs to store
95
+ """
96
+ ...
@@ -15,7 +15,7 @@ class Capability(Enum):
15
15
  ClientPublish = ...
16
16
  """Allow clients to advertise channels to send data messages to the server."""
17
17
 
18
- Connectiongraph = ...
18
+ ConnectionGraph = ...
19
19
  """Allow clients to subscribe and make connection graph updates"""
20
20
 
21
21
  Parameters = ...
foxglove/cloud.py ADDED
@@ -0,0 +1,61 @@
1
+ from typing import Protocol
2
+
3
+ from ._foxglove_py.websocket import (
4
+ ChannelView,
5
+ Client,
6
+ ClientChannel,
7
+ )
8
+
9
+
10
+ class CloudSinkListener(Protocol):
11
+ """
12
+ A mechanism to register callbacks for handling client message events.
13
+ """
14
+
15
+ def on_subscribe(self, client: Client, channel: ChannelView) -> None:
16
+ """
17
+ Called when a client subscribes to a channel.
18
+
19
+ :param client: The client (id) that sent the message.
20
+ :param channel: The channel (id, topic) that the message was sent on.
21
+ """
22
+ return None
23
+
24
+ def on_unsubscribe(self, client: Client, channel: ChannelView) -> None:
25
+ """
26
+ Called when a client unsubscribes from a channel or disconnects.
27
+
28
+ :param client: The client (id) that sent the message.
29
+ :param channel: The channel (id, topic) that the message was sent on.
30
+ """
31
+ return None
32
+
33
+ def on_client_advertise(self, client: Client, channel: ClientChannel) -> None:
34
+ """
35
+ Called when a client advertises a channel.
36
+
37
+ :param client: The client (id) that sent the message.
38
+ :param channel: The client channel that is being advertised.
39
+ """
40
+ return None
41
+
42
+ def on_client_unadvertise(self, client: Client, client_channel_id: int) -> None:
43
+ """
44
+ Called when a client unadvertises a channel.
45
+
46
+ :param client: The client (id) that is unadvertising the channel.
47
+ :param client_channel_id: The client channel id that is being unadvertised.
48
+ """
49
+ return None
50
+
51
+ def on_message_data(
52
+ self, client: Client, client_channel_id: int, data: bytes
53
+ ) -> None:
54
+ """
55
+ Called when a message is received from a client.
56
+
57
+ :param client: The client (id) that sent the message.
58
+ :param client_channel_id: The client channel id that the message was sent on.
59
+ :param data: The message data.
60
+ """
61
+ return None
File without changes
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import pathlib
4
+ from typing import TYPE_CHECKING, Any, Literal
5
+
6
+ import anywidget
7
+ import traitlets
8
+
9
+ if TYPE_CHECKING:
10
+ from .notebook_buffer import NotebookBuffer
11
+
12
+
13
+ class FoxgloveWidget(anywidget.AnyWidget):
14
+ """
15
+ A widget that displays a Foxglove viewer in a notebook.
16
+
17
+ :param buffer: The NotebookBuffer object that contains the data to display in the widget.
18
+ :param layout_storage_key: The storage key of the layout to use for the widget.
19
+ :param width: The width of the widget. Defaults to "full".
20
+ :param height: The height of the widget in pixels. Defaults to 500.
21
+ :param src: The source URL of the Foxglove viewer. Defaults to "https://embed.foxglove.dev/".
22
+ """
23
+
24
+ _esm = pathlib.Path(__file__).parent / "static" / "widget.js"
25
+ width = traitlets.Union(
26
+ [traitlets.Int(), traitlets.Enum(values=["full"])], default_value="full"
27
+ ).tag(sync=True)
28
+ height = traitlets.Int(default_value=500).tag(sync=True)
29
+ src = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
30
+ _layout_params = traitlets.Dict(
31
+ per_key_traits={
32
+ "storage_key": traitlets.Unicode(),
33
+ "opaque_layout": traitlets.Dict(allow_none=True, default_value=None),
34
+ "force": traitlets.Bool(False),
35
+ },
36
+ allow_none=True,
37
+ default_value=None,
38
+ ).tag(sync=True)
39
+
40
+ def __init__(
41
+ self,
42
+ buffer: NotebookBuffer,
43
+ layout_storage_key: str,
44
+ width: int | Literal["full"] | None = None,
45
+ height: int | None = None,
46
+ src: str | None = None,
47
+ **kwargs: Any,
48
+ ):
49
+ super().__init__(**kwargs)
50
+ if width is not None:
51
+ self.width = width
52
+ else:
53
+ self.width = "full"
54
+ if height is not None:
55
+ self.height = height
56
+ if src is not None:
57
+ self.src = src
58
+
59
+ self.select_layout(layout_storage_key, **kwargs)
60
+
61
+ # Callback to get the data to display in the widget
62
+ self._buffer = buffer
63
+ # Keep track of when the widget is ready to receive data
64
+ self._ready = False
65
+ # Pending data to be sent when the widget is ready
66
+ self._pending_data: list[bytes] = []
67
+ self.on_msg(self._handle_custom_msg)
68
+ self.refresh()
69
+
70
+ def select_layout(self, storage_key: str, **kwargs: Any) -> None:
71
+ """
72
+ Select a layout in the Foxglove viewer.
73
+ """
74
+ opaque_layout = kwargs.get("opaque_layout", None)
75
+ force_layout = kwargs.get("force_layout", False)
76
+
77
+ self._layout_params = {
78
+ "storage_key": storage_key,
79
+ "opaque_layout": opaque_layout if isinstance(opaque_layout, dict) else None,
80
+ "force": force_layout,
81
+ }
82
+
83
+ def refresh(self) -> None:
84
+ """
85
+ Refresh the widget by getting the data from the callback function and sending it
86
+ to the widget.
87
+ """
88
+ data = self._buffer.get_data()
89
+ if not self._ready:
90
+ self._pending_data = data
91
+ else:
92
+ self.send({"type": "update-data"}, data)
93
+
94
+ def _handle_custom_msg(self, msg: dict, buffers: list[bytes]) -> None:
95
+ if msg["type"] == "ready":
96
+ self._ready = True
97
+
98
+ if len(self._pending_data) > 0:
99
+ self.send({"type": "update-data"}, self._pending_data)
100
+ self._pending_data = []
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import uuid
5
+ from tempfile import TemporaryDirectory
6
+ from typing import Any, Literal
7
+
8
+ from mcap.reader import make_reader
9
+
10
+ from .._foxglove_py import Context, open_mcap
11
+ from .foxglove_widget import FoxgloveWidget
12
+
13
+
14
+ class NotebookBuffer:
15
+ """
16
+ A data buffer to collect and manage messages and visualization in Jupyter notebooks.
17
+
18
+ The NotebookBuffer object will buffer all data logged to the provided context. When you
19
+ are ready to visualize the data, you can call the :meth:`show` method to display an embedded
20
+ Foxglove visualization widget. The widget provides a fully-featured Foxglove interface
21
+ directly within your Jupyter notebook, allowing you to explore multi-modal robotics data
22
+ including 3D scenes, plots, images, and more.
23
+
24
+ :param context: The Context used to log the messages. If no Context is provided, the global
25
+ context will be used. Logged messages will be buffered.
26
+ """
27
+
28
+ def __init__(self, context: Context | None = None):
29
+ """
30
+ Initialize a new NotebookBuffer for collecting logged messages.
31
+ """
32
+ # We need to keep the temporary directory alive until the writer is closed
33
+ self._temp_directory = TemporaryDirectory()
34
+ self._context = context
35
+ self._files: list[str] = []
36
+ self._create_writer()
37
+
38
+ def show(
39
+ self,
40
+ layout_storage_key: str,
41
+ width: int | Literal["full"] | None = None,
42
+ height: int | None = None,
43
+ src: str | None = None,
44
+ **kwargs: Any,
45
+ ) -> FoxgloveWidget:
46
+ """
47
+ Show the Foxglove viewer. Call this method as the last step of a notebook cell
48
+ to display the viewer.
49
+ """
50
+ widget = FoxgloveWidget(
51
+ buffer=self,
52
+ width=width,
53
+ height=height,
54
+ src=src,
55
+ layout_storage_key=layout_storage_key,
56
+ **kwargs,
57
+ )
58
+ return widget
59
+
60
+ def clear(self) -> None:
61
+ """
62
+ Clear the buffered data.
63
+ """
64
+ self._writer.close()
65
+ # Delete the temporary directory and all its contents
66
+ self._temp_directory.cleanup()
67
+ # Reset files list
68
+ self._files = []
69
+ # Create a new temporary directory
70
+ self._temp_directory = TemporaryDirectory()
71
+ self._create_writer()
72
+
73
+ def get_data(self) -> list[bytes]:
74
+ """
75
+ Retrieve all collected data.
76
+ """
77
+ # close the current writer
78
+ self._writer.close()
79
+
80
+ if len(self._files) > 1:
81
+ if is_mcap_empty(self._files[-1]):
82
+ # If the last file is empty, remove the last file since it won't add any new data
83
+ # to the buffer
84
+ os.remove(self._files[-1])
85
+ self._files.pop()
86
+ elif is_mcap_empty(self._files[0]):
87
+ # If the first file is empty, remove the first file since it won't add any new data
88
+ # to the buffer
89
+ os.remove(self._files[0])
90
+ self._files.pop(0)
91
+
92
+ # read the content of the files
93
+ contents: list[bytes] = []
94
+ for file_name in self._files:
95
+ with open(file_name, "rb") as f_read:
96
+ contents.append(f_read.read())
97
+
98
+ self._create_writer()
99
+
100
+ return contents
101
+
102
+ def _create_writer(self) -> None:
103
+ random_id = uuid.uuid4().hex[:8]
104
+ file_name = f"{self._temp_directory.name}/log-{random_id}.mcap"
105
+ self._files.append(file_name)
106
+ self._writer = open_mcap(path=file_name, context=self._context)
107
+
108
+
109
+ def is_mcap_empty(file_name: str) -> bool:
110
+ with open(file_name, "rb") as f_read:
111
+ iter = make_reader(f_read).iter_messages()
112
+ is_empty = next(iter, None) is None
113
+
114
+ return is_empty
@@ -0,0 +1 @@
1
+ var y=EventTarget,p="https://embed.foxglove.dev/",g="foxglove.default-layout";function c(){let t,e;return{promise:new Promise((s,i)=>{t=s,e=i}),resolve:t,reject:e}}var h=class extends y{#e;#i;#h;#r=!1;#n;#t={dataSource:void 0,selectLayout:void 0,extension:[]};#a;#s=!1;constructor(e){super();let{parent:a,src:s,orgSlug:i,initialDataSource:o,initialLayout:n,initialLayoutParams:r,initialExtensions:l,colorScheme:d="auto"}=e;this.#h=i,this.#n=c();let u=s??p;try{this.#i=new URL(u)}catch{throw new Error(`[FoxgloveViewer] Invalid server URL: ${u}`)}window.addEventListener("message",this.#d),o&&this.setDataSource(o),n!=null&&r==null&&this.setLayoutData(n),r!=null&&this.selectLayout(r),l&&this.installExtensions(l),this.#e=document.createElement("iframe"),this.#e.src=u,this.#e.title="Foxglove",this.#e.allow="cross-origin-isolated",this.#e.style.width="100%",this.#e.style.height="100%",this.#e.style.border="none",this.setColorScheme(d),a.appendChild(this.#e)}setDataSource(e){this.#l({type:"set-data-source",payload:e})}selectLayout(e){this.#l({type:"select-layout",payload:e})}async getLayout(){return this.#a||(this.#a=c(),await this.#n.promise,this.#o({type:"get-layout"})),await this.#a.promise}setLayoutData(e){this.selectLayout({storageKey:g,opaqueLayout:e,force:!0})}installExtensions(e){this.#l({type:"install-extension",payload:e})}isReady(){return this.#r}destroy(){this.#s=!0,this.#e.remove(),window.removeEventListener("message",this.#d)}isDestroyed(){return this.#s}setColorScheme(e){this.#e.style.colorScheme=e==="auto"?"normal":e}#l(e){switch(e.type){case"install-extension":this.#t.extension.push(e);break;case"set-data-source":this.#t.dataSource=e;break;case"select-layout":this.#t.selectLayout=e;break}this.#r&&this.#o(e)}#o(e){if(this.#s){console.warn("[FoxgloveViewer] Unable to post command. Frame has been destroyed.");return}f(this.#e.contentWindow,"Invariant: iframe should be loaded."),this.#e.contentWindow.postMessage(e,this.#i.href)}#d=e=>{let a=new URL(e.origin);if(!(e.source!==this.#e.contentWindow||a.href!==this.#i.href)){if(this.#s){console.warn("[FoxgloveViewer] Unable to handle message. Frame has been destroyed.");return}switch(e.data.type){case"foxglove-origin-request":this.#o({type:"origin-ack"});break;case"foxglove-handshake-request":this.#r=!0,this.#n.resolve(),this.#o({type:"handshake-ack",payload:{orgSlug:this.#h,initialDataSource:this.#t.dataSource?.payload,initialLayoutParams:this.#t.selectLayout?.payload,initialExtensions:this.#t.extension.flatMap(s=>s.payload)}});break;case"foxglove-handshake-complete":this.dispatchEvent(new Event("ready"));break;case"foxglove-error":this.dispatchEvent(new CustomEvent("error",{detail:e.data.payload}));break;case"foxglove-layout-data":this.#a?(this.#a.resolve(e.data.payload),this.#a=void 0):console.warn("[FoxgloveViewer] Received layout but getLayout was not called.");break;default:console.warn("[FoxgloveViewer] Unhandled message type:",e.data);break}}}};function f(t,e="no additional info provided"){if(!t)throw new Error("Assertion Error: "+e)}function w({model:t,el:e}){let a=document.createElement("div"),s=t.get("_layout_params"),i=new h({parent:a,src:t.get("src"),orgSlug:void 0,initialLayoutParams:s?{storageKey:s.storage_key,opaqueLayout:s.opaque_layout,force:s.force}:void 0});i.addEventListener("ready",()=>{t.send({type:"ready"})}),t.on("msg:custom",(o,n)=>{if(o.type==="update-data"){let r=n.map((l,d)=>new File([l.buffer],`data-${d}.mcap`));i.setDataSource({type:"file",file:r})}}),a.style.width=t.get("width")==="full"?"100%":`${t.get("width")}px`,a.style.height=`${t.get("height")}px`,t.on("change:width",()=>{a.style.width=t.get("width")==="full"?"100%":`${t.get("width")}px`}),t.on("change:height",()=>{a.style.height=`${t.get("height")}px`}),t.on("change:_layout_params",()=>{let o=t.get("_layout_params");o&&i.selectLayout({storageKey:o.storage_key,opaqueLayout:o.opaque_layout,force:o.force})}),e.appendChild(a)}var b={render:w};export{b as default};
@@ -0,0 +1,10 @@
1
+ from foxglove import Context
2
+
3
+
4
+ def test_default_context_is_singleton() -> None:
5
+ assert Context.default() is Context.default()
6
+
7
+
8
+ def test_context_is_distinct() -> None:
9
+ assert Context() is not Context.default()
10
+ assert Context() is not Context()
@@ -1,4 +1,7 @@
1
1
  import logging
2
+ import os
3
+ import subprocess
4
+ import sys
2
5
 
3
6
  import pytest
4
7
  from foxglove import set_log_level
@@ -14,3 +17,46 @@ def test_set_log_level_accepts_string_or_int() -> None:
14
17
  def test_set_log_level_clamps_illegal_values() -> None:
15
18
  set_log_level(-1)
16
19
  set_log_level(2**64)
20
+
21
+
22
+ def test_logging_config_with_env() -> None:
23
+ # Run a script in a child process so logger can be re-initialized from env.
24
+ test_script = """
25
+ import logging
26
+ import foxglove
27
+
28
+ logging.basicConfig(level=logging.DEBUG)
29
+
30
+ server = foxglove.start_server(port=0)
31
+ server.stop()
32
+
33
+ print("test_init_with_env_complete")
34
+ """
35
+
36
+ # Default: unset
37
+ env = os.environ.copy()
38
+ env["FOXGLOVE_LOG_LEVEL"] = ""
39
+
40
+ result = subprocess.run(
41
+ [sys.executable, "-c", test_script],
42
+ env=env,
43
+ capture_output=True,
44
+ text=True,
45
+ timeout=5,
46
+ )
47
+ assert "test_init_with_env_complete" in result.stdout
48
+ assert "Started server" in result.stderr
49
+
50
+ # Quiet the WS server logging
51
+ env = os.environ.copy()
52
+ env["FOXGLOVE_LOG_LEVEL"] = "debug,foxglove::websocket::server=warn"
53
+
54
+ result = subprocess.run(
55
+ [sys.executable, "-c", test_script],
56
+ env=env,
57
+ capture_output=True,
58
+ text=True,
59
+ timeout=5,
60
+ )
61
+ assert "test_init_with_env_complete" in result.stdout
62
+ assert "Started server" not in result.stderr
@@ -2,7 +2,7 @@ from pathlib import Path
2
2
  from typing import Callable, Generator, Optional
3
3
 
4
4
  import pytest
5
- from foxglove import Channel, Context, open_mcap
5
+ from foxglove import Channel, ChannelDescriptor, Context, open_mcap
6
6
  from foxglove.mcap import MCAPWriteOptions
7
7
 
8
8
  chan = Channel("test", schema={"type": "object"})
@@ -114,3 +114,86 @@ def test_write_to_different_contexts(make_tmp_mcap: Callable[[], Path]) -> None:
114
114
  contents2 = tmp_2.read_bytes()
115
115
 
116
116
  assert len(contents1) < len(contents2)
117
+
118
+
119
+ def _verify_metadata_in_file(file_path: Path, expected_metadata: dict) -> None:
120
+ """Helper function to verify metadata in MCAP file matches expected."""
121
+ import mcap.reader
122
+
123
+ with open(file_path, "rb") as f:
124
+ reader = mcap.reader.make_reader(f)
125
+
126
+ found_metadata = {}
127
+ metadata_count = 0
128
+
129
+ for record in reader.iter_metadata():
130
+ metadata_count += 1
131
+ found_metadata[record.name] = dict(record.metadata)
132
+
133
+ # Verify count
134
+ assert metadata_count == len(
135
+ expected_metadata
136
+ ), f"Expected {len(expected_metadata)} metadata records, found {metadata_count}"
137
+
138
+ # Verify metadata names and content
139
+ assert set(found_metadata.keys()) == set(
140
+ expected_metadata.keys()
141
+ ), "Metadata names don't match"
142
+
143
+ for name, expected_kv in expected_metadata.items():
144
+ assert (
145
+ found_metadata[name] == expected_kv
146
+ ), f"Metadata '{name}' has wrong key-value pairs"
147
+
148
+
149
+ def test_write_metadata(tmp_mcap: Path) -> None:
150
+ """Test writing metadata to MCAP file."""
151
+ # Define expected metadata
152
+ expected_metadata = {
153
+ "test1": {"key1": "value1", "key2": "value2"},
154
+ "test2": {"a": "1", "b": "2"},
155
+ "test3": {"x": "y", "z": "w"},
156
+ }
157
+
158
+ with open_mcap(tmp_mcap) as writer:
159
+ # This should not raise an error
160
+ writer.write_metadata("empty", {})
161
+
162
+ # Write basic metadata
163
+ writer.write_metadata("test1", expected_metadata["test1"])
164
+
165
+ # Write multiple metadata records
166
+ writer.write_metadata("test2", expected_metadata["test2"])
167
+ writer.write_metadata("test3", expected_metadata["test3"])
168
+
169
+ # Write empty metadata (should be skipped)
170
+ writer.write_metadata("empty_test", {})
171
+
172
+ # Log some messages
173
+ for ii in range(5):
174
+ chan.log({"foo": ii})
175
+
176
+ # Verify metadata was written correctly
177
+ _verify_metadata_in_file(tmp_mcap, expected_metadata)
178
+
179
+
180
+ def test_channel_filter(make_tmp_mcap: Callable[[], Path]) -> None:
181
+ tmp_1 = make_tmp_mcap()
182
+ tmp_2 = make_tmp_mcap()
183
+
184
+ ch1 = Channel("/1", schema={"type": "object"})
185
+ ch2 = Channel("/2", schema={"type": "object"})
186
+
187
+ def filter(ch: ChannelDescriptor) -> bool:
188
+ return ch.topic.startswith("/1")
189
+
190
+ mcap1 = open_mcap(tmp_1, channel_filter=filter)
191
+ mcap2 = open_mcap(tmp_2, channel_filter=None)
192
+
193
+ ch1.log({})
194
+ ch2.log({})
195
+
196
+ mcap1.close()
197
+ mcap2.close()
198
+
199
+ assert tmp_1.stat().st_size < tmp_2.stat().st_size
@@ -17,7 +17,9 @@ def test_server_interface() -> None:
17
17
  """
18
18
  Exercise the server interface; will also be checked with mypy.
19
19
  """
20
- server = start_server(port=0, session_id="test-session")
20
+ server = start_server(
21
+ port=0, session_id="test-session", channel_filter=lambda _: True
22
+ )
21
23
  assert isinstance(server.port, int)
22
24
  assert server.port != 0
23
25
 
foxglove/websocket.py CHANGED
@@ -1,13 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import sys
4
3
  from collections.abc import Callable
5
- from typing import Protocol, Union
6
-
7
- if sys.version_info >= (3, 10):
8
- from typing import TypeAlias
9
- else:
10
- from typing import Any as TypeAlias
4
+ from typing import Protocol, TypeAlias, Union
11
5
 
12
6
  from ._foxglove_py.websocket import (
13
7
  Capability,
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: foxglove-sdk
3
+ Version: 0.16.1
4
+ Classifier: Programming Language :: Python :: 3
5
+ Classifier: Programming Language :: Rust
6
+ Requires-Dist: anywidget ; extra == 'notebook'
7
+ Requires-Dist: mcap ; extra == 'notebook'
8
+ Requires-Dist: traitlets ; extra == 'notebook'
9
+ Provides-Extra: notebook
10
+ Summary: Foxglove Python SDK
11
+ Author-email: Foxglove <support@foxglove.dev>
12
+ License-Expression: MIT
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
+ Project-URL: repository, https://github.com/foxglove/foxglove-sdk
16
+ Project-URL: documentation, https://docs.foxglove.dev/
17
+
18
+ # Foxglove Python SDK
19
+
20
+ The official [Foxglove](https://docs.foxglove.dev/docs) SDK for Python.
21
+
22
+ This package provides support for integrating with the Foxglove platform. It can be used to log
23
+ events to local [MCAP](https://mcap.dev/) files or a local visualization server that communicates
24
+ with the Foxglove app.
25
+
26
+ ## Get Started
27
+
28
+ See https://foxglove.github.io/foxglove-sdk/python/
29
+
30
+ ## Requirements
31
+
32
+ - Python 3.10+
33
+
34
+ ## Examples
35
+
36
+ We're using uv as a Python package manager in the foxglove-sdk-examples.
37
+
38
+ To test that all examples run (as the CI does) you can use `yarn run-python-sdk-examples` in the repo root.
39
+
40
+ To run a specific example (e.g. write-mcap-file) with local changes:
41
+
42
+ ```
43
+ cd python/foxglove-sdk-examples/write-mcap-file
44
+ uv run --with ../../foxglove-sdk main.py [args]
45
+ ```
46
+
47
+ Keep in mind that uv does two layers of caching.
48
+ There's the .venv in your project directory, plus a global cache at ~/.cache/uv.
49
+
50
+ uv tries to be smart about not rebuilding things it has already built,
51
+ which means that if you make changes and you want them to show up,
52
+ you also need to run `uv cache clean`.
53
+
@@ -0,0 +1,33 @@
1
+ foxglove/__init__.py,sha256=07Fz3ZZJdqsXRl0uKpIOID8ysa1HpQ741pQlvz1yA3I,8222
2
+ foxglove/_foxglove_py.cpython-314-i386-linux-gnu.so,sha256=7wXvtX6aTr5t70xbhB6mJ02UA_ubDdCUx7CK_NtQAL8,6627112
3
+ foxglove/_foxglove_py/__init__.pyi,sha256=lh2gtNThduHDOIO1pQ7NaGnjAPr6s_VlbdBMRKM2dns,5201
4
+ foxglove/_foxglove_py/channels.pyi,sha256=c3WXhK5FfeCJ1aDGB-AsayIossnC8v8qFij0XWl1AEA,67327
5
+ foxglove/_foxglove_py/cloud.pyi,sha256=9Hqj7b9S2CTiWeWOIqaAw3GSmR-cGoSL4R7fi5WI-QA,255
6
+ foxglove/_foxglove_py/mcap.pyi,sha256=3ZLmrcomTN0bXoseDYGyAOd33I7fkzT4WkT130RlKJA,3693
7
+ foxglove/_foxglove_py/schemas.pyi,sha256=oVF6fwYlgoZ8a1FmlLhVgCYbz-ueM52NyuuTtwhGuFc,23092
8
+ foxglove/_foxglove_py/schemas_wkt.pyi,sha256=_nHeIdbOKMgPeL5V360-vZXCnEtyRIFzEd_JVsK49qo,2065
9
+ foxglove/_foxglove_py/websocket.pyi,sha256=CrfkGZMqVR4xn2cV7fQHUA2T6h2WddzMOQfSaj-KWBw,8097
10
+ foxglove/benchmarks/test_mcap_serialization.py,sha256=Ab_J2Mz8Vya5ZD8Yypp2jdfhaOCxYW7hw5fos7LyFXk,4682
11
+ foxglove/channel.py,sha256=MRiiOm09ZNATOvVCFuvn19KB9HkVCtsDTJYiqE7LQlA,8452
12
+ foxglove/channels/__init__.py,sha256=qTr5-HKlr4e386onX9OiYBPWUsS1n2vO23PncL0TiPY,2398
13
+ foxglove/cloud.py,sha256=eOHV8ZCnut59uBLiw1c5MiwbIALeVwPzxo2Yb-2jbII,1942
14
+ foxglove/mcap.py,sha256=LR9TSyRlDWuHZpXR8iglmDp-S-4BRqgmvTOiUKHwlsA,200
15
+ foxglove/notebook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ foxglove/notebook/foxglove_widget.py,sha256=mOXElZLZSIQfvzbZfMd2jtvmWA-H4Z8j7fa39Vwns34,3418
17
+ foxglove/notebook/notebook_buffer.py,sha256=ooQIb8xcyNeuCoRm7CL0Uhnh9VPcd41YMUNYRu15FYg,3801
18
+ foxglove/notebook/static/widget.js,sha256=gPbW5C8KwvmNJS0q1KpHVEfoEaHcesMZKtAVz_Ag3XE,3967
19
+ foxglove/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ foxglove/schemas/__init__.py,sha256=bqYBLc0HXRe-BMRP3VnF9IUB2yKYsvku3pZnulss3GY,3232
21
+ foxglove/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ foxglove/tests/test_channel.py,sha256=coIPG-P_8Uczhw2-zRo4FoanlkReiE1CKUppM9mO1oE,7146
23
+ foxglove/tests/test_context.py,sha256=OvtvjzsRaZZHPT0RyRkwrpBip-7uLaxVPwlu2RJ12yY,256
24
+ foxglove/tests/test_logging.py,sha256=G2Mljb9nN5BOY5qKAeYo0x_2blY1iIRh9s80k0O3vaI,1472
25
+ foxglove/tests/test_mcap.py,sha256=F_g9WLazTt-fJaRPQIS1Fjgf-gK_bwhgswdz17tqdEE,5646
26
+ foxglove/tests/test_parameters.py,sha256=18YsSPNnSM3VjVCh-Ag4S9mBv2G-x2zSwi5wzQClvqo,4861
27
+ foxglove/tests/test_schemas.py,sha256=4empQg8gqS7MD63ibU3kdQgJUSsUpnNQQWQ7oAf-4xk,423
28
+ foxglove/tests/test_server.py,sha256=PHraaIt4m27MXn1UveV6BRpzYCMFOQfjQD4d4ZHRjIY,2893
29
+ foxglove/tests/test_time.py,sha256=By_sM5r87s9iu4Df12r6p9DJrzTeZSLys3XGUGhvUps,4661
30
+ foxglove/websocket.py,sha256=QDYNUJ4x9P92pLvnK1kOkMDD4EVg9CeOqMLSKlJasT0,5700
31
+ foxglove_sdk-0.16.1.dist-info/METADATA,sha256=f7b04K7IcKaEJYvatpBxegYJJm0X_v_SiNPlPtRU0G8,1716
32
+ foxglove_sdk-0.16.1.dist-info/WHEEL,sha256=PIHWx_H737_urdRR7Zu8WjJH_pxvFim5cnS0vv-DVNQ,143
33
+ foxglove_sdk-0.16.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.10.2)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314-manylinux_2_17_i686
5
+ Tag: cp314-cp314-manylinux2014_i686
@@ -1,29 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: foxglove-sdk
3
- Version: 0.14.2
4
- Classifier: Programming Language :: Python :: 3
5
- Classifier: Programming Language :: Rust
6
- Summary: Foxglove Python SDK
7
- Author-email: Foxglove <support@foxglove.dev>
8
- License-Expression: MIT
9
- Requires-Python: >=3.9
10
- Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
11
- Project-URL: repository, https://github.com/foxglove/foxglove-sdk
12
- Project-URL: documentation, https://docs.foxglove.dev/
13
-
14
- # Foxglove Python SDK
15
-
16
- The official [Foxglove](https://docs.foxglove.dev/docs) SDK for Python.
17
-
18
- This package provides support for integrating with the Foxglove platform. It can be used to log
19
- events to local [MCAP](https://mcap.dev/) files or a local visualization server that communicates
20
- with the Foxglove app.
21
-
22
- ## Get Started
23
-
24
- See https://foxglove.github.io/foxglove-sdk/python/
25
-
26
- ## Requirements
27
-
28
- - Python 3.9+
29
-
@@ -1,26 +0,0 @@
1
- foxglove/__init__.py,sha256=AUUn0WLpVoZqSs60bqy6kzvjV7I2pDxJ5TCmnCNPuww,4240
2
- foxglove/_foxglove_py.cpython-314-i386-linux-gnu.so,sha256=CjaFgUVGKytJwNveknU-wZqeJtzT5E-8HbFskbWPV1s,6621484
3
- foxglove/_foxglove_py/__init__.pyi,sha256=76zIyKkXRGp6OW9R8U4daMVPX3mBM_Th5JKo-jzEykU,4296
4
- foxglove/_foxglove_py/channels.pyi,sha256=c3WXhK5FfeCJ1aDGB-AsayIossnC8v8qFij0XWl1AEA,67327
5
- foxglove/_foxglove_py/mcap.pyi,sha256=r858qyn4FZMmjD9ZbLP8eZktBNUgMYjwEEHLgMyN2Bw,3265
6
- foxglove/_foxglove_py/schemas.pyi,sha256=oVF6fwYlgoZ8a1FmlLhVgCYbz-ueM52NyuuTtwhGuFc,23092
7
- foxglove/_foxglove_py/schemas_wkt.pyi,sha256=_nHeIdbOKMgPeL5V360-vZXCnEtyRIFzEd_JVsK49qo,2065
8
- foxglove/_foxglove_py/websocket.pyi,sha256=quclOE3BvKcH6QZbEweQc4p_JUPYEHYC4pdQlrCjw_w,8097
9
- foxglove/benchmarks/test_mcap_serialization.py,sha256=Ab_J2Mz8Vya5ZD8Yypp2jdfhaOCxYW7hw5fos7LyFXk,4682
10
- foxglove/channel.py,sha256=MRiiOm09ZNATOvVCFuvn19KB9HkVCtsDTJYiqE7LQlA,8452
11
- foxglove/channels/__init__.py,sha256=qTr5-HKlr4e386onX9OiYBPWUsS1n2vO23PncL0TiPY,2398
12
- foxglove/mcap.py,sha256=LR9TSyRlDWuHZpXR8iglmDp-S-4BRqgmvTOiUKHwlsA,200
13
- foxglove/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- foxglove/schemas/__init__.py,sha256=bqYBLc0HXRe-BMRP3VnF9IUB2yKYsvku3pZnulss3GY,3232
15
- foxglove/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- foxglove/tests/test_channel.py,sha256=coIPG-P_8Uczhw2-zRo4FoanlkReiE1CKUppM9mO1oE,7146
17
- foxglove/tests/test_logging.py,sha256=r219wdtEO8f6T0LfTTbTmWrEtHD_WznMm_g4SoJ6Byc,355
18
- foxglove/tests/test_mcap.py,sha256=GpYbsyoSghV7RECl6QRwR1CfQ_mATujLSzrs9f-XolE,3081
19
- foxglove/tests/test_parameters.py,sha256=18YsSPNnSM3VjVCh-Ag4S9mBv2G-x2zSwi5wzQClvqo,4861
20
- foxglove/tests/test_schemas.py,sha256=4empQg8gqS7MD63ibU3kdQgJUSsUpnNQQWQ7oAf-4xk,423
21
- foxglove/tests/test_server.py,sha256=FGcnezEuMtublhIQilFSytqPKQDZpCmY4jJzMJhIWn8,2848
22
- foxglove/tests/test_time.py,sha256=By_sM5r87s9iu4Df12r6p9DJrzTeZSLys3XGUGhvUps,4661
23
- foxglove/websocket.py,sha256=oPcI2dttjr-RdL7z30MKEEF1cswyoDu_VetEaxAiwtQ,5812
24
- foxglove_sdk-0.14.2.dist-info/METADATA,sha256=NawweZpB1AfDNSjBd9YN1swgd7Nwc7hEs3_E79UWmPM,873
25
- foxglove_sdk-0.14.2.dist-info/WHEEL,sha256=MiSJMZ2VQZYQ1U6qYyxWoq0cYeXLCtiJVjg3_Ejsx5c,125
26
- foxglove_sdk-0.14.2.dist-info/RECORD,,
@@ -1,4 +0,0 @@
1
- Wheel-Version: 1.0
2
- Generator: maturin (1.9.4)
3
- Root-Is-Purelib: false
4
- Tag: cp314-cp314-manylinux_2_17_i686.manylinux2014_i686