python-osc 1.8.3__tar.gz → 1.9.0__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 (42) hide show
  1. {python-osc-1.8.3/python_osc.egg-info → python_osc-1.9.0}/PKG-INFO +2 -2
  2. {python-osc-1.8.3 → python_osc-1.9.0}/pyproject.toml +2 -2
  3. {python-osc-1.8.3 → python_osc-1.9.0/python_osc.egg-info}/PKG-INFO +2 -2
  4. {python-osc-1.8.3 → python_osc-1.9.0}/python_osc.egg-info/SOURCES.txt +5 -1
  5. python_osc-1.9.0/pythonosc/dispatcher.py +338 -0
  6. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/osc_bundle.py +6 -3
  7. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/osc_bundle_builder.py +8 -6
  8. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/osc_message.py +15 -6
  9. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/osc_message_builder.py +50 -19
  10. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/osc_packet.py +20 -13
  11. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/osc_server.py +59 -21
  12. python_osc-1.9.0/pythonosc/osc_tcp_server.py +326 -0
  13. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/parsing/ntp.py +21 -23
  14. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/parsing/osc_types.py +81 -75
  15. python_osc-1.9.0/pythonosc/slip.py +105 -0
  16. python_osc-1.9.0/pythonosc/tcp_client.py +271 -0
  17. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/parsing/test_ntp.py +1 -1
  18. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/parsing/test_osc_types.py +98 -86
  19. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/test_dispatcher.py +78 -46
  20. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/test_osc_bundle.py +22 -20
  21. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/test_osc_bundle_builder.py +3 -3
  22. python_osc-1.9.0/pythonosc/test/test_osc_message.py +143 -0
  23. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/test_osc_message_builder.py +23 -11
  24. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/test_osc_packet.py +6 -6
  25. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/test_osc_server.py +58 -19
  26. python_osc-1.9.0/pythonosc/test/test_osc_tcp_server.py +353 -0
  27. python_osc-1.9.0/pythonosc/test/test_tcp_client.py +69 -0
  28. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/test_udp_client.py +12 -12
  29. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/udp_client.py +60 -10
  30. python-osc-1.8.3/pythonosc/dispatcher.py +0 -206
  31. python-osc-1.8.3/pythonosc/py.typed +0 -0
  32. python-osc-1.8.3/pythonosc/test/test_osc_message.py +0 -152
  33. {python-osc-1.8.3 → python_osc-1.9.0}/LICENSE.txt +0 -0
  34. {python-osc-1.8.3 → python_osc-1.9.0}/MANIFEST.in +0 -0
  35. {python-osc-1.8.3 → python_osc-1.9.0}/README.rst +0 -0
  36. {python-osc-1.8.3 → python_osc-1.9.0}/python_osc.egg-info/dependency_links.txt +0 -0
  37. {python-osc-1.8.3 → python_osc-1.9.0}/python_osc.egg-info/top_level.txt +0 -0
  38. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/__init__.py +0 -0
  39. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/parsing/__init__.py +0 -0
  40. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/__init__.py +0 -0
  41. {python-osc-1.8.3 → python_osc-1.9.0}/pythonosc/test/parsing/__init__.py +0 -0
  42. {python-osc-1.8.3 → python_osc-1.9.0}/setup.cfg +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.0
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
 
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "python-osc"
3
- version="1.8.3"
3
+ version="1.9.0"
4
4
  description="Open Sound Control server and client implementations in pure Python"
5
5
  readme="README.rst"
6
- requires-python=">=3.7"
6
+ requires-python=">=3.10"
7
7
  license = {file = "LICENSE.txt"}
