py2max 0.2.1__py3-none-any.whl

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 (48) hide show
  1. py2max/__init__.py +67 -0
  2. py2max/__main__.py +6 -0
  3. py2max/cli.py +1251 -0
  4. py2max/core/__init__.py +39 -0
  5. py2max/core/abstract.py +146 -0
  6. py2max/core/box.py +231 -0
  7. py2max/core/common.py +19 -0
  8. py2max/core/patcher.py +1658 -0
  9. py2max/core/patchline.py +68 -0
  10. py2max/exceptions.py +385 -0
  11. py2max/export/__init__.py +20 -0
  12. py2max/export/converters.py +345 -0
  13. py2max/export/svg.py +393 -0
  14. py2max/layout/__init__.py +26 -0
  15. py2max/layout/base.py +463 -0
  16. py2max/layout/flow.py +405 -0
  17. py2max/layout/grid.py +374 -0
  18. py2max/layout/matrix.py +628 -0
  19. py2max/log.py +338 -0
  20. py2max/maxref/__init__.py +78 -0
  21. py2max/maxref/category.py +163 -0
  22. py2max/maxref/db.py +1082 -0
  23. py2max/maxref/legacy.py +324 -0
  24. py2max/maxref/parser.py +703 -0
  25. py2max/py.typed +0 -0
  26. py2max/server/__init__.py +54 -0
  27. py2max/server/client.py +295 -0
  28. py2max/server/inline.py +312 -0
  29. py2max/server/repl.py +561 -0
  30. py2max/server/rpc.py +240 -0
  31. py2max/server/websocket.py +997 -0
  32. py2max/static/cola.min.js +4 -0
  33. py2max/static/d3.v7.min.js +2 -0
  34. py2max/static/dagre-bundle.js +328 -0
  35. py2max/static/elk.bundled.js +6663 -0
  36. py2max/static/index.html +168 -0
  37. py2max/static/interactive.html +589 -0
  38. py2max/static/interactive.js +2111 -0
  39. py2max/static/live-preview.js +324 -0
  40. py2max/static/svg.min.js +13 -0
  41. py2max/static/svg.min.js.map +1 -0
  42. py2max/transformers.py +168 -0
  43. py2max/utils.py +83 -0
  44. py2max-0.2.1.dist-info/METADATA +390 -0
  45. py2max-0.2.1.dist-info/RECORD +48 -0
  46. py2max-0.2.1.dist-info/WHEEL +4 -0
  47. py2max-0.2.1.dist-info/entry_points.txt +3 -0
  48. py2max-0.2.1.dist-info/licenses/LICENSE +19 -0
