python-osc 1.8.3__tar.gz → 1.9.1__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 (44) hide show
  1. {python-osc-1.8.3/python_osc.egg-info → python_osc-1.9.1}/PKG-INFO +8 -5
  2. {python-osc-1.8.3 → python_osc-1.9.1}/README.rst +6 -3
  3. python_osc-1.9.1/pyproject.toml +58 -0
  4. {python-osc-1.8.3 → python_osc-1.9.1/python_osc.egg-info}/PKG-INFO +8 -5
  5. {python-osc-1.8.3 → python_osc-1.9.1}/python_osc.egg-info/SOURCES.txt +5 -2
  6. python_osc-1.9.1/pythonosc/dispatcher.py +337 -0
  7. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/osc_bundle.py +8 -5
  8. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/osc_bundle_builder.py +8 -7
  9. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/osc_message.py +11 -6
  10. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/osc_message_builder.py +46 -19
  11. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/osc_packet.py +20 -13
  12. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/osc_server.py +59 -21
  13. python_osc-1.9.1/pythonosc/osc_tcp_server.py +326 -0
  14. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/parsing/ntp.py +21 -23
  15. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/parsing/osc_types.py +81 -75
  16. python_osc-1.9.1/pythonosc/slip.py +105 -0
  17. python_osc-1.9.1/pythonosc/tcp_client.py +271 -0
  18. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/parsing/test_ntp.py +1 -1
  19. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/parsing/test_osc_types.py +98 -86
  20. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/test_dispatcher.py +78 -46
  21. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/test_osc_bundle.py +22 -20
  22. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/test_osc_bundle_builder.py +3 -3
  23. python_osc-1.9.1/pythonosc/test/test_osc_message.py +143 -0
  24. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/test_osc_message_builder.py +23 -11
  25. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/test_osc_packet.py +6 -6
  26. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/test_osc_server.py +58 -19
  27. python_osc-1.9.1/pythonosc/test/test_osc_tcp_server.py +353 -0
  28. python_osc-1.9.1/pythonosc/test/test_tcp_client.py +69 -0
  29. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/test_udp_client.py +12 -12
  30. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/udp_client.py +60 -10
  31. python_osc-1.9.1/setup.cfg +4 -0
  32. python-osc-1.8.3/pyproject.toml +0 -21
  33. python-osc-1.8.3/pythonosc/dispatcher.py +0 -206
  34. python-osc-1.8.3/pythonosc/py.typed +0 -0
  35. python-osc-1.8.3/pythonosc/test/test_osc_message.py +0 -152
  36. python-osc-1.8.3/setup.cfg +0 -18
  37. {python-osc-1.8.3 → python_osc-1.9.1}/LICENSE.txt +0 -0
  38. {python-osc-1.8.3 → python_osc-1.9.1}/MANIFEST.in +0 -0
  39. {python-osc-1.8.3 → python_osc-1.9.1}/python_osc.egg-info/dependency_links.txt +0 -0
  40. {python-osc-1.8.3 → python_osc-1.9.1}/python_osc.egg-info/top_level.txt +0 -0
  41. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/__init__.py +0 -0
  42. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/parsing/__init__.py +0 -0
  43. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/__init__.py +0 -0
  44. {python-osc-1.8.3 → python_osc-1.9.1}/pythonosc/test/parsing/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-osc
3
- Version: 1.8.3
3
+ Version: 1.9.1
4
4
  Summary: Open Sound Control server and client implementations in pure Python
5
5
  Author-email: attwad <tmusoft@gmail.com>
6
6
  License: This is free and unencumbered software released into the public domain.
@@ -36,7 +36,7 @@ Classifier: License :: Freely Distributable
36
36
  Classifier: Programming Language :: Python :: 3
37
37
  Classifier: Topic :: Multimedia :: Sound/Audio
38
38
  Classifier: Topic :: System :: Networking
39
- Requires-Python: >=3.7
39
+ Requires-Python: >=3.10
40
40
  Description-Content-Type: text/x-rst
41
41
  License-File: LICENSE.txt
42
42
 
@@ -60,10 +60,12 @@ and is currently in a stable state.
60
60
  Features
61
61
  ========
62
62
 