8
8
  authors = [
9
9
  {name = "attwad", email = "tmusoft@gmail.com"},
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-osc
3
- Version: 1.8.3
3
+ Version: 1.9.0
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
 
@@ -15,7 +15,9 @@ pythonosc/osc_message.py
15
15
  pythonosc/osc_message_builder.py
16
16
  pythonosc/osc_packet.py
17
17
  pythonosc/osc_server.py
18
- pythonosc/py.typed
18
+ pythonosc/osc_tcp_server.py
19
+ pythonosc/slip.py
20
+ pythonosc/tcp_client.py
19
21
  pythonosc/udp_client.py
20
22
  pythonosc/parsing/__init__.py
21
23
  pythonosc/parsing/ntp.py
@@ -28,6 +30,8 @@ pythonosc/test/test_osc_message.py
28
30
  pythonosc/test/test_osc_message_builder.py
29
31
  pythonosc/test/test_osc_packet.py
30
32
  pythonosc/test/test_osc_server.py
33
+ pythonosc/test/test_osc_tcp_server.py
34
+ pythonosc/test/test_tcp_client.py
31
35
  pythonosc/test/test_udp_client.py
32
36
  pythonosc/test/parsing/__init__.py
33
37
  pythonosc/test/parsing/test_ntp.py
@@ -0,0 +1,338 @@
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
+ "Address '%s' doesn't have handler '%s' mapped to it"
179
+ % (address, handler)
180
+ ) from e
181
+
182
+ def handlers_for_address(
183
+ self, address_pattern: str
184
+ ) -> Generator[Handler, None, None]:
185
+ """Yields handlers matching an address
186
+
187
+
188
+ Args:
189
+ address_pattern: Address to match
190
+
191
+ Returns:
192
+ Generator yielding Handlers matching address_pattern
193
+ """
194
+ # First convert the address_pattern into a matchable regexp.
195
+ # '?' in the OSC Address Pattern matches any single character.
196
+ # Let's consider numbers and _ "characters" too here, it's not said
197
+ # explicitly in the specification but it sounds good.
198
+ escaped_address_pattern = re.escape(address_pattern)
199
+ pattern = escaped_address_pattern.replace("\\?", "\\w?")
200
+ # '*' in the OSC Address Pattern matches any sequence of zero or more
201
+ # characters.
202
+ pattern = pattern.replace("\\*", "[\\w|\\+]*")
203
+ # The rest of the syntax in the specification is like the re module so
204
+ # we're fine.
205
+ pattern = pattern + "$"
206
+ patterncompiled = re.compile(pattern)
207
+ matched = False
208
+
209
+ for addr, handlers in self._map.items():
210
+ if patterncompiled.match(addr) or (
211
+ ("*" in addr)
212
+ and re.match(addr.replace("*", "[^/]*?/*"), address_pattern)
213
+ ):
214
+ yield from handlers
215
+ matched = True
216
+
217
+ if not matched and self._default_handler:
218
+ logging.debug("No handler matched but default handler present, added it.")
219
+ yield self._default_handler
220
+
221
+ def call_handlers_for_packet(
222
+ self, data: bytes, client_address: Tuple[str, int]
223
+ ) -> List:
224
+ """Invoke handlers for all messages in OSC packet
225
+
226
+ The incoming OSC Packet is decoded and the handlers for each included message is found and invoked.
227
+
228
+ Args:
229
+ data: Data of packet
230
+ client_address: Address of client this packet originated from
231
+ Returns: A list of strings or tuples to be converted to OSC messages and returned to the client
232
+ """
233
+ results = list()
234
+ # Get OSC messages from all bundles or standalone message.
235
+ try:
236
+ packet = osc_packet.OscPacket(data)
237
+ for timed_msg in packet.messages:
238
+ now = time.time()
239
+ handlers = self.handlers_for_address(timed_msg.message.address)
240
+ if not handlers:
241
+ continue
242
+ # If the message is to be handled later, then so be it.
243
+ if timed_msg.time > now:
244
+ time.sleep(timed_msg.time - now)
245
+ for handler in handlers:
246
+ result = handler.invoke(client_address, timed_msg.message)
247
+ if result is not None:
248
+ results.append(result)
249
+ except osc_packet.ParseError:
250
+ pass
251
+ return results
252
+
253
+ async def async_call_handlers_for_packet(
254
+ self, data: bytes, client_address: Tuple[str, int]
255
+ ) -> List:
256
+ """
257
+ This function calls the handlers registered to the dispatcher for
258
+ every message it found in the packet.
259
+ The process/thread granularity is thus the OSC packet, not the handler.
260
+
261
+ If parameters were registered with the dispatcher, then the handlers are
262
+ called this way:
263
+ handler('/address that triggered the message',
264
+ registered_param_list, osc_msg_arg1, osc_msg_arg2, ...)
265
+ if no parameters were registered, then it is just called like this:
266
+ handler('/address that triggered the message',
267
+ osc_msg_arg1, osc_msg_arg2, osc_msg_param3, ...)
268
+ """
269
+
270
+ # Get OSC messages from all bundles or standalone message.
271
+ results = []
272
+ try:
273
+ packet = osc_packet.OscPacket(data)
274
+ for timed_msg in packet.messages:
275
+ now = time.time()
276
+ handlers = self.handlers_for_address(timed_msg.message.address)
277
+ if not handlers:
278
+ continue
279
+ # If the message is to be handled later, then so be it.
280
+ if timed_msg.time > now:
281
+ time.sleep(timed_msg.time - now)
282
+ for handler in handlers:
283
+ if inspect.iscoroutinefunction(handler.callback):
284
+ if handler.needs_reply_address:
285
+ result = await handler.callback(
286
+ client_address,
287
+ timed_msg.message.address,
288
+ handler.args,
289
+ *timed_msg.message,
290
+ )
291
+ elif handler.args:
292
+ result = await handler.callback(
293
+ timed_msg.message.address,
294
+ handler.args,
295
+ *timed_msg.message,
296
+ )
297
+ else:
298
+ result = await handler.callback(
299
+ timed_msg.message.address, *timed_msg.message
300
+ )
301
+ else:
302
+ if handler.needs_reply_address:
303
+ result = handler.callback(
304
+ client_address,
305
+ timed_msg.message.address,
306
+ handler.args,
307
+ *timed_msg.message,
308
+ )
309
+ elif handler.args:
310
+ result = handler.callback(
311
+ timed_msg.message.address,
312
+ handler.args,
313
+ *timed_msg.message,
314
+ )
315
+ else:
316
+ result = handler.callback(
317
+ timed_msg.message.address, *timed_msg.message
318
+ )
319
+ if result:
320
+ results.append(result)
321
+ except osc_packet.ParseError as e:
322
+ pass
323
+ return results
324
+
325
+ def set_default_handler(
326
+ self, handler: Callable, needs_reply_address: bool = False
327
+ ) -> None:
328
+ """Sets the default handler
329
+
330
+ The default handler is invoked every time no other handler is mapped to an address.
331
+
332
+ Args:
333
+ handler: Callback function to handle unmapped requests
334
+ needs_reply_address: Whether the callback shall be passed the client address
335
+ """
336
+ self._default_handler = (
337
+ None if (handler is None) else Handler(handler, [], needs_reply_address)
338
+ )
@@ -37,7 +37,9 @@ class OscBundle(object):
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,7 +61,8 @@ 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
+ "Could not identify content type of dgram %r" % content_dgram
65
+ )
63
66
  except (osc_types.ParseError, osc_message.ParseError, IndexError) as e:
