foxglove-sdk 0.14.3__cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl → 0.16.0__cp311-cp311-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.
- foxglove/__init__.py +107 -1
- foxglove/_foxglove_py/__init__.pyi +32 -2
- foxglove/_foxglove_py/cloud.pyi +9 -0
- foxglove/_foxglove_py/mcap.pyi +12 -0
- foxglove/_foxglove_py/websocket.pyi +1 -1
- foxglove/_foxglove_py.cpython-311-i386-linux-gnu.so +0 -0
- foxglove/cloud.py +61 -0
- foxglove/notebook/__init__.py +0 -0
- foxglove/notebook/foxglove_widget.py +100 -0
- foxglove/notebook/notebook_buffer.py +114 -0
- foxglove/notebook/static/widget.js +1 -0
- foxglove/tests/test_context.py +10 -0
- foxglove/tests/test_logging.py +46 -0
- foxglove/tests/test_mcap.py +84 -1
- foxglove/tests/test_server.py +3 -1
- foxglove/websocket.py +1 -7
- foxglove_sdk-0.16.0.dist-info/METADATA +53 -0
- foxglove_sdk-0.16.0.dist-info/RECORD +33 -0
- foxglove_sdk-0.16.0.dist-info/WHEEL +5 -0
- foxglove_sdk-0.14.3.dist-info/METADATA +0 -29
- foxglove_sdk-0.14.3.dist-info/RECORD +0 -26
- foxglove_sdk-0.14.3.dist-info/WHEEL +0 -4
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
|
|
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:
|
|
@@ -122,6 +123,19 @@ class Context:
|
|
|
122
123
|
"""
|
|
123
124
|
...
|
|
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
|
+
|
|
125
139
|
def start_server(
|
|
126
140
|
*,
|
|
127
141
|
name: str | None = None,
|
|
@@ -134,12 +148,25 @@ def start_server(
|
|
|
134
148
|
asset_handler: AssetHandler | None = None,
|
|
135
149
|
context: Context | None = None,
|
|
136
150
|
session_id: str | None = None,
|
|
151
|
+
channel_filter: SinkChannelFilter | None = None,
|
|
137
152
|
) -> WebSocketServer:
|
|
138
153
|
"""
|
|
139
154
|
Start a websocket server for live visualization.
|
|
140
155
|
"""
|
|
141
156
|
...
|
|
142
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
|
+
|
|
143
170
|
def enable_logging(level: int) -> None:
|
|
144
171
|
"""
|
|
145
172
|
Forward SDK logs to python's logging facility.
|
|
@@ -163,6 +190,7 @@ def open_mcap(
|
|
|
163
190
|
*,
|
|
164
191
|
allow_overwrite: bool = False,
|
|
165
192
|
context: Context | None = None,
|
|
193
|
+
channel_filter: SinkChannelFilter | None = None,
|
|
166
194
|
writer_options: MCAPWriteOptions | None = None,
|
|
167
195
|
) -> MCAPWriter:
|
|
168
196
|
"""
|
|
@@ -170,6 +198,8 @@ def open_mcap(
|
|
|
170
198
|
|
|
171
199
|
If a context is provided, the MCAP file will be associated with that context. Otherwise, the
|
|
172
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.
|
|
173
203
|
"""
|
|
174
204
|
...
|
|
175
205
|
|
foxglove/_foxglove_py/mcap.pyi
CHANGED
|
@@ -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
|
-
|
|
18
|
+
ConnectionGraph = ...
|
|
19
19
|
"""Allow clients to subscribe and make connection graph updates"""
|
|
20
20
|
|
|
21
21
|
Parameters = ...
|
|
Binary file
|
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};
|
foxglove/tests/test_logging.py
CHANGED
|
@@ -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
|
foxglove/tests/test_mcap.py
CHANGED
|
@@ -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
|
foxglove/tests/test_server.py
CHANGED
|
@@ -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(
|
|
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.0
|
|
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-311-i386-linux-gnu.so,sha256=sXLOZN3d19OUBsGc0KvFBkN17YgdSPoXhF_YCZAfXp0,6594648
|
|
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.0.dist-info/METADATA,sha256=r2_JiLJxVEcJDF_iOUqhFBaBBimySx7MLBx7WK74kkk,1716
|
|
32
|
+
foxglove_sdk-0.16.0.dist-info/WHEEL,sha256=2GUmKSUwUQJrpN7MRq7VmVOrxBaUriuUpSqZTzKwHaA,143
|
|
33
|
+
foxglove_sdk-0.16.0.dist-info/RECORD,,
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: foxglove-sdk
|
|
3
|
-
Version: 0.14.3
|
|
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-311-i386-linux-gnu.so,sha256=XDzTt0Q_FSt7GbjDHntJwN8ZQ2MRBDQy2WihGlV-3bI,6574728
|
|
3
|
-
foxglove/_foxglove_py/__init__.pyi,sha256=m4qspFPqeBFWWgTJY3zwCXC0IMOCOZHgByj2jn4zuKA,4420
|
|
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.3.dist-info/METADATA,sha256=2ChBXt0QovWeaXEyUUov5-N_E12r61cB27yu9sb2aOg,873
|
|
25
|
-
foxglove_sdk-0.14.3.dist-info/WHEEL,sha256=lRpHre82bIxBLRuXbtsaEsOHXiG0J0HgGiLREzoU_Io,125
|
|
26
|
-
foxglove_sdk-0.14.3.dist-info/RECORD,,
|