python-osc 1.10.0__py3-none-any.whl → 1.10.3__py3-none-any.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-osc
3
- Version: 1.10.0
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
@@ -114,7 +114,7 @@ Simple client
114
114
  help="The port the OSC server is listening on")
115
115
  args = parser.parse_args()
116
116
 
117
- client = udp_client.SimpleUDPClient(args.ip, args.port)
117
+ client = udp_client.SimpleUDPClient(args.ip, args.port, timeout=10)
118
118
 
119
119
  for x in range(10):
120
120
  client.send_message("/filter", random.random())
@@ -158,7 +158,7 @@ Simple server
158
158
  dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
159
159
 
160
160
  server = osc_server.ThreadingOSCUDPServer(
161
- (args.ip, args.port), dispatcher)
161
+ (args.ip, args.port), dispatcher, timeout=10)
162
162
  print("Serving on {}".format(server.server_address))
163
163
  server.serve_forever()
164
164
 
@@ -0,0 +1,36 @@
1
+ pythonosc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pythonosc/dispatcher.py,sha256=L1d_jm33uwsSHrNlfaLhTXflc70apcK5FwRTJTuEf3Q,15051
3
+ pythonosc/osc_bundle.py,sha256=k8CR9K-MFr8iKj3pYSj72PE2nyhZsIle-GnVpqSrcvM,4151
4
+ pythonosc/osc_bundle_builder.py,sha256=1m1K1bC3NIbUNjNo8lRFQ-Hciyq4hzWEPQm48c1W8DA,2113
5
+ pythonosc/osc_message.py,sha256=GlWwFMhquvt6JuKo8hC4q_S9aSHYTGsfTUUPvO-5uaQ,4797
6
+ pythonosc/osc_message_builder.py,sha256=CHgpPdU9mfTZ52Dt10J0nrvG85OzP474lAhzKGSU_dE,8080
7
+ pythonosc/osc_packet.py,sha256=72LVRxaPaTwkkih7tsPWgWJlyeHxO7xt3SOOK8YMh50,2778
8
+ pythonosc/osc_server.py,sha256=j-cwrZYeiZJTrbYXaW01JiH0H0IFRQLqXrfv0BsdxTY,7445
9
+ pythonosc/osc_tcp_server.py,sha256=Bh_WJHjrtWMtU1gOpY7X1jZEpZNJN5hztllx_UoHT1M,7141
10
+ pythonosc/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ pythonosc/parsing/framing.py,sha256=P31sXt9_mO9bUUNjd6bBlS7T3EGUEh129PZ93QzeOhs,4492
12
+ pythonosc/parsing/ntp.py,sha256=9lpFonFynAVYtW5J2ttgnbjM0ej8mTl0BFsm82Ud8Ek,1877
13
+ pythonosc/parsing/osc_types.py,sha256=-YfmtM0q-WVliJwXJaYEcY5MfGYzp0xyTu2eZ6z9GSE,15008
14
+ pythonosc/parsing/slip.py,sha256=KhEBQ3v21A3liulQWMiBYw3oVY_BtEb1wZj1q3tnrIk,3676
15
+ pythonosc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ pythonosc/tcp_client.py,sha256=ywJaAMWSF_a3kuNAYEe_of-jccwXsnOHM84G7cuHDmc,8039
17
+ pythonosc/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ pythonosc/test/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ pythonosc/test/parsing/test_framing.py,sha256=IYvt_FgLUkeFabbleSuxVqcMiRToy38ugxYaMBTcL9o,5048
20
+ pythonosc/test/parsing/test_ntp.py,sha256=KY4XiArsNZtni46Glq0F6dR3psVDbOROdOjFupTWVuI,605
21
+ pythonosc/test/parsing/test_osc_types.py,sha256=TQz0-yCT7FCN3DjpR3e22tiLpX829rGeogj1tuim-P8,13805
22
+ pythonosc/test/test_dispatcher.py,sha256=C0TsHqoino_2_Dnu10WCBROtB9zi4a2pjft4NvFO7Ps,9367
23
+ pythonosc/test/test_empty_string_messages.py,sha256=F6OpZP5xJP3RY_leWigJd6MdQF6Cdm_v5js3_U8Ba6Q,3768
24
+ pythonosc/test/test_osc_bundle.py,sha256=eXsIl_Jpo9lVmcDVG9o4rWlpTyo7XfBG294_uXp-6Vg,4837
25
+ pythonosc/test/test_osc_bundle_builder.py,sha256=7P2DPBun2TrT_4gUq2_haEJiNC8dbGHwzaRF5MJzJHs,1534
26
+ pythonosc/test/test_osc_message.py,sha256=sDU73FGbEKL5tLYyD04FUsC4aJ1jWxJJmoAaRoWTuuI,13250
27
+ pythonosc/test/test_osc_message_builder.py,sha256=RaFPH9biU6P7LZ-p3N7H5Cfg5w6d2BBxVrwGA9EWwCw,3982
28
+ pythonosc/test/test_osc_packet.py,sha256=GCK7mYKv0bzOwCuhwbA7eaxPOjsn-pTzTbpMzWTXo-s,2163
29
+ pythonosc/test/test_osc_server.py,sha256=MtPJ6ZdEIArRrZxkIPF2_0Dlic0Ct3beQlHZpFgIOaQ,6738
30
+ pythonosc/test/test_osc_tcp_server.py,sha256=Yhj5EGsYMpSwsxuHIMFyi5GEZIpPKxSA-LstWVJXhJs,13972
31
+ pythonosc/test/test_tcp_client.py,sha256=BxRhzaiSoiY5Iz_cYlO45WWKejpdLT7HFciMpu5zXX4,2605
32
+ pythonosc/test/test_udp_client.py,sha256=qLP5SPem6T40JbAoRlf5_N258pTvLzgu3BAqC6PkzMI,3487
33
+ pythonosc/udp_client.py,sha256=4E9GU8dPV3RilCskedzzx2jVcgK6EGp1UXrGjYV3ZaI,5105
34
+ python_osc-1.10.3.dist-info/WHEEL,sha256=3OmCxFA0ffBIsX4x0o8CdfSvZAOvO3mzEG8U_z5TvV4,80
35
+ python_osc-1.10.3.dist-info/METADATA,sha256=vpKkB59PbIqVCpXGHAI3iN0K05Ql7_TMyw6UPLndAAk,6387
36
+ python_osc-1.10.3.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.11.3
2
+ Generator: uv 0.13.8
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
pythonosc/dispatcher.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Maps OSC addresses to handler functions"""
2
2
 
3
+ import asyncio
3
4
  import collections
4
5
  import inspect
5
6
  import logging
@@ -80,6 +81,46 @@ class Handler(object):
80
81
  else:
81
82
  return self.callback(message.address, *message)
82
83
 
84
+ async def async_invoke(
85
+ self, client_address: Tuple[str, int], message: OscMessage
86
+ ) -> Union[None, AnyStr, Tuple[AnyStr, ArgValue]]:
87
+ """Invokes the associated callback function (asynchronously)
88
+
89
+ Args:
90
+ client_address: Address match that causes the invocation
91
+ message: Message causing invocation
92
+ Returns:
93
+ The result of the handler function can be None, a string OSC address, or a tuple of the OSC address
94
+ and arguments.
95
+ """
96
+ cb = self.callback
97
+ is_async = inspect.iscoroutinefunction(cb)
98
+
99
+ if self.needs_reply_address:
100
+ if self.args:
101
+ if is_async:
102
+ return await cb(
103
+ client_address, message.address, self.args, *message
104
+ )
105
+ else:
106
+ return cb(client_address, message.address, self.args, *message)
107
+ else:
108
+ if is_async:
109
+ return await cb(client_address, message.address, *message)
110
+ else:
111
+ return cb(client_address, message.address, *message)
112
+ else:
113
+ if self.args:
114
+ if is_async:
115
+ return await cb(message.address, self.args, *message)
116
+ else:
117
+ return cb(message.address, self.args, *message)
118
+ else:
119
+ if is_async:
120
+ return await cb(message.address, *message)
121
+ else:
122
+ return cb(message.address, *message)
123
+
83
124
 
84
125
  class Dispatcher(object):
85
126
  """Maps Handlers to OSC addresses and dispatches messages to the handler on matched addresses