64
67
  raise ParseError("Could not parse a content datagram: %s" % e)
65
68
 
@@ -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,21 @@ 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
56
  "Content must be either OscBundle or OscMessage"
56
- "found {}".format(type(content)))
57
+ "found {}".format(type(content))
58
+ )
57
59
  return osc_bundle.OscBundle(dgram)
58
60
  except osc_types.BuildError as be:
59
- raise BuildError('Could not build the bundle {}'.format(be))
61
+ raise BuildError("Could not build the bundle {}".format(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,25 @@ 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
+ "Unexpected closing bracket in type tag: {0}".format(
77
+ type_tag
78
+ )
79
+ )
73
80
  param_stack.pop()
74
81
  # TODO: Support more exotic types as described in the specification.
75
82
  else:
76
- logging.warning('Unhandled parameter type: {0}'.format(param))
83
+ logging.warning("Unhandled parameter type: {0}".format(param))
77
84
  continue
78
85
  if param not in "[]":
79
86
  param_stack[-1].append(val)
80
87
  if len(param_stack) != 1:
81
- raise ParseError('Missing closing bracket in type tag: {0}'.format(type_tag))
88
+ raise ParseError(
89
+ "Missing closing bracket in type tag: {0}".format(type_tag)
90
+ )
82
91
  self._parameters = params
83
92
  except osc_types.ParseError as pe:
84
- raise ParseError('Found incorrect datagram, ignoring it', pe)
93
+ raise ParseError("Found incorrect datagram, ignoring it", pe)
85
94
 
86
95
  @property
87
96
  def address(self) -> str:
@@ -91,7 +100,7 @@ class OscMessage(object):
91
100
  @staticmethod
92
101
  def dgram_is_message(dgram: bytes) -> bool:
93
102
  """Returns whether this datagram starts as an OSC message."""
94
- return dgram.startswith(b'/')
103
+ return dgram.startswith(b"/")
95
104
 
96
105
  @property
97
106
  def size(self) -> int:
@@ -1,15 +1,17 @@
1
1
  """Build OSC messages for client applications."""
2
2
 
3
+ from typing import Any, Iterable, List, Optional, Tuple, Union
4
+
3
5
  from pythonosc import osc_message
4
6
  from pythonosc.parsing import osc_types
5
7
 
6
- from typing import List, Tuple, Union, Any, Optional
7
-
8
8
  ArgValue = Union[str, bytes, bool, int, float, osc_types.MidiPacket, list]
9
9
 
10
+
10
11
  class BuildError(Exception):
11
12
  """Error raised when an incomplete message is trying to be built."""
12
13
 
14
+
13
15
  class OscMessageBuilder(object):
14
16
  """Builds arbitrary OscMessage instances."""
15
17
 
@@ -29,8 +31,18 @@ class OscMessageBuilder(object):
29
31
  ARG_TYPE_ARRAY_STOP = "]"
30
32
 
31
33
  _SUPPORTED_ARG_TYPES = (
32
- ARG_TYPE_FLOAT, ARG_TYPE_DOUBLE, ARG_TYPE_INT, ARG_TYPE_INT64, ARG_TYPE_BLOB, ARG_TYPE_STRING,
33
- ARG_TYPE_RGBA, ARG_TYPE_MIDI, ARG_TYPE_TRUE, ARG_TYPE_FALSE, ARG_TYPE_NIL)
34
+ ARG_TYPE_FLOAT,
35
+ ARG_TYPE_DOUBLE,
36
+ ARG_TYPE_INT,
37
+ ARG_TYPE_INT64,
38
+ ARG_TYPE_BLOB,
39
+ ARG_TYPE_STRING,
40
+ ARG_TYPE_RGBA,
41
+ ARG_TYPE_MIDI,
42
+ ARG_TYPE_TRUE,
43
+ ARG_TYPE_FALSE,
44
+ ARG_TYPE_NIL,
45
+ )
34
46
 
