foxglove-sdk 0.16.3__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.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 +245 -0
- foxglove/_foxglove_py/__init__.pyi +233 -0
- foxglove/_foxglove_py/channels.pyi +2792 -0
- foxglove/_foxglove_py/cloud.pyi +9 -0
- foxglove/_foxglove_py/mcap.pyi +125 -0
- foxglove/_foxglove_py/schemas.pyi +1009 -0
- foxglove/_foxglove_py/schemas_wkt.pyi +85 -0
- foxglove/_foxglove_py/websocket.pyi +394 -0
- foxglove/_foxglove_py.cpython-312-aarch64-linux-gnu.so +0 -0
- foxglove/benchmarks/test_mcap_serialization.py +160 -0
- foxglove/channel.py +241 -0
- foxglove/channels/__init__.py +94 -0
- foxglove/cloud.py +61 -0
- foxglove/mcap.py +12 -0
- foxglove/notebook/__init__.py +0 -0
- foxglove/notebook/foxglove_widget.py +100 -0
- foxglove/notebook/notebook_buffer.py +114 -0
- foxglove/py.typed +0 -0
- foxglove/schemas/__init__.py +163 -0
- foxglove/tests/__init__.py +0 -0
- foxglove/tests/test_channel.py +243 -0
- foxglove/tests/test_context.py +10 -0
- foxglove/tests/test_logging.py +62 -0
- foxglove/tests/test_mcap.py +477 -0
- foxglove/tests/test_parameters.py +178 -0
- foxglove/tests/test_schemas.py +17 -0
- foxglove/tests/test_server.py +141 -0
- foxglove/tests/test_time.py +137 -0
- foxglove/websocket.py +220 -0
- foxglove_sdk-0.16.3.dist-info/METADATA +53 -0
- foxglove_sdk-0.16.3.dist-info/RECORD +32 -0
- foxglove_sdk-0.16.3.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import typing
|
|
3
|
+
from urllib.parse import parse_qs, urlparse
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from foxglove import (
|
|
7
|
+
Capability,
|
|
8
|
+
Channel,
|
|
9
|
+
Context,
|
|
10
|
+
ServerListener,
|
|
11
|
+
Service,
|
|
12
|
+
start_server,
|
|
13
|
+
)
|
|
14
|
+
from foxglove.websocket import PlaybackState, PlaybackStatus, ServiceSchema, StatusLevel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_server_interface() -> None:
|
|
18
|
+
"""
|
|
19
|
+
Exercise the server interface; will also be checked with mypy.
|
|
20
|
+
"""
|
|
21
|
+
server = start_server(
|
|
22
|
+
port=0, session_id="test-session", channel_filter=lambda _: True
|
|
23
|
+
)
|
|
24
|
+
assert isinstance(server.port, int)
|
|
25
|
+
assert server.port != 0
|
|
26
|
+
|
|
27
|
+
raw_url = server.app_url()
|
|
28
|
+
assert raw_url is not None
|
|
29
|
+
url = urlparse(raw_url)
|
|
30
|
+
assert url.scheme == "https"
|
|
31
|
+
assert url.netloc == "app.foxglove.dev"
|
|
32
|
+
assert parse_qs(url.query) == {
|
|
33
|
+
"ds": ["foxglove-websocket"],
|
|
34
|
+
"ds.url": [f"ws://127.0.0.1:{server.port}"],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
raw_url = server.app_url(layout_id="lay_123", open_in_desktop=True)
|
|
38
|
+
assert raw_url is not None
|
|
39
|
+
url = urlparse(raw_url)
|
|
40
|
+
assert url.scheme == "https"
|
|
41
|
+
assert url.netloc == "app.foxglove.dev"
|
|
42
|
+
assert parse_qs(url.query) == {
|
|
43
|
+
"ds": ["foxglove-websocket"],
|
|
44
|
+
"ds.url": [f"ws://127.0.0.1:{server.port}"],
|
|
45
|
+
"layoutId": ["lay_123"],
|
|
46
|
+
"openIn": ["desktop"],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
server.publish_status("test message", StatusLevel.Info, "some-id")
|
|
50
|
+
server.broadcast_time(time.time_ns())
|
|
51
|
+
server.broadcast_playback_state(
|
|
52
|
+
PlaybackState(
|
|
53
|
+
status=PlaybackStatus.Paused,
|
|
54
|
+
playback_speed=1.0,
|
|
55
|
+
current_time=time.time_ns(),
|
|
56
|
+
did_seek=False,
|
|
57
|
+
request_id=None,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
server.remove_status(["some-id"])
|
|
61
|
+
server.clear_session("new-session")
|
|
62
|
+
server.stop()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_server_listener_provides_default_implementation() -> None:
|
|
66
|
+
class DefaultServerListener(ServerListener):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
listener = DefaultServerListener()
|
|
70
|
+
|
|
71
|
+
listener.on_parameters_subscribe(["test"])
|
|
72
|
+
listener.on_parameters_unsubscribe(["test"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_services_interface() -> None:
|
|
76
|
+
test_svc = Service(
|
|
77
|
+
name="test",
|
|
78
|
+
schema=ServiceSchema(name="test-schema"),
|
|
79
|
+
handler=lambda *_: b"{}",
|
|
80
|
+
)
|
|
81
|
+
test2_svc = Service(
|
|
82
|
+
name="test2",
|
|
83
|
+
schema=ServiceSchema(name="test-schema"),
|
|
84
|
+
handler=lambda *_: b"{}",
|
|
85
|
+
)
|
|
86
|
+
server = start_server(
|
|
87
|
+
port=0,
|
|
88
|
+
capabilities=[Capability.Services],
|
|
89
|
+
supported_encodings=["json"],
|
|
90
|
+
services=[test_svc],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Add a new service.
|
|
94
|
+
server.add_services([test2_svc])
|
|
95
|
+
|
|
96
|
+
# Can't add a service with the same name.
|
|
97
|
+
with pytest.raises(RuntimeError):
|
|
98
|
+
server.add_services([test_svc])
|
|
99
|
+
|
|
100
|
+
# Remove services.
|
|
101
|
+
server.remove_services(["test", "test2"])
|
|
102
|
+
|
|
103
|
+
# Re-add a service.
|
|
104
|
+
server.add_services([test_svc])
|
|
105
|
+
|
|
106
|
+
server.stop()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_context_can_be_attached_to_server() -> None:
|
|
110
|
+
ctx1 = Context()
|
|
111
|
+
ctx2 = Context()
|
|
112
|
+
|
|
113
|
+
server1 = start_server(port=0, context=ctx1)
|
|
114
|
+
server2 = start_server(port=0, context=ctx2)
|
|
115
|
+
|
|
116
|
+
ch1 = Channel("/1", context=ctx1)
|
|
117
|
+
ch2 = Channel("/2", context=ctx2)
|
|
118
|
+
ch1.log("test")
|
|
119
|
+
ch2.log("test")
|
|
120
|
+
|
|
121
|
+
server1.stop()
|
|
122
|
+
server2.stop()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@typing.no_type_check
|
|
126
|
+
def test_server_with_invalid_playback_time_range() -> None:
|
|
127
|
+
with pytest.raises(TypeError):
|
|
128
|
+
# Tuple of a single element
|
|
129
|
+
start_server(port=0, playback_time_range=(123,))
|
|
130
|
+
|
|
131
|
+
with pytest.raises(TypeError):
|
|
132
|
+
# Tuple with invalid types
|
|
133
|
+
start_server(port=0, playback_time_range=("not-a-time", None))
|
|
134
|
+
|
|
135
|
+
with pytest.raises(TypeError):
|
|
136
|
+
# Not a tuple
|
|
137
|
+
start_server(port=0, playback_time_range=23443)
|
|
138
|
+
|
|
139
|
+
with pytest.raises(TypeError):
|
|
140
|
+
# Tuple with too many elements
|
|
141
|
+
start_server(port=0, playback_time_range=(123, 456, 789))
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from foxglove.schemas import Duration, Timestamp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_duration_normalization() -> None:
|
|
8
|
+
assert Duration(sec=0, nsec=1_111_222_333) == Duration(sec=1, nsec=111_222_333)
|
|
9
|
+
assert Duration(sec=0, nsec=2**32 - 1) == Duration(sec=4, nsec=294_967_295)
|
|
10
|
+
assert Duration(sec=-2, nsec=1_000_000_001) == Duration(sec=-1, nsec=1)
|
|
11
|
+
assert Duration(sec=-(2**31), nsec=1_000_000_001) == Duration(
|
|
12
|
+
sec=-(2**31) + 1, nsec=1
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# argument conversions
|
|
16
|
+
d = Duration(sec=-(2**31))
|
|
17
|
+
assert d.sec == -(2**31)
|
|
18
|
+
assert d.nsec == 0
|
|
19
|
+
|
|
20
|
+
d = Duration(sec=0, nsec=2**32 - 1)
|
|
21
|
+
assert d.sec == 4
|
|
22
|
+
assert d.nsec == 294_967_295
|
|
23
|
+
|
|
24
|
+
d = Duration(sec=2**31 - 1, nsec=999_999_999)
|
|
25
|
+
assert d.sec == 2**31 - 1
|
|
26
|
+
assert d.nsec == 999_999_999
|
|
27
|
+
|
|
28
|
+
with pytest.raises(OverflowError):
|
|
29
|
+
Duration(sec=-(2**31) - 1)
|
|
30
|
+
with pytest.raises(OverflowError):
|
|
31
|
+
Duration(sec=2**31)
|
|
32
|
+
with pytest.raises(OverflowError):
|
|
33
|
+
Duration(sec=0, nsec=-1)
|
|
34
|
+
with pytest.raises(OverflowError):
|
|
35
|
+
Duration(sec=0, nsec=2**32)
|
|
36
|
+
|
|
37
|
+
# overflow past upper bound
|
|
38
|
+
with pytest.raises(OverflowError):
|
|
39
|
+
Duration(sec=2**31 - 1, nsec=1_000_000_000)
|
|
40
|
+
|
|
41
|
+
# we don't handle this corner case, where seconds is beyond the lower
|
|
42
|
+
# bound, but nanoseconds overflow to bring the duration within range.
|
|
43
|
+
with pytest.raises(OverflowError):
|
|
44
|
+
Duration(sec=-(2**31) - 1, nsec=1_000_000_000)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_duration_from_secs() -> None:
|
|
48
|
+
assert Duration.from_secs(1.123) == Duration(sec=1, nsec=123_000_000)
|
|
49
|
+
assert Duration.from_secs(-0.123) == Duration(sec=-1, nsec=877_000_000)
|
|
50
|
+
assert Duration.from_secs(-1.123) == Duration(sec=-2, nsec=877_000_000)
|
|
51
|
+
|
|
52
|
+
with pytest.raises(OverflowError):
|
|
53
|
+
Duration.from_secs(-1e42)
|
|
54
|
+
|
|
55
|
+
with pytest.raises(OverflowError):
|
|
56
|
+
Duration.from_secs(1e42)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_duration_from_timedelta() -> None:
|
|
60
|
+
td = datetime.timedelta(seconds=1, milliseconds=123)
|
|
61
|
+
assert Duration.from_timedelta(td) == Duration(sec=1, nsec=123_000_000)
|
|
62
|
+
|
|
63
|
+
# no loss of precision
|
|
64
|
+
td = datetime.timedelta(days=9876, microseconds=123_456)
|
|
65
|
+
assert Duration.from_timedelta(td) == Duration(sec=853_286_400, nsec=123_456_000)
|
|
66
|
+
|
|
67
|
+
# timedeltas are normalized
|
|
68
|
+
td = datetime.timedelta(seconds=8 * 24 * 3600, milliseconds=99_111)
|
|
69
|
+
assert Duration.from_timedelta(td) == Duration(sec=691_299, nsec=111_000_000)
|
|
70
|
+
|
|
71
|
+
with pytest.raises(OverflowError):
|
|
72
|
+
Duration.from_timedelta(datetime.timedelta.min)
|
|
73
|
+
|
|
74
|
+
with pytest.raises(OverflowError):
|
|
75
|
+
Duration.from_timedelta(datetime.timedelta.max)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_timestamp_normalization() -> None:
|
|
79
|
+
assert Timestamp(sec=0, nsec=1_111_222_333) == Timestamp(sec=1, nsec=111_222_333)
|
|
80
|
+
assert Timestamp(sec=0, nsec=2**32 - 1) == Timestamp(sec=4, nsec=294_967_295)
|
|
81
|
+
|
|
82
|
+
# argument conversions
|
|
83
|
+
t = Timestamp(sec=0)
|
|
84
|
+
assert t.sec == 0
|
|
85
|
+
assert t.nsec == 0
|
|
86
|
+
|
|
87
|
+
t = Timestamp(sec=0, nsec=2**32 - 1)
|
|
88
|
+
assert t.sec == 4
|
|
89
|
+
assert t.nsec == 294_967_295
|
|
90
|
+
|
|
91
|
+
t = Timestamp(sec=2**32 - 1, nsec=999_999_999)
|
|
92
|
+
assert t.sec == 2**32 - 1
|
|
93
|
+
assert t.nsec == 999_999_999
|
|
94
|
+
|
|
95
|
+
with pytest.raises(OverflowError):
|
|
96
|
+
Timestamp(sec=-1)
|
|
97
|
+
with pytest.raises(OverflowError):
|
|
98
|
+
Timestamp(sec=2**32)
|
|
99
|
+
with pytest.raises(OverflowError):
|
|
100
|
+
Timestamp(sec=0, nsec=-1)
|
|
101
|
+
with pytest.raises(OverflowError):
|
|
102
|
+
Timestamp(sec=0, nsec=2**32)
|
|
103
|
+
|
|
104
|
+
# overflow past upper bound
|
|
105
|
+
with pytest.raises(OverflowError):
|
|
106
|
+
Timestamp(sec=2**32 - 1, nsec=1_000_000_000)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_timestamp_from_epoch_secs() -> None:
|
|
110
|
+
assert Timestamp.from_epoch_secs(1.123) == Timestamp(sec=1, nsec=123_000_000)
|
|
111
|
+
|
|
112
|
+
with pytest.raises(OverflowError):
|
|
113
|
+
Timestamp.from_epoch_secs(-1.0)
|
|
114
|
+
|
|
115
|
+
with pytest.raises(OverflowError):
|
|
116
|
+
Timestamp.from_epoch_secs(1e42)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_timestamp_from_datetime() -> None:
|
|
120
|
+
utc = datetime.timezone.utc
|
|
121
|
+
dt = datetime.datetime(1970, 1, 1, tzinfo=utc)
|
|
122
|
+
assert Timestamp.from_datetime(dt) == Timestamp(sec=0)
|
|
123
|
+
|
|
124
|
+
# no loss of precision
|
|
125
|
+
dt = datetime.datetime(2025, 1, 1, microsecond=42, tzinfo=utc)
|
|
126
|
+
assert Timestamp.from_datetime(dt) == Timestamp(sec=1_735_689_600, nsec=42_000)
|
|
127
|
+
|
|
128
|
+
# alternative timezone
|
|
129
|
+
local_tz = datetime.timezone(datetime.timedelta(hours=-1))
|
|
130
|
+
dt = datetime.datetime(1970, 1, 1, 0, 0, 1, 123_000, tzinfo=local_tz)
|
|
131
|
+
assert Timestamp.from_datetime(dt) == Timestamp(sec=3601, nsec=123_000_000)
|
|
132
|
+
|
|
133
|
+
with pytest.raises(OverflowError):
|
|
134
|
+
Timestamp.from_datetime(datetime.datetime(1969, 12, 31, tzinfo=utc))
|
|
135
|
+
|
|
136
|
+
with pytest.raises(OverflowError):
|
|
137
|
+
Timestamp.from_datetime(datetime.datetime(2106, 2, 8, tzinfo=utc))
|
foxglove/websocket.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Optional, Protocol, TypeAlias, Union
|
|
5
|
+
|
|
6
|
+
from ._foxglove_py.websocket import (
|
|
7
|
+
Capability,
|
|
8
|
+
ChannelView,
|
|
9
|
+
Client,
|
|
10
|
+
ClientChannel,
|
|
11
|
+
ConnectionGraph,
|
|
12
|
+
MessageSchema,
|
|
13
|
+
Parameter,
|
|
14
|
+
ParameterType,
|
|
15
|
+
ParameterValue,
|
|
16
|
+
PlaybackCommand,
|
|
17
|
+
PlaybackControlRequest,
|
|
18
|
+
PlaybackState,
|
|
19
|
+
PlaybackStatus,
|
|
20
|
+
Service,
|
|
21
|
+
ServiceRequest,
|
|
22
|
+
ServiceSchema,
|
|
23
|
+
StatusLevel,
|
|
24
|
+
WebSocketServer,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
ServiceHandler: TypeAlias = Callable[[ServiceRequest], bytes]
|
|
28
|
+
AssetHandler: TypeAlias = Callable[[str], "bytes | None"]
|
|
29
|
+
AnyParameterValue: TypeAlias = Union[
|
|
30
|
+
ParameterValue.Integer,
|
|
31
|
+
ParameterValue.Bool,
|
|
32
|
+
ParameterValue.Float64,
|
|
33
|
+
ParameterValue.String,
|
|
34
|
+
ParameterValue.Array,
|
|
35
|
+
ParameterValue.Dict,
|
|
36
|
+
]
|
|
37
|
+
AnyInnerParameterValue: TypeAlias = Union[
|
|
38
|
+
AnyParameterValue,
|
|
39
|
+
bool,
|
|
40
|
+
int,
|
|
41
|
+
float,
|
|
42
|
+
str,
|
|
43
|
+
"list[AnyInnerParameterValue]",
|
|
44
|
+
"dict[str, AnyInnerParameterValue]",
|
|
45
|
+
]
|
|
46
|
+
AnyNativeParameterValue: TypeAlias = Union[
|
|
47
|
+
AnyInnerParameterValue,
|
|
48
|
+
bytes,
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ServerListener(Protocol):
|
|
53
|
+
"""
|
|
54
|
+
A mechanism to register callbacks for handling client message events.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def on_subscribe(self, client: Client, channel: ChannelView) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Called by the server when a client subscribes to a channel.
|
|
60
|
+
|
|
61
|
+
:param client: The client (id) that sent the message.
|
|
62
|
+
:param channel: The channel (id, topic) that the message was sent on.
|
|
63
|
+
"""
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def on_unsubscribe(self, client: Client, channel: ChannelView) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Called by the server when a client unsubscribes from a channel or disconnects.
|
|
69
|
+
|
|
70
|
+
:param client: The client (id) that sent the message.
|
|
71
|
+
:param channel: The channel (id, topic) that the message was sent on.
|
|
72
|
+
"""
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def on_client_advertise(self, client: Client, channel: ClientChannel) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Called by the server when a client advertises a channel.
|
|
78
|
+
|
|
79
|
+
:param client: The client (id) that sent the message.
|
|
80
|
+
:param channel: The client channel that is being advertised.
|
|
81
|
+
"""
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def on_client_unadvertise(self, client: Client, client_channel_id: int) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Called by the server when a client unadvertises a channel.
|
|
87
|
+
|
|
88
|
+
:param client: The client (id) that is unadvertising the channel.
|
|
89
|
+
:param client_channel_id: The client channel id that is being unadvertised.
|
|
90
|
+
"""
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def on_message_data(
|
|
94
|
+
self, client: Client, client_channel_id: int, data: bytes
|
|
95
|
+
) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Called by the server when a message is received from a client.
|
|
98
|
+
|
|
99
|
+
:param client: The client (id) that sent the message.
|
|
100
|
+
:param client_channel_id: The client channel id that the message was sent on.
|
|
101
|
+
:param data: The message data.
|
|
102
|
+
"""
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def on_get_parameters(
|
|
106
|
+
self,
|
|
107
|
+
client: Client,
|
|
108
|
+
param_names: list[str],
|
|
109
|
+
request_id: str | None = None,
|
|
110
|
+
) -> list[Parameter]:
|
|
111
|
+
"""
|
|
112
|
+
Called by the server when a client requests parameters.
|
|
113
|
+
|
|
114
|
+
Requires :py:data:`Capability.Parameters`.
|
|
115
|
+
|
|
116
|
+
:param client: The client (id) that sent the message.
|
|
117
|
+
:param param_names: The names of the parameters to get.
|
|
118
|
+
:param request_id: An optional request id.
|
|
119
|
+
"""
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
def on_set_parameters(
|
|
123
|
+
self,
|
|
124
|
+
client: Client,
|
|
125
|
+
parameters: list[Parameter],
|
|
126
|
+
request_id: str | None = None,
|
|
127
|
+
) -> list[Parameter]:
|
|
128
|
+
"""
|
|
129
|
+
Called by the server when a client sets parameters.
|
|
130
|
+
Note that only `parameters` which have changed are included in the callback, but the return
|
|
131
|
+
value must include all parameters. If a parameter that is unset is included in the return
|
|
132
|
+
value, it will not be published to clients.
|
|
133
|
+
|
|
134
|
+
Requires :py:data:`Capability.Parameters`.
|
|
135
|
+
|
|
136
|
+
:param client: The client (id) that sent the message.
|
|
137
|
+
:param parameters: The parameters to set.
|
|
138
|
+
:param request_id: An optional request id.
|
|
139
|
+
"""
|
|
140
|
+
return parameters
|
|
141
|
+
|
|
142
|
+
def on_parameters_subscribe(
|
|
143
|
+
self,
|
|
144
|
+
param_names: list[str],
|
|
145
|
+
) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Called by the server when a client subscribes to one or more parameters for the first time.
|
|
148
|
+
|
|
149
|
+
Requires :py:data:`Capability.Parameters`.
|
|
150
|
+
|
|
151
|
+
:param param_names: The names of the parameters to subscribe to.
|
|
152
|
+
"""
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
def on_parameters_unsubscribe(
|
|
156
|
+
self,
|
|
157
|
+
param_names: list[str],
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Called by the server when the last client subscription to one or more parameters has been
|
|
161
|
+
removed.
|
|
162
|
+
|
|
163
|
+
Requires :py:data:`Capability.Parameters`.
|
|
164
|
+
|
|
165
|
+
:param param_names: The names of the parameters to unsubscribe from.
|
|
166
|
+
"""
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def on_connection_graph_subscribe(self) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Called by the server when the first client subscribes to the connection graph.
|
|
172
|
+
"""
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def on_connection_graph_unsubscribe(self) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Called by the server when the last client unsubscribes from the connection graph.
|
|
178
|
+
"""
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def on_playback_control_request(
|
|
182
|
+
self, playback_control_request: PlaybackControlRequest
|
|
183
|
+
) -> Optional[PlaybackState]:
|
|
184
|
+
"""
|
|
185
|
+
Called by the server when it receives an updated player state from the client.
|
|
186
|
+
|
|
187
|
+
Requires :py:data:`Capability.RangedPlayback`.
|
|
188
|
+
|
|
189
|
+
:meta private:
|
|
190
|
+
:param playback_control_request: The playback control request sent from the client
|
|
191
|
+
"""
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
__all__ = [
|
|
196
|
+
"AnyInnerParameterValue",
|
|
197
|
+
"AnyNativeParameterValue",
|
|
198
|
+
"AnyParameterValue",
|
|
199
|
+
"AssetHandler",
|
|
200
|
+
"Capability",
|
|
201
|
+
"ChannelView",
|
|
202
|
+
"Client",
|
|
203
|
+
"ClientChannel",
|
|
204
|
+
"ConnectionGraph",
|
|
205
|
+
"MessageSchema",
|
|
206
|
+
"Parameter",
|
|
207
|
+
"ParameterType",
|
|
208
|
+
"ParameterValue",
|
|
209
|
+
"PlaybackCommand",
|
|
210
|
+
"PlaybackControlRequest",
|
|
211
|
+
"PlaybackState",
|
|
212
|
+
"PlaybackStatus",
|
|
213
|
+
"ServerListener",
|
|
214
|
+
"Service",
|
|
215
|
+
"ServiceHandler",
|
|
216
|
+
"ServiceRequest",
|
|
217
|
+
"ServiceSchema",
|
|
218
|
+
"StatusLevel",
|
|
219
|
+
"WebSocketServer",
|
|
220
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: foxglove-sdk
|
|
3
|
+
Version: 0.16.3
|
|
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: documentation, https://docs.foxglove.dev/
|
|
16
|
+
Project-URL: repository, https://github.com/foxglove/foxglove-sdk
|
|
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,32 @@
|
|
|
1
|
+
foxglove/__init__.py,sha256=utf_uU_ViL7xDrTqheP6JGs3ezfPsnve2EXFdS0uq-A,8490
|
|
2
|
+
foxglove/_foxglove_py/__init__.pyi,sha256=Ppw0LllCD5Jos1ZGPwEefRFJXVEj9ZWd_Nez2hUYjSs,6072
|
|
3
|
+
foxglove/_foxglove_py/channels.pyi,sha256=c3WXhK5FfeCJ1aDGB-AsayIossnC8v8qFij0XWl1AEA,67327
|
|
4
|
+
foxglove/_foxglove_py/cloud.pyi,sha256=9Hqj7b9S2CTiWeWOIqaAw3GSmR-cGoSL4R7fi5WI-QA,255
|
|
5
|
+
foxglove/_foxglove_py/mcap.pyi,sha256=yh3RDXNiqSN5URCQF0efxezmWldvdQk_VHlW-clS0Cw,4936
|
|
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=NuCIpXp9P2bPNLyvjVYqcaeL71wKRyYB6RbTfkE6X7A,10511
|
|
9
|
+
foxglove/_foxglove_py.cpython-312-aarch64-linux-gnu.so,sha256=wR2OWmMU5uQJgyXfvPqEAw4FYo227AEb_ZHEO7K3yKM,6461552
|
|
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/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
foxglove/schemas/__init__.py,sha256=bqYBLc0HXRe-BMRP3VnF9IUB2yKYsvku3pZnulss3GY,3232
|
|
20
|
+
foxglove/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
foxglove/tests/test_channel.py,sha256=coIPG-P_8Uczhw2-zRo4FoanlkReiE1CKUppM9mO1oE,7146
|
|
22
|
+
foxglove/tests/test_context.py,sha256=OvtvjzsRaZZHPT0RyRkwrpBip-7uLaxVPwlu2RJ12yY,256
|
|
23
|
+
foxglove/tests/test_logging.py,sha256=G2Mljb9nN5BOY5qKAeYo0x_2blY1iIRh9s80k0O3vaI,1472
|
|
24
|
+
foxglove/tests/test_mcap.py,sha256=Y4OYgWvkPSD288rJdlrgg1haVGeDRR39sWeUcb6VO8E,14863
|
|
25
|
+
foxglove/tests/test_parameters.py,sha256=18YsSPNnSM3VjVCh-Ag4S9mBv2G-x2zSwi5wzQClvqo,4861
|
|
26
|
+
foxglove/tests/test_schemas.py,sha256=4empQg8gqS7MD63ibU3kdQgJUSsUpnNQQWQ7oAf-4xk,423
|
|
27
|
+
foxglove/tests/test_server.py,sha256=rR50x21OOUMVuyYmYqiRTxOrgJFyL4ZL9W4pOshfAFc,3795
|
|
28
|
+
foxglove/tests/test_time.py,sha256=By_sM5r87s9iu4Df12r6p9DJrzTeZSLys3XGUGhvUps,4661
|
|
29
|
+
foxglove/websocket.py,sha256=PvsSGmWFayjRV20u86ZEZ9zcUD7nBC9j6CKPWNZy5ks,6331
|
|
30
|
+
foxglove_sdk-0.16.3.dist-info/METADATA,sha256=-R_TiQnJptVFk1IqvoAQ10Fbk0wSm17dC-gEUe8MPDw,1716
|
|
31
|
+
foxglove_sdk-0.16.3.dist-info/WHEEL,sha256=52Zi36pDPQlOrLthVvXF6f11TX-HRQ2sllWxsSJsrtQ,149
|
|
32
|
+
foxglove_sdk-0.16.3.dist-info/RECORD,,
|