@@ -0,0 +1,68 @@
1
+ """Patchline class for representing connections between Max objects."""
2
+
3
+ from typing import Optional, Tuple, Union
4
+
5
+ from .abstract import AbstractPatchline
6
+
7
+
8
+ class Patchline(AbstractPatchline):
9
+ """Represents a connection between two Max objects.
10
+
11
+ A Patchline connects an outlet of one object to an inlet of another,
12
+ enabling signal or message flow between objects in a patch.
13
+
14
+ Args:
15
+ source: Source connection as [object_id, outlet_index].
16
+ destination: Destination connection as [object_id, inlet_index].
17
+ **kwds: Additional patchline properties.
18
+
19
+ Attributes:
20
+ source: Source object ID and outlet index.
21
+ destination: Destination object ID and inlet index.
22
+ """
23
+
24
+ def __init__(
25
+ self, source: Optional[list] = None, destination: Optional[list] = None, **kwds
26
+ ):
27
+ self.source = source or []
28
+ self.destination = destination or []
29
+ self._kwds = kwds
30
+
31
+ def __repr__(self):
32
+ return f"Patchline({self.source} -> {self.destination})"
33
+
34
+ @property
35
+ def src(self):
36
+ """first object from source list"""
37
+ return self.source[0]
38
+
39
+ @property
40
+ def dst(self):
41
+ """first object from destination list"""
42
+ return self.destination[0]
43
+
44
+ def to_tuple(self) -> Tuple[str, str, str, str, Union[str, int]]:
45
+ """Return a tuple describing the patchline."""
46
+ return (
47
+ self.source[0],
48
+ self.source[1],
49
+ self.destination[0],
50
+ self.destination[1],
51
+ self._kwds.get("order", 0),
52
+ )
53
+
54
+ def to_dict(self):
55
+ """create dict from object with extra kwds included"""
56
+ d = vars(self).copy()
57
+ to_del = [k for k in d if k.startswith("_")]
58
+ for k in to_del:
59
+ del d[k]
60
+ d.update(self._kwds)
61
+ return dict(patchline=d)
62
+
63
+ @classmethod
64
+ def from_dict(cls, obj_dict: dict):
65
+ """convert to`Patchline` object from dict"""
66
+ patchline = cls()
67
+ patchline.__dict__.update(obj_dict)
68
+ return patchline
py2max/exceptions.py ADDED
@@ -0,0 +1,385 @@
1
+ """Exception hierarchy for py2max.
2
+
3
+ This module defines a comprehensive exception hierarchy for error handling
4
+ throughout the py2max library. All exceptions inherit from Py2MaxError for
5
+ easy catching of library-specific errors.
6
+
7
+ Exception Hierarchy:
8
+ Py2MaxError (base)
9
+ ├── ValidationError
10
+ │ ├── InvalidConnectionError (connections between objects)
11
+ │ ├── InvalidObjectError (invalid object configuration)
12
+ │ └── InvalidPatchError (invalid patcher state)
13
+ ├── ConfigurationError
14
+ │ ├── LayoutError (layout manager issues)
15
+ │ └── DatabaseError (database configuration issues)
16
+ ├── IOError (subclass of built-in IOError)
17
+ │ ├── PatcherIOError (file I/O errors)
18
+ │ └── MaxRefError (maxref.xml parsing errors)
19
+ └── InternalError (unexpected internal errors)
20
+
21
+ Example:
22
+ >>> from py2max.exceptions import InvalidConnectionError
23
+ >>> try:
24
+ ... p.add_line(osc, gain, outlet=999)
25
+ ... except InvalidConnectionError as e:
26
+ ... print(f"Connection error: {e}")
27
+ """
28
+
29
+ from typing import Any, Optional
30
+
31
+
32
+ class Py2MaxError(Exception):
33
+ """Base exception for all py2max errors.
34
+
35
+ All py2max-specific exceptions inherit from this class, allowing users
36
+ to catch all library errors with a single exception handler.
37
+
38
+ Attributes:
39
+ message: Error message describing what went wrong.
40
+ context: Optional dict with additional error context.
41
+ """
42
+
43
+ def __init__(self, message: str, context: Optional[dict[str, Any]] = None):
44
+ """Initialize exception with message and optional context.
45
+
46
+ Args:
47
+ message: Error message describing the problem.
48
+ context: Optional dict with additional error context (object IDs, etc.).
49
+ """
50
+ super().__init__(message)
51
+ self.message = message
52
+ self.context = context or {}
53
+
54
+ def __str__(self) -> str:
55
+ """Return formatted error message with context.
56
+
57
+ Returns:
58
+ Formatted error message including context if available.
59
+ """
60
+ if self.context:
61
+ context_str = ", ".join(f"{k}={v}" for k, v in self.context.items())
62
+ return f"{self.message} ({context_str})"
63
+ return self.message
64
+
65
+
66
+ # ----------------------------------------------------------------------------
67
+ # Validation Errors
68
+
69
+
70
+ class ValidationError(Py2MaxError):
71
+ """Base class for validation errors.
72
+
73
+ Raised when user input or object state fails validation checks.
74
+ """
75
+
76
+
77
+ class InvalidConnectionError(ValidationError):
78
+ """Raised when attempting to create an invalid patchline connection.
79
+
80
+ This exception is raised when validation is enabled and an invalid
81
+ connection is attempted, such as:
82
+ - Connecting to a non-existent inlet or outlet
83
+ - Connecting incompatible signal types
84
+ - Creating duplicate connections
85
+
86
+ Attributes:
87
+ src: Source object identifier.
88
+ dst: Destination object identifier.
89
+ outlet: Source outlet index (if applicable).
90
+ inlet: Destination inlet index (if applicable).
91
+ """
92
+
93
+ def __init__(
94
+ self,
95
+ message: str,
96
+ src: Optional[str] = None,
97
+ dst: Optional[str] = None,
98
+ outlet: Optional[int] = None,
99
+ inlet: Optional[int] = None,
100
+ ):
101
+ """Initialize connection error with connection details.
102
+
103
+ Args:
104
+ message: Error message.
105
+ src: Source object ID.
106
+ dst: Destination object ID.
107
+ outlet: Source outlet index.
108
+ inlet: Destination inlet index.
109
+ """
110
+ context: dict[str, Any] = {}
111
+ if src:
112
+ context["src"] = src
113
+ if dst:
114
+ context["dst"] = dst
115
+ if outlet is not None:
116
+ context["outlet"] = outlet
117
+ if inlet is not None:
118
+ context["inlet"] = inlet
119
+ super().__init__(message, context)
120
+ self.src = src
121
+ self.dst = dst
122
+ self.outlet = outlet
123
+ self.inlet = inlet
124
+
125
+
126
+ class InvalidObjectError(ValidationError):
127
+ """Raised when object configuration is invalid.
128
+
129
+ Examples:
130
+ - Invalid object type name
131
+ - Missing required parameters
132
+ - Invalid parameter values
133
+ - Unknown maxclass
134
+
135
+ Attributes:
136
+ object_id: Object identifier.
137
+ maxclass: Max object class name.
138
+ """
139
+
140
+ def __init__(
141
+ self,
142
+ message: str,
143
+ object_id: Optional[str] = None,
144
+ maxclass: Optional[str] = None,
145
+ ):
146
+ """Initialize object error with object details.
147
+
148
+ Args:
149
+ message: Error message.
150
+ object_id: Object ID.
151
+ maxclass: Max object class name.
152
+ """
153
+ context = {}
154
+ if object_id:
155
+ context["object_id"] = object_id
156
+ if maxclass:
157
+ context["maxclass"] = maxclass
158
+ super().__init__(message, context)
159
+ self.object_id = object_id
160
+ self.maxclass = maxclass
161
+
162
+
163
+ class InvalidPatchError(ValidationError):
164
+ """Raised when patcher state is invalid.
165
+
166
+ Examples:
167
+ - Circular subpatcher references
168
+ - Missing required objects
169
+ - Orphaned connections
170
+ - Invalid patch structure
171
+
172
+ Attributes:
173
+ patch_path: Path to the patcher file.
174
+ """
175
+
176
+ def __init__(self, message: str, patch_path: Optional[str] = None):
177
+ """Initialize patch error with patch details.
178
+
179
+ Args:
180
+ message: Error message.
181
+ patch_path: Path to patcher file.
182
+ """
183
+ context = {}
184
+ if patch_path:
185
+ context["patch_path"] = patch_path
186
+ super().__init__(message, context)
187
+ self.patch_path = patch_path
188
+
189
+
190
+ # ----------------------------------------------------------------------------
191
+ # Configuration Errors
192
+
193
+
194
+ class ConfigurationError(Py2MaxError):
195
+ """Base class for configuration errors.
196
+
197
+ Raised when library configuration is invalid or incompatible.
198
+ """
199
+
200
+
201
+ class LayoutError(ConfigurationError):
202
+ """Raised when layout manager encounters an error.
203
+
204
+ Examples:
205
+ - Unknown layout type
206
+ - Invalid layout parameters
207
+ - Layout algorithm failure
208
+
209
+ Attributes:
210
+ layout_type: Layout manager type.
211
+ """
212
+
213
+ def __init__(self, message: str, layout_type: Optional[str] = None):
214
+ """Initialize layout error with layout details.
215
+
216
+ Args:
217
+ message: Error message.
218
+ layout_type: Layout manager type (e.g., 'grid', 'flow').
219
+ """
220
+ context = {}
221
+ if layout_type:
222
+ context["layout_type"] = layout_type
223
+ super().__init__(message, context)
224
+ self.layout_type = layout_type
225
+
226
+
227
+ class DatabaseError(ConfigurationError):
228
+ """Raised when database operations fail.
229
+
230
+ Examples:
231
+ - Database connection failure
232
+ - Schema mismatch
233
+ - Data corruption
234
+ - Query errors
235
+
236
+ Attributes:
237
+ db_path: Path to database file.
238
+ operation: Operation that failed.
239
+ """
240
+
241
+ def __init__(
242
+ self,
243
+ message: str,
244
+ db_path: Optional[str] = None,
245
+ operation: Optional[str] = None,
246
+ ):
247
+ """Initialize database error with database details.
248
+
249
+ Args:
250
+ message: Error message.
251
+ db_path: Path to database file.
252
+ operation: Operation that failed (e.g., 'query', 'insert').
253
+ """
254
+ context = {}
255
+ if db_path:
256
+ context["db_path"] = str(db_path)
257
+ if operation:
258
+ context["operation"] = operation
259
+ super().__init__(message, context)
260
+ self.db_path = db_path
261
+ self.operation = operation
262
+
263
+
264
+ # ----------------------------------------------------------------------------
265
+ # I/O Errors
266
+
267
+
268
+ class PatcherIOError(Py2MaxError, IOError):
269
+ """Raised when patcher file I/O operations fail.
270
+
271
+ Examples:
272
+ - File not found
273
+ - Permission denied
274
+ - Invalid JSON format
275
+ - Disk full
276
+
277
+ Attributes:
278
+ file_path: Path to the file.
279
+ operation: I/O operation that failed.
280
+ """
281
+
282
+ def __init__(
283
+ self,
284
+ message: str,
285
+ file_path: Optional[str] = None,
286
+ operation: Optional[str] = None,
287
+ ):
288
+ """Initialize I/O error with file details.
289
+
290
+ Args:
291
+ message: Error message.
292
+ file_path: Path to file.
293
+ operation: Operation that failed (e.g., 'read', 'write').
294
+ """
295
+ context = {}
296
+ if file_path:
297
+ context["file_path"] = str(file_path)
298
+ if operation:
299
+ context["operation"] = operation
300
+ Py2MaxError.__init__(self, message, context)
301
+ self.file_path = file_path
302
+ self.operation = operation
303
+
304
+
305
+ class MaxRefError(Py2MaxError, IOError):
306
+ """Raised when MaxRef XML parsing or lookup fails.
307
+
308
+ Examples:
309
+ - MaxRef XML file not found
310
+ - Invalid XML format
311
+ - Object not found in MaxRef database
312
+ - Max installation not found
313
+
314
+ Attributes:
315
+ object_name: Max object name being looked up.
316
+ xml_path: Path to .maxref.xml file.
317
+ """
318
+
319
+ def __init__(
320
+ self,
321
+ message: str,
322
+ object_name: Optional[str] = None,
323
+ xml_path: Optional[str] = None,
324
+ ):
325
+ """Initialize MaxRef error with lookup details.
326
+
327
+ Args:
328
+ message: Error message.
329
+ object_name: Name of Max object being looked up.
330
+ xml_path: Path to .maxref.xml file.
331
+ """
332
+ context = {}
333
+ if object_name:
334
+ context["object_name"] = object_name
335
+ if xml_path:
336
+ context["xml_path"] = str(xml_path)
337
+ Py2MaxError.__init__(self, message, context)
338
+ self.object_name = object_name
339
+ self.xml_path = xml_path
340
+
341
+
342
+ # ----------------------------------------------------------------------------
343
+ # Internal Errors
344
+
345
+
346
+ class InternalError(Py2MaxError):
347
+ """Raised for unexpected internal errors.
348
+
349
+ This exception indicates a bug in the library code and should be reported.
350
+ Users should not typically need to catch this exception.
351
+
352
+ Attributes:
353
+ location: Location in code where error occurred.
354
+ """
355
+
356
+ def __init__(self, message: str, location: Optional[str] = None):
357
+ """Initialize internal error with location.
358
+
359
+ Args:
360
+ message: Error message.
361
+ location: Code location (e.g., 'Patcher.add_line').
362
+ """
363
+ context = {}
364
+ if location:
365
+ context["location"] = location
366
+ super().__init__(f"Internal error: {message}", context)
367
+ self.location = location
368
+
369
+
370
+ # ----------------------------------------------------------------------------
371
+ # Exports
372
+
373
+ __all__ = [
374
+ "Py2MaxError",
375
+ "ValidationError",
376
+ "InvalidConnectionError",
377
+ "InvalidObjectError",
378
+ "InvalidPatchError",
379
+ "ConfigurationError",
380
+ "LayoutError",
381
+ "DatabaseError",
382
+ "PatcherIOError",
383
+ "MaxRefError",
384
+ "InternalError",
385
+ ]
@@ -0,0 +1,20 @@
1
+ """Export functionality for py2max patches.
2
+
3
+ This subpackage provides format conversion and export utilities:
4
+
5
+ - SVG export for visual patch representation
6
+ - Python script generation for patch recreation
7
+ - SQLite database population from maxref files
8
+ """
9
+
10
+ from .svg import export_svg, export_svg_string
11
+ from .converters import maxpat_to_python, maxref_to_sqlite
12
+
13
+ __all__ = [
14
+ # SVG export
15
+ "export_svg",
16
+ "export_svg_string",
17
+ # Converters
18
+ "maxpat_to_python",
19
+ "maxref_to_sqlite",
20
+ ]