python-osc 1.10.0__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.0 → python_osc-1.10.3}/PKG-INFO +3 -3
  2. {python_osc-1.10.0 → python_osc-1.10.3}/README.rst +2 -2
  3. python_osc-1.10.3/pyproject.toml +59 -0
  4. python_osc-1.10.0/pyproject.toml → python_osc-1.10.3/pyproject.toml.orig +2 -1
  5. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/dispatcher.py +70 -49
  6. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_bundle.py +6 -0
  7. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_message_builder.py +20 -9
  8. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_server.py +26 -0
  9. python_osc-1.10.3/pythonosc/osc_tcp_server.py +230 -0
  10. python_osc-1.10.3/pythonosc/parsing/framing.py +166 -0
  11. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/parsing/osc_types.py +13 -7
  12. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/tcp_client.py +70 -128
  13. python_osc-1.10.3/pythonosc/test/parsing/test_framing.py +158 -0
  14. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/parsing/test_osc_types.py +28 -0
  15. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_dispatcher.py +82 -1
  16. python_osc-1.10.3/pythonosc/test/test_empty_string_messages.py +87 -0
  17. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_bundle.py +21 -2
  18. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_message.py +5 -0
  19. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_server.py +55 -1
  20. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_tcp_server.py +45 -27
  21. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_tcp_client.py +2 -1
  22. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_udp_client.py +25 -0
  23. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/udp_client.py +24 -9
  24. python_osc-1.10.0/pythonosc/osc_tcp_server.py +0 -326
  25. {python_osc-1.10.0 → python_osc-1.10.3}/LICENSE.txt +0 -0
  26. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/__init__.py +0 -0
  27. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_bundle_builder.py +0 -0
  28. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_message.py +0 -0
  29. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_packet.py +0 -0
  30. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/parsing/__init__.py +0 -0
  31. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/parsing/ntp.py +0 -0
  32. {python_osc-1.10.0/pythonosc → python_osc-1.10.3/pythonosc/parsing}/slip.py +0 -0
  33. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/py.typed +0 -0
  34. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/__init__.py +0 -0
  35. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/parsing/__init__.py +0 -0
  36. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/parsing/test_ntp.py +0 -0
  37. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_bundle_builder.py +0 -0
  38. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_message_builder.py +0 -0
  39. {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_packet.py +0 -0
@@ -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
 
@@ -73,7 +73,7 @@ Simple client
73
73
  help="The port the OSC server is listening on")
74
74
  args = parser.parse_args()
75
75
 
76
- client = udp_client.SimpleUDPClient(args.ip, args.port)
76
+ client = udp_client.SimpleUDPClient(args.ip, args.port, timeout=10)
77
77
 
78
78
  for x in range(10):
79
79
  client.send_message("/filter", random.random())
@@ -117,7 +117,7 @@ Simple server
117
117
  dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
118
118
 
119
119
  server = osc_server.ThreadingOSCUDPServer(
120
- (args.ip, args.port), dispatcher)
120
+ (args.ip, args.port), dispatcher, timeout=10)
121
121
  print("Serving on {}".format(server.server_address))
122
122
  server.serve_forever()
123
123
 
@@ -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.0"
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"
@@ -32,6 +32,7 @@ dev = [
32
32
  "mypy",
33
33
  "ruff",
34
34
  "pytest-cov",
35
+ "pre-commit",
35
36
  ]
36
37
 
37
38
  [tool.uv.build-backend]
@@ -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
@@ -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
@@ -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
@@ -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()