@@ -87,9 +128,20 @@ class Dispatcher(object):
87
128
  Maps OSC addresses to handler functions and invokes the correct handler when a message comes in.
88
129
  """
89
130
 
90
- def __init__(self) -> None:
131
+ def __init__(self, strict_timing: bool = True) -> None:
132
+ """Initialize the dispatcher.
133
+
134
+ Args:
135
+ strict_timing: Whether to automatically schedule messages with future timetags.
136
+ If True (default), the dispatcher will wait (using sleep) until the specified
137
+ timetag before invoking handlers.
138
+ If False, messages are dispatched immediately regardless of their timetag.
139
+ Disabling this can prevent memory/thread accumulation issues when receiving
140
+ many future-dated messages.
141
+ """
91
142
  self._map: DefaultDict[str, List[Handler]] = collections.defaultdict(list)
92
143
  self._default_handler: Optional[Handler] = None
144
+ self._strict_timing = strict_timing
93
145
 
94
146
  def map(
95
147
  self,
@@ -102,13 +154,15 @@ class Dispatcher(object):
102
154
 
103
155
  The callback function must have one of the following signatures:
104
156
 
105
- ``def some_cb(address: str, *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
106
- ``def some_cb(address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr,
107
- Tuple(str, ArgValue)]:``
157
+ .. code-block:: python
158
+
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]]: ...
108
163
 
109
- ``def some_cb(client_address: Tuple[str, int], address: str, *osc_args: List[Any]) -> Union[None, AnyStr,
110
- Tuple(str, ArgValue)]:``
111
- ``def some_cb(client_address: Tuple[str, int], address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
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.
112
166
 
113
167
  The callback function can return None, or a string representing an OSC address to be returned to the client,
114
168
  or a tuple that includes the address and ArgValue which will be converted to an OSC message and returned to
@@ -240,8 +294,8 @@ class Dispatcher(object):
240
294
  matched = False
241
295
  for addr, handlers in self._map.items():
242
296
  if patterncompiled.match(addr) or (
243
- ("*" in addr)
244
- and re.match(addr.replace("*", "[^/]*?/*"), address_pattern)
297
+ "*" in addr
298
+ and re.match(addr.replace("*", ".*?") + "$", address_pattern)
245
299
  ):
246
300
  yield from handlers
247
301
  matched = True
@@ -272,7 +326,7 @@ class Dispatcher(object):
272
326
  if not handlers:
273
327
  continue
274
328
  # If the message is to be handled later, then so be it.
275
- if timed_msg.time > now:
329
+ if self._strict_timing and timed_msg.time > now:
276
330
  time.sleep(timed_msg.time - now)
277
331
  for handler in handlers:
278
332
  result = handler.invoke(client_address, timed_msg.message)
@@ -309,46 +363,13 @@ class Dispatcher(object):
309
363
  if not handlers:
310
364
  continue
311
365
  # If the message is to be handled later, then so be it.
312
- if timed_msg.time > now:
313
- time.sleep(timed_msg.time - now)
366
+ if self._strict_timing and timed_msg.time > now:
367
+ await asyncio.sleep(timed_msg.time - now)
314
368
  for handler in handlers:
315
- if inspect.iscoroutinefunction(handler.callback):
316
- if handler.needs_reply_address:
317
- result = await handler.callback(
318
- client_address,
319
- timed_msg.message.address,
320
- handler.args,
321
- *timed_msg.message,
322
- )
323
- elif handler.args:
324
- result = await handler.callback(
325
- timed_msg.message.address,
326
- handler.args,
327
- *timed_msg.message,
328
- )
329
- else:
330
- result = await handler.callback(
331
- timed_msg.message.address, *timed_msg.message
332
- )
333
- else:
334
- if handler.needs_reply_address:
335
- result = handler.callback(
336
- client_address,
337
- timed_msg.message.address,
338
- handler.args,
339
- *timed_msg.message,
340
- )
341
- elif handler.args:
342
- result = handler.callback(
343
- timed_msg.message.address,
344
- handler.args,
345
- *timed_msg.message,
346
- )
347
- else:
348
- result = handler.callback(
349
- timed_msg.message.address, *timed_msg.message
350
- )
351
- if result:
369
+ result = await handler.async_invoke(
370
+ client_address, timed_msg.message
371
+ )
372
+ if result is not None:
352
373
  results.append(result)
353
374
  except osc_packet.ParseError:
354
375
  pass
pythonosc/osc_bundle.py CHANGED
@@ -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.
@@ -5,7 +5,11 @@ from typing import Any, Iterable, List, Optional, Tuple, Union
5
5
  from pythonosc import osc_message
6
6
  from pythonosc.parsing import osc_types
7
7
 
8
- ArgValue = Union[str, bytes, bool, int, float, osc_types.MidiPacket, list, None]
8
+ # Represents a single OSC argument value.
9
+ # Can be a primitive type, a MIDI packet, or a list/tuple for nested OSC arrays.
10
+ ArgValue = Union[
11
+ str, bytes, bool, int, float, osc_types.MidiPacket, List[Any], Tuple[Any, ...], None
12
+ ]
9
13
 
10
14
 
11
15
  class BuildError(Exception):
@@ -68,9 +72,9 @@ class OscMessageBuilder(object):
68
72
  """Returns the (type, value) arguments list of this message."""
69
73
  return self._args
70
74
 
71
- def _valid_type(self, arg_type: str) -> bool:
72
- if arg_type in self._SUPPORTED_ARG_TYPES:
73
- return True
75
+ def _valid_type(self, arg_type: Union[str, List[Any]]) -> bool:
76
+ if isinstance(arg_type, str):
77
+ return arg_type in self._SUPPORTED_ARG_TYPES
74
78
  elif isinstance(arg_type, list):
75
79
  for sub_type in arg_type:
76
80
  if not self._valid_type(sub_type):
@@ -78,7 +82,9 @@ class OscMessageBuilder(object):
78
82
  return True
79
83
  return False
80
84
 
81
- def add_arg(self, arg_value: ArgValue, arg_type: Optional[str] = None) -> None:
85
+ def add_arg(
86
+ self, arg_value: ArgValue, arg_type: Optional[Union[str, List[Any]]] = None
87
+ ) -> None:
82
88
  """Add a typed argument to this message.
