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.
- {python_osc-1.10.0 → python_osc-1.10.3}/PKG-INFO +3 -3
- {python_osc-1.10.0 → python_osc-1.10.3}/README.rst +2 -2
- python_osc-1.10.3/pyproject.toml +59 -0
- python_osc-1.10.0/pyproject.toml → python_osc-1.10.3/pyproject.toml.orig +2 -1
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/dispatcher.py +70 -49
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_bundle.py +6 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_message_builder.py +20 -9
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_server.py +26 -0
- python_osc-1.10.3/pythonosc/osc_tcp_server.py +230 -0
- python_osc-1.10.3/pythonosc/parsing/framing.py +166 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/parsing/osc_types.py +13 -7
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/tcp_client.py +70 -128
- python_osc-1.10.3/pythonosc/test/parsing/test_framing.py +158 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/parsing/test_osc_types.py +28 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_dispatcher.py +82 -1
- python_osc-1.10.3/pythonosc/test/test_empty_string_messages.py +87 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_bundle.py +21 -2
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_message.py +5 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_server.py +55 -1
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_tcp_server.py +45 -27
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_tcp_client.py +2 -1
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_udp_client.py +25 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/udp_client.py +24 -9
- python_osc-1.10.0/pythonosc/osc_tcp_server.py +0 -326
- {python_osc-1.10.0 → python_osc-1.10.3}/LICENSE.txt +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/__init__.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_bundle_builder.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_message.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/osc_packet.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/parsing/__init__.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/parsing/ntp.py +0 -0
- {python_osc-1.10.0/pythonosc → python_osc-1.10.3/pythonosc/parsing}/slip.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/py.typed +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/__init__.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/parsing/__init__.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/parsing/test_ntp.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_bundle_builder.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.3}/pythonosc/test/test_osc_message_builder.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
244
|
-
and re.match(addr.replace("*", "
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
|
73
|
-
return
|
|
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(
|
|
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(
|
|
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()
|