63
- * UDP blocking/threading/forking/asyncio server implementations
64
- * UDP client
63
+ * UDP and TCP blocking/threading/forking/asyncio server implementations
64
+ * UDP and TCP clients, including asyncio support
65
+ * TCP support for 1.0 and 1.1 protocol formats
65
66
  * int, int64, float, string, double, MIDI, timestamps, blob, nil OSC arguments
66
67
  * simple OSC address<->callback matching system
68
+ * support for sending responses from callback handlers in client and server
67
69
  * extensive unit test coverage
68
70
  * basic client and server examples
69
71
 
@@ -188,7 +190,8 @@ Building bundles
188
190
  # The bundle has 5 elements in total now.
189
191
 
190
192
  bundle = bundle.build()
191
- # You can now send it via a client as described in other examples.
193
+ # You can now send it via a client with the `.send()` method:
194
+ client.send(bundle)
192
195
 
193
196
  License?
194
197
  ========
@@ -18,10 +18,12 @@ and is currently in a stable state.
18
18
  Features
19
19
  ========
20
20
 
21
- * UDP blocking/threading/forking/asyncio server implementations
22
- * UDP client
21
+ * UDP and TCP blocking/threading/forking/asyncio server implementations
22
+ * UDP and TCP clients, including asyncio support
23
+ * TCP support for 1.0 and 1.1 protocol formats
23
24
  * int, int64, float, string, double, MIDI, timestamps, blob, nil OSC arguments
24
25
  * simple OSC address<->callback matching system
26
+ * support for sending responses from callback handlers in client and server
25
27
  * extensive unit test coverage
26
28
  * basic client and server examples
27
29
 
@@ -146,7 +148,8 @@ Building bundles
146
148
  # The bundle has 5 elements in total now.
147
149
 
148
150
  bundle = bundle.build()
149
- # You can now send it via a client as described in other examples.
151
+ # You can now send it via a client with the `.send()` method:
152
+ client.send(bundle)
150
153
 
151
154
  License?
152
155
  ========
@@ -0,0 +1,58 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "python-osc"
7
+ version = "1.9.1"
8
+ description = "Open Sound Control server and client implementations in pure Python"
9
+ readme = "README.rst"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE.txt" }
12
+ authors = [
13
+ { name = "attwad", email = "tmusoft@gmail.com" },
14
+ ]
15
+ keywords = ["osc", "sound", "midi", "music"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "License :: Freely Distributable",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Multimedia :: Sound/Audio",
22
+ "Topic :: System :: Networking",
23
+ ]
24
+
25
+ [project.urls]
26
+ Repository = "https://github.com/attwad/python-osc"
27
+
28
+ [tool.mypy]
29
+ # Would be great to turn this on, however there's too many cases it would break
30
+ # right now.
31
+ # disallow_any_generics = true
32
+
33
+ disallow_subclassing_any = true
34
+
35
+ # Allow functions _without_ type annotations, but require that annotations be
36
+ # complete (possibly including the `Any` type) where they are present.
37
+ disallow_incomplete_defs = true
38
+ # check_untyped_defs = true
39
+ disallow_untyped_decorators = true
40
+
41
+ # # Would be great to turn these on eventually
42
+ # no_implicit_optional = true
43
+ # strict_optional = true
44
+
45
+ warn_redundant_casts = true
46
+ warn_unused_ignores = true
47
+ show_error_codes = true
48
+ # # Would be great to turn this on eventually
49
+ # # warn_return_any = true
50
+ # warn_unreachable = true
51
+
52
+ # implicit_reexport = False
53
+ # strict_equality = true
54
+
55
+ scripts_are_modules = true
56
+ warn_unused_configs = true
57
+
58
+ enable_error_code = "ignore-without-code"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-osc
3
- Version: 1.8.3
3
+ Version: 1.9.1
4
4
  Summary: Open Sound Control server and client implementations in pure Python
5
5
  Author-email: attwad <tmusoft@gmail.com>
6
6
  License: This is free and unencumbered software released into the public domain.
@@ -36,7 +36,7 @@ Classifier: License :: Freely Distributable
36
36
  Classifier: Programming Language :: Python :: 3
37
37
  Classifier: Topic :: Multimedia :: Sound/Audio
38
38
  Classifier: Topic :: System :: Networking