35
47
  def __init__(self, address: Optional[str] = None) -> None:
36
48
  """Initialize a new builder for a message.
@@ -78,8 +90,10 @@ class OscMessageBuilder(object):
78
90
  """
79
91
  if arg_type and not self._valid_type(arg_type):
80
92
  raise ValueError(
81
- 'arg_type must be one of {}, or an array of valid types'
82
- .format(self._SUPPORTED_ARG_TYPES))
93
+ "arg_type must be one of {}, or an array of valid types".format(
94
+ self._SUPPORTED_ARG_TYPES
95
+ )
96
+ )
83
97
  if not arg_type:
84
98
  arg_type = self._get_arg_type(arg_value)
85
99
  if isinstance(arg_type, list):
@@ -122,7 +136,7 @@ class OscMessageBuilder(object):
122
136
  elif arg_value is None:
123
137
  arg_type = self.ARG_TYPE_NIL
124
138
  else:
125
- raise ValueError('Infered arg_value type is not supported')
139
+ raise ValueError("Infered arg_value type is not supported")
126
140
  return arg_type
127
141
 
128
142
  def build(self) -> osc_message.OscMessage:
@@ -136,18 +150,18 @@ class OscMessageBuilder(object):
136
150
  - an osc_message.OscMessage instance.
137
151
  """
138
152
  if not self._address:
139
- raise BuildError('OSC addresses cannot be empty')
140
- dgram = b''
153
+ raise BuildError("OSC addresses cannot be empty")
154
+ dgram = b""
141
155
  try:
142
156
  # Write the address.
143
157
  dgram += osc_types.write_string(self._address)
144
158
  if not self._args:
145
- dgram += osc_types.write_string(',')
159
+ dgram += osc_types.write_string(",")
146
160
  return osc_message.OscMessage(dgram)
147
161
 
148
162
  # Write the parameters.
149
163
  arg_types = "".join([arg[0] for arg in self._args])
150
- dgram += osc_types.write_string(',' + arg_types)
164
+ dgram += osc_types.write_string("," + arg_types)
151
165
  for arg_type, value in self._args:
152
166
  if arg_type == self.ARG_TYPE_STRING:
153
167
  dgram += osc_types.write_string(value) # type: ignore[arg-type]
@@ -165,16 +179,33 @@ class OscMessageBuilder(object):
165
179
  dgram += osc_types.write_rgba(value) # type: ignore[arg-type]
166
180
  elif arg_type == self.ARG_TYPE_MIDI:
167
181
  dgram += osc_types.write_midi(value) # type: ignore[arg-type]
168
- elif arg_type in (self.ARG_TYPE_TRUE,
169
- self.ARG_TYPE_FALSE,
170
- self.ARG_TYPE_ARRAY_START,
171
- self.ARG_TYPE_ARRAY_STOP,
172
- self.ARG_TYPE_NIL):
182
+ elif arg_type in (
183
+ self.ARG_TYPE_TRUE,
184
+ self.ARG_TYPE_FALSE,
185
+ self.ARG_TYPE_ARRAY_START,
186
+ self.ARG_TYPE_ARRAY_STOP,
187
+ self.ARG_TYPE_NIL,
188
+ ):
173
189
  continue
174
190
  else:
175
- raise BuildError('Incorrect parameter type found {}'.format(
176
- arg_type))
191
+ raise BuildError(
192
+ "Incorrect parameter type found {}".format(arg_type)
193
+ )
177
194
 
178
195
  return osc_message.OscMessage(dgram)
179
196
  except osc_types.BuildError as be:
180
- raise BuildError('Could not build the message: {}'.format(be))
197
+ raise BuildError("Could not build the message: {}".format(be))
198
+
199
+
200
+ def build_msg(address: str, value: ArgValue = "") -> osc_message.OscMessage:
201
+ builder = OscMessageBuilder(address=address)
202
+ values: ArgValue
203
+ if value == "":
204
+ values = []
205
+ elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
206
+ values = [value]
207
+ else:
208
+ values = value
209
+ for val in values:
210
+ builder.add_arg(val)
211
+ return builder.build()