83
89
 
84
90
  Args:
@@ -193,12 +199,17 @@ class OscMessageBuilder(object):
193
199
  raise BuildError(f"Could not build the message: {be}")
194
200
 
195
201
 
196
- def build_msg(address: str, value: ArgValue = "") -> osc_message.OscMessage:
202
+ def build_msg(
203
+ address: str, value: Union[ArgValue, Iterable[ArgValue]] = ()
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
+ """
197
210
  builder = OscMessageBuilder(address=address)
198
211
  values: Iterable[Any]
199
- if value == "":
200
- values = []
201
- elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
212
+ if not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
202
213
  values = [value]
203
214
  else:
204
215
  values = value
pythonosc/osc_server.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import os
5
+ import socket
5
6
  import socketserver
6
7
  from socket import socket as _socket
7
8
  from typing import Any, Coroutine, Tuple, Union, cast
@@ -58,11 +59,16 @@ def _is_valid_request(request: _RequestType) -> bool:
58
59
  class OSCUDPServer(socketserver.UDPServer):
59
60
  """Superclass for different flavors of OSC UDP servers"""
60
61
 
62
+ # Receive complete UDP datagrams instead of socketserver's 8192-byte default.
63
+ max_packet_size = 65535
64
+
61
65
  def __init__(
62
66
  self,
63
67
  server_address: Tuple[str, int],
64
68
  dispatcher: Dispatcher,
65
69
  bind_and_activate: bool = True,
70
+ timeout: float | None = None,
71
+ family: socket.AddressFamily | None = None,
66
72
  ) -> None:
67
73
  """Initialize
68
74
 
@@ -70,9 +76,29 @@ class OSCUDPServer(socketserver.UDPServer):
70
76
  server_address: IP and port of server
71
77
  dispatcher: Dispatcher this server will use
72
78
  (optional) bind_and_activate: default=True defines if the server has to start on call of constructor
79
+ (optional) timeout: Default timeout in seconds for socket operations
80
+ (optional) family: socket.AF_INET or socket.AF_INET6. If None, it will be inferred from server_address.
73
81
  """
82
+ if family is not None:
83
+ self.address_family = family
84
+ else:
85
+ # Try to infer address family from server_address
86
+ try:
87
+ infos = socket.getaddrinfo(
88
+ server_address[0],
89
+ server_address[1],
90
+ type=socket.SOCK_DGRAM,
91
+ family=socket.AF_UNSPEC,
92
+ )
93
+ if infos:
94
+ self.address_family = infos[0][0]
95
+ except (socket.gaierror, IndexError):
96
+ # Fallback to default if resolution fails
97
+ pass
98
+
74
99
  super().__init__(server_address, _UDPHandler, bind_and_activate)
75
100
  self._dispatcher = dispatcher
101
+ self.timeout = timeout
76
102
 
77
103
  def verify_request(
78
104
  self, request: _RequestType, client_address: _AddressType
@@ -33,67 +33,37 @@ loop.run_forever()
33
33
  # mypy: disable-error-code="attr-defined"
34
34
 
35
35
  import asyncio
36
- import logging
37
36
  import os
37
+ import socket
38
38
  import socketserver
39
- import struct
40
- from typing import List, Tuple
39
+ from typing import Tuple
41
40
 
42
- from pythonosc import osc_message_builder, slip
41
+ from pythonosc import osc_message_builder
43
42
  from pythonosc.dispatcher import Dispatcher
43
+ from pythonosc.parsing.framing import (
44
+ BINARY_LENGTH_FRAMING,
45
+ SLIP_FRAMING,
46
+ Framing,
47
+ FramingParser,
48
+ )
44
49
 
45
- LOG = logging.getLogger()
46
50
  MODE_1_0 = "1.0"
47
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
+ }
48
56
 
49
57
 
50
- class _TCPHandler1_0(socketserver.BaseRequestHandler):
51
- """Handles correct OSC1.0 messages.
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
52
63
 
53
- Whether this will be run on its own thread, the server's or a whole new
54
- process depends on the server you instantiated, look at their documentation.
55
-
56
- This method is called after a basic sanity check was done on the datagram,
57
- basically whether this datagram looks like an osc message or bundle,
58
- if not the server won't even bother to call it and so no new
59
- threads/processes will be spawned.
60
- """
61
64
 
62
- def handle(self) -> None:
63
- LOG.debug("handle OSC 1.0 protocol")
64
- while True:
65
- lengthbuf = self.recvall(4)
66
- if lengthbuf == b"":
67
- break
68
- (length,) = struct.unpack("!I", lengthbuf)
69
- data = self.recvall(length)
70
- if data == b"":
71
- break
72
-
73
- resp = self.server.dispatcher.call_handlers_for_packet(
74
- data, self.client_address
75
- )
76
- # resp = _call_handlers_for_packet(data, self.server.dispatcher)
77
- for r in resp:
78
- if not isinstance(r, tuple):
79
- r = [r]
80
- msg = osc_message_builder.build_msg(r[0], r[1:])
81
- b = struct.pack("!I", len(msg.dgram))
82
- self.request.sendall(b + msg.dgram)
83
-
84
- def recvall(self, count: int) -> bytes:
85
- buf = b""
86
- while count > 0:
87
- newbuf = self.request.recv(count)
88
- if not newbuf:
89
- return b""
90
- buf += newbuf
91
- count -= len(newbuf)
92
- return buf
93
-
94
-
95
- class _TCPHandler1_1(socketserver.BaseRequestHandler):
96
- """Handles correct OSC1.1 messages.
65
+ class TCPHandler(socketserver.BaseRequestHandler):
66
+ """Handles correct OSC messages.
97
67
 
98
68
  Whether this will be run on its own thread, the server's or a whole new
99
69
  process depends on the server you instantiated, look at their documentation.
@@ -105,14 +75,12 @@ class _TCPHandler1_1(socketserver.BaseRequestHandler):
105
75
  """
106
76
 
107
77
  def handle(self) -> None:
108
- LOG.debug("handle OSC 1.1 protocol")
109
- while True:
110
- packets = self.recvall()
111
- if not packets:
112
- break
113
-
114
- for p in packets:
115
- # resp = _call_handlers_for_packet(p, self.server.dispatcher)
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):
116
84
  resp = self.server.dispatcher.call_handlers_for_packet(
117
85
  p, self.client_address
118
86
  )
