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 +42 -0
- dmstudio/agent.py +62 -0
- dmstudio/bootstrap.py +92 -0
- dmstudio/command_registry.py +264 -0
- dmstudio/dialog.py +81 -0
- dmstudio/dm_io.py +177 -0
- dmstudio/dmcommands.py +3820 -0
- dmstudio/dmcommands_generated.py +54273 -0
- dmstudio/dmfiles.py +210 -0
- dmstudio/dmfiles_generated.py +2161 -0
- dmstudio/initialize.py +167 -0
- dmstudio/mcp_server.py +202 -0
- dmstudio/notebook_builder.py +184 -0
- dmstudio/sandbox.py +182 -0
- dmstudio/special.py +251 -0
- dmstudio/superprocess.py +146 -0
- dmstudio/version.py +3 -0
- dmstudio_rm-2.0.0b4.dist-info/METADATA +427 -0
- dmstudio_rm-2.0.0b4.dist-info/RECORD +23 -0
- dmstudio_rm-2.0.0b4.dist-info/WHEEL +5 -0
- dmstudio_rm-2.0.0b4.dist-info/entry_points.txt +2 -0
- dmstudio_rm-2.0.0b4.dist-info/licenses/LICENSE.txt +679 -0
- dmstudio_rm-2.0.0b4.dist-info/top_level.txt +1 -0
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)
|