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.
- py2max/__init__.py +67 -0
- py2max/__main__.py +6 -0
- py2max/cli.py +1251 -0
- py2max/core/__init__.py +39 -0
- py2max/core/abstract.py +146 -0
- py2max/core/box.py +231 -0
- py2max/core/common.py +19 -0
- py2max/core/patcher.py +1658 -0
- py2max/core/patchline.py +68 -0
- py2max/exceptions.py +385 -0
- py2max/export/__init__.py +20 -0
- py2max/export/converters.py +345 -0
- py2max/export/svg.py +393 -0
- py2max/layout/__init__.py +26 -0
- py2max/layout/base.py +463 -0
- py2max/layout/flow.py +405 -0
- py2max/layout/grid.py +374 -0
- py2max/layout/matrix.py +628 -0
- py2max/log.py +338 -0
- py2max/maxref/__init__.py +78 -0
- py2max/maxref/category.py +163 -0
- py2max/maxref/db.py +1082 -0
- py2max/maxref/legacy.py +324 -0
- py2max/maxref/parser.py +703 -0
- py2max/py.typed +0 -0
- py2max/server/__init__.py +54 -0
- py2max/server/client.py +295 -0
- py2max/server/inline.py +312 -0
- py2max/server/repl.py +561 -0
- py2max/server/rpc.py +240 -0
- py2max/server/websocket.py +997 -0
- py2max/static/cola.min.js +4 -0
- py2max/static/d3.v7.min.js +2 -0
- py2max/static/dagre-bundle.js +328 -0
- py2max/static/elk.bundled.js +6663 -0
- py2max/static/index.html +168 -0
- py2max/static/interactive.html +589 -0
- py2max/static/interactive.js +2111 -0
- py2max/static/live-preview.js +324 -0
- py2max/static/svg.min.js +13 -0
- py2max/static/svg.min.js.map +1 -0
- py2max/transformers.py +168 -0
- py2max/utils.py +83 -0
- py2max-0.2.1.dist-info/METADATA +390 -0
- py2max-0.2.1.dist-info/RECORD +48 -0
- py2max-0.2.1.dist-info/WHEEL +4 -0
- py2max-0.2.1.dist-info/entry_points.txt +3 -0
- py2max-0.2.1.dist-info/licenses/LICENSE +19 -0
py2max/maxref/db.py
ADDED
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
"""db.py - SQLite database for Max object reference data"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import sys
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from .parser import (
|
|
11
|
+
get_all_jit_objects,
|
|
12
|
+
get_all_m4l_objects,
|
|
13
|
+
get_all_max_objects,
|
|
14
|
+
get_all_msp_objects,
|
|
15
|
+
get_available_objects,
|
|
16
|
+
get_object_info,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MaxRefDB:
|
|
21
|
+
"""SQLite database for Max object reference data
|
|
22
|
+
|
|
23
|
+
By default, uses a cached database at:
|
|
24
|
+
- macOS: ~/Library/Caches/py2max/maxref.db
|
|
25
|
+
- Linux: ~/.cache/py2max/maxref.db
|
|
26
|
+
- Windows: ~/AppData/Local/py2max/Cache/maxref.db
|
|
27
|
+
|
|
28
|
+
The cache is automatically populated on first use.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def get_cache_dir() -> Path:
|
|
33
|
+
"""Get platform-specific cache directory for py2max
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Path to cache directory:
|
|
37
|
+
- macOS: ~/Library/Caches/py2max
|
|
38
|
+
- Linux: ~/.cache/py2max
|
|
39
|
+
- Windows: ~/AppData/Local/py2max/Cache
|
|
40
|
+
"""
|
|
41
|
+
home = Path.home()
|
|
42
|
+
|
|
43
|
+
if sys.platform == "darwin": # macOS
|
|
44
|
+
cache_dir = home / "Library" / "Caches" / "py2max"
|
|
45
|
+
elif sys.platform == "win32": # Windows
|
|
46
|
+
cache_dir = home / "AppData" / "Local" / "py2max" / "Cache"
|
|
47
|
+
else: # Linux and others
|
|
48
|
+
cache_dir = home / ".cache" / "py2max"
|
|
49
|
+
|
|
50
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
return cache_dir
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def get_default_db_path() -> Path:
|
|
55
|
+
"""Get default database path
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Path to default maxref.db in cache directory
|
|
59
|
+
"""
|
|
60
|
+
return MaxRefDB.get_cache_dir() / "maxref.db"
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self, db_path: Optional[Union[Path, str]] = None, auto_populate: bool = True
|
|
64
|
+
):
|
|
65
|
+
"""Initialize database connection
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
db_path: Path to SQLite database file.
|
|
69
|
+
If None, uses platform-specific cache location.
|
|
70
|
+
Use ":memory:" for in-memory database.
|
|
71
|
+
auto_populate: If True and database is empty, automatically populate
|
|
72
|
+
with all Max objects (only applies to file-based databases).
|
|
73
|
+
"""
|
|
74
|
+
if db_path is None:
|
|
75
|
+
self.db_path: Union[Path, str] = self.get_default_db_path()
|
|
76
|
+
self._use_cache = True
|
|
77
|
+
elif db_path == ":memory:":
|
|
78
|
+
self.db_path = ":memory:"
|
|
79
|
+
self._use_cache = False
|
|
80
|
+
else:
|
|
81
|
+
self.db_path = Path(db_path)
|
|
82
|
+
self._use_cache = False
|
|
83
|
+
|
|
84
|
+
self._conn = None
|
|
85
|
+
# For in-memory databases, maintain a persistent connection
|
|
86
|
+
if self.db_path == ":memory:":
|
|
87
|
+
self._conn = sqlite3.connect(":memory:")
|
|
88
|
+
self._conn.row_factory = sqlite3.Row
|
|
89
|
+
|
|
90
|
+
self._init_schema()
|
|
91
|
+
|
|
92
|
+
# Auto-populate cache on first use
|
|
93
|
+
if auto_populate and self._use_cache and self.count == 0:
|
|
94
|
+
self._auto_populate_cache()
|
|
95
|
+
|
|
96
|
+
def __len__(self) -> int:
|
|
97
|
+
"""Return number of objects in database"""
|
|
98
|
+
return self.count
|
|
99
|
+
|
|
100
|
+
def __enter__(self) -> "MaxRefDB":
|
|
101
|
+
"""Context manager entry"""
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
105
|
+
"""Context manager exit - close connection"""
|
|
106
|
+
self.close()
|
|
107
|
+
|
|
108
|
+
def __del__(self) -> None:
|
|
109
|
+
"""Destructor - ensure connection is closed"""
|
|
110
|
+
self.close()
|
|
111
|
+
|
|
112
|
+
def close(self) -> None:
|
|
113
|
+
"""Close database connection
|
|
114
|
+
|
|
115
|
+
Safe to call multiple times. Should be called when done with
|
|
116
|
+
in-memory databases to prevent ResourceWarning.
|
|
117
|
+
"""
|
|
118
|
+
if self._conn is not None:
|
|
119
|
+
try:
|
|
120
|
+
self._conn.close()
|
|
121
|
+
except Exception:
|
|
122
|
+
pass # Ignore errors during close
|
|
123
|
+
self._conn = None
|
|
124
|
+
|
|
125
|
+
def __contains__(self, name: str) -> bool:
|
|
126
|
+
"""Check if object exists in database"""
|
|
127
|
+
with self._get_cursor() as cursor:
|
|
128
|
+
cursor.execute("SELECT 1 FROM objects WHERE name = ?", (name,))
|
|
129
|
+
return cursor.fetchone() is not None
|
|
130
|
+
|
|
131
|
+
def __getitem__(self, name: str) -> Dict[str, Any]:
|
|
132
|
+
"""Get object by name using subscript notation"""
|
|
133
|
+
obj = self.get_object(name)
|
|
134
|
+
if obj is None:
|
|
135
|
+
raise KeyError(f"Object '{name}' not found in database")
|
|
136
|
+
return obj
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def create_database(cls, db_path: Path, populate: bool = True) -> "MaxRefDB":
|
|
140
|
+
"""Create and optionally populate a Max reference database
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
db_path: Path to SQLite database file
|
|
144
|
+
populate: Whether to populate with all available .maxref.xml files
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
MaxRefDB instance
|
|
148
|
+
"""
|
|
149
|
+
db = cls(db_path)
|
|
150
|
+
if populate:
|
|
151
|
+
db.populate_from_maxref()
|
|
152
|
+
return db
|
|
153
|
+
|
|
154
|
+
@contextmanager
|
|
155
|
+
def _get_cursor(self):
|
|
156
|
+
"""Context manager for database cursor"""
|
|
157
|
+
if self._conn is not None:
|
|
158
|
+
# Use persistent connection for in-memory databases
|
|
159
|
+
cursor = self._conn.cursor()
|
|
160
|
+
try:
|
|
161
|
+
yield cursor
|
|
162
|
+
self._conn.commit()
|
|
163
|
+
except Exception:
|
|
164
|
+
self._conn.rollback()
|
|
165
|
+
raise
|
|
166
|
+
else:
|
|
167
|
+
# Use new connection for file-based databases
|
|
168
|
+
conn = sqlite3.connect(self.db_path)
|
|
169
|
+
conn.row_factory = sqlite3.Row
|
|
170
|
+
cursor = conn.cursor()
|
|
171
|
+
try:
|
|
172
|
+
yield cursor
|
|
173
|
+
conn.commit()
|
|
174
|
+
except Exception:
|
|
175
|
+
conn.rollback()
|
|
176
|
+
raise
|
|
177
|
+
finally:
|
|
178
|
+
conn.close()
|
|
179
|
+
|
|
180
|
+
def _init_schema(self):
|
|
181
|
+
"""Create database schema"""
|
|
182
|
+
with self._get_cursor() as cursor:
|
|
183
|
+
# Main objects table
|
|
184
|
+
cursor.execute("""
|
|
185
|
+
CREATE TABLE IF NOT EXISTS objects (
|
|
186
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
187
|
+
name TEXT UNIQUE NOT NULL,
|
|
188
|
+
digest TEXT,
|
|
189
|
+
description TEXT,
|
|
190
|
+
category TEXT,
|
|
191
|
+
module_pathname TEXT,
|
|
192
|
+
autofit INTEGER DEFAULT 0,
|
|
193
|
+
root_attrs TEXT
|
|
194
|
+
)
|
|
195
|
+
""")
|
|
196
|
+
|
|
197
|
+
# Metadata table
|
|
198
|
+
cursor.execute("""
|
|
199
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
200
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
201
|
+
object_id INTEGER NOT NULL,
|
|
202
|
+
key TEXT NOT NULL,
|
|
203
|
+
value TEXT NOT NULL,
|
|
204
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
205
|
+
)
|
|
206
|
+
""")
|
|
207
|
+
|
|
208
|
+
# Inlets table
|
|
209
|
+
cursor.execute("""
|
|
210
|
+
CREATE TABLE IF NOT EXISTS inlets (
|
|
211
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
212
|
+
object_id INTEGER NOT NULL,
|
|
213
|
+
inlet_id TEXT NOT NULL,
|
|
214
|
+
inlet_type TEXT,
|
|
215
|
+
digest TEXT,
|
|
216
|
+
description TEXT,
|
|
217
|
+
attrs TEXT,
|
|
218
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
219
|
+
)
|
|
220
|
+
""")
|
|
221
|
+
|
|
222
|
+
# Outlets table
|
|
223
|
+
cursor.execute("""
|
|
224
|
+
CREATE TABLE IF NOT EXISTS outlets (
|
|
225
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
226
|
+
object_id INTEGER NOT NULL,
|
|
227
|
+
outlet_id TEXT NOT NULL,
|
|
228
|
+
outlet_type TEXT,
|
|
229
|
+
digest TEXT,
|
|
230
|
+
description TEXT,
|
|
231
|
+
attrs TEXT,
|
|
232
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
233
|
+
)
|
|
234
|
+
""")
|
|
235
|
+
|
|
236
|
+
# Object arguments table
|
|
237
|
+
cursor.execute("""
|
|
238
|
+
CREATE TABLE IF NOT EXISTS objargs (
|
|
239
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
240
|
+
object_id INTEGER NOT NULL,
|
|
241
|
+
name TEXT,
|
|
242
|
+
optional INTEGER DEFAULT 0,
|
|
243
|
+
arg_type TEXT,
|
|
244
|
+
digest TEXT,
|
|
245
|
+
description TEXT,
|
|
246
|
+
attrs TEXT,
|
|
247
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
248
|
+
)
|
|
249
|
+
""")
|
|
250
|
+
|
|
251
|
+
# Methods table
|
|
252
|
+
cursor.execute("""
|
|
253
|
+
CREATE TABLE IF NOT EXISTS methods (
|
|
254
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
255
|
+
object_id INTEGER NOT NULL,
|
|
256
|
+
name TEXT NOT NULL,
|
|
257
|
+
digest TEXT,
|
|
258
|
+
description TEXT,
|
|
259
|
+
attrs TEXT,
|
|
260
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
261
|
+
)
|
|
262
|
+
""")
|
|
263
|
+
|
|
264
|
+
# Method arguments table
|
|
265
|
+
cursor.execute("""
|
|
266
|
+
CREATE TABLE IF NOT EXISTS method_args (
|
|
267
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
268
|
+
method_id INTEGER NOT NULL,
|
|
269
|
+
name TEXT,
|
|
270
|
+
optional INTEGER DEFAULT 0,
|
|
271
|
+
arg_type TEXT,
|
|
272
|
+
attrs TEXT,
|
|
273
|
+
FOREIGN KEY (method_id) REFERENCES methods(id) ON DELETE CASCADE
|
|
274
|
+
)
|
|
275
|
+
""")
|
|
276
|
+
|
|
277
|
+
# Attributes table
|
|
278
|
+
cursor.execute("""
|
|
279
|
+
CREATE TABLE IF NOT EXISTS attributes (
|
|
280
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
281
|
+
object_id INTEGER NOT NULL,
|
|
282
|
+
name TEXT NOT NULL,
|
|
283
|
+
can_get INTEGER DEFAULT 1,
|
|
284
|
+
can_set INTEGER DEFAULT 1,
|
|
285
|
+
attr_type TEXT,
|
|
286
|
+
size INTEGER,
|
|
287
|
+
digest TEXT,
|
|
288
|
+
description TEXT,
|
|
289
|
+
attrs TEXT,
|
|
290
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
291
|
+
)
|
|
292
|
+
""")
|
|
293
|
+
|
|
294
|
+
# Attribute enum values table
|
|
295
|
+
cursor.execute("""
|
|
296
|
+
CREATE TABLE IF NOT EXISTS attribute_enums (
|
|
297
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
298
|
+
attribute_id INTEGER NOT NULL,
|
|
299
|
+
name TEXT,
|
|
300
|
+
digest TEXT,
|
|
301
|
+
description TEXT,
|
|
302
|
+
attrs TEXT,
|
|
303
|
+
FOREIGN KEY (attribute_id) REFERENCES attributes(id) ON DELETE CASCADE
|
|
304
|
+
)
|
|
305
|
+
""")
|
|
306
|
+
|
|
307
|
+
# Examples table
|
|
308
|
+
cursor.execute("""
|
|
309
|
+
CREATE TABLE IF NOT EXISTS examples (
|
|
310
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
311
|
+
object_id INTEGER NOT NULL,
|
|
312
|
+
name TEXT,
|
|
313
|
+
caption TEXT,
|
|
314
|
+
attrs TEXT,
|
|
315
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
316
|
+
)
|
|
317
|
+
""")
|
|
318
|
+
|
|
319
|
+
# See also references table
|
|
320
|
+
cursor.execute("""
|
|
321
|
+
CREATE TABLE IF NOT EXISTS seealso (
|
|
322
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
323
|
+
object_id INTEGER NOT NULL,
|
|
324
|
+
ref_name TEXT NOT NULL,
|
|
325
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
326
|
+
)
|
|
327
|
+
""")
|
|
328
|
+
|
|
329
|
+
# Misc table (for Output, Connections, etc.)
|
|
330
|
+
cursor.execute("""
|
|
331
|
+
CREATE TABLE IF NOT EXISTS misc (
|
|
332
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
333
|
+
object_id INTEGER NOT NULL,
|
|
334
|
+
category TEXT NOT NULL,
|
|
335
|
+
entry_name TEXT NOT NULL,
|
|
336
|
+
description TEXT,
|
|
337
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
338
|
+
)
|
|
339
|
+
""")
|
|
340
|
+
|
|
341
|
+
# Palette information table
|
|
342
|
+
cursor.execute("""
|
|
343
|
+
CREATE TABLE IF NOT EXISTS palette (
|
|
344
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
345
|
+
object_id INTEGER NOT NULL,
|
|
346
|
+
category TEXT,
|
|
347
|
+
palette_index INTEGER,
|
|
348
|
+
attrs TEXT,
|
|
349
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
350
|
+
)
|
|
351
|
+
""")
|
|
352
|
+
|
|
353
|
+
# Parameter information table
|
|
354
|
+
cursor.execute("""
|
|
355
|
+
CREATE TABLE IF NOT EXISTS parameter (
|
|
356
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
357
|
+
object_id INTEGER NOT NULL,
|
|
358
|
+
attrs TEXT,
|
|
359
|
+
FOREIGN KEY (object_id) REFERENCES objects(id) ON DELETE CASCADE
|
|
360
|
+
)
|
|
361
|
+
""")
|
|
362
|
+
|
|
363
|
+
# Create indices for faster queries
|
|
364
|
+
cursor.execute(
|
|
365
|
+
"CREATE INDEX IF NOT EXISTS idx_objects_name ON objects(name)"
|
|
366
|
+
)
|
|
367
|
+
cursor.execute(
|
|
368
|
+
"CREATE INDEX IF NOT EXISTS idx_metadata_object ON metadata(object_id)"
|
|
369
|
+
)
|
|
370
|
+
cursor.execute(
|
|
371
|
+
"CREATE INDEX IF NOT EXISTS idx_inlets_object ON inlets(object_id)"
|
|
372
|
+
)
|
|
373
|
+
cursor.execute(
|
|
374
|
+
"CREATE INDEX IF NOT EXISTS idx_outlets_object ON outlets(object_id)"
|
|
375
|
+
)
|
|
376
|
+
cursor.execute(
|
|
377
|
+
"CREATE INDEX IF NOT EXISTS idx_methods_object ON methods(object_id)"
|
|
378
|
+
)
|
|
379
|
+
cursor.execute(
|
|
380
|
+
"CREATE INDEX IF NOT EXISTS idx_attributes_object ON attributes(object_id)"
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def populate(
|
|
384
|
+
self, object_names: Optional[List[str]] = None, category: Optional[str] = None
|
|
385
|
+
):
|
|
386
|
+
"""Populate database from .maxref.xml files
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
object_names: List of object names to import. If None, imports based on category.
|
|
390
|
+
category: Category to import ('max', 'msp', 'jit', 'm4l', or None for all).
|
|
391
|
+
Ignored if object_names is provided.
|
|
392
|
+
"""
|
|
393
|
+
if object_names is None:
|
|
394
|
+
if category is None:
|
|
395
|
+
object_names = get_available_objects()
|
|
396
|
+
elif category.lower() == "max":
|
|
397
|
+
object_names = get_all_max_objects()
|
|
398
|
+
elif category.lower() == "msp":
|
|
399
|
+
object_names = get_all_msp_objects()
|
|
400
|
+
elif category.lower() == "jit":
|
|
401
|
+
object_names = get_all_jit_objects()
|
|
402
|
+
elif category.lower() == "m4l":
|
|
403
|
+
object_names = get_all_m4l_objects()
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError(
|
|
406
|
+
f"Unknown category: {category}. Use 'max', 'msp', 'jit', 'm4l', or None"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
for name in object_names:
|
|
410
|
+
data = get_object_info(name)
|
|
411
|
+
if data:
|
|
412
|
+
self.insert_object(name, data)
|
|
413
|
+
|
|
414
|
+
# Deprecated methods - use populate() instead
|
|
415
|
+
def populate_from_maxref(self, object_names: Optional[List[str]] = None):
|
|
416
|
+
"""Populate database from .maxref.xml files (deprecated: use populate())
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
object_names: List of object names to import. If None, imports all available objects.
|
|
420
|
+
"""
|
|
421
|
+
self.populate(object_names)
|
|
422
|
+
|
|
423
|
+
def populate_all_objects(self):
|
|
424
|
+
"""Populate database with all available Max objects (deprecated: use populate())"""
|
|
425
|
+
self.populate(category=None)
|
|
426
|
+
|
|
427
|
+
def populate_all_max_objects(self):
|
|
428
|
+
"""Populate database with all Max objects (deprecated: use populate(category='max'))"""
|
|
429
|
+
self.populate(category="max")
|
|
430
|
+
|
|
431
|
+
def populate_all_jit_objects(self):
|
|
432
|
+
"""Populate database with all Jitter objects (deprecated: use populate(category='jit'))"""
|
|
433
|
+
self.populate(category="jit")
|
|
434
|
+
|
|
435
|
+
def populate_all_msp_objects(self):
|
|
436
|
+
"""Populate database with all MSP objects (deprecated: use populate(category='msp'))"""
|
|
437
|
+
self.populate(category="msp")
|
|
438
|
+
|
|
439
|
+
def populate_all_m4l_objects(self):
|
|
440
|
+
"""Populate database with all Max for Live objects (deprecated: use populate(category='m4l'))"""
|
|
441
|
+
self.populate(category="m4l")
|
|
442
|
+
|
|
443
|
+
# -------------------------------------------------------------------------
|
|
444
|
+
# Insert helpers
|
|
445
|
+
# -------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
def _extract_extra_attrs(
|
|
448
|
+
self, data: Dict[str, Any], exclude: List[str]
|
|
449
|
+
) -> Optional[str]:
|
|
450
|
+
"""Extract extra attributes not in exclude list and JSON-encode them."""
|
|
451
|
+
extra = {k: v for k, v in data.items() if k not in exclude}
|
|
452
|
+
return json.dumps(extra) if extra else None
|
|
453
|
+
|
|
454
|
+
def _insert_main_object(
|
|
455
|
+
self, cursor: sqlite3.Cursor, name: str, data: Dict[str, Any]
|
|
456
|
+
) -> int:
|
|
457
|
+
"""Insert main object record and return object_id."""
|
|
458
|
+
known_keys = {
|
|
459
|
+
"digest",
|
|
460
|
+
"description",
|
|
461
|
+
"metadata",
|
|
462
|
+
"inlets",
|
|
463
|
+
"outlets",
|
|
464
|
+
"objargs",
|
|
465
|
+
"methods",
|
|
466
|
+
"attributes",
|
|
467
|
+
"examples",
|
|
468
|
+
"seealso",
|
|
469
|
+
"misc",
|
|
470
|
+
"palette",
|
|
471
|
+
"parameter",
|
|
472
|
+
"name",
|
|
473
|
+
"category",
|
|
474
|
+
"module_pathname",
|
|
475
|
+
"autofit",
|
|
476
|
+
}
|
|
477
|
+
root_attrs = {k: v for k, v in data.items() if k not in known_keys}
|
|
478
|
+
|
|
479
|
+
cursor.execute(
|
|
480
|
+
"""
|
|
481
|
+
INSERT OR REPLACE INTO objects
|
|
482
|
+
(name, digest, description, category, module_pathname, autofit, root_attrs)
|
|
483
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
484
|
+
""",
|
|
485
|
+
(
|
|
486
|
+
name,
|
|
487
|
+
data.get("digest"),
|
|
488
|
+
data.get("description"),
|
|
489
|
+
data.get("category"),
|
|
490
|
+
data.get("module_pathname"),
|
|
491
|
+
1 if data.get("autofit") == "1" else 0,
|
|
492
|
+
json.dumps(root_attrs) if root_attrs else None,
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
return cursor.lastrowid # type: ignore[return-value]
|
|
496
|
+
|
|
497
|
+
def _delete_related_records(self, cursor: sqlite3.Cursor, object_id: int) -> None:
|
|
498
|
+
"""Delete all related records for an object (for REPLACE operations)."""
|
|
499
|
+
tables = [
|
|
500
|
+
"metadata",
|
|
501
|
+
"inlets",
|
|
502
|
+
"outlets",
|
|
503
|
+
"objargs",
|
|
504
|
+
"methods",
|
|
505
|
+
"attributes",
|
|
506
|
+
"examples",
|
|
507
|
+
"seealso",
|
|
508
|
+
"misc",
|
|
509
|
+
"palette",
|
|
510
|
+
"parameter",
|
|
511
|
+
]
|
|
512
|
+
for table in tables:
|
|
513
|
+
cursor.execute(f"DELETE FROM {table} WHERE object_id = ?", (object_id,))
|
|
514
|
+
|
|
515
|
+
def _insert_metadata(
|
|
516
|
+
self, cursor: sqlite3.Cursor, object_id: int, metadata: Dict[str, Any]
|
|
517
|
+
) -> None:
|
|
518
|
+
"""Insert metadata key-value pairs."""
|
|
519
|
+
for key, value in metadata.items():
|
|
520
|
+
values = value if isinstance(value, list) else [value]
|
|
521
|
+
for v in values:
|
|
522
|
+
cursor.execute(
|
|
523
|
+
"INSERT INTO metadata (object_id, key, value) VALUES (?, ?, ?)",
|
|
524
|
+
(object_id, key, v),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def _insert_inlets_outlets(
|
|
528
|
+
self, cursor: sqlite3.Cursor, object_id: int, items: List[Dict], table: str
|
|
529
|
+
) -> None:
|
|
530
|
+
"""Insert inlet or outlet records."""
|
|
531
|
+
id_col = "inlet_id" if table == "inlets" else "outlet_id"
|
|
532
|
+
type_col = "inlet_type" if table == "inlets" else "outlet_type"
|
|
533
|
+
exclude = ["id", "type", "digest", "description"]
|
|
534
|
+
|
|
535
|
+
for item in items:
|
|
536
|
+
cursor.execute(
|
|
537
|
+
f"""
|
|
538
|
+
INSERT INTO {table} (object_id, {id_col}, {type_col}, digest, description, attrs)
|
|
539
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
540
|
+
""",
|
|
541
|
+
(
|
|
542
|
+
object_id,
|
|
543
|
+
item.get("id"),
|
|
544
|
+
item.get("type"),
|
|
545
|
+
item.get("digest"),
|
|
546
|
+
item.get("description"),
|
|
547
|
+
self._extract_extra_attrs(item, exclude),
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def _insert_objargs(
|
|
552
|
+
self, cursor: sqlite3.Cursor, object_id: int, objargs: List[Dict]
|
|
553
|
+
) -> None:
|
|
554
|
+
"""Insert object argument records."""
|
|
555
|
+
exclude = ["name", "optional", "type", "digest", "description"]
|
|
556
|
+
for objarg in objargs:
|
|
557
|
+
cursor.execute(
|
|
558
|
+
"""
|
|
559
|
+
INSERT INTO objargs (object_id, name, optional, arg_type, digest, description, attrs)
|
|
560
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
561
|
+
""",
|
|
562
|
+
(
|
|
563
|
+
object_id,
|
|
564
|
+
objarg.get("name"),
|
|
565
|
+
1 if objarg.get("optional") == "1" else 0,
|
|
566
|
+
objarg.get("type"),
|
|
567
|
+
objarg.get("digest"),
|
|
568
|
+
objarg.get("description"),
|
|
569
|
+
self._extract_extra_attrs(objarg, exclude),
|
|
570
|
+
),
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def _insert_methods(
|
|
574
|
+
self, cursor: sqlite3.Cursor, object_id: int, methods: Dict[str, Dict]
|
|
575
|
+
) -> None:
|
|
576
|
+
"""Insert method records and their arguments."""
|
|
577
|
+
method_exclude = ["digest", "description", "args", "attributes"]
|
|
578
|
+
arg_exclude = ["name", "optional", "type"]
|
|
579
|
+
|
|
580
|
+
for method_name, method_data in methods.items():
|
|
581
|
+
cursor.execute(
|
|
582
|
+
"""
|
|
583
|
+
INSERT INTO methods (object_id, name, digest, description, attrs)
|
|
584
|
+
VALUES (?, ?, ?, ?, ?)
|
|
585
|
+
""",
|
|
586
|
+
(
|
|
587
|
+
object_id,
|
|
588
|
+
method_name,
|
|
589
|
+
method_data.get("digest"),
|
|
590
|
+
method_data.get("description"),
|
|
591
|
+
self._extract_extra_attrs(method_data, method_exclude),
|
|
592
|
+
),
|
|
593
|
+
)
|
|
594
|
+
method_id = cursor.lastrowid
|
|
595
|
+
|
|
596
|
+
for arg in method_data.get("args", []):
|
|
597
|
+
cursor.execute(
|
|
598
|
+
"""
|
|
599
|
+
INSERT INTO method_args (method_id, name, optional, arg_type, attrs)
|
|
600
|
+
VALUES (?, ?, ?, ?, ?)
|
|
601
|
+
""",
|
|
602
|
+
(
|
|
603
|
+
method_id,
|
|
604
|
+
arg.get("name"),
|
|
605
|
+
1 if arg.get("optional") == "1" else 0,
|
|
606
|
+
arg.get("type"),
|
|
607
|
+
self._extract_extra_attrs(arg, arg_exclude),
|
|
608
|
+
),
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
def _insert_attributes(
|
|
612
|
+
self, cursor: sqlite3.Cursor, object_id: int, attributes: Dict[str, Dict]
|
|
613
|
+
) -> None:
|
|
614
|
+
"""Insert attribute records and their enum values."""
|
|
615
|
+
attr_exclude = [
|
|
616
|
+
"get",
|
|
617
|
+
"set",
|
|
618
|
+
"type",
|
|
619
|
+
"size",
|
|
620
|
+
"digest",
|
|
621
|
+
"description",
|
|
622
|
+
"enumlist",
|
|
623
|
+
"attributes",
|
|
624
|
+
]
|
|
625
|
+
enum_exclude = ["name", "digest", "description"]
|
|
626
|
+
|
|
627
|
+
for attr_name, attr_data in attributes.items():
|
|
628
|
+
size_val = attr_data.get("size")
|
|
629
|
+
cursor.execute(
|
|
630
|
+
"""
|
|
631
|
+
INSERT INTO attributes (object_id, name, can_get, can_set, attr_type, size, digest, description, attrs)
|
|
632
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
633
|
+
""",
|
|
634
|
+
(
|
|
635
|
+
object_id,
|
|
636
|
+
attr_name,
|
|
637
|
+
1 if attr_data.get("get") == "1" else 0,
|
|
638
|
+
1 if attr_data.get("set") == "1" else 0,
|
|
639
|
+
attr_data.get("type"),
|
|
640
|
+
int(size_val) if size_val else None,
|
|
641
|
+
attr_data.get("digest"),
|
|
642
|
+
attr_data.get("description"),
|
|
643
|
+
self._extract_extra_attrs(attr_data, attr_exclude),
|
|
644
|
+
),
|
|
645
|
+
)
|
|
646
|
+
attr_id = cursor.lastrowid
|
|
647
|
+
|
|
648
|
+
for enum in attr_data.get("enumlist", []):
|
|
649
|
+
cursor.execute(
|
|
650
|
+
"""
|
|
651
|
+
INSERT INTO attribute_enums (attribute_id, name, digest, description, attrs)
|
|
652
|
+
VALUES (?, ?, ?, ?, ?)
|
|
653
|
+
""",
|
|
654
|
+
(
|
|
655
|
+
attr_id,
|
|
656
|
+
enum.get("name"),
|
|
657
|
+
enum.get("digest"),
|
|
658
|
+
enum.get("description"),
|
|
659
|
+
self._extract_extra_attrs(enum, enum_exclude),
|
|
660
|
+
),
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
def _insert_examples(
|
|
664
|
+
self, cursor: sqlite3.Cursor, object_id: int, examples: List[Dict]
|
|
665
|
+
) -> None:
|
|
666
|
+
"""Insert example records."""
|
|
667
|
+
exclude = ["name", "caption"]
|
|
668
|
+
for example in examples:
|
|
669
|
+
cursor.execute(
|
|
670
|
+
"""
|
|
671
|
+
INSERT INTO examples (object_id, name, caption, attrs)
|
|
672
|
+
VALUES (?, ?, ?, ?)
|
|
673
|
+
""",
|
|
674
|
+
(
|
|
675
|
+
object_id,
|
|
676
|
+
example.get("name"),
|
|
677
|
+
example.get("caption"),
|
|
678
|
+
self._extract_extra_attrs(example, exclude),
|
|
679
|
+
),
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
def _insert_seealso(
|
|
683
|
+
self, cursor: sqlite3.Cursor, object_id: int, refs: List[str]
|
|
684
|
+
) -> None:
|
|
685
|
+
"""Insert see-also references."""
|
|
686
|
+
for ref in refs:
|
|
687
|
+
cursor.execute(
|
|
688
|
+
"INSERT INTO seealso (object_id, ref_name) VALUES (?, ?)",
|
|
689
|
+
(object_id, ref),
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
def _insert_misc(
|
|
693
|
+
self, cursor: sqlite3.Cursor, object_id: int, misc: Dict[str, Dict[str, str]]
|
|
694
|
+
) -> None:
|
|
695
|
+
"""Insert misc entries."""
|
|
696
|
+
for category, entries in misc.items():
|
|
697
|
+
for entry_name, description in entries.items():
|
|
698
|
+
cursor.execute(
|
|
699
|
+
"""
|
|
700
|
+
INSERT INTO misc (object_id, category, entry_name, description)
|
|
701
|
+
VALUES (?, ?, ?, ?)
|
|
702
|
+
""",
|
|
703
|
+
(object_id, category, entry_name, description),
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
def _insert_palette(
|
|
707
|
+
self, cursor: sqlite3.Cursor, object_id: int, palette: Dict[str, Any]
|
|
708
|
+
) -> None:
|
|
709
|
+
"""Insert palette info."""
|
|
710
|
+
if not palette:
|
|
711
|
+
return
|
|
712
|
+
exclude = ["category", "palette_index"]
|
|
713
|
+
idx = palette.get("palette_index")
|
|
714
|
+
cursor.execute(
|
|
715
|
+
"""
|
|
716
|
+
INSERT INTO palette (object_id, category, palette_index, attrs)
|
|
717
|
+
VALUES (?, ?, ?, ?)
|
|
718
|
+
""",
|
|
719
|
+
(
|
|
720
|
+
object_id,
|
|
721
|
+
palette.get("category"),
|
|
722
|
+
int(idx) if idx else None,
|
|
723
|
+
self._extract_extra_attrs(palette, exclude),
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
def _insert_parameter(
|
|
728
|
+
self, cursor: sqlite3.Cursor, object_id: int, parameter: Dict[str, Any]
|
|
729
|
+
) -> None:
|
|
730
|
+
"""Insert parameter info."""
|
|
731
|
+
if not parameter:
|
|
732
|
+
return
|
|
733
|
+
cursor.execute(
|
|
734
|
+
"INSERT INTO parameter (object_id, attrs) VALUES (?, ?)",
|
|
735
|
+
(object_id, json.dumps(parameter)),
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
def insert_object(self, name: str, data: Dict[str, Any]) -> int:
|
|
739
|
+
"""Insert object data into database
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
name: Object name
|
|
743
|
+
data: Object data dictionary from maxref
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
Object ID
|
|
747
|
+
"""
|
|
748
|
+
with self._get_cursor() as cursor:
|
|
749
|
+
object_id = self._insert_main_object(cursor, name, data)
|
|
750
|
+
self._delete_related_records(cursor, object_id)
|
|
751
|
+
|
|
752
|
+
self._insert_metadata(cursor, object_id, data.get("metadata", {}))
|
|
753
|
+
self._insert_inlets_outlets(
|
|
754
|
+
cursor, object_id, data.get("inlets", []), "inlets"
|
|
755
|
+
)
|
|
756
|
+
self._insert_inlets_outlets(
|
|
757
|
+
cursor, object_id, data.get("outlets", []), "outlets"
|
|
758
|
+
)
|
|
759
|
+
self._insert_objargs(cursor, object_id, data.get("objargs", []))
|
|
760
|
+
self._insert_methods(cursor, object_id, data.get("methods", {}))
|
|
761
|
+
self._insert_attributes(cursor, object_id, data.get("attributes", {}))
|
|
762
|
+
self._insert_examples(cursor, object_id, data.get("examples", []))
|
|
763
|
+
self._insert_seealso(cursor, object_id, data.get("seealso", []))
|
|
764
|
+
self._insert_misc(cursor, object_id, data.get("misc", {}))
|
|
765
|
+
self._insert_palette(cursor, object_id, data.get("palette", {}))
|
|
766
|
+
self._insert_parameter(cursor, object_id, data.get("parameter", {}))
|
|
767
|
+
|
|
768
|
+
return object_id
|
|
769
|
+
|
|
770
|
+
# -------------------------------------------------------------------------
|
|
771
|
+
# Get helpers
|
|
772
|
+
# -------------------------------------------------------------------------
|
|
773
|
+
|
|
774
|
+
def _get_simple_list(
|
|
775
|
+
self, cursor: sqlite3.Cursor, table: str, object_id: int
|
|
776
|
+
) -> List[Dict[str, Any]]:
|
|
777
|
+
"""Fetch all rows from a table as a list of dicts."""
|
|
778
|
+
cursor.execute(f"SELECT * FROM {table} WHERE object_id = ?", (object_id,))
|
|
779
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
780
|
+
|
|
781
|
+
def _get_metadata(self, cursor: sqlite3.Cursor, object_id: int) -> Dict[str, Any]:
|
|
782
|
+
"""Fetch metadata, collapsing duplicate keys into lists."""
|
|
783
|
+
cursor.execute(
|
|
784
|
+
"SELECT key, value FROM metadata WHERE object_id = ?", (object_id,)
|
|
785
|
+
)
|
|
786
|
+
metadata: Dict[str, Any] = {}
|
|
787
|
+
for row in cursor.fetchall():
|
|
788
|
+
key, value = row["key"], row["value"]
|
|
789
|
+
if key in metadata:
|
|
790
|
+
if not isinstance(metadata[key], list):
|
|
791
|
+
metadata[key] = [metadata[key]]
|
|
792
|
+
metadata[key].append(value)
|
|
793
|
+
else:
|
|
794
|
+
metadata[key] = value
|
|
795
|
+
return metadata
|
|
796
|
+
|
|
797
|
+
def _get_methods(
|
|
798
|
+
self, cursor: sqlite3.Cursor, object_id: int
|
|
799
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
800
|
+
"""Fetch methods with their arguments."""
|
|
801
|
+
cursor.execute("SELECT * FROM methods WHERE object_id = ?", (object_id,))
|
|
802
|
+
methods: Dict[str, Dict[str, Any]] = {}
|
|
803
|
+
for method_row in cursor.fetchall():
|
|
804
|
+
method_data = dict(method_row)
|
|
805
|
+
method_id = method_data["id"]
|
|
806
|
+
cursor.execute(
|
|
807
|
+
"SELECT * FROM method_args WHERE method_id = ?", (method_id,)
|
|
808
|
+
)
|
|
809
|
+
method_data["args"] = [dict(row) for row in cursor.fetchall()]
|
|
810
|
+
methods[method_data["name"]] = method_data
|
|
811
|
+
return methods
|
|
812
|
+
|
|
813
|
+
def _get_attributes(
|
|
814
|
+
self, cursor: sqlite3.Cursor, object_id: int
|
|
815
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
816
|
+
"""Fetch attributes with their enum values."""
|
|
817
|
+
cursor.execute("SELECT * FROM attributes WHERE object_id = ?", (object_id,))
|
|
818
|
+
attributes: Dict[str, Dict[str, Any]] = {}
|
|
819
|
+
for attr_row in cursor.fetchall():
|
|
820
|
+
attr_data = dict(attr_row)
|
|
821
|
+
attr_id = attr_data["id"]
|
|
822
|
+
cursor.execute(
|
|
823
|
+
"SELECT * FROM attribute_enums WHERE attribute_id = ?", (attr_id,)
|
|
824
|
+
)
|
|
825
|
+
attr_data["enumlist"] = [dict(row) for row in cursor.fetchall()]
|
|
826
|
+
attributes[attr_data["name"]] = attr_data
|
|
827
|
+
return attributes
|
|
828
|
+
|
|
829
|
+
def _get_seealso(self, cursor: sqlite3.Cursor, object_id: int) -> List[str]:
|
|
830
|
+
"""Fetch see-also references as a list of names."""
|
|
831
|
+
cursor.execute("SELECT ref_name FROM seealso WHERE object_id = ?", (object_id,))
|
|
832
|
+
return [row["ref_name"] for row in cursor.fetchall()]
|
|
833
|
+
|
|
834
|
+
def _get_misc(
|
|
835
|
+
self, cursor: sqlite3.Cursor, object_id: int
|
|
836
|
+
) -> Dict[str, Dict[str, str]]:
|
|
837
|
+
"""Fetch misc entries grouped by category."""
|
|
838
|
+
cursor.execute("SELECT * FROM misc WHERE object_id = ?", (object_id,))
|
|
839
|
+
misc: Dict[str, Dict[str, str]] = {}
|
|
840
|
+
for row in cursor.fetchall():
|
|
841
|
+
category = row["category"]
|
|
842
|
+
if category not in misc:
|
|
843
|
+
misc[category] = {}
|
|
844
|
+
misc[category][row["entry_name"]] = row["description"]
|
|
845
|
+
return misc
|
|
846
|
+
|
|
847
|
+
def _get_palette(self, cursor: sqlite3.Cursor, object_id: int) -> Dict[str, Any]:
|
|
848
|
+
"""Fetch palette info."""
|
|
849
|
+
cursor.execute("SELECT * FROM palette WHERE object_id = ?", (object_id,))
|
|
850
|
+
row = cursor.fetchone()
|
|
851
|
+
return dict(row) if row else {}
|
|
852
|
+
|
|
853
|
+
def _get_parameter(self, cursor: sqlite3.Cursor, object_id: int) -> Dict[str, Any]:
|
|
854
|
+
"""Fetch parameter info."""
|
|
855
|
+
cursor.execute("SELECT attrs FROM parameter WHERE object_id = ?", (object_id,))
|
|
856
|
+
row = cursor.fetchone()
|
|
857
|
+
if row and row["attrs"]:
|
|
858
|
+
return json.loads(row["attrs"])
|
|
859
|
+
return {}
|
|
860
|
+
|
|
861
|
+
def get_object(self, name: str) -> Optional[Dict[str, Any]]:
|
|
862
|
+
"""Get object data from database
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
name: Object name
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
Object data dictionary or None if not found
|
|
869
|
+
"""
|
|
870
|
+
with self._get_cursor() as cursor:
|
|
871
|
+
cursor.execute("SELECT * FROM objects WHERE name = ?", (name,))
|
|
872
|
+
row = cursor.fetchone()
|
|
873
|
+
if not row:
|
|
874
|
+
return None
|
|
875
|
+
|
|
876
|
+
data = dict(row)
|
|
877
|
+
object_id = data["id"]
|
|
878
|
+
|
|
879
|
+
# Parse and merge root_attrs
|
|
880
|
+
if data["root_attrs"]:
|
|
881
|
+
data.update(json.loads(data["root_attrs"]))
|
|
882
|
+
del data["root_attrs"]
|
|
883
|
+
|
|
884
|
+
# Fetch all related data
|
|
885
|
+
data["metadata"] = self._get_metadata(cursor, object_id)
|
|
886
|
+
data["inlets"] = self._get_simple_list(cursor, "inlets", object_id)
|
|
887
|
+
data["outlets"] = self._get_simple_list(cursor, "outlets", object_id)
|
|
888
|
+
data["objargs"] = self._get_simple_list(cursor, "objargs", object_id)
|
|
889
|
+
data["methods"] = self._get_methods(cursor, object_id)
|
|
890
|
+
data["attributes"] = self._get_attributes(cursor, object_id)
|
|
891
|
+
data["examples"] = self._get_simple_list(cursor, "examples", object_id)
|
|
892
|
+
data["seealso"] = self._get_seealso(cursor, object_id)
|
|
893
|
+
data["misc"] = self._get_misc(cursor, object_id)
|
|
894
|
+
data["palette"] = self._get_palette(cursor, object_id)
|
|
895
|
+
data["parameter"] = self._get_parameter(cursor, object_id)
|
|
896
|
+
|
|
897
|
+
return data
|
|
898
|
+
|
|
899
|
+
def search(self, query: str, fields: Optional[List[str]] = None) -> List[str]:
|
|
900
|
+
"""Search for objects by name, digest, or description
|
|
901
|
+
|
|
902
|
+
Args:
|
|
903
|
+
query: Search query string
|
|
904
|
+
fields: List of fields to search in. Default: ['name', 'digest', 'description']
|
|
905
|
+
|
|
906
|
+
Returns:
|
|
907
|
+
List of matching object names
|
|
908
|
+
"""
|
|
909
|
+
if fields is None:
|
|
910
|
+
fields = ["name", "digest", "description"]
|
|
911
|
+
|
|
912
|
+
with self._get_cursor() as cursor:
|
|
913
|
+
conditions = []
|
|
914
|
+
for field in fields:
|
|
915
|
+
if field in ["name", "digest", "description", "category"]:
|
|
916
|
+
conditions.append(f"{field} LIKE ?")
|
|
917
|
+
|
|
918
|
+
where_clause = " OR ".join(conditions)
|
|
919
|
+
sql = f"SELECT name FROM objects WHERE {where_clause} ORDER BY name"
|
|
920
|
+
|
|
921
|
+
cursor.execute(sql, tuple([f"%{query}%" for _ in conditions]))
|
|
922
|
+
return [row["name"] for row in cursor.fetchall()]
|
|
923
|
+
|
|
924
|
+
def search_objects(
|
|
925
|
+
self, query: str, fields: Optional[List[str]] = None
|
|
926
|
+
) -> List[str]:
|
|
927
|
+
"""Search for objects by name, digest, or description (deprecated: use search())
|
|
928
|
+
|
|
929
|
+
Args:
|
|
930
|
+
query: Search query string
|
|
931
|
+
fields: List of fields to search in. Default: ['name', 'digest', 'description']
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
List of matching object names
|
|
935
|
+
"""
|
|
936
|
+
return self.search(query, fields)
|
|
937
|
+
|
|
938
|
+
def by_category(self, category: str) -> List[str]:
|
|
939
|
+
"""Get all objects in a category
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
category: Category name
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
List of object names
|
|
946
|
+
"""
|
|
947
|
+
with self._get_cursor() as cursor:
|
|
948
|
+
cursor.execute(
|
|
949
|
+
"SELECT name FROM objects WHERE category = ? ORDER BY name", (category,)
|
|
950
|
+
)
|
|
951
|
+
return [row["name"] for row in cursor.fetchall()]
|
|
952
|
+
|
|
953
|
+
def get_objects_by_category(self, category: str) -> List[str]:
|
|
954
|
+
"""Get all objects in a category (deprecated: use by_category())
|
|
955
|
+
|
|
956
|
+
Args:
|
|
957
|
+
category: Category name
|
|
958
|
+
|
|
959
|
+
Returns:
|
|
960
|
+
List of object names
|
|
961
|
+
"""
|
|
962
|
+
return self.by_category(category)
|
|
963
|
+
|
|
964
|
+
@property
|
|
965
|
+
def categories(self) -> List[str]:
|
|
966
|
+
"""All unique categories in database"""
|
|
967
|
+
with self._get_cursor() as cursor:
|
|
968
|
+
cursor.execute(
|
|
969
|
+
"SELECT DISTINCT category FROM objects WHERE category IS NOT NULL ORDER BY category"
|
|
970
|
+
)
|
|
971
|
+
return [row["category"] for row in cursor.fetchall()]
|
|
972
|
+
|
|
973
|
+
def get_all_categories(self) -> List[str]:
|
|
974
|
+
"""Get all unique categories (deprecated: use .categories property)
|
|
975
|
+
|
|
976
|
+
Returns:
|
|
977
|
+
List of category names
|
|
978
|
+
"""
|
|
979
|
+
return self.categories
|
|
980
|
+
|
|
981
|
+
@property
|
|
982
|
+
def count(self) -> int:
|
|
983
|
+
"""Total number of objects in database"""
|
|
984
|
+
with self._get_cursor() as cursor:
|
|
985
|
+
cursor.execute("SELECT COUNT(*) as count FROM objects")
|
|
986
|
+
return cursor.fetchone()["count"]
|
|
987
|
+
|
|
988
|
+
@property
|
|
989
|
+
def objects(self) -> List[str]:
|
|
990
|
+
"""All object names in database"""
|
|
991
|
+
with self._get_cursor() as cursor:
|
|
992
|
+
cursor.execute("SELECT name FROM objects ORDER BY name")
|
|
993
|
+
return [row["name"] for row in cursor.fetchall()]
|
|
994
|
+
|
|
995
|
+
def get_object_count(self) -> int:
|
|
996
|
+
"""Get total number of objects in database (deprecated: use .count property)
|
|
997
|
+
|
|
998
|
+
Returns:
|
|
999
|
+
Object count
|
|
1000
|
+
"""
|
|
1001
|
+
return self.count
|
|
1002
|
+
|
|
1003
|
+
def export(self, output_path: Path):
|
|
1004
|
+
"""Export entire database to JSON file
|
|
1005
|
+
|
|
1006
|
+
Args:
|
|
1007
|
+
output_path: Path to output JSON file
|
|
1008
|
+
"""
|
|
1009
|
+
with self._get_cursor() as cursor:
|
|
1010
|
+
cursor.execute("SELECT name FROM objects ORDER BY name")
|
|
1011
|
+
all_objects = {}
|
|
1012
|
+
|
|
1013
|
+
for row in cursor.fetchall():
|
|
1014
|
+
name = row["name"]
|
|
1015
|
+
all_objects[name] = self.get_object(name)
|
|
1016
|
+
|
|
1017
|
+
output_path.write_text(json.dumps(all_objects, indent=2))
|
|
1018
|
+
|
|
1019
|
+
def export_to_json(self, output_path: Path):
|
|
1020
|
+
"""Export entire database to JSON file (deprecated: use export())
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
output_path: Path to output JSON file
|
|
1024
|
+
"""
|
|
1025
|
+
self.export(output_path)
|
|
1026
|
+
|
|
1027
|
+
def load(self, input_path: Path):
|
|
1028
|
+
"""Import objects from JSON file
|
|
1029
|
+
|
|
1030
|
+
Args:
|
|
1031
|
+
input_path: Path to input JSON file
|
|
1032
|
+
"""
|
|
1033
|
+
data = json.loads(input_path.read_text())
|
|
1034
|
+
for name, obj_data in data.items():
|
|
1035
|
+
self.insert_object(name, obj_data)
|
|
1036
|
+
|
|
1037
|
+
def import_from_json(self, input_path: Path):
|
|
1038
|
+
"""Import objects from JSON file (deprecated: use load())
|
|
1039
|
+
|
|
1040
|
+
Args:
|
|
1041
|
+
input_path: Path to input JSON file
|
|
1042
|
+
"""
|
|
1043
|
+
self.load(input_path)
|
|
1044
|
+
|
|
1045
|
+
def summary(self) -> Dict[str, Any]:
|
|
1046
|
+
"""Get database summary statistics
|
|
1047
|
+
|
|
1048
|
+
Returns:
|
|
1049
|
+
Dictionary with count, categories, and category counts
|
|
1050
|
+
"""
|
|
1051
|
+
summary = {
|
|
1052
|
+
"total_objects": self.count,
|
|
1053
|
+
"categories": {},
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
categories_dict: Dict[str, int] = {}
|
|
1057
|
+
for category in self.categories:
|
|
1058
|
+
categories_dict[category] = len(self.by_category(category))
|
|
1059
|
+
summary["categories"] = categories_dict
|
|
1060
|
+
|
|
1061
|
+
return summary
|
|
1062
|
+
|
|
1063
|
+
def __repr__(self) -> str:
|
|
1064
|
+
"""String representation of database"""
|
|
1065
|
+
if self.db_path == ":memory:":
|
|
1066
|
+
location = "in-memory"
|
|
1067
|
+
else:
|
|
1068
|
+
location = str(self.db_path)
|
|
1069
|
+
return f"MaxRefDB({location}, {self.count} objects)"
|
|
1070
|
+
|
|
1071
|
+
def _auto_populate_cache(self):
|
|
1072
|
+
"""Auto-populate cache database with all Max objects"""
|
|
1073
|
+
import sys
|
|
1074
|
+
|
|
1075
|
+
print("Initializing py2max cache (one-time setup)...", file=sys.stderr)
|
|
1076
|
+
print(f"Location: {self.db_path}", file=sys.stderr)
|
|
1077
|
+
print("Populating with all Max objects (1157 objects)...", file=sys.stderr)
|
|
1078
|
+
self.populate()
|
|
1079
|
+
print(f"Cache ready with {self.count} objects", file=sys.stderr)
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
__all__ = ["MaxRefDB"]
|