dmstudio-rm 2.0.0b4__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.
dmstudio/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ '''
2
+ dmstudio package
3
+ ----------------
4
+
5
+ Python package for Datamine Studio RM scripting via Windows COM automation.
6
+
7
+ Submodules:
8
+ dmcommands - Auto-generated wrappers for all Studio RM commands.
9
+ dmfiles - Studio commands that generate Datamine files (INPFIL etc).
10
+ initialize - COM initialization helpers.
11
+ special - Special/adapted Studio command helpers.
12
+ superprocess - Multi-command workflow helpers.
13
+ agent - Backward compatibility re-export layer for legacy AI agent scripts.
14
+ notebook_builder- Jupyter Notebook builder for auditable agent workflows.
15
+ sandbox - Sandbox management and dataset copy helpers.
16
+ dm_io - High-level DataFrame ↔ Datamine file I/O operations.
17
+ dialog - Windows modal dialog auto-dismissal context.
18
+ bootstrap - Tutorial bootstrapping and download helpers.
19
+ '''
20
+
21
+ # Proactively import pandas at the package root to avoid Python 3.14 / Cython circular import conflicts in debuggers
22
+ try:
23
+ import pandas as _pd
24
+ except ImportError:
25
+ pass
26
+
27
+ from dmstudio import dmcommands
28
+ from dmstudio import dmfiles
29
+ from dmstudio import dmcommands_generated
30
+ from dmstudio import dmfiles_generated
31
+ from dmstudio import initialize
32
+ from dmstudio import special
33
+ from dmstudio import superprocess
34
+ from dmstudio import agent
35
+ from dmstudio import notebook_builder
36
+ from dmstudio import sandbox
37
+ from dmstudio import dm_io
38
+ from dmstudio import dialog
39
+ from dmstudio import bootstrap
40
+
41
+ # Shortcuts
42
+ from dmstudio.bootstrap import download_tutorials
dmstudio/agent.py ADDED
@@ -0,0 +1,62 @@
1
+ '''
2
+ dmstudio.agent
3
+ --------------
4
+
5
+ AI agent helper module for Datamine Studio RM scripting.
6
+
7
+ Command discovery (list_commands, get_command_schema, search_commands) is
8
+ implemented in dmstudio.command_registry and re-exported here for backwards
9
+ compatibility. Import directly from command_registry when you need discovery
10
+ without COM, threading, or pandas side-effects.
11
+
12
+ Provides:
13
+ - list_commands() : Return all available Datamine command names and descriptions.
14
+ - get_command_schema() : Return the JSON schema (parameters, types) for a given command.
15
+ - search_commands() : Fuzzy-search commands by name or keyword.
16
+ - read_datamine() : Read a .dm or .dmx binary file into a pandas DataFrame using
17
+ the DmFile.DmTableADO COM object — no proprietary deps required.
18
+ - dialog_dismiss_context(): Context manager that auto-dismisses blocking Studio RM modal
19
+ dialogs in a background thread (opt-in).
20
+ '''
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Command discovery — re-exported from command_registry for backwards compat
25
+ # ---------------------------------------------------------------------------
26
+ from dmstudio.command_registry import ( # noqa: F401
27
+ list_commands,
28
+ get_command_schema,
29
+ search_commands,
30
+ )
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # DataFrame I/O — re-exported from dm_io for backwards compat
34
+ # ---------------------------------------------------------------------------
35
+ from dmstudio.dm_io import ( # noqa: F401
36
+ read_datamine,
37
+ to_datamine,
38
+ patch_dataframe,
39
+ )
40
+ patch_dataframe()
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Auto-dialog dismissal context manager — re-exported from dialog for compat
45
+ # ---------------------------------------------------------------------------
46
+ from dmstudio.dialog import dialog_dismiss_context # noqa: F401
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Sandbox management — re-exported from sandbox for backwards compat
51
+ # ---------------------------------------------------------------------------
52
+ from dmstudio.sandbox import ( # noqa: F401
53
+ copy_database_files,
54
+ initialize_sandbox,
55
+ )
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Tutorial bootstrapping — re-exported from bootstrap for backwards compat
60
+ # ---------------------------------------------------------------------------
61
+ from dmstudio.bootstrap import download_tutorials # noqa: F401
62
+
dmstudio/bootstrap.py ADDED
@@ -0,0 +1,92 @@
1
+ '''
2
+ dmstudio.bootstrap
3
+ ------------------
4
+
5
+ Bootstrapping utility to download tutorials and sample datasets.
6
+ '''
7
+ import os
8
+ import shutil
9
+ import tempfile
10
+ import urllib.request
11
+ import zipfile
12
+
13
+
14
+ def download_tutorials(target_dir):
15
+ '''
16
+ download_tutorials
17
+ ------------------
18
+
19
+ Download the latest tutorials and sample datasets from the GitHub repository
20
+ and extract them into the specified target directory.
21
+
22
+ Parameters:
23
+ -----------
24
+ target_dir: str
25
+ Absolute or relative path to the folder where the "tutorials" directory
26
+ should be created.
27
+ '''
28
+ # 1. Validate target directory
29
+ target_dir = os.path.abspath(target_dir)
30
+ os.makedirs(target_dir, exist_ok=True)
31
+
32
+ url = 'https://github.com/nazabrory/dmstudio-rm/archive/refs/heads/master.zip'
33
+ fallback_url = 'https://github.com/nazabrory/dmstudio-rm/archive/refs/heads/main.zip'
34
+
35
+ # 2. Create temporary file
36
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as tmp_file:
37
+ tmp_path = tmp_file.name
38
+
39
+ try:
40
+ # Download the zip
41
+ try:
42
+ print('Downloading tutorials from {}...'.format(url))
43
+ urllib.request.urlretrieve(url, tmp_path)
44
+ except Exception:
45
+ print('Failed to download from default branch. Trying fallback: {}...'.format(fallback_url))
46
+ urllib.request.urlretrieve(fallback_url, tmp_path)
47
+
48
+ # 3. Extract the tutorials folder
49
+ print('Extracting tutorials...')
50
+ with zipfile.ZipFile(tmp_path, 'r') as zip_ref:
51
+ # Find the top level directory name inside the zip (typically 'dmstudio-main')
52
+ top_dir = None
53
+ for name in zip_ref.namelist():
54
+ if name.endswith('/tutorials/'):
55
+ top_dir = name.split('/')[0]
56
+ break
57
+
58
+ if not top_dir:
59
+ # Try fallback: look for any 'tutorials/' entry
60
+ for name in zip_ref.namelist():
61
+ if 'tutorials/' in name:
62
+ top_dir = name.split('/')[0]
63
+ break
64
+
65
+ if not top_dir:
66
+ raise RuntimeError('Could not find \'tutorials\' folder in the downloaded zip archive.')
67
+
68
+ # Extract only files in the tutorials folder
69
+ tutorials_prefix = '{}/tutorials/'.format(top_dir)
70
+ extracted_count = 0
71
+ for member in zip_ref.infolist():
72
+ if member.filename.startswith(tutorials_prefix) and not member.is_dir():
73
+ # Compute relative path under 'tutorials/'
74
+ rel_path = member.filename[len(tutorials_prefix):]
75
+ out_path = os.path.join(target_dir, 'tutorials', rel_path)
76
+
77
+ # Ensure parent dir exists
78
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
79
+
80
+ # Write file
81
+ with zip_ref.open(member) as source, open(out_path, 'wb') as target:
82
+ shutil.copyfileobj(source, target)
83
+ extracted_count += 1
84
+
85
+ print('Successfully extracted {} tutorial files to: {}'.format(
86
+ extracted_count, os.path.join(target_dir, 'tutorials')
87
+ ))
88
+
89
+ finally:
90
+ # Clean up temporary file
91
+ if os.path.exists(tmp_path):
92
+ os.remove(tmp_path)
@@ -0,0 +1,264 @@
1
+ '''
2
+ dmstudio.command_registry
3
+ --------------------------
4
+
5
+ Command discovery module for Datamine Studio RM.
6
+
7
+ Provides a read-only index over all command wrappers exposed by
8
+ dmstudio.dmcommands.init, dmstudio.dmfiles.init, and their generated fallback equivalents.
9
+
10
+ Interface (seam):
11
+ list_commands() -> list[dict]
12
+ get_command_schema(str) -> dict
13
+ search_commands(str) -> list[dict]
14
+
15
+ No COM, threading, or pandas dependency — safe to import in any context,
16
+ including environments without Datamine Studio RM installed.
17
+ '''
18
+ import inspect
19
+
20
+ from dmstudio import dmcommands, dmfiles
21
+
22
+ try:
23
+ from dmstudio import dmcommands_generated
24
+ except ImportError:
25
+ dmcommands_generated = None
26
+
27
+ try:
28
+ from dmstudio import dmfiles_generated
29
+ except ImportError:
30
+ dmfiles_generated = None
31
+
32
+
33
+ # Canonical list of verified commands
34
+ VERIFIED_COMMANDS = {
35
+ 'accmlt', 'append', 'cokrig', 'compdh', 'copy',
36
+ 'count', 'delete', 'diffrn', 'dmedit', 'extra',
37
+ 'holes3d', 'ijkgen', 'join', 'mgsort', 'modtra',
38
+ 'output', 'selcop', 'seldel', 'selexy', 'sortx',
39
+ 'stats', 'tongrad', 'inpfil', 'estima', 'protom'
40
+ }
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Internal helpers (implementation — not part of the interface)
45
+ # ---------------------------------------------------------------------------
46
+
47
+ def _get_init_classes():
48
+ '''Return the list of init classes to inspect (both dmcommands/dmfiles and generated/experimental).'''
49
+ classes = []
50
+ if inspect.isclass(dmcommands.init):
51
+ classes.append(dmcommands.init)
52
+ else:
53
+ classes.append(type(dmcommands.init))
54
+ if inspect.isclass(dmfiles.init):
55
+ classes.append(dmfiles.init)
56
+ else:
57
+ classes.append(type(dmfiles.init))
58
+
59
+ if dmcommands_generated is not None:
60
+ if inspect.isclass(dmcommands_generated.init):
61
+ classes.append(dmcommands_generated.init)
62
+ else:
63
+ classes.append(type(dmcommands_generated.init))
64
+
65
+ if dmfiles_generated is not None:
66
+ if inspect.isclass(dmfiles_generated.init):
67
+ classes.append(dmfiles_generated.init)
68
+ else:
69
+ classes.append(type(dmfiles_generated.init))
70
+
71
+ return classes
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Public interface
76
+ # ---------------------------------------------------------------------------
77
+
78
+ def list_commands():
79
+ '''
80
+ list_commands
81
+ -------------
82
+
83
+ Return a list of all available Datamine command wrapper names exposed
84
+ by verified and experimental modules. Each entry is a dict
85
+ with 'name', 'doc', and 'experimental' keys.
86
+
87
+ Returns:
88
+ --------
89
+ list of dict
90
+ [{'name': 'copy', 'doc': 'COPY ...', 'experimental': False}, ...]
91
+ '''
92
+ results = []
93
+ seen = set()
94
+ classes = _get_init_classes()
95
+
96
+ for cls in classes:
97
+ for name in dir(cls):
98
+ if name.startswith('_') or name in ('run_command', 'parse_infields_list'):
99
+ continue
100
+ if name in seen:
101
+ continue
102
+ try:
103
+ attr = getattr(cls, name)
104
+ except Exception:
105
+ continue
106
+ if callable(attr):
107
+ doc = inspect.getdoc(attr) or ''
108
+ first_line = doc.split('\n')[0].strip()
109
+ experimental = name.lower() not in VERIFIED_COMMANDS
110
+ results.append({
111
+ 'name': name,
112
+ 'doc': first_line,
113
+ 'experimental': experimental
114
+ })
115
+ seen.add(name)
116
+
117
+ return sorted(results, key=lambda x: x['name'])
118
+
119
+
120
+ def get_command_schema(cmd_name):
121
+ '''
122
+ get_command_schema
123
+ ------------------
124
+
125
+ Return a JSON-serialisable schema for a given Datamine command wrapper.
126
+
127
+ Parameters:
128
+ -----------
129
+ cmd_name: str
130
+ Name of the command (case-insensitive), e.g. 'copy', 'COPY', 'protom'.
131
+
132
+ Returns:
133
+ --------
134
+ dict
135
+ {
136
+ 'name': str,
137
+ 'doc': str,
138
+ 'parameters': [{'name': str, 'default': any, 'annotation': str}, ...],
139
+ 'experimental': bool
140
+ }
141
+
142
+ Raises:
143
+ -------
144
+ ValueError
145
+ If the command is not found.
146
+ '''
147
+ cmd_name_lower = cmd_name.lower()
148
+ classes = _get_init_classes()
149
+ func = None
150
+
151
+ # Search init class methods first (where all wrappers live)
152
+ for cls in classes:
153
+ for name in dir(cls):
154
+ if name.lower() == cmd_name_lower:
155
+ try:
156
+ func = getattr(cls, name)
157
+ if callable(func):
158
+ break
159
+ except Exception:
160
+ pass
161
+ if func is not None:
162
+ break
163
+
164
+ # Fallback 1: search module-level names in dmcommands
165
+ if func is None:
166
+ for name in dir(dmcommands):
167
+ if name.lower() == cmd_name_lower:
168
+ try:
169
+ attr = getattr(dmcommands, name)
170
+ if callable(attr):
171
+ func = attr
172
+ break
173
+ except Exception:
174
+ pass
175
+
176
+ # Fallback 2: search module-level names in dmfiles
177
+ if func is None:
178
+ for name in dir(dmfiles):
179
+ if name.lower() == cmd_name_lower:
180
+ try:
181
+ attr = getattr(dmfiles, name)
182
+ if callable(attr):
183
+ func = attr
184
+ break
185
+ except Exception:
186
+ pass
187
+
188
+ # Fallback 3: search module-level names in dmcommands_generated
189
+ if func is None and dmcommands_generated is not None:
190
+ for name in dir(dmcommands_generated):
191
+ if name.lower() == cmd_name_lower:
192
+ try:
193
+ attr = getattr(dmcommands_generated, name)
194
+ if callable(attr):
195
+ func = attr
196
+ break
197
+ except Exception as e:
198
+ pass
199
+
200
+ # Fallback 4: search module-level names in dmfiles_generated
201
+ if func is None and dmfiles_generated is not None:
202
+ for name in dir(dmfiles_generated):
203
+ if name.lower() == cmd_name_lower:
204
+ try:
205
+ attr = getattr(dmfiles_generated, name)
206
+ if callable(attr):
207
+ func = attr
208
+ break
209
+ except Exception as e:
210
+ pass
211
+
212
+ if func is None:
213
+ raise ValueError('Command not found: {}'.format(cmd_name))
214
+
215
+ try:
216
+ sig = inspect.signature(func)
217
+ except (ValueError, TypeError):
218
+ sig = None
219
+
220
+ params = []
221
+ if sig:
222
+ for pname, param in sig.parameters.items():
223
+ if pname in ('self', 'cls'):
224
+ continue
225
+ default = param.default
226
+ if default is inspect.Parameter.empty:
227
+ default = None
228
+ annotation = str(param.annotation) if param.annotation is not inspect.Parameter.empty else 'any'
229
+ params.append({'name': pname, 'default': str(default), 'annotation': annotation})
230
+
231
+ doc = inspect.getdoc(func) or ''
232
+
233
+ return {
234
+ 'name': cmd_name,
235
+ 'doc': doc,
236
+ 'parameters': params,
237
+ 'experimental': cmd_name_lower not in VERIFIED_COMMANDS,
238
+ }
239
+
240
+
241
+ def search_commands(query):
242
+ '''
243
+ search_commands
244
+ ---------------
245
+
246
+ Search command names and docstrings for a keyword or phrase.
247
+
248
+ Parameters:
249
+ -----------
250
+ query: str
251
+ Search term (case-insensitive).
252
+
253
+ Returns:
254
+ --------
255
+ list of dict
256
+ Matching commands: [{'name': str, 'doc': str, 'experimental': bool}, ...]
257
+ '''
258
+ query_lower = query.lower()
259
+ results = []
260
+ for entry in list_commands():
261
+ if query_lower in entry['name'].lower() or query_lower in entry['doc'].lower():
262
+ results.append(entry)
263
+ return results
264
+
dmstudio/dialog.py ADDED
@@ -0,0 +1,81 @@
1
+ '''
2
+ dmstudio.dialog
3
+ ---------------
4
+
5
+ Context manager that spawns a background thread to automatically dismiss
6
+ blocking "#32770" modal dialogs produced by Datamine Studio RM.
7
+ '''
8
+ import threading
9
+ import time
10
+ import contextlib
11
+
12
+ try:
13
+ import win32gui
14
+ import win32con
15
+ import win32api
16
+ WIN32_AVAILABLE = True
17
+ except ImportError:
18
+ WIN32_AVAILABLE = False
19
+
20
+
21
+ @contextlib.contextmanager
22
+ def dialog_dismiss_context(title='Studio RM', interval=1.0):
23
+ '''
24
+ dialog_dismiss_context
25
+ ----------------------
26
+
27
+ Context manager that spawns a background thread to automatically dismiss
28
+ blocking "#32770" modal dialogs produced by Datamine Studio RM (e.g.
29
+ "Duplicate Project File" prompts). This is opt-in: it only runs when
30
+ explicitly used as a context manager.
31
+
32
+ Parameters:
33
+ -----------
34
+ title: str
35
+ Window title to match for dismissal. Default: 'Studio RM'.
36
+ interval: float
37
+ Polling interval in seconds. Default: 1.0.
38
+
39
+ Usage:
40
+ ------
41
+ from dmstudio import dialog, dmcommands
42
+ cmd = dmcommands.init(version='StudioRM')
43
+
44
+ with dialog.dialog_dismiss_context():
45
+ cmd.copy(in_i='source', out_o='dest')
46
+ '''
47
+ stop_event = threading.Event()
48
+ t = None
49
+
50
+ def _dismiss_loop():
51
+ if not WIN32_AVAILABLE:
52
+ return
53
+ while not stop_event.is_set():
54
+ try:
55
+ def _enum_callback(hwnd, _):
56
+ if not win32gui.IsWindowVisible(hwnd):
57
+ return
58
+ cls = win32gui.GetClassName(hwnd)
59
+ ttl = win32gui.GetWindowText(hwnd)
60
+ if cls == '#32770' and title in ttl:
61
+ # Try 'Yes' button (ID=6), then 'OK' button (ID=1)
62
+ for btn_id in (6, 1):
63
+ try:
64
+ win32api.SendMessage(hwnd, win32con.WM_COMMAND, btn_id, 0)
65
+ except Exception:
66
+ pass
67
+ win32gui.EnumWindows(_enum_callback, None)
68
+ except Exception:
69
+ pass
70
+ time.sleep(interval)
71
+
72
+ if WIN32_AVAILABLE:
73
+ t = threading.Thread(target=_dismiss_loop, daemon=True)
74
+ t.start()
75
+
76
+ try:
77
+ yield
78
+ finally:
79
+ stop_event.set()
80
+ if t is not None:
81
+ t.join(timeout=interval * 2)