39
- Requires-Python: >=3.7
39
+ Requires-Python: >=3.10
40
40
  Description-Content-Type: text/x-rst
41
41
  License-File: LICENSE.txt
42
42
 
@@ -60,10 +60,12 @@ and is currently in a stable state.
60
60
  Features
61
61
  ========
62
62
 
63
- * UDP blocking/threading/forking/asyncio server implementations
64
- * UDP client
63
+ * UDP and TCP blocking/threading/forking/asyncio server implementations
64
+ * UDP and TCP clients, including asyncio support
65
+ * TCP support for 1.0 and 1.1 protocol formats
65
66
  * int, int64, float, string, double, MIDI, timestamps, blob, nil OSC arguments
66
67
  * simple OSC address<->callback matching system
68
+ * support for sending responses from callback handlers in client and server
67
69
  * extensive unit test coverage
68
70
  * basic client and server examples
69
71
 
@@ -188,7 +190,8 @@ Building bundles
188
190
  # The bundle has 5 elements in total now.
189
191
 
190
192
  bundle = bundle.build()
191
- # You can now send it via a client as described in other examples.
193
+ # You can now send it via a client with the `.send()` method:
194
+ client.send(bundle)
192
195
 
193
196
  License?
194
197
  ========
@@ -2,7 +2,6 @@ LICENSE.txt
2
2
  MANIFEST.in
3
3
  README.rst
4
4
  pyproject.toml
5
- setup.cfg
6
5
  python_osc.egg-info/PKG-INFO
7
6
  python_osc.egg-info/SOURCES.txt
8
7
  python_osc.egg-info/dependency_links.txt
@@ -15,7 +14,9 @@ pythonosc/osc_message.py
15
14
  pythonosc/osc_message_builder.py
16
15
  pythonosc/osc_packet.py
17
16
  pythonosc/osc_server.py
18
- pythonosc/py.typed
17
+ pythonosc/osc_tcp_server.py
18
+ pythonosc/slip.py
19
+ pythonosc/tcp_client.py
19
20
  pythonosc/udp_client.py
20
21
  pythonosc/parsing/__init__.py
21
22
  pythonosc/parsing/ntp.py
@@ -28,6 +29,8 @@ pythonosc/test/test_osc_message.py
28
29
  pythonosc/test/test_osc_message_builder.py
29
30
  pythonosc/test/test_osc_packet.py
30
31
  pythonosc/test/test_osc_server.py
32
+ pythonosc/test/test_osc_tcp_server.py
33
+ pythonosc/test/test_tcp_client.py
31
34
  pythonosc/test/test_udp_client.py
32
35
  pythonosc/test/parsing/__init__.py
33
36
  pythonosc/test/parsing/test_ntp.py
