foxglove-sdk 0.6.2__cp312-cp312-win32.whl → 0.16.6__cp312-cp312-win32.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/channel.py CHANGED
@@ -1,204 +1,241 @@
1
- import hashlib
2
- import json
3
- from base64 import b64encode
4
- from typing import Any, Dict, Optional, Union, cast
5
-
6
- from . import _foxglove_py as _foxglove
7
- from . import channels as _channels
8
- from . import schemas as _schemas
9
-
10
- JsonSchema = Dict[str, Any]
11
- JsonMessage = Dict[str, Any]
12
-
13
-
14
- class Channel:
15
- """
16
- A channel that can be used to log binary messages or JSON messages.
17
- """
18
-
19
- __slots__ = ["base", "message_encoding"]
20
- base: _foxglove.BaseChannel
21
- message_encoding: str
22
-
23
- def __init__(
24
- self,
25
- topic: str,
26
- *,
27
- schema: Union[JsonSchema, _foxglove.Schema, None] = None,
28
- message_encoding: Optional[str] = None,
29
- ):
30
- """
31
- Create a new channel for logging messages on a topic.
32
-
33
- :param topic: The topic name.
34
- :param message_encoding: The message encoding. Optional if
35
- :py:param:`schema` is a dictionary, in which case the message
36
- encoding is presumed to be "json".
37
- :param schema: A definition of your schema. Pass a :py:class:`Schema`
38
- for full control. If a dictionary is passed, it will be treated as a
39
- JSON schema.
40
-
41
- If both message_encoding and schema are None, then the channel will use JSON encoding, and
42
- allow any dict to be logged.
43
-
44
- :raises KeyError: if a channel already exists for the given topic.
45
- """
46
- if topic in _channels_by_topic:
47
- raise ValueError(f"Channel for topic '{topic}' already exists")
48
-
49
- message_encoding, schema = _normalize_schema(message_encoding, schema)
50
-
51
- self.message_encoding = message_encoding
52
-
53
- self.base = _foxglove.BaseChannel(
54
- topic,
55
- message_encoding,
56
- schema,
57
- )
58
-
59
- _channels_by_topic[topic] = self
60
-
61
- def __repr__(self) -> str:
62
- return f"Channel(id={self.id()}, topic='{self.topic()}', schema='{self.schema_name()}')"
63
-
64
- def id(self) -> int:
65
- """The unique ID of the channel"""
66
- return self.base.id()
67
-
68
- def topic(self) -> str:
69
- """The topic name of the channel"""
70
- return self.base.topic()
71
-
72
- def schema_name(self) -> Optional[str]:
73
- """The name of the schema for the channel"""
74
- return self.base.schema_name()
75
-
76
- def log(
77
- self,
78
- msg: Union[JsonMessage, list[Any], bytes, str],
79
- *,
80
- log_time: Optional[int] = None,
81
- ) -> None:
82
- """
83
- Log a message on the channel.
84
-
85
- :param msg: the message to log. If the channel uses JSON encoding, you may pass a
86
- dictionary or list. Otherwise, you are responsible for serializing the message.
87
- :param log_time: The optional time the message was logged.
88
- """
89
- if self.message_encoding == "json" and isinstance(msg, (dict, list)):
90
- return self.base.log(json.dumps(msg).encode("utf-8"), log_time)
91
-
92
- if isinstance(msg, str):
93
- msg = msg.encode("utf-8")
94
-
95
- if isinstance(msg, bytes):
96
- return self.base.log(msg, log_time)
97
-
98
- raise TypeError(f"Unsupported message type: {type(msg)}")
99
-
100
- def close(self) -> None:
101
- """
102
- Close the channel.
103
-
104
- You can use this to explicitly unadvertise the channel to sinks that subscribe to
105
- channels dynamically, such as the :py:class:`foxglove.websocket.WebSocketServer`.
106
-
107
- Attempts to log on a closed channel will elicit a throttled warning message.
108
- """
109
- self.base.close()
110
-
111
-
112
- _channels_by_topic: Dict[str, Channel] = {}
113
-
114
-
115
- def log(
116
- topic: str,
117
- message: Union[JsonMessage, list[Any], bytes, str, _schemas.FoxgloveSchema],
118
- *,
119
- log_time: Optional[int] = None,
120
- ) -> None:
121
- """Log a message on a topic.
122
-
123
- Creates a new channel the first time called for a given topic.
124
- For Foxglove types in the schemas module, this creates a typed channel
125
- (see :py:mod:`foxglove.channels` for supported types).
126
- For bytes and str, this creates a simple schemaless channel and logs the bytes as-is.
127
- For dict and list, this creates a schemaless json channel.
128
-
129
- The type of the message must be kept consistent for each topic or an error will be raised.
130
- This can be avoided by creating and using the channels directly instead of using this function.
131
-
132
- Note: this raises an error if a channel with the same topic was created by other means.
133
- This limitation may be lifted in the future.
134
-
135
- :param topic: The topic name.
136
- :param message: The message to log.
137
- :param log_time: The optional time the message was logged.
138
- """
139
- channel: Optional[Any] = _channels_by_topic.get(topic, None)
140
- if channel is None:
141
- schema_name = type(message).__name__
142
- if isinstance(message, (bytes, str)):
143
- channel = Channel(topic)
144
- elif isinstance(message, (dict, list)):
145
- channel = Channel(topic, message_encoding="json")
146
- else:
147
- channel_name = f"{schema_name}Channel"
148
- channel_cls = getattr(_channels, channel_name, None)
149
- if channel_cls is not None:
150
- channel = channel_cls(topic)
151
- if channel is None:
152
- raise ValueError(
153
- f"No Foxglove schema channel found for message type {schema_name}"
154
- )
155
- _channels_by_topic[topic] = channel
156
-
157
- # mypy isn't smart enough to realize that when channel is a Channel, message a compatible type
158
- channel.log(
159
- cast(Any, message),
160
- log_time=log_time,
161
- )
162
-
163
-
164
- def _normalize_schema(
165
- message_encoding: Optional[str],
166
- schema: Union[JsonSchema, _foxglove.Schema, None] = None,
167
- ) -> tuple[str, Optional[_foxglove.Schema]]:
168
- if isinstance(schema, _foxglove.Schema):
169
- if message_encoding is None:
170
- raise ValueError("message encoding is required")
171
- return message_encoding, schema
172
-
173
- if schema is None and (message_encoding is None or message_encoding == "json"):
174
- # Schemaless support via JSON encoding; same as specifying an empty dict schema
175
- schema = {}
176
- message_encoding = "json"
177
-
178
- if isinstance(schema, dict):
179
- # Dicts default to json encoding. An empty dict is equivalent to the empty schema (b"")
180
- if message_encoding and message_encoding != "json":
181
- raise ValueError("message_encoding must be 'json' when schema is a dict")
182
- if schema and schema.get("type") != "object":
183
- raise ValueError("Only object schemas are supported")
184
-
185
- data = json.dumps(schema).encode("utf-8") if schema else b""
186
- name = schema["title"] if "title" in schema else _default_schema_name(data)
187
-
188
- return (
189
- "json",
190
- _foxglove.Schema(
191
- name=name,
192
- encoding="jsonschema",
193
- data=data,
194
- ),
195
- )
196
-
197
- raise TypeError(f"Invalid schema type: {type(schema)}")
198
-
199
-
200
- def _default_schema_name(data: bytes) -> str:
201
- # Provide a consistent, readable, and reasonably unique name for a given schema so the app can
202
- # identify it to the user.
203
- hash = hashlib.shake_128(data).digest(6)
204
- return "schema-" + b64encode(hash, b"-_").decode("utf-8")
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from base64 import b64encode
6
+ from typing import Any, cast
7
+
8
+ from . import Context
9
+ from . import _foxglove_py as _foxglove
10
+ from . import channels as _channels
11
+ from . import schemas as _schemas
12
+
13
+ JsonSchema = dict[str, Any]
14
+ JsonMessage = dict[str, Any]
15
+
16
+
17
+ class Channel:
18
+ """
19
+ A channel that can be used to log binary messages or JSON messages.
20
+ """
21
+
22
+ __slots__ = ["base"]
23
+ base: _foxglove.BaseChannel
24
+
25
+ def __init__(
26
+ self,
27
+ topic: str,
28
+ *,
29
+ schema: JsonSchema | _foxglove.Schema | None = None,
30
+ message_encoding: str | None = None,
31
+ context: Context | None = None,
32
+ metadata: dict[str, str] | None = None,
33
+ ) -> None:
34
+ """
35
+ Create a new channel for logging messages on a topic.
36
+
37
+ :param topic: The topic name. You should choose a unique topic name per channel.
38
+ :param message_encoding: The message encoding. Optional if :any:`schema` is a
39
+ dictionary, in which case the message encoding is presumed to be "json".
40
+ :param schema: A definition of your schema. Pass a :py:class:`Schema` for full control. If a
41
+ dictionary is passed, it will be treated as a JSON schema.
42
+ :param metadata: A dictionary of key/value strings to add to the channel. A type error is
43
+ raised if any key or value is not a string.
44
+
45
+ If both message_encoding and schema are None, then the channel will use JSON encoding, and
46
+ allow any dict to be logged.
47
+ """
48
+ message_encoding, schema = _normalize_schema(message_encoding, schema)
49
+
50
+ if context is not None:
51
+ self.base = context._create_channel(
52
+ topic, message_encoding=message_encoding, schema=schema
53
+ )
54
+ else:
55
+ self.base = _foxglove.BaseChannel(
56
+ topic,
57
+ message_encoding,
58
+ schema,
59
+ metadata,
60
+ )
61
+
62
+ _channels_by_id[self.base.id()] = self
63
+
64
+ def __repr__(self) -> str:
65
+ return f"Channel(id={self.id()}, topic='{self.topic()}', schema='{self.schema_name()}')"
66
+
67
+ def id(self) -> int:
68
+ """The unique ID of the channel"""
69
+ return self.base.id()
70
+
71
+ def topic(self) -> str:
72
+ """The topic name of the channel"""
73
+ return self.base.topic()
74
+
75
+ @property
76
+ def message_encoding(self) -> str:
77
+ """The message encoding for the channel"""
78
+ return self.base.message_encoding
79
+
80
+ def metadata(self) -> dict[str, str]:
81
+ """
82
+ Returns a copy of the channel's metadata.
83
+
84
+ Note that changes made to the returned dictionary will not be applied to
85
+ the channel's metadata.
86
+ """
87
+ return self.base.metadata()
88
+
89
+ def schema(self) -> _foxglove.Schema | None:
90
+ """
91
+ Returns a copy of the channel's metadata.
92
+
93
+ Note that changes made to the returned object will not be applied to
94
+ the channel's schema.
95
+ """
96
+ return self.base.schema()
97
+
98
+ def schema_name(self) -> str | None:
99
+ """The name of the schema for the channel"""
100
+ return self.base.schema_name()
101
+
102
+ def has_sinks(self) -> bool:
103
+ """Returns true if at least one sink is subscribed to this channel"""
104
+ return self.base.has_sinks()
105
+
106
+ def log(
107
+ self,
108
+ msg: JsonMessage | list[Any] | bytes | str,
109
+ *,
110
+ log_time: int | None = None,
111
+ sink_id: int | None = None,
112
+ ) -> None:
113
+ """
114
+ Log a message on the channel.
115
+
116
+ :param msg: the message to log. If the channel uses JSON encoding, you may pass a
117
+ dictionary or list. Otherwise, you are responsible for serializing the message.
118
+ :param log_time: The optional time the message was logged.
119
+ """
120
+ if self.message_encoding == "json" and isinstance(msg, (dict, list)):
121
+ return self.base.log(json.dumps(msg).encode("utf-8"), log_time)
122
+
123
+ if isinstance(msg, str):
124
+ msg = msg.encode("utf-8")
125
+
126
+ if isinstance(msg, bytes):
127
+ return self.base.log(msg, log_time, sink_id)
128
+
129
+ raise TypeError(f"Unsupported message type: {type(msg)}")
130
+
131
+ def close(self) -> None:
132
+ """
133
+ Close the channel.
134
+
135
+ You can use this to explicitly unadvertise the channel to sinks that subscribe to
136
+ channels dynamically, such as the :py:class:`foxglove.websocket.WebSocketServer`.
137
+
138
+ Attempts to log on a closed channel will elicit a throttled warning message.
139
+ """
140
+ self.base.close()
141
+
142
+
143
+ _channels_by_id: dict[int, Channel] = {}
144
+
145
+
146
+ def log(
147
+ topic: str,
148
+ message: JsonMessage | list[Any] | bytes | str | _schemas.FoxgloveSchema,
149
+ *,
150
+ log_time: int | None = None,
151
+ sink_id: int | None = None,
152
+ ) -> None:
153
+ """Log a message on a topic.
154
+
155
+ Creates a new channel the first time called for a given topic.
156
+ For Foxglove types in the schemas module, this creates a typed channel
157
+ (see :py:mod:`foxglove.channels` for supported types).
158
+ For bytes and str, this creates a simple schemaless channel and logs the bytes as-is.
159
+ For dict and list, this creates a schemaless json channel.
160
+
161
+ The type of the message must be kept consistent for each topic or an error will be raised.
162
+ This can be avoided by creating and using the channels directly instead of using this function.
163
+
164
+ Note: this raises an error if a channel with the same topic was created by other means.
165
+ This limitation may be lifted in the future.
166
+
167
+ :param topic: The topic name.
168
+ :param message: The message to log.
169
+ :param log_time: The optional time the message was logged.
170
+ """
171
+ base_channel = _foxglove.get_channel_for_topic(topic)
172
+ channel = _channels_by_id.get(base_channel.id(), None) if base_channel else None
173
+
174
+ if channel is None:
175
+ schema_name = type(message).__name__
176
+ if isinstance(message, (bytes, str)):
177
+ channel = Channel(topic)
178
+ elif isinstance(message, (dict, list)):
179
+ channel = Channel(topic, message_encoding="json")
180
+ else:
181
+ channel_name = f"{schema_name}Channel"
182
+ channel_cls = getattr(_channels, channel_name, None)
183
+ if channel_cls is not None:
184
+ channel = channel_cls(topic)
185
+ if channel is None:
186
+ raise ValueError(
187
+ f"No Foxglove schema channel found for message type {schema_name}"
188
+ )
189
+
190
+ channel_id = channel.base.id() if hasattr(channel, "base") else channel.id()
191
+ _channels_by_id[channel_id] = channel
192
+
193
+ # mypy isn't smart enough to realize that when channel is a Channel, message a compatible type
194
+ channel.log(
195
+ cast(Any, message),
196
+ log_time=log_time,
197
+ sink_id=sink_id,
198
+ )
199
+
200
+
201
+ def _normalize_schema(
202
+ message_encoding: str | None,
203
+ schema: JsonSchema | _foxglove.Schema | None = None,
204
+ ) -> tuple[str, _foxglove.Schema | None]:
205
+ if isinstance(schema, _foxglove.Schema):
206
+ if message_encoding is None:
207
+ raise ValueError("message encoding is required")
208
+ return message_encoding, schema
209
+
210
+ if schema is None and (message_encoding is None or message_encoding == "json"):
211
+ # Schemaless support via JSON encoding; same as specifying an empty dict schema
212
+ schema = {}
213
+ message_encoding = "json"
214
+
215
+ if isinstance(schema, dict):
216
+ # Dicts default to json encoding. An empty dict is equivalent to the empty schema (b"")
217
+ if message_encoding and message_encoding != "json":
218
+ raise ValueError("message_encoding must be 'json' when schema is a dict")
219
+ if schema and schema.get("type") != "object":
220
+ raise ValueError("Only object schemas are supported")
221
+
222
+ data = json.dumps(schema).encode("utf-8") if schema else b""
223
+ name = schema["title"] if "title" in schema else _default_schema_name(data)
224
+
225
+ return (
226
+ "json",
227
+ _foxglove.Schema(
228
+ name=name,
229
+ encoding="jsonschema",
230
+ data=data,
231
+ ),
232
+ )
233
+
234
+ raise TypeError(f"Invalid schema type: {type(schema)}")
235
+
236
+
237
+ def _default_schema_name(data: bytes) -> str:
238
+ # Provide a consistent, readable, and reasonably unique name for a given schema so the app can
239
+ # identify it to the user.
240
+ hash = hashlib.shake_128(data).digest(6)
241
+ return "schema-" + b64encode(hash, b"-_").decode("utf-8")
@@ -1,72 +1,96 @@
1
- """
2
- This defines channels to easily log messages conforming to well-known Foxglove schemas.
3
-
4
- See the :py:mod:`foxglove.schemas` module for available definitions.
5
- """
6
-
7
- # Generated by https://github.com/foxglove/foxglove-sdk
8
- from foxglove._foxglove_py.channels import (
9
- CameraCalibrationChannel,
10
- CircleAnnotationChannel,
11
- ColorChannel,
12
- CompressedImageChannel,
13
- CompressedVideoChannel,
14
- FrameTransformChannel,
15
- FrameTransformsChannel,
16
- GeoJsonChannel,
17
- GridChannel,
18
- ImageAnnotationsChannel,
19
- KeyValuePairChannel,
20
- LaserScanChannel,
21
- LocationFixChannel,
22
- LogChannel,
23
- PackedElementFieldChannel,
24
- Point2Channel,
25
- Point3Channel,
26
- PointCloudChannel,
27
- PointsAnnotationChannel,
28
- PoseChannel,
29
- PoseInFrameChannel,
30
- PosesInFrameChannel,
31
- QuaternionChannel,
32
- RawImageChannel,
33
- SceneEntityChannel,
34
- SceneEntityDeletionChannel,
35
- SceneUpdateChannel,
36
- TextAnnotationChannel,
37
- Vector2Channel,
38
- Vector3Channel,
39
- )
40
-
41
- __all__ = [
42
- "CameraCalibrationChannel",
43
- "CircleAnnotationChannel",
44
- "ColorChannel",
45
- "CompressedImageChannel",
46
- "CompressedVideoChannel",
47
- "FrameTransformChannel",
48
- "FrameTransformsChannel",
49
- "GeoJsonChannel",
50
- "GridChannel",
51
- "ImageAnnotationsChannel",
52
- "KeyValuePairChannel",
53
- "LaserScanChannel",
54
- "LocationFixChannel",
55
- "LogChannel",
56
- "PackedElementFieldChannel",
57
- "Point2Channel",
58
- "Point3Channel",
59
- "PointCloudChannel",
60
- "PointsAnnotationChannel",
61
- "PoseChannel",
62
- "PoseInFrameChannel",
63
- "PosesInFrameChannel",
64
- "QuaternionChannel",
65
- "RawImageChannel",
66
- "SceneEntityChannel",
67
- "SceneEntityDeletionChannel",
68
- "SceneUpdateChannel",
69
- "TextAnnotationChannel",
70
- "Vector2Channel",
71
- "Vector3Channel",
72
- ]
1
+ """
2
+ This module defines channels to easily log messages conforming to well-known Foxglove schemas.
3
+
4
+ See the :py:mod:`foxglove.schemas` module for available definitions.
5
+ """
6
+
7
+ # Generated by https://github.com/foxglove/foxglove-sdk
8
+ from foxglove._foxglove_py.channels import (
9
+ ArrowPrimitiveChannel,
10
+ CameraCalibrationChannel,
11
+ CircleAnnotationChannel,
12
+ ColorChannel,
13
+ CompressedImageChannel,
14
+ CompressedVideoChannel,
15
+ CubePrimitiveChannel,
16
+ CylinderPrimitiveChannel,
17
+ FrameTransformChannel,
18
+ FrameTransformsChannel,
19
+ GeoJsonChannel,
20
+ GridChannel,
21
+ ImageAnnotationsChannel,
22
+ KeyValuePairChannel,
23
+ LaserScanChannel,
24
+ LinePrimitiveChannel,
25
+ LocationFixChannel,
26
+ LocationFixesChannel,
27
+ LogChannel,
28
+ ModelPrimitiveChannel,
29
+ PackedElementFieldChannel,
30
+ Point2Channel,
31
+ Point3Channel,
32
+ Point3InFrameChannel,
33
+ PointCloudChannel,
34
+ PointsAnnotationChannel,
35
+ PoseChannel,
36
+ PoseInFrameChannel,
37
+ PosesInFrameChannel,
38
+ QuaternionChannel,
39
+ RawAudioChannel,
40
+ RawImageChannel,
41
+ SceneEntityChannel,
42
+ SceneEntityDeletionChannel,
43
+ SceneUpdateChannel,
44
+ SpherePrimitiveChannel,
45
+ TextAnnotationChannel,
46
+ TextPrimitiveChannel,
47
+ TriangleListPrimitiveChannel,
48
+ Vector2Channel,
49
+ Vector3Channel,
50
+ VoxelGridChannel,
51
+ )
52
+
53
+ __all__ = [
54
+ "ArrowPrimitiveChannel",
55
+ "CameraCalibrationChannel",
56
+ "CircleAnnotationChannel",
57
+ "ColorChannel",
58
+ "CompressedImageChannel",
59
+ "CompressedVideoChannel",
60
+ "CubePrimitiveChannel",
61
+ "CylinderPrimitiveChannel",
62
+ "FrameTransformChannel",
63
+ "FrameTransformsChannel",
64
+ "GeoJsonChannel",
65
+ "GridChannel",
66
+ "ImageAnnotationsChannel",
67
+ "KeyValuePairChannel",
68
+ "LaserScanChannel",
69
+ "LinePrimitiveChannel",
70
+ "LocationFixChannel",
71
+ "LocationFixesChannel",
72
+ "LogChannel",
73
+ "ModelPrimitiveChannel",
74
+ "PackedElementFieldChannel",
75
+ "Point2Channel",
76
+ "Point3Channel",
77
+ "Point3InFrameChannel",
78
+ "PointCloudChannel",
79
+ "PointsAnnotationChannel",
80
+ "PoseChannel",
81
+ "PoseInFrameChannel",
82
+ "PosesInFrameChannel",
83
+ "QuaternionChannel",
84
+ "RawAudioChannel",
85
+ "RawImageChannel",
86
+ "SceneEntityChannel",
87
+ "SceneEntityDeletionChannel",
88
+ "SceneUpdateChannel",
89
+ "SpherePrimitiveChannel",
90
+ "TextAnnotationChannel",
91
+ "TextPrimitiveChannel",
92
+ "TriangleListPrimitiveChannel",
93
+ "Vector2Channel",
94
+ "Vector3Channel",
95
+ "VoxelGridChannel",
96
+ ]