python-osc 1.10.2__tar.gz → 1.10.3__tar.gz

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.
Files changed (39) hide show
  1. {python_osc-1.10.2 → python_osc-1.10.3}/PKG-INFO +1 -1
  2. python_osc-1.10.3/pyproject.toml +59 -0
  3. python_osc-1.10.2/pyproject.toml → python_osc-1.10.3/pyproject.toml.orig +1 -1
  4. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/dispatcher.py +8 -6
  5. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/osc_bundle.py +6 -0
  6. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/osc_message_builder.py +7 -4
  7. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/osc_server.py +3 -0
  8. python_osc-1.10.3/pythonosc/osc_tcp_server.py +230 -0
  9. python_osc-1.10.3/pythonosc/parsing/framing.py +166 -0
  10. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/parsing/osc_types.py +13 -7
  11. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/tcp_client.py +49 -126
  12. python_osc-1.10.3/pythonosc/test/parsing/test_framing.py +158 -0
  13. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/parsing/test_osc_types.py +28 -0
  14. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_dispatcher.py +21 -0
  15. python_osc-1.10.3/pythonosc/test/test_empty_string_messages.py +87 -0
  16. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_osc_bundle.py +21 -2
  17. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_osc_message.py +5 -0
  18. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_osc_server.py +31 -1
  19. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_osc_tcp_server.py +45 -27
  20. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_tcp_client.py +2 -1
  21. python_osc-1.10.2/pythonosc/osc_tcp_server.py +0 -346
  22. {python_osc-1.10.2 → python_osc-1.10.3}/LICENSE.txt +0 -0
  23. {python_osc-1.10.2 → python_osc-1.10.3}/README.rst +0 -0
  24. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/__init__.py +0 -0
  25. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/osc_bundle_builder.py +0 -0
  26. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/osc_message.py +0 -0
  27. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/osc_packet.py +0 -0
  28. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/parsing/__init__.py +0 -0
  29. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/parsing/ntp.py +0 -0
  30. {python_osc-1.10.2/pythonosc → python_osc-1.10.3/pythonosc/parsing}/slip.py +0 -0
  31. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/py.typed +0 -0
  32. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/__init__.py +0 -0
  33. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/parsing/__init__.py +0 -0
  34. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/parsing/test_ntp.py +0 -0
  35. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_osc_bundle_builder.py +0 -0
  36. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_osc_message_builder.py +0 -0
  37. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_osc_packet.py +0 -0
  38. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/test/test_udp_client.py +0 -0
  39. {python_osc-1.10.2 → python_osc-1.10.3}/pythonosc/udp_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-osc
3
- Version: 1.10.2
3
+ Version: 1.10.3
4
4
  Summary: Open Sound Control server and client implementations in pure Python
5
5
  Keywords: osc,sound,midi,music
6
6
  Author: attwad
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["uv-build"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "python-osc"
7
+ version = "1.10.3"
8
+ description = "Open Sound Control server and client implementations in pure Python"
9
+ readme = "README.rst"
10
+ requires-python = ">=3.10"
11
+ keywords = [
12
+ "osc",
13
+ "sound",
14
+ "midi",
15
+ "music",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Intended Audience :: Developers",
20
+ "License :: Freely Distributable",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Multimedia :: Sound/Audio",
23
+ "Topic :: System :: Networking",
24
+ ]
25
+ dependencies = []
26
+
27
+ [project.license]
28
+ file = "LICENSE.txt"
29
+
30
+ [[project.authors]]
31
+ name = "attwad"
32
+ email = "tmusoft@gmail.com"
33
+
34
+ [project.urls]
35
+ Repository = "https://github.com/attwad/python-osc"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pytest",
40
+ "mypy",
41
+ "ruff",
42
+ "pytest-cov",
43
+ "pre-commit",
44
+ ]
45
+
46
+ [tool.uv.build-backend]
47
+ module-name = "pythonosc"
48
+ module-root = "."
49
+
50
+ [tool.mypy]
51
+ disallow_subclassing_any = true
52
+ disallow_incomplete_defs = true
53
+ disallow_untyped_decorators = true
54
+ warn_redundant_casts = true
55
+ warn_unused_ignores = true
56
+ show_error_codes = true
57
+ scripts_are_modules = true
58
+ warn_unused_configs = true
59
+ enable_error_code = "ignore-without-code"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "python-osc"
7
- version = "1.10.2"
7
+ version = "1.10.3"
8
8
  description = "Open Sound Control server and client implementations in pure Python"
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.10"
@@ -154,13 +154,15 @@ class Dispatcher(object):
154
154
 