@@ -120,47 +88,44 @@ class _TCPHandler1_1(socketserver.BaseRequestHandler):
120
88
  if not isinstance(r, tuple):
121
89
  r = [r]
122
90
  msg = osc_message_builder.build_msg(r[0], r[1:])
123
- self.request.sendall(slip.encode(msg.dgram))
124
-
125
- def recvall(self) -> List[bytes]:
126
- buf = self.request.recv(4096)
127
- if not buf:
128
- return []
129
- # If the last byte is not an END marker there could be more data coming
130
- while buf[-1] != 192:
131
- newbuf = self.request.recv(4096)
132
- if not newbuf:
133
- # Maybe should raise an exception here?
134
- break
135
- buf += newbuf
136
-
137
- packets = [slip.decode(p) for p in buf.split(slip.END_END)]
138
- return packets
91
+ self.request.sendall(self.server.framing.encode(msg.dgram))
139
92
 
140
93
 
141
94
  class OSCTCPServer(socketserver.TCPServer):
142
95
  """Superclass for different flavors of OSCTCPServer"""
143
96
 
97
+ dispatcher: Dispatcher
98
+ framing: Framing
99
+
144
100
  def __init__(
145
101
  self,
146
102
  server_address: Tuple[str | bytes | bytearray, int],
147
103
  dispatcher: Dispatcher,
148
104
  mode: str = MODE_1_1,
105
+ family: socket.AddressFamily | None = None,
149
106
  ):