@@ -0,0 +1,337 @@
1
+ """Maps OSC addresses to handler functions
2
+ """
3
+
4
+ import collections
5
+ import inspect
6
+ import logging
7
+ import re
8
+ import time
9
+ from pythonosc import osc_packet
10
+ from typing import (
11
+ overload,
12
+ List,
13
+ Union,
14
+ Any,
15
+ AnyStr,
16
+ Generator,
17
+ Tuple,
18
+ Callable,
19
+ Optional,
20
+ DefaultDict,
21
+ )
22
+ from pythonosc.osc_message import OscMessage
23
+ from pythonosc.osc_message_builder import ArgValue
24
+
25
+
26
+ class Handler(object):
27
+ """Wrapper for a callback function that will be called when an OSC message is sent to the right address.
28
+
29
+ Represents a handler callback function that will be called whenever an OSC message is sent to the address this
30
+ handler is mapped to. It passes the address, the fixed arguments (if any) as well as all osc arguments from the
31
+ message if any were passed.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ _callback: Callable,
37
+ _args: Union[Any, List[Any]],
38
+ _needs_reply_address: bool = False,
39
+ ) -> None:
40
+ """
41
+ Args:
42
+ _callback Function that is called when handler is invoked
43
+ _args: Message causing invocation
44
+ _needs_reply_address Whether the client's ip address shall be passed as an argument or not
45
+ """
46
+ self.callback = _callback
47
+ self.args = _args
48
+ self.needs_reply_address = _needs_reply_address
49
+
50
+ # needed for test module
51
+ def __eq__(self, other: Any) -> bool:
52
+ return (
53
+ isinstance(self, type(other))
54
+ and self.callback == other.callback
55
+ and self.args == other.args
56
+ and self.needs_reply_address == other.needs_reply_address
57
+ )
58
+
59
+ def invoke(
60
+ self, client_address: Tuple[str, int], message: OscMessage
61
+ ) -> Union[None, AnyStr, Tuple[AnyStr, ArgValue]]:
62
+ """Invokes the associated callback function
63
+
64
+ Args:
65
+ client_address: Address match that causes the invocation
66
+ message: Message causing invocation
67
+ Returns:
68
+ The result of the handler function can be None, a string OSC address, or a tuple of the OSC address
69
+ and arguments.
70
+ """
71
+ if self.needs_reply_address:
72
+ if self.args:
73
+ return self.callback(
74
+ client_address, message.address, self.args, *message
75
+ )
76
+ else:
77
+ return self.callback(client_address, message.address, *message)
78
+ else:
79
+ if self.args:
80
+ return self.callback(message.address, self.args, *message)
81
+ else:
82
+ return self.callback(message.address, *message)
83
+
84
+
85
+ class Dispatcher(object):
86
+ """Maps Handlers to OSC addresses and dispatches messages to the handler on matched addresses
87
+
88
+ Maps OSC addresses to handler functions and invokes the correct handler when a message comes in.
89
+ """
90
+
91
+ def __init__(self) -> None:
92
+ self._map: DefaultDict[str, List[Handler]] = collections.defaultdict(list)
93
+ self._default_handler: Optional[Handler] = None
94
+
95
+ def map(
96
+ self,
97
+ address: str,
98
+ handler: Callable,
99
+ *args: Union[Any, List[Any]],
100
+ needs_reply_address: bool = False,
101
+ ) -> Handler:
102
+ """Map an address to a handler
103
+
104
+ The callback function must have one of the following signatures:
105
+
106
+ ``def some_cb(address: str, *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
107
+ ``def some_cb(address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr,
108
+ Tuple(str, ArgValue)]:``
109
+
110
+ ``def some_cb(client_address: Tuple[str, int], address: str, *osc_args: List[Any]) -> Union[None, AnyStr,
111
+ Tuple(str, ArgValue)]:``
112
+ ``def some_cb(client_address: Tuple[str, int], address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
113
+
114
+ The callback function can return None, or a string representing an OSC address to be returned to the client,
115
+ or a tuple that includes the address and ArgValue which will be converted to an OSC message and returned to
116
+ the client.
117
+
118
+ Args:
119
+ address: Address to be mapped
120
+ handler: Callback function that will be called as the handler for the given address
121
+ *args: Fixed arguements that will be passed to the callback function
122
+ needs_reply_address: Whether the IP address from which the message originated from shall be passed as
123
+ an argument to the handler callback
124
+
125
+ Returns:
126
+ The handler object that will be invoked should the given address match
127
+
128
+ """
129
+ # TODO: Check the spec:
130
+ # http://opensoundcontrol.org/spec-1_0
131
+ # regarding multiple mappings
132
+ handlerobj = Handler(handler, list(args), needs_reply_address)
133
+ self._map[address].append(handlerobj)
134
+ return handlerobj
135
+
136
+ @overload
137
+ def unmap(self, address: str, handler: Handler) -> None:
138
+ """Remove an already mapped handler from an address
139
+
140
+ Args:
141
+ address (str): Address to be unmapped
142
+ handler (Handler): A Handler object as returned from map().
143
+ """
144
+ pass
145
+
146
+ @overload
147
+ def unmap(
148
+ self,
149
+ address: str,
150
+ handler: Callable,
151
+ *args: Union[Any, List[Any]],
152
+ needs_reply_address: bool = False,
153
+ ) -> None:
154
+ """Remove an already mapped handler from an address
155
+
156
+ Args:
157
+ address: Address to be unmapped
158
+ handler: A function that will be run when the address matches with
159
+ the OscMessage passed as parameter.
160
+ args: Any additional arguments that will be always passed to the
161
+ handlers after the osc messages arguments if any.
162
+ needs_reply_address: True if the handler function needs the
163
+ originating client address passed (as the first argument).
164
+ """
165
+ pass
166
+
167
+ def unmap(self, address, handler, *args, needs_reply_address=False):
168
+ try:
169
+ if isinstance(handler, Handler):
170
+ self._map[address].remove(handler)
171
+ else:
172
+ self._map[address].remove(
173
+ Handler(handler, list(args), needs_reply_address)
174
+ )
175
+ except ValueError as e:
176
+ if str(e) == "list.remove(x): x not in list":
177
+ raise ValueError(
178
+ f"Address '{address}' doesn't have handler '{handler}' mapped to it"
179
+ ) from e
180
+
181
+ def handlers_for_address(
182
+ self, address_pattern: str
183
+ ) -> Generator[Handler, None, None]:
184
+ """Yields handlers matching an address
185
+
186
+
187
+ Args:
188
+ address_pattern: Address to match
189
+
190
+ Returns:
191
+ Generator yielding Handlers matching address_pattern
192
+ """
193
+ # First convert the address_pattern into a matchable regexp.
194
+ # '?' in the OSC Address Pattern matches any single character.
195
+ # Let's consider numbers and _ "characters" too here, it's not said
196
+ # explicitly in the specification but it sounds good.
197
+ escaped_address_pattern = re.escape(address_pattern)
198
+ pattern = escaped_address_pattern.replace("\\?", "\\w?")
199
+ # '*' in the OSC Address Pattern matches any sequence of zero or more
200
+ # characters.
201
+ pattern = pattern.replace("\\*", "[\\w|\\+]*")
202
+ # The rest of the syntax in the specification is like the re module so
203
+ # we're fine.
204
+ pattern = f"{pattern}$"
205
+ patterncompiled = re.compile(pattern)
206
+ matched = False
207
+
208
+ for addr, handlers in self._map.items():
209
+ if patterncompiled.match(addr) or (
210
+ ("*" in addr)
211
+ and re.match(addr.replace("*", "[^/]*?/*"), address_pattern)
212
+ ):
213
+ yield from handlers
214
+ matched = True
215
+
216
+ if not matched and self._default_handler:
217
+ logging.debug("No handler matched but default handler present, added it.")
218
+ yield self._default_handler
219
+
220
+ def call_handlers_for_packet(
221
+ self, data: bytes, client_address: Tuple[str, int]
222
+ ) -> List:
223
+ """Invoke handlers for all messages in OSC packet
224
+
225
+ The incoming OSC Packet is decoded and the handlers for each included message is found and invoked.
226
+
227
+ Args:
228
+ data: Data of packet
229
+ client_address: Address of client this packet originated from
230
+ Returns: A list of strings or tuples to be converted to OSC messages and returned to the client
231
+ """
232
+ results = list()
233
+ # Get OSC messages from all bundles or standalone message.
234
+ try:
235
+ packet = osc_packet.OscPacket(data)
236
+ for timed_msg in packet.messages:
237
+ now = time.time()
238
+ handlers = self.handlers_for_address(timed_msg.message.address)
239
+ if not handlers:
240
+ continue
241
+ # If the message is to be handled later, then so be it.
242
+ if timed_msg.time > now:
243
+ time.sleep(timed_msg.time - now)
244
+ for handler in handlers:
245
+ result = handler.invoke(client_address, timed_msg.message)
246
+ if result is not None:
247
+ results.append(result)
248
+ except osc_packet.ParseError:
249
+ pass
250
+ return results
251
+
252
+ async def async_call_handlers_for_packet(
253
+ self, data: bytes, client_address: Tuple[str, int]
254
+ ) -> List:
255
+ """
256
+ This function calls the handlers registered to the dispatcher for
257
+ every message it found in the packet.
258
+ The process/thread granularity is thus the OSC packet, not the handler.
259
+
260
+ If parameters were registered with the dispatcher, then the handlers are
261
+ called this way:
262
+ handler('/address that triggered the message',
263
+ registered_param_list, osc_msg_arg1, osc_msg_arg2, ...)
264
+ if no parameters were registered, then it is just called like this:
265
+ handler('/address that triggered the message',
266
+ osc_msg_arg1, osc_msg_arg2, osc_msg_param3, ...)
267
+ """
268
+
269
+ # Get OSC messages from all bundles or standalone message.
270
+ results = []
271
+ try:
272
+ packet = osc_packet.OscPacket(data)
273
+ for timed_msg in packet.messages:
274
+ now = time.time()
275
+ handlers = self.handlers_for_address(timed_msg.message.address)
276
+ if not handlers:
277
+ continue
278
+ # If the message is to be handled later, then so be it.
279
+ if timed_msg.time > now:
280
+ time.sleep(timed_msg.time - now)
281
+ for handler in handlers:
282
+ if inspect.iscoroutinefunction(handler.callback):
283
+ if handler.needs_reply_address:
284
+ result = await handler.callback(
285
+ client_address,
286
+ timed_msg.message.address,
287
+ handler.args,
288
+ *timed_msg.message,
289
+ )
290
+ elif handler.args:
291
+ result = await handler.callback(
292
+ timed_msg.message.address,
293
+ handler.args,
294
+ *timed_msg.message,
295
+ )
296
+ else:
297
+ result = await handler.callback(
298
+ timed_msg.message.address, *timed_msg.message
299
+ )
300
+ else:
301
+ if handler.needs_reply_address:
302
+ result = handler.callback(
303
+ client_address,
304
+ timed_msg.message.address,
305
+ handler.args,
306
+ *timed_msg.message,
307
+ )
308
+ elif handler.args:
309
+ result = handler.callback(
310
+ timed_msg.message.address,
311
+ handler.args,
312
+ *timed_msg.message,
313
+ )
314
+ else:
315
+ result = handler.callback(
316
+ timed_msg.message.address, *timed_msg.message
317
+ )
318
+ if result:
319
+ results.append(result)
320
+ except osc_packet.ParseError as e:
321
+ pass
322
+ return results
323
+
324
+ def set_default_handler(
325
+ self, handler: Callable, needs_reply_address: bool = False
326
+ ) -> None:
327
+ """Sets the default handler
328
+
329
+ The default handler is invoked every time no other handler is mapped to an address.
330
+
331
+ Args:
332
+ handler: Callback function to handle unmapped requests
333
+ needs_reply_address: Whether the callback shall be passed the client address
334
+ """
335
+ self._default_handler = (
336
+ None if (handler is None) else Handler(handler, [], needs_reply_address)
337
+ )
@@ -33,11 +33,13 @@ class OscBundle(object):
33
33
  try:
34
34
  self._timestamp, index = osc_types.get_date(self._dgram, index)
35
35
  except osc_types.ParseError as pe:
36
- raise ParseError("Could not get the date from the datagram: %s" % pe)
36
+ raise ParseError(f"Could not get the date from the datagram: {pe}")
37
37
  # Get the contents as a list of OscBundle and OscMessage.
38
38
  self._contents = self._parse_contents(index)
39
39
 
40
- def _parse_contents(self, index: int) -> List[Union['OscBundle', osc_message.OscMessage]]:
40
+ def _parse_contents(
41
+ self, index: int
42
+ ) -> List[Union["OscBundle", osc_message.OscMessage]]:
41
43
  contents = [] # type: List[Union[OscBundle, osc_message.OscMessage]]
42
44
 
43
45
  try:
@@ -49,7 +51,7 @@ class OscBundle(object):
49
51
  # Get the sub content size.
50
52
  content_size, index = osc_types.get_int(self._dgram, index)
51
53
  # Get the datagram for the sub content.
52
- content_dgram = self._dgram[index:index + content_size]
54
+ content_dgram = self._dgram[index : index + content_size]
53
55
  # Increment our position index up to the next possible content.
54
56
  index += content_size
55
57
  # Parse the content into an OSC message or bundle.
@@ -59,9 +61,10 @@ class OscBundle(object):
59
61
  contents.append(osc_message.OscMessage(content_dgram))
60
62
  else:
61
63
  logging.warning(
62
- "Could not identify content type of dgram %r" % content_dgram)
64
+ f"Could not identify content type of dgram {content_dgram!r}"
65
+ )
63
66
  except (osc_types.ParseError, osc_message.ParseError, IndexError) as e:
64
- raise ParseError("Could not parse a content datagram: %s" % e)
67
+ raise ParseError(f"Could not parse a content datagram: {e}")
65
68
 
66
69
  return contents
67
70
 
@@ -25,7 +25,7 @@ class OscBundleBuilder(object):
25
25
  seconds since the epoch in UTC or IMMEDIATELY.
26
26
  """
27
27
  self._timestamp = timestamp
28
- self._contents = [] # type: List[osc_bundle.OscBundle]
28
+ self._contents: List[osc_bundle.OscBundle] = []
29
29
 
30
30
  def add_content(self, content: osc_bundle.OscBundle) -> None:
31
31
  """Add a new content to this bundle.
@@ -41,19 +41,20 @@ class OscBundleBuilder(object):
41
41
  Raises:
42
42
  - BuildError: if we could not build the bundle.
43
43
  """
44
- dgram = b'#bundle\x00'
44
+ dgram = b"#bundle\x00"
45
45
  try:
46
46
  dgram += osc_types.write_date(self._timestamp)
47
47
  for content in self._contents:
48
- if (type(content) == osc_message.OscMessage
49
- or type(content) == osc_bundle.OscBundle):
48
+ if isinstance(content, osc_message.OscMessage) or isinstance(
49
+ content, osc_bundle.OscBundle
50
+ ):
50
51
  size = content.size
51
52
  dgram += osc_types.write_int(size)
52
53
  dgram += content.dgram
53
54
  else:
54
55
  raise BuildError(
55
- "Content must be either OscBundle or OscMessage"
56
- "found {}".format(type(content)))
56
+ f"Content must be either OscBundle or OscMessage, found {type(content)}"
57
+ )
57
58
  return osc_bundle.OscBundle(dgram)
58
59
  except osc_types.BuildError as be:
59
- raise BuildError('Could not build the bundle {}'.format(be))
60
+ raise BuildError(f"Could not build the bundle {be}")
@@ -22,6 +22,9 @@ class OscMessage(object):
22
22
  self._parameters = [] # type: List[Any]
23
23
  self._parse_datagram()
24
24
 
25
+ def __str__(self):
26
+ return f"{self.address} {' '.join(str(p) for p in self.params)}"
27
+
25
28
  def _parse_datagram(self) -> None:
26
29
  try:
27
30
  self._address_regexp, index = osc_types.get_string(self._dgram, 0)
@@ -31,7 +34,7 @@ class OscMessage(object):
31
34
 
32
35
  # Get the parameters types.
33
36
  type_tag, index = osc_types.get_string(self._dgram, index)
34
- if type_tag.startswith(','):
37
+ if type_tag.startswith(","):
35
38
  type_tag = type_tag[1:]
36
39
 
37
40
  params = [] # type: List[Any]
@@ -69,19 +72,21 @@ class OscMessage(object):
69
72
  param_stack.append(array)
70
73
  elif param == "]": # Array stop.
71
74
  if len(param_stack) < 2:
72
- raise ParseError('Unexpected closing bracket in type tag: {0}'.format(type_tag))
75
+ raise ParseError(
76
+ f"Unexpected closing bracket in type tag: {type_tag}"
77
+ )
73
78
  param_stack.pop()
74
79
  # TODO: Support more exotic types as described in the specification.
75
80
  else:
76
- logging.warning('Unhandled parameter type: {0}'.format(param))
81
+ logging.warning(f"Unhandled parameter type: {param}")
77
82
  continue
78
83
  if param not in "[]":
79
84
  param_stack[-1].append(val)
80
85
  if len(param_stack) != 1:
81
- raise ParseError('Missing closing bracket in type tag: {0}'.format(type_tag))
86
+ raise ParseError(f"Missing closing bracket in type tag: {type_tag}")
82
87
  self._parameters = params
83
88
  except osc_types.ParseError as pe:
84
- raise ParseError('Found incorrect datagram, ignoring it', pe)
89
+ raise ParseError("Found incorrect datagram, ignoring it", pe)
85
90
 
86
91
  @property
87
92
  def address(self) -> str:
@@ -91,7 +96,7 @@ class OscMessage(object):
91
96
  @staticmethod
92
97
  def dgram_is_message(dgram: bytes) -> bool:
93
98
  """Returns whether this datagram starts as an OSC message."""
94
- return dgram.startswith(b'/')
99
+ return dgram.startswith(b"/")
95
100
 
96
101
  @property
97
102
  def size(self) -> int: