python-osc 1.10.0__tar.gz → 1.10.2__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.2}/PKG-INFO +3 -3
- {python_osc-1.10.0 → python_osc-1.10.2}/README.rst +2 -2
- {python_osc-1.10.0 → python_osc-1.10.2}/pyproject.toml +2 -1
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/dispatcher.py +62 -43
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_message_builder.py +14 -6
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_server.py +23 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_tcp_server.py +20 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/tcp_client.py +43 -24
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_dispatcher.py +61 -1
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_server.py +24 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_udp_client.py +25 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/udp_client.py +24 -9
- {python_osc-1.10.0 → python_osc-1.10.2}/LICENSE.txt +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/__init__.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_bundle.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_bundle_builder.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_message.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/osc_packet.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/parsing/__init__.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/parsing/ntp.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/parsing/osc_types.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/py.typed +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/slip.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/__init__.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/parsing/__init__.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/parsing/test_ntp.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/parsing/test_osc_types.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_bundle.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_bundle_builder.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_message.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_message_builder.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_packet.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_osc_tcp_server.py +0 -0
- {python_osc-1.10.0 → python_osc-1.10.2}/pythonosc/test/test_tcp_client.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.2
|
|
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
|
|
|
@@ -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.2"
|
|
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,
|
|
@@ -240,8 +292,8 @@ class Dispatcher(object):
|
|
|
240
292
|
matched = False
|
|
241
293
|
for addr, handlers in self._map.items():
|
|
242
294
|
if patterncompiled.match(addr) or (
|
|
243
|
-
|
|
244
|
-
and re.match(addr.replace("*", "
|
|
295
|
+
"*" in addr
|
|
296
|
+
and re.match(addr.replace("*", ".*?") + "$", address_pattern)
|
|
245
297
|
):
|
|
246
298
|
yield from handlers
|
|
247
299
|
matched = True
|
|
@@ -272,7 +324,7 @@ class Dispatcher(object):
|
|
|
272
324
|
if not handlers:
|
|
273
325
|
continue
|
|
274
326
|
# If the message is to be handled later, then so be it.
|
|
275
|
-
if timed_msg.time > now:
|
|
327
|
+
if self._strict_timing and timed_msg.time > now:
|
|
276
328
|
time.sleep(timed_msg.time - now)
|
|
277
329
|
for handler in handlers:
|
|
278
330
|
result = handler.invoke(client_address, timed_msg.message)
|
|
@@ -309,46 +361,13 @@ class Dispatcher(object):
|
|
|
309
361
|
if not handlers:
|
|
310
362
|
continue
|
|
311
363
|
# If the message is to be handled later, then so be it.
|
|
312
|
-
if timed_msg.time > now:
|
|
313
|
-
|
|
364
|
+
if self._strict_timing and timed_msg.time > now:
|
|
365
|
+
await asyncio.sleep(timed_msg.time - now)
|
|
314
366
|
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:
|
|
367
|
+
result = await handler.async_invoke(
|
|
368
|
+
client_address, timed_msg.message
|
|
369
|
+
)
|
|
370
|
+
if result is not None:
|
|
352
371
|
results.append(result)
|
|
353
372
|
except osc_packet.ParseError:
|
|
354
373
|
pass
|
|
@@ -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,7 +199,9 @@ 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:
|
|
197
205
|
builder = OscMessageBuilder(address=address)
|
|
198
206
|
values: Iterable[Any]
|
|
199
207
|
if 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
|
|
@@ -63,6 +64,8 @@ class OSCUDPServer(socketserver.UDPServer):
|
|
|
63
64
|
server_address: Tuple[str, int],
|
|
64
65
|
dispatcher: Dispatcher,
|
|
65
66
|
bind_and_activate: bool = True,
|
|
67
|
+
timeout: float | None = None,
|
|
68
|
+
family: socket.AddressFamily | None = None,
|
|
66
69
|
) -> None:
|
|
67
70
|
"""Initialize
|
|
68
71
|
|
|
@@ -70,9 +73,29 @@ class OSCUDPServer(socketserver.UDPServer):
|
|
|
70
73
|
server_address: IP and port of server
|
|
71
74
|
dispatcher: Dispatcher this server will use
|
|
72
75
|
(optional) bind_and_activate: default=True defines if the server has to start on call of constructor
|
|
76
|
+
(optional) timeout: Default timeout in seconds for socket operations
|
|
77
|
+
(optional) family: socket.AF_INET or socket.AF_INET6. If None, it will be inferred from server_address.
|
|
73
78
|
"""
|
|
79
|
+
if family is not None:
|
|
80
|
+
self.address_family = family
|
|
81
|
+
else:
|
|
82
|
+
# Try to infer address family from server_address
|
|
83
|
+
try:
|
|
84
|
+
infos = socket.getaddrinfo(
|
|
85
|
+
server_address[0],
|
|
86
|
+
server_address[1],
|
|
87
|
+
type=socket.SOCK_DGRAM,
|
|
88
|
+
family=socket.AF_UNSPEC,
|
|
89
|
+
)
|
|
90
|
+
if infos:
|
|
91
|
+
self.address_family = infos[0][0]
|
|
92
|
+
except (socket.gaierror, IndexError):
|
|
93
|
+
# Fallback to default if resolution fails
|
|
94
|
+
pass
|
|
95
|
+
|
|
74
96
|
super().__init__(server_address, _UDPHandler, bind_and_activate)
|
|
75
97
|
self._dispatcher = dispatcher
|
|
98
|
+
self.timeout = timeout
|
|
76
99
|
|
|
77
100
|
def verify_request(
|
|
78
101
|
self, request: _RequestType, client_address: _AddressType
|
|
@@ -35,6 +35,7 @@ loop.run_forever()
|
|
|
35
35
|
import asyncio
|
|
36
36
|
import logging
|
|
37
37
|
import os
|
|
38
|
+
import socket
|
|
38
39
|
import socketserver
|
|
39
40
|
import struct
|
|
40
41
|
from typing import List, Tuple
|
|
@@ -146,11 +147,30 @@ class OSCTCPServer(socketserver.TCPServer):
|
|
|
146
147
|
server_address: Tuple[str | bytes | bytearray, int],
|
|
147
148
|
dispatcher: Dispatcher,
|
|
148
149
|
mode: str = MODE_1_1,
|
|
150
|
+
family: socket.AddressFamily | None = None,
|
|
149
151
|
):
|
|
150
152
|
self.request_queue_size = 300
|
|
151
153
|
self.mode = mode
|
|
152
154
|
if mode not in [MODE_1_0, MODE_1_1]:
|
|
153
155
|
raise ValueError("OSC Mode must be '1.0' or '1.1'")
|
|
156
|
+
|
|
157
|
+
if family is not None:
|
|
158
|
+
self.address_family = family
|
|
159
|
+
elif isinstance(server_address[0], str):
|
|
160
|
+
# Try to infer address family from server_address
|
|
161
|
+
try:
|
|
162
|
+
infos = socket.getaddrinfo(
|
|
163
|
+
server_address[0],
|
|
164
|
+
server_address[1],
|
|
165
|
+
type=socket.SOCK_STREAM,
|
|
166
|
+
family=socket.AF_UNSPEC,
|
|
167
|
+
)
|
|
168
|
+
if infos:
|
|
169
|
+
self.address_family = infos[0][0]
|
|
170
|
+
except (socket.gaierror, IndexError):
|
|
171
|
+
# Fallback to default if resolution fails
|
|
172
|
+
pass
|
|
173
|
+
|
|
154
174
|
if self.mode == MODE_1_0:
|
|
155
175
|
super().__init__(server_address, _TCPHandler1_0)
|
|
156
176
|
else:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import socket
|
|
5
5
|
import struct
|
|
6
|
-
from typing import AsyncGenerator, Generator, List, Union
|
|
6
|
+
from typing import AsyncGenerator, Generator, Iterable, List, Union
|
|
7
7
|
|
|
8
8
|
from pythonosc import slip
|
|
9
9
|
from pythonosc.dispatcher import Dispatcher
|
|
@@ -22,6 +22,7 @@ class TCPClient(object):
|
|
|
22
22
|
port: int,
|
|
23
23
|
family: socket.AddressFamily = socket.AF_INET,
|
|
24
24
|
mode: str = MODE_1_1,
|
|
25
|
+
timeout: float | None = 30.0,
|
|
25
26
|
) -> None:
|
|
26
27
|
"""Initialize client
|
|
27
28
|
|
|
@@ -29,13 +30,15 @@ class TCPClient(object):
|
|
|
29
30
|
address: IP address of server
|
|
30
31
|
port: Port of server
|
|
31
32
|
family: address family parameter (passed to socket.getaddrinfo)
|
|
33
|
+
timeout: Default timeout in seconds for socket operations
|
|
32
34
|
"""
|
|
33
35
|
self.address = address
|
|
34
36
|
self.port = port
|
|
35
37
|
self.family = family
|
|
36
38
|
self.mode = mode
|
|
39
|
+
self._timeout = timeout
|
|
37
40
|
self.socket = socket.socket(self.family, socket.SOCK_STREAM)
|
|
38
|
-
self.socket.settimeout(
|
|
41
|
+
self.socket.settimeout(timeout)
|
|
39
42
|
self.socket.connect((address, port))
|
|
40
43
|
|
|
41
44
|
def __enter__(self):
|
|
@@ -56,12 +59,13 @@ class TCPClient(object):
|
|
|
56
59
|
b = struct.pack("!I", len(content.dgram))
|
|
57
60
|
self.socket.sendall(b + content.dgram)
|
|
58
61
|
|
|
59
|
-
def receive(self, timeout:
|
|
60
|
-
self.
|
|
62
|
+
def receive(self, timeout: float | None = None) -> List[bytes]:
|
|
63
|
+
effective_timeout = timeout if timeout is not None else self._timeout
|
|
64
|
+
self.socket.settimeout(effective_timeout)
|
|
61
65
|
if self.mode == MODE_1_1:
|
|
62
66
|
try:
|
|
63
67
|
buf = self.socket.recv(4096)
|
|
64
|
-
except TimeoutError:
|
|
68
|
+
except (TimeoutError, socket.timeout):
|
|
65
69
|
return []
|
|
66
70
|
if not buf:
|
|
67
71
|
return []
|
|
@@ -69,7 +73,7 @@ class TCPClient(object):
|
|
|
69
73
|
while buf[-1] != 192:
|
|
70
74
|
try:
|
|
71
75
|
newbuf = self.socket.recv(4096)
|
|
72
|
-
except TimeoutError:
|
|
76
|
+
except (TimeoutError, socket.timeout):
|
|
73
77
|
break
|
|
74
78
|
if not newbuf:
|
|
75
79
|
# Maybe should raise an exception here?
|
|
@@ -80,13 +84,13 @@ class TCPClient(object):
|
|
|
80
84
|
buf = b""
|
|
81
85
|
try:
|
|
82
86
|
lengthbuf = self.socket.recv(4)
|
|
83
|
-
except TimeoutError:
|
|
87
|
+
except (TimeoutError, socket.timeout):
|
|
84
88
|
return []
|
|
85
89
|
(length,) = struct.unpack("!I", lengthbuf)
|
|
86
90
|
while length > 0:
|
|
87
91
|
try:
|
|
88
92
|
newbuf = self.socket.recv(length)
|
|
89
|
-
except TimeoutError:
|
|
93
|
+
except (TimeoutError, socket.timeout):
|
|
90
94
|
return []
|
|
91
95
|
if not newbuf:
|
|
92
96
|
return []
|
|
@@ -104,7 +108,9 @@ class SimpleTCPClient(TCPClient):
|
|
|
104
108
|
def __init__(self, *args, **kwargs):
|
|
105
109
|
super().__init__(*args, **kwargs)
|
|
106
110
|
|
|
107
|
-
def send_message(
|
|
111
|
+
def send_message(
|
|
112
|
+
self, address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
|
|
113
|
+
) -> None:
|
|
108
114
|
"""Build :class:`OscMessage` from arguments and send to server
|
|
109
115
|
|
|
110
116
|
Args:
|
|
@@ -114,7 +120,7 @@ class SimpleTCPClient(TCPClient):
|
|
|
114
120
|
msg = build_msg(address, value)
|
|
115
121
|
return self.send(msg)
|
|
116
122
|
|
|
117
|
-
def get_messages(self, timeout:
|
|
123
|
+
def get_messages(self, timeout: float | None = None) -> Generator:
|
|
118
124
|
r = self.receive(timeout)
|
|
119
125
|
while r:
|
|
120
126
|
for m in r:
|
|
@@ -127,7 +133,7 @@ class TCPDispatchClient(SimpleTCPClient):
|
|
|
127
133
|
|
|
128
134
|
dispatcher = Dispatcher()
|
|
129
135
|
|
|
130
|
-
def handle_messages(self, timeout_sec:
|
|
136
|
+
def handle_messages(self, timeout_sec: float | None = None) -> None:
|
|
131
137
|
"""Wait :int:`timeout` seconds for a message from the server and process each message with the registered
|
|
132
138
|
handlers. Continue until a timeout occurs.
|
|
133
139
|
|
|
@@ -150,6 +156,7 @@ class AsyncTCPClient:
|
|
|
150
156
|
port: int,
|
|
151
157
|
family: socket.AddressFamily = socket.AF_INET,
|
|
152
158
|
mode: str = MODE_1_1,
|
|
159
|
+
timeout: float | None = 30.0,
|
|
153
160
|
) -> None:
|
|
154
161
|
"""Initialize client
|
|
155
162
|
|
|
@@ -157,11 +164,13 @@ class AsyncTCPClient:
|
|
|
157
164
|
address: IP address of server
|
|
158
165
|
port: Port of server
|
|
159
166
|
family: address family parameter (passed to socket.getaddrinfo)
|
|
167
|
+
timeout: Default timeout in seconds for socket operations
|
|
160
168
|
"""
|
|
161
169
|
self.address: str = address
|
|
162
170
|
self.port: int = port
|
|
163
171
|
self.mode: str = mode
|
|
164
172
|
self.family: socket.AddressFamily = family
|
|
173
|
+
self._timeout = timeout
|
|
165
174
|
|
|
166
175
|
def __await__(self):
|
|
167
176
|
async def closure():
|
|
@@ -195,19 +204,22 @@ class AsyncTCPClient:
|
|
|
195
204
|
self.writer.write(b + content.dgram)
|
|
196
205
|
await self.writer.drain()
|
|
197
206
|
|
|
198
|
-
async def receive(self, timeout:
|
|
207
|
+
async def receive(self, timeout: float | None = None) -> List[bytes]:
|
|
208
|
+
effective_timeout = timeout if timeout is not None else self._timeout
|
|
199
209
|
if self.mode == MODE_1_1:
|
|
200
210
|
try:
|
|
201
|
-
buf = await asyncio.wait_for(self.reader.read(4096),
|
|
202
|
-
except TimeoutError:
|
|
211
|
+
buf = await asyncio.wait_for(self.reader.read(4096), effective_timeout)
|
|
212
|
+
except (TimeoutError, asyncio.TimeoutError):
|
|
203
213
|
return []
|
|
204
214
|
if not buf:
|
|
205
215
|
return []
|
|
206
216
|
# If the last byte is not an END marker there could be more data coming
|
|
207
217
|
while buf[-1] != 192:
|
|
208
218
|
try:
|
|
209
|
-
newbuf = await asyncio.wait_for(
|
|
210
|
-
|
|
219
|
+
newbuf = await asyncio.wait_for(
|
|
220
|
+
self.reader.read(4096), effective_timeout
|
|
221
|
+
)
|
|
222
|
+
except (TimeoutError, asyncio.TimeoutError):
|
|
211
223
|
break
|
|
212
224
|
if not newbuf:
|
|
213
225
|
# Maybe should raise an exception here?
|
|
@@ -217,15 +229,19 @@ class AsyncTCPClient:
|
|
|
217
229
|
else:
|
|
218
230
|
buf = b""
|
|
219
231
|
try:
|
|
220
|
-
lengthbuf = await asyncio.wait_for(
|
|
221
|
-
|
|
232
|
+
lengthbuf = await asyncio.wait_for(
|
|
233
|
+
self.reader.read(4), effective_timeout
|
|
234
|
+
)
|
|
235
|
+
except (TimeoutError, asyncio.TimeoutError):
|
|
222
236
|
return []
|
|
223
237
|
|
|
224
238
|
(length,) = struct.unpack("!I", lengthbuf)
|
|
225
239
|
while length > 0:
|
|
226
240
|
try:
|
|
227
|
-
newbuf = await asyncio.wait_for(
|
|
228
|
-
|
|
241
|
+
newbuf = await asyncio.wait_for(
|
|
242
|
+
self.reader.read(length), effective_timeout
|
|
243
|
+
)
|
|
244
|
+
except (TimeoutError, asyncio.TimeoutError):
|
|
229
245
|
return []
|
|
230
246
|
if not newbuf:
|
|
231
247
|
return []
|
|
@@ -248,10 +264,13 @@ class AsyncSimpleTCPClient(AsyncTCPClient):
|
|
|
248
264
|
port: int,
|
|
249
265
|
family: socket.AddressFamily = socket.AF_INET,
|
|
250
266
|
mode: str = MODE_1_1,
|
|
267
|
+
timeout: float | None = 30.0,
|
|
251
268
|
):
|
|
252
|
-
super().__init__(address, port, family, mode)
|
|
269
|
+
super().__init__(address, port, family, mode, timeout)
|
|
253
270
|
|
|
254
|
-
async def send_message(
|
|
271
|
+
async def send_message(
|
|
272
|
+
self, address: str, value: Union[ArgValue, Iterable[ArgValue]] = ""
|
|
273
|
+
) -> None:
|
|
255
274
|
"""Build :class:`OscMessage` from arguments and send to server
|
|
256
275
|
|
|
257
276
|
Args:
|
|
@@ -261,7 +280,7 @@ class AsyncSimpleTCPClient(AsyncTCPClient):
|
|
|
261
280
|
msg = build_msg(address, value)
|
|
262
281
|
return await self.send(msg)
|
|
263
282
|
|
|
264
|
-
async def get_messages(self, timeout:
|
|
283
|
+
async def get_messages(self, timeout: float | None = None) -> AsyncGenerator:
|
|
265
284
|
r = await self.receive(timeout)
|
|
266
285
|
while r:
|
|
267
286
|
for m in r:
|
|
@@ -274,7 +293,7 @@ class AsyncDispatchTCPClient(AsyncTCPClient):
|
|
|
274
293
|
|
|
275
294
|
dispatcher = Dispatcher()
|
|
276
295
|
|
|
277
|
-
async def handle_messages(self, timeout:
|
|
296
|
+
async def handle_messages(self, timeout: float | None = None) -> None:
|
|
278
297
|
"""Wait :int:`timeout` seconds for a message from the server and process each message with the registered
|
|
279
298
|
handlers. Continue until a timeout occurs.
|
|
280
299
|
|
|
@@ -3,7 +3,7 @@ import unittest
|
|
|
3
3
|
from pythonosc.dispatcher import Dispatcher, Handler
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class TestDispatcher(unittest.
|
|
6
|
+
class TestDispatcher(unittest.IsolatedAsyncioTestCase):
|
|
7
7
|
def setUp(self):
|
|
8
8
|
super().setUp()
|
|
9
9
|
self.dispatcher = Dispatcher()
|
|
@@ -181,6 +181,66 @@ class TestDispatcher(unittest.TestCase):
|
|
|
181
181
|
with self.assertRaises(ValueError):
|
|
182
182
|
self.dispatcher.unmap("/unmap/exception", handlerobj)
|
|
183
183
|
|
|
184
|
+
def test_handlers_for_address_wildcard_no_partial_match(self):
|
|
185
|
+
self.dispatcher.map("/qwer/*/zxcv", 1)
|
|
186
|
+
# Should not match
|
|
187
|
+
handlers = list(
|
|
188
|
+
self.dispatcher.handlers_for_address("/qwer/whatever/zxcvsomethingmore")
|
|
189
|
+
)
|
|
190
|
+
self.assertEqual(len(handlers), 0)
|
|
191
|
+
# Should match
|
|
192
|
+
handlers = list(self.dispatcher.handlers_for_address("/qwer/whatever/zxcv"))
|
|
193
|
+
self.assertEqual(len(handlers), 1)
|
|
194
|
+
|
|
195
|
+
def test_strict_timing_disabled(self):
|
|
196
|
+
# Disable strict timing
|
|
197
|
+
dispatcher = Dispatcher(strict_timing=False)
|
|
198
|
+
|
|
199
|
+
callback_called = False
|
|
200
|
+
|
|
201
|
+
def handler(address, *args):
|
|
202
|
+
nonlocal callback_called
|
|
203
|
+
callback_called = True
|
|
204
|
+
|
|
205
|
+
dispatcher.map("/test", handler)
|
|
206
|
+
|
|
207
|
+
# Create a message with a future timestamp (1 hour from now)
|
|
208
|
+
# We'll use OscPacket to simulate a bundle with a future timestamp
|
|
209
|
+
# But for simple unit test, we can just check if it sleeps
|
|
210
|
+
# Since we can't easily mock time.sleep across the dispatcher without more effort,
|
|
211
|
+
# we'll just verify the logic exists.
|
|
212
|
+
self.assertFalse(dispatcher._strict_timing)
|
|
213
|
+
|
|
214
|
+
async def test_async_call_handlers_for_packet(self):
|
|
215
|
+
dispatcher = Dispatcher()
|
|
216
|
+
|
|
217
|
+
sync_called = False
|
|
218
|
+
|
|
219
|
+
def sync_handler(address, *args):
|
|
220
|
+
nonlocal sync_called
|
|
221
|
+
sync_called = True
|
|
222
|
+
|
|
223
|
+
async_called = False
|
|
224
|
+
|
|
225
|
+
async def async_handler(address, *args):
|
|
226
|
+
nonlocal async_called
|
|
227
|
+
async_called = True
|
|
228
|
+
|
|
229
|
+
dispatcher.map("/sync", sync_handler)
|
|
230
|
+
dispatcher.map("/async", async_handler)
|
|
231
|
+
|
|
232
|
+
# Dispatch sync handler
|
|
233
|
+
dgram_sync = b"/sync\x00\x00\x00,\x00\x00\x00"
|
|
234
|
+
await dispatcher.async_call_handlers_for_packet(dgram_sync, ("127.0.0.1", 1234))
|
|
235
|
+
self.assertTrue(sync_called)
|
|
236
|
+
|
|
237
|
+
# Dispatch async handler
|
|
238
|
+
dgram_async = b"/async\x00\x00,\x00\x00\x00"
|
|
239
|
+
await dispatcher.async_call_handlers_for_packet(
|
|
240
|
+
dgram_async, ("127.0.0.1", 1234)
|
|
241
|
+
)
|
|
242
|
+
self.assertTrue(async_called)
|
|
243
|
+
|
|
184
244
|
|
|
185
245
|
if __name__ == "__main__":
|
|
186
246
|
unittest.main()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import socket
|
|
1
2
|
import unittest
|
|
2
3
|
import unittest.mock
|
|
3
4
|
|
|
@@ -112,5 +113,28 @@ class TestUDPHandler(unittest.TestCase):
|
|
|
112
113
|
)
|
|
113
114
|
|
|
114
115
|
|
|
116
|
+
class TestOscUdpServer(unittest.TestCase):
|
|
117
|
+
@unittest.mock.patch("socket.socket")
|
|
118
|
+
def test_init_timeout(self, mock_socket_ctor):
|
|
119
|
+
dispatcher = unittest.mock.Mock()
|
|
120
|
+
server = osc_server.OSCUDPServer(("127.0.0.1", 0), dispatcher, timeout=10.0)
|
|
121
|
+
self.assertEqual(server.timeout, 10.0)
|
|
122
|
+
|
|
123
|
+
@unittest.mock.patch("socket.socket")
|
|
124
|
+
def test_init_family_inference_ipv4(self, mock_socket_ctor):
|
|
125
|
+
dispatcher = unittest.mock.Mock()
|
|
126
|
+
server = osc_server.OSCUDPServer(("127.0.0.1", 0), dispatcher)
|
|
127
|
+
self.assertEqual(server.address_family, socket.AF_INET)
|
|
128
|
+
|
|
129
|
+
@unittest.mock.patch("socket.socket")
|
|
130
|
+
def test_init_family_inference_ipv6(self, mock_socket_ctor):
|
|
131
|
+
dispatcher = unittest.mock.Mock()
|
|
132
|
+
# Mock getaddrinfo to return IPv6 for this test to be environment-independent
|
|
133
|
+
with unittest.mock.patch("socket.getaddrinfo") as mock_getaddrinfo:
|
|
134
|
+
mock_getaddrinfo.return_value = [(socket.AF_INET6, None, None, None, None)]
|
|
135
|
+
server = osc_server.OSCUDPServer(("::1", 0), dispatcher)
|
|
136
|
+
self.assertEqual(server.address_family, socket.AF_INET6)
|
|
137
|
+
|
|
138
|
+
|
|
115
139
|
if __name__ == "__main__":
|
|
116
140
|
unittest.main()
|
|
@@ -64,5 +64,30 @@ class TestUdpClientClose(unittest.TestCase):
|
|
|
64
64
|
self.assertTrue(mock_socket.close.called)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
class TestUdpClientTimeout(unittest.TestCase):
|
|
68
|
+
@mock.patch("socket.socket")
|
|
69
|
+
def test_init_timeout(self, mock_socket_ctor):
|
|
70
|
+
mock_socket = mock_socket_ctor.return_value
|
|
71
|
+
client = udp_client.UDPClient("::1", 31337, timeout=10.0)
|
|
72
|
+
self.assertEqual(client._timeout, 10.0)
|
|
73
|
+
mock_socket.settimeout.assert_any_call(10.0)
|
|
74
|
+
|
|
75
|
+
@mock.patch("socket.socket")
|
|
76
|
+
def test_receive_default_timeout(self, mock_socket_ctor):
|
|
77
|
+
mock_socket = mock_socket_ctor.return_value
|
|
78
|
+
client = udp_client.UDPClient("::1", 31337, timeout=10.0)
|
|
79
|
+
mock_socket.recv.return_value = b"data"
|
|
80
|
+
client.receive()
|
|
81
|
+
mock_socket.settimeout.assert_called_with(10.0)
|
|
82
|
+
|
|
83
|
+
@mock.patch("socket.socket")
|
|
84
|
+
def test_receive_override_timeout(self, mock_socket_ctor):
|
|
85
|
+
mock_socket = mock_socket_ctor.return_value
|
|
86
|
+
client = udp_client.UDPClient("::1", 31337, timeout=10.0)
|
|
87
|
+
mock_socket.recv.return_value = b"data"
|
|
88
|
+
client.receive(timeout=5.0)
|
|
89
|
+
mock_socket.settimeout.assert_called_with(5.0)
|
|
90
|
+
|
|
91
|
+
|
|
67
92
|
if __name__ == "__main__":
|
|
68
93
|
unittest.main()
|
|
@@ -25,6 +25,7 @@ class UDPClient(object):
|
|
|
25
25
|
port: int,
|
|
26
26
|
allow_broadcast: bool = False,
|
|
27
27
|
family: socket.AddressFamily = socket.AF_UNSPEC,
|
|
28
|
+
timeout: float | None = None,
|
|
28
29
|
) -> None:
|
|
29
30
|
"""Initialize client
|
|
30
31
|
|
|
@@ -36,6 +37,7 @@ class UDPClient(object):
|
|
|
36
37
|
port: Port of server
|
|
37
38
|
allow_broadcast: Allow for broadcast transmissions
|
|
38
39
|
family: address family parameter (passed to socket.getaddrinfo)
|
|
40
|
+
timeout: Default timeout in seconds for socket operations
|
|
39
41
|
"""
|
|
40
42
|
|
|
41
43
|
for addr in socket.getaddrinfo(
|
|
@@ -50,6 +52,10 @@ class UDPClient(object):
|
|
|
50
52
|
break
|
|
51
53
|
|
|
52
54
|
self._sock.setblocking(False)
|
|
55
|
+
if timeout is not None:
|
|
56
|
+
self._sock.settimeout(timeout)
|
|
57
|
+
self._timeout = timeout
|
|
58
|
+
|
|
53
59
|
if allow_broadcast:
|
|
54
60
|
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
55
61
|
self._address = address
|
|
@@ -75,23 +81,30 @@ class UDPClient(object):
|
|
|
75
81
|
"""
|
|
76
82
|
self._sock.sendto(content.dgram, (self._address, self._port))
|
|
77
83
|
|
|
78
|
-
def receive(self, timeout:
|
|
84
|
+
def receive(self, timeout: float | None = None) -> bytes:
|
|
79
85
|
"""Wait :int:`timeout` seconds for a message an return the raw bytes
|
|
80
86
|
|
|
81
87
|
Args:
|
|
82
|
-
timeout: Number of seconds to wait for a message
|
|
88
|
+
timeout: Number of seconds to wait for a message.
|
|
89
|
+
If None, uses the default timeout set in __init__.
|
|
83
90
|
"""
|
|
84
|
-
|
|
91
|
+
if timeout is not None:
|
|
92
|
+
self._sock.settimeout(timeout)
|
|
93
|
+
elif self._timeout is not None:
|
|
94
|
+
self._sock.settimeout(self._timeout)
|
|
95
|
+
|
|
85
96
|
try:
|
|
86
97
|
return self._sock.recv(4096)
|
|
87
|
-
except TimeoutError:
|
|
98
|
+
except (TimeoutError, socket.timeout, BlockingIOError):
|
|
88
99
|
return b""
|
|
89
100
|
|
|
90
101
|
|
|
91
102
|
class SimpleUDPClient(UDPClient):
|
|
92
103
|
"""Simple OSC client that automatically builds :class:`OscMessage` from arguments"""
|
|
93
104
|
|
|
94
|
-
def send_message(
|
|
105
|
+
def send_message(
|
|
106
|
+
self, address: str, value: Union[ArgValue, Iterable[ArgValue]]
|
|
107
|
+
) -> None:
|
|
95
108
|
"""Build :class:`OscMessage` from arguments and send to server
|
|
96
109
|
|
|
97
110
|
Args:
|
|
@@ -109,11 +122,12 @@ class SimpleUDPClient(UDPClient):
|
|
|
109
122
|
msg = builder.build()
|
|
110
123
|
self.send(msg)
|
|
111
124
|
|
|
112
|
-
def get_messages(self, timeout:
|
|
125
|
+
def get_messages(self, timeout: float | None = None) -> Generator:
|
|
113
126
|
"""Wait :int:`timeout` seconds for a message from the server and convert it to a :class:`OscMessage`
|
|
114
127
|
|
|
115
128
|
Args:
|
|
116
|
-
timeout: Time in seconds to wait for a message
|
|
129
|
+
timeout: Time in seconds to wait for a message.
|
|
130
|
+
If None, uses the default timeout set in __init__.
|
|
117
131
|
"""
|
|
118
132
|
msg = self.receive(timeout)
|
|
119
133
|
while msg:
|
|
@@ -126,12 +140,13 @@ class DispatchClient(SimpleUDPClient):
|
|
|
126
140
|
|
|
127
141
|
dispatcher = Dispatcher()
|
|
128
142
|
|
|
129
|
-
def handle_messages(self, timeout:
|
|
143
|
+
def handle_messages(self, timeout: float | None = None) -> None:
|
|
130
144
|
"""Wait :int:`timeout` seconds for a message from the server and process each message with the registered
|
|
131
145
|
handlers. Continue until a timeout occurs.
|
|
132
146
|
|
|
133
147
|
Args:
|
|
134
|
-
timeout: Time in seconds to wait for a message
|
|
148
|
+
timeout: Time in seconds to wait for a message.
|
|
149
|
+
If None, uses the default timeout set in __init__.
|
|
135
150
|
"""
|
|
136
151
|
msg = self.receive(timeout)
|
|
137
152
|
while msg:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|