150
107
  self.request_queue_size = 300
151
- self.mode = mode
152
- if mode not in [MODE_1_0, MODE_1_1]:
153
- raise ValueError("OSC Mode must be '1.0' or '1.1'")
154
- if self.mode == MODE_1_0:
155
- super().__init__(server_address, _TCPHandler1_0)
156
- else:
157
- super().__init__(server_address, _TCPHandler1_1)
158
- self._dispatcher = dispatcher
108
+ self.dispatcher = dispatcher
109
+ self.framing = get_framing(mode)
159
110
 
160
- @property
161
- def dispatcher(self):
162
- """Dispatcher accessor for handlers to dispatch osc messages."""
163
- return self._dispatcher
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)
164
129
 
165
130
 
166
131
  class BlockingOSCTCPServer(OSCTCPServer):
@@ -210,8 +175,8 @@ class AsyncOSCTCPServer:
210
175
  """
211
176
  self._port = port
212
177
  self._server_address = server_address
213
- self._dispatcher = dispatcher
214
- self._mode = mode
178
+ self.dispatcher = dispatcher
179
+ self.framing = get_framing(mode)
215
180
 
216
181
  async def __aenter__(self):
217
182
  return self
@@ -225,9 +190,6 @@ class AsyncOSCTCPServer:
225
190
  self.handle, self._server_address, self._port
226
191
  )
227
192
 
228
- addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
229
- LOG.debug(f"Serving on {addrs}")
230
-
231
193
  async with self._server:
232
194
  await self._server.serve_forever()
233
195
 
@@ -235,86 +197,28 @@ class AsyncOSCTCPServer:
235
197
  self._server.close()
236
198
  await self._server.wait_closed()
237
199
 
238
- @property
239
- def dispatcher(self):
240
- return self._dispatcher
241
-
242
200
  async def handle(
243
201
  self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
244
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)
245
215
  client_address = ("", 0)
246
216
  sock = writer.transport.get_extra_info("socket")
247
217
  if sock is not None:
248
218
  client_address = sock.getpeername()
249
219
 
250
- if self._mode == MODE_1_1:
251
- await self.handle_1_1(reader, writer, client_address)
252
- else:
253
- await self.handle1_0(reader, writer, client_address)
254
- writer.write_eof()
255
- LOG.debug("Close the connection")
256
- writer.close()
257
- await writer.wait_closed()
258
-
259
- async def handle1_0(
260
- self,
261
- reader: asyncio.StreamReader,
262
- writer: asyncio.StreamWriter,
263
- client_address: Tuple[str, int],
264
- ) -> None:
265
- LOG.debug("Incoming socket open 1.0")
266
- while True:
267
- try:
268
- buf = await reader.read(4)
269
- except Exception as e:
270
- LOG.exception("Read error", e)
271
- return
272
- if buf == b"":
273
- break
274
- (length,) = struct.unpack("!I", buf)
275
- buf = b""
276
- while length > 0:
277
- newbuf = await reader.read(length)
278
- if not newbuf:
279
- break
280
- buf += newbuf
281
- length -= len(newbuf)
282
-
283
- result = await self.dispatcher.async_call_handlers_for_packet(
284
- buf, client_address
285
- )
286
- for r in result:
287
- if not isinstance(r, tuple):
288
- r = [r]
289
- msg = osc_message_builder.build_msg(r[0], r[1:])
290
- b = struct.pack("!I", len(msg.dgram))
291
- writer.write(b + msg.dgram)
292
- await writer.drain()
293
-
294
- async def handle_1_1(
295
- self,
296
- reader: asyncio.StreamReader,
297
- writer: asyncio.StreamWriter,
298
- client_address: Tuple[str, int],
299
- ) -> None:
300
- LOG.debug("Incoming socket open 1.1")
301
- while True:
302
- try:
303
- buf = await reader.read(4096)
304
- except Exception as e:
305
- LOG.exception("Read error", e)
306
- return
307
- if buf == b"":
308
- break
309
- while len(buf) > 0 and buf[-1] != 192:
310
- newbuf = await reader.read(4096)
311
- if not newbuf:
312
- # Maybe should raise an exception here?
313
- break
314
- buf += newbuf
315
-
316
- packets = [slip.decode(p) for p in buf.split(slip.END_END)]
317
- for p in packets:
220
+ while chunk := await reader.read(16384):
221
+ for p in parser.feed(chunk):
318
222
  result = await self.dispatcher.async_call_handlers_for_packet(
319
223
  p, client_address
320
224
  )
@@ -322,5 +226,5 @@ class AsyncOSCTCPServer:
322
226
  if not isinstance(r, tuple):
323
227
  r = [r]
324
228
  msg = osc_message_builder.build_msg(r[0], r[1:])
325
- writer.write(slip.encode(msg.dgram))
229
+ writer.write(self.framing.encode(msg.dgram))
326
230
  await writer.drain()