foxglove-sdk 0.15.1__cp39-cp39-win_amd64.whl → 0.15.2__cp39-cp39-win_amd64.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 +241 -183
- foxglove/_foxglove_py/__init__.pyi +210 -210
- foxglove/_foxglove_py/channels.pyi +2792 -2792
- foxglove/_foxglove_py/cloud.pyi +9 -9
- foxglove/_foxglove_py/mcap.pyi +96 -96
- foxglove/_foxglove_py/schemas.pyi +1009 -1009
- foxglove/_foxglove_py/schemas_wkt.pyi +85 -85
- foxglove/_foxglove_py/websocket.pyi +321 -321
- foxglove/_foxglove_py.cp39-win_amd64.pyd +0 -0
- foxglove/benchmarks/test_mcap_serialization.py +160 -160
- foxglove/channel.py +241 -241
- foxglove/channels/__init__.py +94 -94
- foxglove/cloud.py +61 -61
- foxglove/mcap.py +12 -12
- 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/schemas/__init__.py +163 -163
- foxglove/tests/test_channel.py +243 -243
- foxglove/tests/test_context.py +10 -10
- foxglove/tests/test_logging.py +62 -62
- foxglove/tests/test_mcap.py +199 -199
- foxglove/tests/test_parameters.py +178 -178
- foxglove/tests/test_schemas.py +17 -17
- foxglove/tests/test_server.py +112 -112
- foxglove/tests/test_time.py +137 -137
- foxglove/websocket.py +205 -205
- {foxglove_sdk-0.15.1.dist-info → foxglove_sdk-0.15.2.dist-info}/METADATA +40 -36
- foxglove_sdk-0.15.2.dist-info/RECORD +33 -0
- foxglove_sdk-0.15.1.dist-info/RECORD +0 -29
- {foxglove_sdk-0.15.1.dist-info → foxglove_sdk-0.15.2.dist-info}/WHEEL +0 -0
foxglove/tests/test_mcap.py
CHANGED
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing import Callable, Generator, Optional
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from foxglove import Channel, ChannelDescriptor, Context, open_mcap
|
|
6
|
-
from foxglove.mcap import MCAPWriteOptions
|
|
7
|
-
|
|
8
|
-
chan = Channel("test", schema={"type": "object"})
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.fixture
|
|
12
|
-
def make_tmp_mcap(
|
|
13
|
-
tmp_path_factory: pytest.TempPathFactory,
|
|
14
|
-
) -> Generator[Callable[[], Path], None, None]:
|
|
15
|
-
mcap: Optional[Path] = None
|
|
16
|
-
dir: Optional[Path] = None
|
|
17
|
-
|
|
18
|
-
def _make_tmp_mcap() -> Path:
|
|
19
|
-
nonlocal dir, mcap
|
|
20
|
-
dir = tmp_path_factory.mktemp("test", numbered=True)
|
|
21
|
-
mcap = dir / "test.mcap"
|
|
22
|
-
return mcap
|
|
23
|
-
|
|
24
|
-
yield _make_tmp_mcap
|
|
25
|
-
|
|
26
|
-
if mcap is not None and dir is not None:
|
|
27
|
-
try:
|
|
28
|
-
mcap.unlink()
|
|
29
|
-
dir.rmdir()
|
|
30
|
-
except FileNotFoundError:
|
|
31
|
-
pass
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@pytest.fixture
|
|
35
|
-
def tmp_mcap(make_tmp_mcap: Callable[[], Path]) -> Generator[Path, None, None]:
|
|
36
|
-
yield make_tmp_mcap()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def test_open_with_str(tmp_mcap: Path) -> None:
|
|
40
|
-
open_mcap(str(tmp_mcap))
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_overwrite(tmp_mcap: Path) -> None:
|
|
44
|
-
tmp_mcap.touch()
|
|
45
|
-
with pytest.raises(FileExistsError):
|
|
46
|
-
open_mcap(tmp_mcap)
|
|
47
|
-
open_mcap(tmp_mcap, allow_overwrite=True)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def test_explicit_close(tmp_mcap: Path) -> None:
|
|
51
|
-
mcap = open_mcap(tmp_mcap)
|
|
52
|
-
for ii in range(20):
|
|
53
|
-
chan.log({"foo": ii})
|
|
54
|
-
size_before_close = tmp_mcap.stat().st_size
|
|
55
|
-
mcap.close()
|
|
56
|
-
assert tmp_mcap.stat().st_size > size_before_close
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_context_manager(tmp_mcap: Path) -> None:
|
|
60
|
-
with open_mcap(tmp_mcap):
|
|
61
|
-
for ii in range(20):
|
|
62
|
-
chan.log({"foo": ii})
|
|
63
|
-
size_before_close = tmp_mcap.stat().st_size
|
|
64
|
-
assert tmp_mcap.stat().st_size > size_before_close
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def test_writer_compression(make_tmp_mcap: Callable[[], Path]) -> None:
|
|
68
|
-
tmp_1 = make_tmp_mcap()
|
|
69
|
-
tmp_2 = make_tmp_mcap()
|
|
70
|
-
|
|
71
|
-
# Compression is enabled by default
|
|
72
|
-
mcap_1 = open_mcap(tmp_1)
|
|
73
|
-
mcap_2 = open_mcap(tmp_2, writer_options=MCAPWriteOptions(compression=None))
|
|
74
|
-
|
|
75
|
-
for _ in range(20):
|
|
76
|
-
chan.log({"foo": "bar"})
|
|
77
|
-
|
|
78
|
-
mcap_1.close()
|
|
79
|
-
mcap_2.close()
|
|
80
|
-
|
|
81
|
-
assert tmp_1.stat().st_size < tmp_2.stat().st_size
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def test_writer_custom_profile(tmp_mcap: Path) -> None:
|
|
85
|
-
options = MCAPWriteOptions(profile="--custom-profile-1--")
|
|
86
|
-
with open_mcap(tmp_mcap, writer_options=options):
|
|
87
|
-
chan.log({"foo": "bar"})
|
|
88
|
-
|
|
89
|
-
contents = tmp_mcap.read_bytes()
|
|
90
|
-
assert contents.find(b"--custom-profile-1--") > -1
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_write_to_different_contexts(make_tmp_mcap: Callable[[], Path]) -> None:
|
|
94
|
-
tmp_1 = make_tmp_mcap()
|
|
95
|
-
tmp_2 = make_tmp_mcap()
|
|
96
|
-
|
|
97
|
-
ctx1 = Context()
|
|
98
|
-
ctx2 = Context()
|
|
99
|
-
|
|
100
|
-
options = MCAPWriteOptions(compression=None)
|
|
101
|
-
mcap1 = open_mcap(tmp_1, writer_options=options, context=ctx1)
|
|
102
|
-
mcap2 = open_mcap(tmp_2, writer_options=options, context=ctx2)
|
|
103
|
-
|
|
104
|
-
ch1 = Channel("ctx1", context=ctx1)
|
|
105
|
-
ch1.log({"a": "b"})
|
|
106
|
-
|
|
107
|
-
ch2 = Channel("ctx2", context=ctx2)
|
|
108
|
-
ch2.log({"has-more-data": "true"})
|
|
109
|
-
|
|
110
|
-
mcap1.close()
|
|
111
|
-
mcap2.close()
|
|
112
|
-
|
|
113
|
-
contents1 = tmp_1.read_bytes()
|
|
114
|
-
contents2 = tmp_2.read_bytes()
|
|
115
|
-
|
|
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
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Callable, Generator, Optional
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from foxglove import Channel, ChannelDescriptor, Context, open_mcap
|
|
6
|
+
from foxglove.mcap import MCAPWriteOptions
|
|
7
|
+
|
|
8
|
+
chan = Channel("test", schema={"type": "object"})
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def make_tmp_mcap(
|
|
13
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
14
|
+
) -> Generator[Callable[[], Path], None, None]:
|
|
15
|
+
mcap: Optional[Path] = None
|
|
16
|
+
dir: Optional[Path] = None
|
|
17
|
+
|
|
18
|
+
def _make_tmp_mcap() -> Path:
|
|
19
|
+
nonlocal dir, mcap
|
|
20
|
+
dir = tmp_path_factory.mktemp("test", numbered=True)
|
|
21
|
+
mcap = dir / "test.mcap"
|
|
22
|
+
return mcap
|
|
23
|
+
|
|
24
|
+
yield _make_tmp_mcap
|
|
25
|
+
|
|
26
|
+
if mcap is not None and dir is not None:
|
|
27
|
+
try:
|
|
28
|
+
mcap.unlink()
|
|
29
|
+
dir.rmdir()
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def tmp_mcap(make_tmp_mcap: Callable[[], Path]) -> Generator[Path, None, None]:
|
|
36
|
+
yield make_tmp_mcap()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_open_with_str(tmp_mcap: Path) -> None:
|
|
40
|
+
open_mcap(str(tmp_mcap))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_overwrite(tmp_mcap: Path) -> None:
|
|
44
|
+
tmp_mcap.touch()
|
|
45
|
+
with pytest.raises(FileExistsError):
|
|
46
|
+
open_mcap(tmp_mcap)
|
|
47
|
+
open_mcap(tmp_mcap, allow_overwrite=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_explicit_close(tmp_mcap: Path) -> None:
|
|
51
|
+
mcap = open_mcap(tmp_mcap)
|
|
52
|
+
for ii in range(20):
|
|
53
|
+
chan.log({"foo": ii})
|
|
54
|
+
size_before_close = tmp_mcap.stat().st_size
|
|
55
|
+
mcap.close()
|
|
56
|
+
assert tmp_mcap.stat().st_size > size_before_close
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_context_manager(tmp_mcap: Path) -> None:
|
|
60
|
+
with open_mcap(tmp_mcap):
|
|
61
|
+
for ii in range(20):
|
|
62
|
+
chan.log({"foo": ii})
|
|
63
|
+
size_before_close = tmp_mcap.stat().st_size
|
|
64
|
+
assert tmp_mcap.stat().st_size > size_before_close
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_writer_compression(make_tmp_mcap: Callable[[], Path]) -> None:
|
|
68
|
+
tmp_1 = make_tmp_mcap()
|
|
69
|
+
tmp_2 = make_tmp_mcap()
|
|
70
|
+
|
|
71
|
+
# Compression is enabled by default
|
|
72
|
+
mcap_1 = open_mcap(tmp_1)
|
|
73
|
+
mcap_2 = open_mcap(tmp_2, writer_options=MCAPWriteOptions(compression=None))
|
|
74
|
+
|
|
75
|
+
for _ in range(20):
|
|
76
|
+
chan.log({"foo": "bar"})
|
|
77
|
+
|
|
78
|
+
mcap_1.close()
|
|
79
|
+
mcap_2.close()
|
|
80
|
+
|
|
81
|
+
assert tmp_1.stat().st_size < tmp_2.stat().st_size
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_writer_custom_profile(tmp_mcap: Path) -> None:
|
|
85
|
+
options = MCAPWriteOptions(profile="--custom-profile-1--")
|
|
86
|
+
with open_mcap(tmp_mcap, writer_options=options):
|
|
87
|
+
chan.log({"foo": "bar"})
|
|
88
|
+
|
|
89
|
+
contents = tmp_mcap.read_bytes()
|
|
90
|
+
assert contents.find(b"--custom-profile-1--") > -1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_write_to_different_contexts(make_tmp_mcap: Callable[[], Path]) -> None:
|
|
94
|
+
tmp_1 = make_tmp_mcap()
|
|
95
|
+
tmp_2 = make_tmp_mcap()
|
|
96
|
+
|
|
97
|
+
ctx1 = Context()
|
|
98
|
+
ctx2 = Context()
|
|
99
|
+
|
|
100
|
+
options = MCAPWriteOptions(compression=None)
|
|
101
|
+
mcap1 = open_mcap(tmp_1, writer_options=options, context=ctx1)
|
|
102
|
+
mcap2 = open_mcap(tmp_2, writer_options=options, context=ctx2)
|
|
103
|
+
|
|
104
|
+
ch1 = Channel("ctx1", context=ctx1)
|
|
105
|
+
ch1.log({"a": "b"})
|
|
106
|
+
|
|
107
|
+
ch2 = Channel("ctx2", context=ctx2)
|
|
108
|
+
ch2.log({"has-more-data": "true"})
|
|
109
|
+
|
|
110
|
+
mcap1.close()
|
|
111
|
+
mcap2.close()
|
|
112
|
+
|
|
113
|
+
contents1 = tmp_1.read_bytes()
|
|
114
|
+
contents2 = tmp_2.read_bytes()
|
|
115
|
+
|
|
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
|