pyopensoundcontrol 1.0.8__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Morph Tollon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ include LICENSE
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyopensoundcontrol
3
+ Version: 1.0.8
4
+ Summary: A package to add fully featured OSC support to python
5
+ Author-email: Morph Tollon <code@morphtollon.co.uk>
6
+ Maintainer-email: Morph Tollon <code@morphtollon.co.uk>
7
+ License-Expression: MIT
8
+ Project-URL: Repository, https://github.com/Theatre-Tools/PyOSC
9
+ Project-URL: Homepage, https://github.com/Theatre-Tools/PyOSC
10
+ Project-URL: Issues, https://github.com/Theatre-Tools/PyOSC/issues
11
+ Keywords: osc,sound,midi,music,sockets,transport,Lighting,Theatre
12
+ Requires-Python: <4.0.0,>=3.13
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: oscparser<3.0.0,>=2.0.3
16
+ Dynamic: license-file
17
+
18
+ # PyOSC
19
+
20
+ Python library to support OSC (Open Sound Control).
21
+
22
+ ## About the Project
23
+
24
+ I had issues getting the support I needed out of any of the other packages, so I decide how hard can it be...
25
+
26
+ ## Features
27
+ - Support for both UDP and TCP transport protocols
28
+ - Flexible message handling with customizable handlers
29
+ - Support for both OSC version 1.0 and 1.1
30
+ - Background processing of incoming messages to avoid blocking the main thread
31
+ - Simple API for sending OSC messages to remote hosts
@@ -0,0 +1,14 @@
1
+ # PyOSC
2
+
3
+ Python library to support OSC (Open Sound Control).
4
+
5
+ ## About the Project
6
+
7
+ I had issues getting the support I needed out of any of the other packages, so I decide how hard can it be...
8
+
9
+ ## Features
10
+ - Support for both UDP and TCP transport protocols
11
+ - Flexible message handling with customizable handlers
12
+ - Support for both OSC version 1.0 and 1.1
13
+ - Background processing of incoming messages to avoid blocking the main thread
14
+ - Simple API for sending OSC messages to remote hosts
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyopensoundcontrol
3
+ Version: 1.0.8
4
+ Summary: A package to add fully featured OSC support to python
5
+ Author-email: Morph Tollon <code@morphtollon.co.uk>
6
+ Maintainer-email: Morph Tollon <code@morphtollon.co.uk>
7
+ License-Expression: MIT
8
+ Project-URL: Repository, https://github.com/Theatre-Tools/PyOSC
9
+ Project-URL: Homepage, https://github.com/Theatre-Tools/PyOSC
10
+ Project-URL: Issues, https://github.com/Theatre-Tools/PyOSC/issues
11
+ Keywords: osc,sound,midi,music,sockets,transport,Lighting,Theatre
12
+ Requires-Python: <4.0.0,>=3.13
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: oscparser<3.0.0,>=2.0.3
16
+ Dynamic: license-file
17
+
18
+ # PyOSC
19
+
20
+ Python library to support OSC (Open Sound Control).
21
+
22
+ ## About the Project
23
+
24
+ I had issues getting the support I needed out of any of the other packages, so I decide how hard can it be...
25
+
26
+ ## Features
27
+ - Support for both UDP and TCP transport protocols
28
+ - Flexible message handling with customizable handlers
29
+ - Support for both OSC version 1.0 and 1.1
30
+ - Background processing of incoming messages to avoid blocking the main thread
31
+ - Simple API for sending OSC messages to remote hosts
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ pyopensoundcontrol.egg-info/PKG-INFO
6
+ pyopensoundcontrol.egg-info/SOURCES.txt
7
+ pyopensoundcontrol.egg-info/dependency_links.txt
8
+ pyopensoundcontrol.egg-info/requires.txt
9
+ pyopensoundcontrol.egg-info/top_level.txt
10
+ tests/test_call_handler.py
11
+ tests/test_dispatcher.py
12
+ tests/test_integration.py
13
+ tests/test_peer.py
@@ -0,0 +1 @@
1
+ oscparser<3.0.0,>=2.0.3
@@ -0,0 +1,70 @@
1
+ [project]
2
+ name = "pyopensoundcontrol"
3
+ version = "1.0.8"
4
+ description = "A package to add fully featured OSC support to python"
5
+ authors = [
6
+ {name = "Morph Tollon",email = "code@morphtollon.co.uk"}
7
+ ]
8
+
9
+ maintainers = [
10
+ { name = "Morph Tollon", email = "code@morphtollon.co.uk"}
11
+ ]
12
+ keywords = ["osc", "sound", "midi", "music", "sockets", "transport", "Lighting", "Theatre"]
13
+ license = "MIT"
14
+ readme = "README.md"
15
+ requires-python = ">=3.13,<4.0.0"
16
+ dependencies = [
17
+ "oscparser (>=2.0.3,<3.0.0)",
18
+ ]
19
+ [project.urls]
20
+ Repository = "https://github.com/Theatre-Tools/PyOSC"
21
+ Homepage = "https://github.com/Theatre-Tools/PyOSC"
22
+ Issues = "https://github.com/Theatre-Tools/PyOSC/issues"
23
+
24
+ [tool.poetry.dependencies]
25
+ pydantic = "^2.12.5"
26
+
27
+ [tool.poetry]
28
+ packages = [{ include = "pyosc"}]
29
+
30
+ [tool.setuptools]
31
+ py-modules = []
32
+
33
+ [build-system]
34
+ requires = ["setuptools"]
35
+ build-backend = "setuptools.build_meta"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pyright (>=1.1.408,<2.0.0)",
40
+ "ruff>=0.14.10",
41
+ "lefthook (>=2.0.13,<3.0.0)",
42
+ "pydantic (>=2.12.5,<3.0.0)",
43
+ "pytest (>=8.0.0,<9.0.0)",
44
+ "pytest-cov (>=4.1.0,<5.0.0)",
45
+ "pytest-timeout (>=2.2.0,<3.0.0)"
46
+ ]
47
+ ci = [
48
+ "pydantic (>=2.12.5,<3.0.0)",
49
+ "twine (>=6.2.0,<7.0.0)",
50
+ "pytest (>=8.0.0,<9.0.0)",
51
+ "pytest-cov (>=4.1.0,<5.0.0)"
52
+ ]
53
+ docs = [
54
+ "mkdocs-material (>=9.7.1,<10.0.0)",
55
+ "mkdocs-git-revision-date-localized-plugin (>=1.5.0,<2.0.0)",
56
+ "mkdocs-git-committers-plugin-2 (>=2.5.0,<3.0.0)",
57
+ "mike (>=2.1.3,<3.0.0)",
58
+ "markdown (>=3.10,<4.0)"
59
+ ]
60
+
61
+ [tool.ruff.lint]
62
+ select = ["E4", "E7", "E9", "W1", "W2", "F", "RUF", "I"]
63
+
64
+ [tool.ruff]
65
+ line-length = 127
66
+
67
+ [tool.pyright]
68
+ venvPath = "."
69
+ venv = ".venv"
70
+ exclude = [".venv"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,301 @@
1
+ """Unit tests for the CallHandler class."""
2
+
3
+ import queue
4
+ import time
5
+ import unittest
6
+ from unittest.mock import MagicMock, Mock, patch
7
+
8
+ from oscparser import OSCInt, OSCMessage, OSCString
9
+ from pydantic import BaseModel, Field
10
+
11
+ from pyosc.call_handler import Call, CallHandler
12
+ from pyosc.dispatcher import Dispatcher
13
+ from pyosc.peer import Peer
14
+
15
+
16
+ class ResponseModel(BaseModel):
17
+ """Custom response model for testing."""
18
+
19
+ address: str
20
+ args: tuple
21
+ status: str = Field(default="success")
22
+
23
+
24
+ class TestCall(unittest.TestCase):
25
+ """Test cases for the Call class."""
26
+
27
+ def test_call_initialization(self):
28
+ """Test Call object initialization."""
29
+ q = queue.Queue()
30
+ call = Call(q, OSCMessage)
31
+
32
+ self.assertIs(call.queue, q)
33
+ self.assertEqual(call.validator, OSCMessage)
34
+
35
+
36
+ class TestCallHandler(unittest.TestCase):
37
+ """Test cases for the CallHandler class."""
38
+
39
+ def setUp(self):
40
+ """Set up test fixtures."""
41
+ self.mock_peer = Mock(spec=Peer)
42
+ self.mock_peer.Dispatcher = Dispatcher()
43
+ self.call_handler = CallHandler(self.mock_peer)
44
+
45
+ def tearDown(self):
46
+ """Clean up after tests."""
47
+ if hasattr(self, "call_handler"):
48
+ # Clear any remaining queues
49
+ with self.call_handler.queue_lock:
50
+ self.call_handler.queues.clear()
51
+
52
+ def test_initialization(self):
53
+ """Test CallHandler initialization."""
54
+ self.assertIs(self.call_handler.peer, self.mock_peer)
55
+ self.assertEqual(len(self.call_handler.queues), 0)
56
+ self.assertIsNotNone(self.call_handler.queue_lock)
57
+
58
+ def test_call_basic_success(self):
59
+ """Test basic call with successful response."""
60
+ message = OSCMessage(address="/test/call", args=(OSCInt(value=42),))
61
+
62
+ # Mock send_message
63
+ def mock_send(msg):
64
+ # Simulate receiving response
65
+ response = OSCMessage(address="/test/call", args=(OSCInt(value=100),))
66
+ self.call_handler(response)
67
+
68
+ self.mock_peer.send_message = mock_send
69
+
70
+ result = self.call_handler.call(message, timeout=1.0)
71
+
72
+ self.assertIsNotNone(result)
73
+ assert result is not None # Type narrowing for pyright
74
+ self.assertIsInstance(result, OSCMessage)
75
+ self.assertEqual(result.address, "/test/call")
76
+
77
+ def test_call_with_custom_return_address(self):
78
+ """Test call with custom return address."""
79
+ message = OSCMessage(address="/test/request", args=())
80
+
81
+ def mock_send(msg):
82
+ # Send response to custom address
83
+ response = OSCMessage(address="/test/response", args=(OSCString(value="ok"),))
84
+ self.call_handler(response)
85
+
86
+ self.mock_peer.send_message = mock_send
87
+
88
+ result = self.call_handler.call(message, return_address="/test/response", timeout=1.0)
89
+
90
+ self.assertIsNotNone(result)
91
+ assert result is not None # Type narrowing for pyright
92
+ self.assertEqual(result.address, "/test/response")
93
+
94
+ def test_call_with_validator(self):
95
+ """Test call with custom validator."""
96
+ message = OSCMessage(address="/test/validated", args=())
97
+
98
+ def mock_send(msg):
99
+ response = OSCMessage(
100
+ address="/test/validated",
101
+ args=(OSCString(value="success"),),
102
+ )
103
+ # Manually add status for validation
104
+ response_dict = response.model_dump()
105
+ response_dict["status"] = "success"
106
+ # Simulate the response
107
+ with self.call_handler.queue_lock:
108
+ if "/test/validated" in self.call_handler.queues:
109
+ try:
110
+ validated = ResponseModel.model_validate(response_dict)
111
+ self.call_handler.queues["/test/validated"].queue.put(validated) # type: ignore[arg-type]
112
+ except Exception:
113
+ pass
114
+
115
+ self.mock_peer.send_message = mock_send
116
+
117
+ # Pre-add the handler for this test
118
+ responseq = queue.Queue()
119
+ with self.call_handler.queue_lock:
120
+ self.call_handler.queues["/test/validated"] = Call(responseq, ResponseModel)
121
+ self.mock_peer.Dispatcher.add_handler("/test/validated", self.call_handler)
122
+
123
+ # Send message and get from queue
124
+ self.mock_peer.send_message(message)
125
+ try:
126
+ result = responseq.get(timeout=1.0)
127
+ self.assertIsNotNone(result)
128
+ self.assertIsInstance(result, ResponseModel)
129
+ finally:
130
+ with self.call_handler.queue_lock:
131
+ self.mock_peer.Dispatcher.remove_handler("/test/validated")
132
+ if "/test/validated" in self.call_handler.queues:
133
+ del self.call_handler.queues["/test/validated"]
134
+
135
+ def test_call_timeout(self):
136
+ """Test call timeout when no response received."""
137
+ message = OSCMessage(address="/test/timeout", args=())
138
+
139
+ # Don't send any response
140
+ self.mock_peer.send_message = MagicMock()
141
+
142
+ result = self.call_handler.call(message, timeout=0.1)
143
+
144
+ self.assertIsNone(result)
145
+ # Queue should be cleaned up
146
+ self.assertNotIn("/test/timeout", self.call_handler.queues)
147
+
148
+ def test_call_handler_callable(self):
149
+ """Test CallHandler as callable (message handler)."""
150
+ # Set up a queue for testing
151
+ test_queue = queue.Queue()
152
+ with self.call_handler.queue_lock:
153
+ self.call_handler.queues["/test"] = Call(test_queue, OSCMessage)
154
+
155
+ # Call the handler
156
+ message = OSCMessage(address="/test", args=(OSCInt(value=42),))
157
+ self.call_handler(message)
158
+
159
+ # Check queue received message
160
+ self.assertFalse(test_queue.empty())
161
+ result = test_queue.get(timeout=0.1)
162
+ self.assertIsInstance(result, OSCMessage)
163
+ self.assertEqual(result.address, "/test")
164
+
165
+ def test_call_handler_wrong_address(self):
166
+ """Test CallHandler ignores messages with wrong address."""
167
+ # Set up queue for /test
168
+ test_queue = queue.Queue()
169
+ with self.call_handler.queue_lock:
170
+ self.call_handler.queues["/test"] = Call(test_queue, OSCMessage)
171
+
172
+ # Send message to different address
173
+ message = OSCMessage(address="/other", args=())
174
+ self.call_handler(message)
175
+
176
+ # Queue should remain empty
177
+ self.assertTrue(test_queue.empty())
178
+
179
+ def test_call_handler_validation_error(self):
180
+ """Test CallHandler handles validation errors gracefully."""
181
+
182
+ class StrictModel(BaseModel):
183
+ required_field: str
184
+
185
+ test_queue = queue.Queue()
186
+ with self.call_handler.queue_lock:
187
+ self.call_handler.queues["/test"] = Call(test_queue, StrictModel)
188
+
189
+ # Send message that won't validate
190
+ message = OSCMessage(address="/test", args=())
191
+
192
+ # Should not raise exception
193
+ with patch("builtins.print") as mock_print:
194
+ self.call_handler(message)
195
+ # Should print validation error
196
+ mock_print.assert_called()
197
+
198
+ # Queue should remain empty
199
+ self.assertTrue(test_queue.empty())
200
+
201
+ def test_concurrent_calls(self):
202
+ """Test multiple concurrent calls to different addresses."""
203
+ messages = [OSCMessage(address=f"/test/call{i}", args=(OSCInt(value=i),)) for i in range(5)]
204
+
205
+ results = []
206
+ responses_sent = []
207
+
208
+ def mock_send(msg):
209
+ # Store for later
210
+ responses_sent.append(msg)
211
+
212
+ self.mock_peer.send_message = mock_send
213
+
214
+ # Start calls in threads
215
+ import threading
216
+
217
+ def make_call(msg, idx):
218
+ # Simulate delayed response
219
+ def delayed_response():
220
+ time.sleep(0.05)
221
+ response = OSCMessage(address=msg.address, args=(OSCInt(value=idx * 10),))
222
+ self.call_handler(response)
223
+
224
+ threading.Thread(target=delayed_response, daemon=True).start()
225
+
226
+ result = self.call_handler.call(msg, timeout=1.0)
227
+ results.append((idx, result))
228
+
229
+ threads = []
230
+ for i, msg in enumerate(messages):
231
+ t = threading.Thread(target=make_call, args=(msg, i))
232
+ threads.append(t)
233
+ t.start()
234
+
235
+ # Wait for all threads
236
+ for t in threads:
237
+ t.join()
238
+
239
+ # All calls should succeed
240
+ self.assertEqual(len(results), 5)
241
+ for idx, result in results:
242
+ self.assertIsNotNone(result)
243
+ self.assertIsInstance(result, OSCMessage)
244
+
245
+ def test_queue_lock_thread_safety(self):
246
+ """Test that queue_lock prevents race conditions."""
247
+ iterations = 100
248
+ errors = []
249
+
250
+ def add_remove_queue():
251
+ for i in range(iterations):
252
+ try:
253
+ with self.call_handler.queue_lock:
254
+ self.call_handler.queues[f"/test/{i}"] = Call(queue.Queue(), OSCMessage)
255
+ with self.call_handler.queue_lock:
256
+ if f"/test/{i}" in self.call_handler.queues:
257
+ del self.call_handler.queues[f"/test/{i}"]
258
+ except Exception as e:
259
+ errors.append(e)
260
+
261
+ import threading
262
+
263
+ threads = [threading.Thread(target=add_remove_queue) for _ in range(5)]
264
+ for t in threads:
265
+ t.start()
266
+ for t in threads:
267
+ t.join()
268
+
269
+ # Should have no errors
270
+ self.assertEqual(len(errors), 0)
271
+
272
+ def test_cleanup_after_successful_call(self):
273
+ """Test that queues and handlers are cleaned up after successful call."""
274
+ message = OSCMessage(address="/test/cleanup", args=())
275
+
276
+ def mock_send(msg):
277
+ response = OSCMessage(address="/test/cleanup", args=())
278
+ self.call_handler(response)
279
+
280
+ self.mock_peer.send_message = mock_send
281
+
282
+ result = self.call_handler.call(message, timeout=1.0)
283
+
284
+ self.assertIsNotNone(result)
285
+ # Queue should be cleaned up
286
+ self.assertNotIn("/test/cleanup", self.call_handler.queues)
287
+
288
+ def test_cleanup_after_timeout(self):
289
+ """Test that queues are cleaned up after timeout."""
290
+ message = OSCMessage(address="/test/cleanup", args=())
291
+ self.mock_peer.send_message = MagicMock()
292
+
293
+ result = self.call_handler.call(message, timeout=0.1)
294
+
295
+ self.assertIsNone(result)
296
+ # Queue should be cleaned up
297
+ self.assertNotIn("/test/cleanup", self.call_handler.queues)
298
+
299
+
300
+ if __name__ == "__main__":
301
+ unittest.main()