155
155
  The callback function must have one of the following signatures:
156
156
 
157
- ``def some_cb(address: str, *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
158
- ``def some_cb(address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr,
159
- Tuple(str, ArgValue)]:``
157
+ .. code-block:: python
160
158
 
161
- ``def some_cb(client_address: Tuple[str, int], address: str, *osc_args: List[Any]) -> Union[None, AnyStr,
162
- Tuple(str, ArgValue)]:``
163
- ``def some_cb(client_address: Tuple[str, int], address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
159
+ def some_cb(address: str, *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
160
+ def some_cb(address: str, fixed_args: List[Any], *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
161
+ def some_cb(client_address: Tuple[str, int], address: str, *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
162
+ def some_cb(client_address: Tuple[str, int], address: str, fixed_args: List[Any], *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
163
+
164
+ Each variadic OSC argument is a single ArgValue, not a list of all arguments.
165
+ Any fixed arguments passed to map() are collected into the separate fixed_args list.
164
166
 
165
167
  The callback function can return None, or a string representing an OSC address to be returned to the client,
166
168
  or a tuple that includes the address and ArgValue which will be converted to an OSC message and returned to
@@ -50,6 +50,12 @@ class OscBundle(object):
50
50
  while self._dgram[index:]:
51
51
  # Get the sub content size.
52
52
  content_size, index = osc_types.get_int(self._dgram, index)
53
+ if content_size <= 0 or content_size % 4:
54
+ raise ParseError(
55
+ "Invalid bundle element size: {}".format(content_size)
56
+ )
57
+ if content_size > len(self._dgram) - index:
58
+ raise ParseError("Bundle element exceeds the datagram length")
53
59
  # Get the datagram for the sub content.
54
60
  content_dgram = self._dgram[index : index + content_size]
55
61
  # Increment our position index up to the next possible content.
@@ -200,13 +200,16 @@ class OscMessageBuilder(object):
200
200
 
201
201
 
202
202
  def build_msg(
203
- address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
203
+ address: str, value: Union[ArgValue, Iterable[ArgValue]] = ()
204
204
  ) -> osc_message.OscMessage:
205
+ """Build a message from a scalar value or an iterable of arguments.
206
+
207
+ Omit the value or pass an empty iterable for a message with no arguments.
208
+ An explicitly supplied empty string is retained as a string argument.
209
+ """
205
210
  builder = OscMessageBuilder(address=address)
206
211
  values: Iterable[Any]
207
- if value == "":
208
- values = []
209
- elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
212
+ if not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
210
213
  values = [value]
211
214
  else:
212
215
  values = value
@@ -59,6 +59,9 @@ def _is_valid_request(request: _RequestType) -> bool:
59
59
  class OSCUDPServer(socketserver.UDPServer):
60
60
  """Superclass for different flavors of OSC UDP servers"""
61
61
 
62
+ # Receive complete UDP datagrams instead of socketserver's 8192-byte default.
63
+ max_packet_size = 65535
64
+
62
65
  def __init__(
63
66
  self,
64
67
  server_address: Tuple[str, int],
@@ -0,0 +1,230 @@
1
+ """OSC Servers that receive TCP packets and invoke handlers accordingly.
2
+
3
+ Use like this:
4
+
5
+ dispatcher = dispatcher.Dispatcher()
6
+ # This will print all parameters to stdout.
7
+ dispatcher.map("/bpm", print)
8
+ server = ForkingOSCTCPServer((ip, port), dispatcher)
9
+ server.serve_forever()
10
+
11
+ or run the server on its own thread:
12
+ server = ForkingOSCTCPServer((ip, port), dispatcher)
13
+ server_thread = threading.Thread(target=server.serve_forever)
14
+ server_thread.start()
15
+ ...
16
+ server.shutdown()
17
+
18
+
19
+ Those servers are using the standard socketserver from the standard library:
20
+ http://docs.python.org/library/socketserver.html
21
+
22
+
23
+ Alternatively, the AsyncIOOSCTCPServer server can be integrated with an
24
+ asyncio event loop:
25
+
26
+ loop = asyncio.get_event_loop()
27
+ server = AsyncIOOSCTCPServer(server_address, dispatcher)
28
+ server.serve()
29
+ loop.run_forever()
30
+
31
+ """
32
+
33
+ # mypy: disable-error-code="attr-defined"
34
+
35
+ import asyncio
36
+ import os
37
+ import socket
38
+ import socketserver
39
+ from typing import Tuple
40
+
41
+ from pythonosc import osc_message_builder
42
+ from pythonosc.dispatcher import Dispatcher
43
+ from pythonosc.parsing.framing import (
44
+ BINARY_LENGTH_FRAMING,
45
+ SLIP_FRAMING,
46
+ Framing,
47
+ FramingParser,
48
+ )
49
+
50
+ MODE_1_0 = "1.0"
51
+ MODE_1_1 = "1.1"
52
+ FRAMING_BY_MODE: dict[str, Framing] = {
53
+ MODE_1_0: BINARY_LENGTH_FRAMING,
54
+ MODE_1_1: SLIP_FRAMING,
55
+ }
56
+
57
+
58
+ def get_framing(mode: str) -> Framing:
59
+ try:
60
+ return FRAMING_BY_MODE[mode]
61
+ except KeyError:
62
+ raise ValueError(f"Unsupported OSC mode: {mode}") from None
63
+
64
+
65
+ class TCPHandler(socketserver.BaseRequestHandler):
66
+ """Handles correct OSC messages.
67
+
68
+ Whether this will be run on its own thread, the server's or a whole new
69
+ process depends on the server you instantiated, look at their documentation.
70
+
71
+ This method is called after a basic sanity check was done on the datagram,
72
+ basically whether this datagram looks like an osc message or bundle,
73
+ if not the server won't even bother to call it and so no new
74
+ threads/processes will be spawned.
75
+ """
76
+
77
+ def handle(self) -> None:
78
+ assert isinstance(self.server, OSCTCPServer)
79
+
80
+ parser = FramingParser(self.server.framing)
81
+
82
+ while chunk := self.request.recv(16384):
83
+ for p in parser.feed(chunk):
84
+ resp = self.server.dispatcher.call_handlers_for_packet(
85
+ p, self.client_address
86
+ )
87
+ for r in resp:
88
+ if not isinstance(r, tuple):
89
+ r = [r]
90
+ msg = osc_message_builder.build_msg(r[0], r[1:])
91
+ self.request.sendall(self.server.framing.encode(msg.dgram))
92
+
93
+
94
+ class OSCTCPServer(socketserver.TCPServer):
95
+ """Superclass for different flavors of OSCTCPServer"""
96
+
97
+ dispatcher: Dispatcher
98
+ framing: Framing
99
+
100
+ def __init__(
101
+ self,
102
+ server_address: Tuple[str | bytes | bytearray, int],
103
+ dispatcher: Dispatcher,
104
+ mode: str = MODE_1_1,
105
+ family: socket.AddressFamily | None = None,
106
+ ):
107
+ self.request_queue_size = 300
108
+ self.dispatcher = dispatcher
109
+ self.framing = get_framing(mode)
110
+
111
+ if family is not None:
112
+ self.address_family = family
113
+ elif isinstance(server_address[0], str):
114
+ # Try to infer address family from server_address
115
+ try:
116
+ infos = socket.getaddrinfo(
117
+ server_address[0],
118
+ server_address[1],
119
+ type=socket.SOCK_STREAM,
120
+ family=socket.AF_UNSPEC,
121
+ )
122
+ if infos:
123
+ self.address_family = infos[0][0]
124
+ except (socket.gaierror, IndexError):
125
+ # Fallback to default if resolution fails
126
+ pass
127
+
128
+ super().__init__(server_address, TCPHandler)
129
+
130
+
131
+ class BlockingOSCTCPServer(OSCTCPServer):
132
+ """Blocking version of the TCP server.
133
+
134
+ Each message will be handled sequentially on the same thread.
135
+ Use this is you don't care about latency in your message handling or don't
136
+ have a multiprocess/multithread environment (really?).
137
+ """
138
+
139
+
140
+ class ThreadingOSCTCPServer(socketserver.ThreadingMixIn, OSCTCPServer):
141
+ """Threading version of the OSC TCP server.
142
+
143
+ Each message will be handled in its own new thread.
144
+ Use this when lightweight operations are done by each message handlers.
145
+ """
146
+
147
+
148
+ if hasattr(os, "fork"):
149
+
150
+ class ForkingOSCTCPServer(socketserver.ForkingMixIn, OSCTCPServer):
151
+ """Forking version of the OSC TCP server.
152
+
153
+ Each message will be handled in its own new process.
154
+ Use this when heavyweight operations are done by each message handlers
155
+ and forking a whole new process for each of them is worth it.
156
+ """
157
+
158
+
159
+ class AsyncOSCTCPServer:
160
+ """Asyncio version of the OSC TCP Server.
161
+ Each TCP message is handled by _call_handlers_for_packet, the same method as in the
162
+ OSCTCPServer family of blocking, threading, and forking servers
163
+ """
164
+
165
+ def __init__(
166
+ self,
167
+ server_address: str,
168
+ port: int,
169
+ dispatcher: Dispatcher,
170
+ mode: str = MODE_1_1,
171
+ ):
172
+ """
173
+ :param server_address: tuple of (IP address to bind to, port)
174
+ :param dispatcher: a pythonosc.dispatcher.Dispatcher
175
+ """
176
+ self._port = port
177
+ self._server_address = server_address
178
+ self.dispatcher = dispatcher
179
+ self.framing = get_framing(mode)
180
+
181
+ async def __aenter__(self):
182
+ return self
183
+
184
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
185
+ await self.stop()
186
+
187
+ async def start(self) -> None:
188
+ """creates a socket endpoint and registers it with our event loop"""
189
+ self._server = await asyncio.start_server(
190
+ self.handle, self._server_address, self._port
191
+ )
192
+
193
+ async with self._server:
194
+ await self._server.serve_forever()
195
+
196
+ async def stop(self) -> None:
197
+ self._server.close()
198
+ await self._server.wait_closed()
199
+
200
+ async def handle(
201
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
202
+ ) -> None:
203
+ try:
204
+ await self._handle(reader, writer)
205
+ finally:
206
+ if writer.can_write_eof():
207
+ writer.write_eof()
208
+ writer.close()
209
+ await writer.wait_closed()
210
+
211
+ async def _handle(
212
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
213
+ ) -> None:
214
+ parser = FramingParser(self.framing)
215
+ client_address = ("", 0)
216
+ sock = writer.transport.get_extra_info("socket")
217
+ if sock is not None:
218
+ client_address = sock.getpeername()
219
+
220
+ while chunk := await reader.read(16384):
221
+ for p in parser.feed(chunk):
222
+ result = await self.dispatcher.async_call_handlers_for_packet(
223
+ p, client_address
224
+ )
225
+ for r in result:
226
+ if not isinstance(r, tuple):
227
+ r = [r]
228
+ msg = osc_message_builder.build_msg(r[0], r[1:])
229
+ writer.write(self.framing.encode(msg.dgram))
230
+ await writer.drain()
@@ -0,0 +1,166 @@
1
+ """OSC message codecs for different framing strategies."""
2
+
3
+ from abc import ABCMeta, abstractmethod
4
+ from struct import pack, unpack_from
5
+
6
+ from pythonosc.parsing.slip import (
7
+ END,
8
+ decode as slip_decode,
9
+ encode as slip_encode,
10
+ )
11
+
12
+
13
+ class Framing(metaclass=ABCMeta):
14
+ """Base class for OSC stream framing strategies."""
15
+
16
+ @abstractmethod
17
+ def encode(self, dgram: bytes) -> bytes:
18
+ """Encode a single OSC datagram for transmission on a byte stream.
19
+
20
+ Args:
21
+ dgram: Raw OSC datagram bytes.
22
+
23
+ Returns:
24
+ The framed byte sequence to write to the stream.
25
+ """
26
+
27
+ @abstractmethod
28
+ def decode_stream(
29
+ self, buf: bytes | bytearray, start: int = 0
30
+ ) -> tuple[list[bytes], int]:
31
+ """Decode complete OSC datagrams from a byte buffer.
32
+
33
+ Args:
34
+ buf: Bytes received from a stream.
35
+ start: The index in ``buf`` to start decoding from.
36
+
37
+ Returns:
38
+ A tuple ``(messages, bytes consumed)``.
39
+ """
40
+
41
+
42
+ class SLIPFraming(Framing):
43
+ """SLIP (RFC 1055) framing for OSC 1.1.
44
+
45
+ See the "OSC Delivery Specification 1.1" section from the 2009 NIME paper for details.
46
+ """
47
+
48
+ def encode(self, dgram: bytes) -> bytes:
49
+ return slip_encode(dgram)
50
+
51
+ def decode_stream(
52
+ self, buf: bytes | bytearray, start: int = 0
53
+ ) -> tuple[list[bytes], int]:
54
+ pos = start
55
+ messages = []
56
+
57
+ scan = start
58
+ while (end := buf.find(END, scan)) >= 0:
59
+ if end > scan:
60
+ messages.append(slip_decode(bytes(buf[scan:end])))
61
+ pos = end + len(END)
62
+ scan = end + len(END)
63
+
64
+ # Consume trailing delimiters, but retain an incomplete packet.
65
+ if scan == len(buf):
66
+ pos = scan
67
+
68
+ return messages, pos - start
69
+
70
+
71
+ class BinaryLengthFraming(Framing):
72
+ """Length-prefixed framing for OSC 1.0.
73
+
74
+ See the "OSC Packets" section from OSC 1.0 Specification for details.
75
+ """
76
+
77
+ def encode(self, dgram: bytes) -> bytes:
78
+ if dgram:
79
+ return pack("!I", len(dgram)) + dgram
80
+ return b""
81
+
82
+ def decode_stream(
83
+ self, buf: bytes | bytearray, start: int = 0
84
+ ) -> tuple[list[bytes], int]:
85
+ messages = []
86
+ pos = start
87
+
88
+ while (data_pos := pos + 4) < len(buf):
89
+ (length,) = unpack_from("!I", buf, pos)
90
+ end = data_pos + length
91
+
92
+ if end > len(buf):
93
+ break
94
+
95
+ messages.append(bytes(buf[data_pos:end]))
96
+
97
+ pos = end
98
+
99
+ return messages, pos - start
100
+
101
+
102
+ class FramingParser:
103
+ """Stateful stream decoder for (framed) OSC byte stream.
104
+
105
+ Not safe for concurrent use.
106
+
107
+ Args:
108
+ framing: Strategy used to decode buffered stream data.
109
+ max_buffer_size: Maximum size of the internal buffer.
110
+ """
111
+
112
+ __slots__ = ("buf", "start", "framing", "max_buffer_size")
113
+
114
+ def __init__(self, framing: Framing, max_buffer_size: int = 1048576) -> None:
115
+ self.buf = bytearray()
116
+ self.start = 0
117
+ self.framing = framing
118
+ self.max_buffer_size = max_buffer_size
119
+
120
+ def feed(self, chunk: bytes | bytearray | memoryview) -> list[bytes]:
121
+ """Append stream data to the internal buffer and return any complete OSC datagrams.
122
+
123
+ Args:
124
+ chunk: Newly received bytes from the stream.
125
+
126
+ Returns:
127
+ A list of complete, decoded OSC datagrams.
128
+ """
129
+
130
+ new_buf_len = len(self.buf) + len(chunk)
131
+
132
+ if (pending := new_buf_len - self.start) > self.max_buffer_size:
133
+ raise ValueError(f"Buffer is too large: {pending} > {self.max_buffer_size}")
134
+
135
+ if new_buf_len > self.max_buffer_size:
136
+ del self.buf[: self.start]
137
+ self.start = 0
138
+
139
+ self.buf.extend(chunk)
140
+
141
+ messages, consumed = self.framing.decode_stream(self.buf, self.start)
142
+ available = len(self.buf) - self.start
143
+
144
+ assert 0 <= consumed <= available, (
145
+ f"misbehaving framing decoder: {consumed=} {available=}"
146
+ )
147
+
148
+ self.start += consumed
149
+
150
+ return messages
151
+
152
+ @property
153
+ def remaining(self) -> bytes:
154
+ """Return any unparsed bytes."""
155
+
156
+ return bytes(self.buf[self.start :])
157
+
158
+ def reset(self) -> None:
159
+ """Clear the internal buffer (discard unparsed data)."""
160
+
161
+ del self.buf[:]
162
+ self.start = 0
163
+
164
+
165
+ SLIP_FRAMING: Framing = SLIPFraming()
166
+ BINARY_LENGTH_FRAMING: Framing = BinaryLengthFraming()
@@ -86,6 +86,8 @@ def get_string(dgram: bytes, start_index: int) -> Tuple[str, int]:
86
86
 
87
87
  data_str = dgram[start_index : start_index + offset]
88
88
  return data_str.decode("utf-8"), start_index + total_len
89
+ except UnicodeDecodeError as error:
90
+ raise ParseError(f"Could not decode OSC string: {error}") from error
89
91
  except IndexError as ie:
90
92
  raise ParseError(f"Could not parse datagram {ie}")
91
93
  except TypeError as te:
@@ -307,7 +309,7 @@ def get_blob(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
307
309
 
308
310
  Args:
309
311
  dgram: A datagram packet.
310
- start_index: An index where the float starts in the datagram.
312
+ start_index: An index where the blob starts in the datagram.
311
313
 
312
314
  Returns:
313
315
  A tuple containing the blob and the new end index.
@@ -315,13 +317,17 @@ def get_blob(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
315
317
  Raises:
316
318
  ParseError if the datagram could not be parsed.
317
319
  """
318
- size, int_offset = get_int(dgram, start_index)
319
- # Make the size a multiple of 32 bits.
320
- total_size = size + (-size % _BLOB_DGRAM_PAD)
321
- end_index = int_offset + size
322
- if end_index - start_index > len(dgram[start_index:]):
320
+ if start_index < 0:
321
+ raise ParseError("start_index < 0")
322
+ size, payload_start = get_int(dgram, start_index)
323
+ if size < 0:
324
+ raise ParseError("Blob length cannot be negative")
325
+
326
+ payload_end = payload_start + size
327
+ next_index = payload_end + (-size % _BLOB_DGRAM_PAD)
328
+ if next_index > len(dgram):
323
329
  raise ParseError("Datagram is too short.")
324
- return dgram[int_offset : int_offset + size], int_offset + total_size
330
+ return dgram[payload_start:payload_end], next_index
325
331
 
326
332
 
327
333
  def write_blob(val: bytes) -> bytes: