DescripyTor 0.1.0__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.
- MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/_dx_check.py +382 -0
- MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/descriptor_extractor.py +703 -0
- MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/feature_packages.py +652 -0
- MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/make_picker.py +506 -0
- MolFeatures/Getting_started_with_examples/descriptor_extraction_toolkit/webapp/app.py +1835 -0
- MolFeatures/M1_pre_calculations/__init__.py +0 -0
- MolFeatures/M1_pre_calculations/main.py +69 -0
- MolFeatures/M1_pre_calculations/outwards_senders.py +104 -0
- MolFeatures/M1_pre_calculations/pre_calculation_editors.py +74 -0
- MolFeatures/M2_data_extractor/__init__.py +45 -0
- MolFeatures/M2_data_extractor/cube_sterimol.py +616 -0
- MolFeatures/M2_data_extractor/data_extractor.py +2284 -0
- MolFeatures/M2_data_extractor/extractor_utils/__init__.py +0 -0
- MolFeatures/M2_data_extractor/extractor_utils/bond_angle_length_utils.py +247 -0
- MolFeatures/M2_data_extractor/extractor_utils/dipole_utils.py +275 -0
- MolFeatures/M2_data_extractor/extractor_utils/sterimol_utils.py +913 -0
- MolFeatures/M2_data_extractor/extractor_utils/sterimol_utils_old.py +859 -0
- MolFeatures/M2_data_extractor/extractor_utils/vibrations_utils.py +692 -0
- MolFeatures/M2_data_extractor/feather_extractor.py +740 -0
- MolFeatures/M2_data_extractor/feature_extraction_gui.html +6363 -0
- MolFeatures/M2_data_extractor/gaussian_handler.py +235 -0
- MolFeatures/M2_data_extractor/gui_server.py +454 -0
- MolFeatures/M2_data_extractor/morfeus_sterics.py +214 -0
- MolFeatures/M2_data_extractor/sterimol_standalone.py +1781 -0
- MolFeatures/M3_modeler/__init__.py +0 -0
- MolFeatures/M3_modeler/modeling.py +3566 -0
- MolFeatures/M3_modeler/modeling_utils.py +1200 -0
- MolFeatures/M3_modeler/plot.py +3862 -0
- MolFeatures/M3_modeler/plot_backup.py +3772 -0
- MolFeatures/MolAlign/MCS_optimization.py +131 -0
- MolFeatures/MolAlign/RDKIT_utils.py +655 -0
- MolFeatures/MolAlign/__init__.py +0 -0
- MolFeatures/MolAlign/df_funs.py +99 -0
- MolFeatures/MolAlign/extract_bo.py +251 -0
- MolFeatures/MolAlign/fmcs.py +2654 -0
- MolFeatures/MolAlign/gaussian_handler.py +193 -0
- MolFeatures/MolAlign/loss.py +19 -0
- MolFeatures/MolAlign/molecules/results/json_swap.py +120 -0
- MolFeatures/MolAlign/pybel_ff.py +56 -0
- MolFeatures/MolAlign/renumbering.py +821 -0
- MolFeatures/MolAlign/rotations.py +14 -0
- MolFeatures/MolAlign/trial.py +48 -0
- MolFeatures/__init__.py +16 -0
- MolFeatures/__main__.py +1848 -0
- MolFeatures/utils/__init__.py +0 -0
- MolFeatures/utils/_build_viewer_nb.py +894 -0
- MolFeatures/utils/constants.py +64 -0
- MolFeatures/utils/file_handlers.py +154 -0
- MolFeatures/utils/help_functions.py +1818 -0
- MolFeatures/utils/mcs_and_alignment.py +339 -0
- MolFeatures/utils/obabel_wrappers.py +21 -0
- MolFeatures/utils/rdkit_nx_wrappers.py +67 -0
- MolFeatures/utils/rdkit_utils.py +158 -0
- MolFeatures/utils/visualize.py +875 -0
- descripytor-0.1.0.dist-info/METADATA +723 -0
- descripytor-0.1.0.dist-info/RECORD +58 -0
- descripytor-0.1.0.dist-info/WHEEL +5 -0
- descripytor-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Preflight checks for descriptor_extractor.py runs.
|
|
3
|
+
|
|
4
|
+
This script is intentionally lightweight: it validates paths, optional packages,
|
|
5
|
+
file-name alignment, Gaussian log contents, and atom-index ranges before a run.
|
|
6
|
+
It does not extract descriptors.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import contextlib
|
|
13
|
+
import glob
|
|
14
|
+
import importlib.util
|
|
15
|
+
import io
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
pd = None
|
|
23
|
+
PANDAS_IMPORT_ERROR = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ENGINE_PACKAGES = {
|
|
27
|
+
"rdkit": ["rdkit", "ase"],
|
|
28
|
+
"rdkit_fp": ["rdkit", "ase"],
|
|
29
|
+
"mordred": ["mordred", "rdkit", "ase"],
|
|
30
|
+
"deepchem": ["deepchem"],
|
|
31
|
+
"rafbl": ["moltop"],
|
|
32
|
+
"qm": ["autoqchem"],
|
|
33
|
+
"aqme_qdescp": ["aqme"],
|
|
34
|
+
"morfeus_sterimol": ["morfeus"],
|
|
35
|
+
"morfeus_suite": ["morfeus"],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
XYZ_ENGINES = {
|
|
39
|
+
"xyz_sterimol",
|
|
40
|
+
"xyz_geometry",
|
|
41
|
+
"xyz_buried_volume",
|
|
42
|
+
"morfeus_sterimol",
|
|
43
|
+
"morfeus_suite",
|
|
44
|
+
"rdkit",
|
|
45
|
+
"rdkit_fp",
|
|
46
|
+
"mordred",
|
|
47
|
+
"deepchem",
|
|
48
|
+
"rafbl",
|
|
49
|
+
"aqme_qdescp",
|
|
50
|
+
}
|
|
51
|
+
FEATHER_ENGINES = {"descripytor_full", "descripytor_steric"}
|
|
52
|
+
LOG_ENGINES = {"qm"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Report:
|
|
56
|
+
def __init__(self):
|
|
57
|
+
self.errors = []
|
|
58
|
+
self.warnings = []
|
|
59
|
+
self.ok = []
|
|
60
|
+
|
|
61
|
+
def add(self, level, message):
|
|
62
|
+
getattr(self, level).append(message)
|
|
63
|
+
|
|
64
|
+
def print(self):
|
|
65
|
+
for title, items, mark in (
|
|
66
|
+
("OK", self.ok, "[ok]"),
|
|
67
|
+
("Warnings", self.warnings, "[warn]"),
|
|
68
|
+
("Errors", self.errors, "[error]"),
|
|
69
|
+
):
|
|
70
|
+
if not items:
|
|
71
|
+
continue
|
|
72
|
+
print(f"\n{title}")
|
|
73
|
+
for item in items:
|
|
74
|
+
print(f" {mark} {item}")
|
|
75
|
+
print(f"\nSummary: {len(self.errors)} errors, {len(self.warnings)} warnings, {len(self.ok)} ok")
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def exit_code(self):
|
|
79
|
+
return 2 if self.errors else (1 if self.warnings else 0)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def have(module):
|
|
83
|
+
try:
|
|
84
|
+
return importlib.util.find_spec(module) is not None
|
|
85
|
+
except Exception:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_pandas():
|
|
90
|
+
global pd, PANDAS_IMPORT_ERROR
|
|
91
|
+
if pd is not None or PANDAS_IMPORT_ERROR is not None:
|
|
92
|
+
return pd
|
|
93
|
+
try:
|
|
94
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
95
|
+
import pandas as _pd
|
|
96
|
+
pd = _pd
|
|
97
|
+
except Exception as e: # pragma: no cover - environment-specific
|
|
98
|
+
PANDAS_IMPORT_ERROR = e
|
|
99
|
+
pd = None
|
|
100
|
+
return pd
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_config(path):
|
|
104
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
105
|
+
return json.load(f)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def enabled_engines(cfg):
|
|
109
|
+
return {
|
|
110
|
+
name: ecfg
|
|
111
|
+
for name, ecfg in cfg.get("engines", {}).items()
|
|
112
|
+
if ecfg.get("enabled", True)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def list_stems(folder, pattern):
|
|
117
|
+
if not folder or not os.path.isdir(folder):
|
|
118
|
+
return []
|
|
119
|
+
return sorted(Path(p).stem for p in glob.glob(os.path.join(folder, pattern)))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def xyz_atom_counts(xyz_dir):
|
|
123
|
+
counts = {}
|
|
124
|
+
elements = {}
|
|
125
|
+
for path in glob.glob(os.path.join(xyz_dir, "*.xyz")):
|
|
126
|
+
stem = Path(path).stem
|
|
127
|
+
try:
|
|
128
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
129
|
+
lines = [x.strip() for x in f.readlines() if x.strip()]
|
|
130
|
+
n = int(lines[0])
|
|
131
|
+
els = [line.split()[0] for line in lines[2 : 2 + n]]
|
|
132
|
+
counts[stem] = len(els)
|
|
133
|
+
elements[stem] = els
|
|
134
|
+
except Exception:
|
|
135
|
+
counts[stem] = 0
|
|
136
|
+
elements[stem] = []
|
|
137
|
+
return counts, elements
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def feather_summaries(feather_dir):
|
|
141
|
+
counts = {}
|
|
142
|
+
elements = {}
|
|
143
|
+
capabilities = {}
|
|
144
|
+
pandas = get_pandas()
|
|
145
|
+
if pandas is None or not have("pyarrow"):
|
|
146
|
+
return counts, elements, capabilities
|
|
147
|
+
feather_read_disabled = False
|
|
148
|
+
for path in glob.glob(os.path.join(feather_dir, "*.feather")):
|
|
149
|
+
if feather_read_disabled:
|
|
150
|
+
break
|
|
151
|
+
stem = Path(path).stem
|
|
152
|
+
try:
|
|
153
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
154
|
+
df = pandas.read_feather(path)
|
|
155
|
+
cols = [str(c).strip() for c in df.columns]
|
|
156
|
+
df.columns = cols
|
|
157
|
+
if {"atom", "x", "y", "z"}.issubset(cols):
|
|
158
|
+
xyz = df[["atom", "x", "y", "z"]].dropna()
|
|
159
|
+
elements[stem] = xyz["atom"].astype(str).tolist()
|
|
160
|
+
counts[stem] = len(elements[stem])
|
|
161
|
+
else:
|
|
162
|
+
xyz = df.iloc[:, 0:4].dropna()
|
|
163
|
+
elements[stem] = xyz.iloc[:, 0].astype(str).tolist()
|
|
164
|
+
counts[stem] = len(elements[stem])
|
|
165
|
+
capabilities[stem] = {
|
|
166
|
+
"dipole": all(c in cols for c in ("dip_x", "dip_y", "dip_z")),
|
|
167
|
+
"freq": "Frequency" in cols,
|
|
168
|
+
"ir": "IR" in cols,
|
|
169
|
+
"nbo": "nbo_charge" in cols,
|
|
170
|
+
"hirshfeld": "hirshfeld_charge" in cols,
|
|
171
|
+
"cm5": "cm5_charge" in cols,
|
|
172
|
+
}
|
|
173
|
+
except Exception:
|
|
174
|
+
feather_read_disabled = True
|
|
175
|
+
counts.clear()
|
|
176
|
+
elements.clear()
|
|
177
|
+
capabilities.clear()
|
|
178
|
+
return counts, elements, capabilities
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def flatten_indices(value):
|
|
182
|
+
out = []
|
|
183
|
+
if value is None:
|
|
184
|
+
return out
|
|
185
|
+
if isinstance(value, int):
|
|
186
|
+
return [value]
|
|
187
|
+
if isinstance(value, (list, tuple)):
|
|
188
|
+
for item in value:
|
|
189
|
+
out.extend(flatten_indices(item))
|
|
190
|
+
return out
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def max_picked_index(cfg):
|
|
194
|
+
mx = 0
|
|
195
|
+
engines = cfg.get("engines", {})
|
|
196
|
+
full = engines.get("descripytor_full", {}).get("atoms", {})
|
|
197
|
+
for key, val in full.items():
|
|
198
|
+
if key in {"stretch_threshold", "bend_threshold"}:
|
|
199
|
+
continue
|
|
200
|
+
ints = [x for x in flatten_indices(val) if isinstance(x, int)]
|
|
201
|
+
if ints:
|
|
202
|
+
mx = max(mx, max(ints))
|
|
203
|
+
for name, ecfg in engines.items():
|
|
204
|
+
for key in ("pairs", "angles", "bond_lengths", "sterimol_pairs", "cone_atoms", "pyramid_atoms"):
|
|
205
|
+
ints = [x for x in flatten_indices(ecfg.get(key)) if isinstance(x, int)]
|
|
206
|
+
if ints:
|
|
207
|
+
mx = max(mx, max(ints))
|
|
208
|
+
for key in ("metal_index",):
|
|
209
|
+
if isinstance(ecfg.get(key), int):
|
|
210
|
+
mx = max(mx, ecfg[key])
|
|
211
|
+
return mx
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def scan_logs(log_dir):
|
|
215
|
+
summary = {}
|
|
216
|
+
for path in glob.glob(os.path.join(log_dir, "*.log")) + glob.glob(os.path.join(log_dir, "*.out")):
|
|
217
|
+
stem = Path(path).stem
|
|
218
|
+
try:
|
|
219
|
+
text = Path(path).read_text(encoding="utf-8", errors="ignore")
|
|
220
|
+
summary[stem] = {
|
|
221
|
+
"normal_termination": "Normal termination" in text,
|
|
222
|
+
"freq": "Frequencies --" in text,
|
|
223
|
+
"dipole": "Dipole moment" in text,
|
|
224
|
+
"standard_orientation": "Standard orientation" in text,
|
|
225
|
+
"nbo": bool(re.search(r"\bNBO\b|Natural Population|NPA", text, re.I)),
|
|
226
|
+
}
|
|
227
|
+
except Exception:
|
|
228
|
+
summary[stem] = {}
|
|
229
|
+
return summary
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def compare_name_sets(report, label_a, a, label_b, b):
|
|
233
|
+
if not a or not b:
|
|
234
|
+
return
|
|
235
|
+
set_a, set_b = set(a), set(b)
|
|
236
|
+
missing_b = sorted(set_a - set_b)[:10]
|
|
237
|
+
missing_a = sorted(set_b - set_a)[:10]
|
|
238
|
+
if not missing_a and not missing_b:
|
|
239
|
+
report.add("ok", f"{label_a} names match {label_b} names ({len(set_a & set_b)} shared)")
|
|
240
|
+
else:
|
|
241
|
+
report.add("warnings", f"name mismatch: {label_a} not in {label_b}: {missing_b or 'none'}; {label_b} not in {label_a}: {missing_a or 'none'}")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def run_checks(cfg):
|
|
245
|
+
report = Report()
|
|
246
|
+
engines = enabled_engines(cfg)
|
|
247
|
+
report.add("ok", f"enabled engines: {', '.join(engines) if engines else 'none'}")
|
|
248
|
+
|
|
249
|
+
root_dir = cfg.get("root_dir")
|
|
250
|
+
feather_dir = cfg.get("feather_dir")
|
|
251
|
+
xyz_dir = cfg.get("xyz_dir")
|
|
252
|
+
log_dir = cfg.get("log_dir") or engines.get("qm", {}).get("log_dir")
|
|
253
|
+
derive_xyz = bool(cfg.get("derive_xyz_from_feathers"))
|
|
254
|
+
|
|
255
|
+
if root_dir and os.path.isdir(root_dir):
|
|
256
|
+
report.add("ok", f"root_dir exists: {root_dir}")
|
|
257
|
+
elif root_dir:
|
|
258
|
+
report.add("warnings", f"root_dir not found: {root_dir}")
|
|
259
|
+
|
|
260
|
+
needs_feather = bool(FEATHER_ENGINES & set(engines)) or derive_xyz
|
|
261
|
+
needs_xyz = bool(XYZ_ENGINES & set(engines)) and not derive_xyz
|
|
262
|
+
needs_log = bool(LOG_ENGINES & set(engines))
|
|
263
|
+
|
|
264
|
+
feather_counts, feather_elements, feather_caps = {}, {}, {}
|
|
265
|
+
if needs_feather:
|
|
266
|
+
if not feather_dir or not os.path.isdir(feather_dir):
|
|
267
|
+
report.add("errors", f"feather_dir is required but not found: {feather_dir}")
|
|
268
|
+
else:
|
|
269
|
+
stems = list_stems(feather_dir, "*.feather")
|
|
270
|
+
report.add("ok", f"found {len(stems)} feather files")
|
|
271
|
+
feather_counts, feather_elements, feather_caps = feather_summaries(feather_dir)
|
|
272
|
+
if get_pandas() is None:
|
|
273
|
+
report.add("warnings", "pandas is not importable; feather contents were not scanned")
|
|
274
|
+
elif not have("pyarrow"):
|
|
275
|
+
report.add("warnings", "pyarrow is not importable; feather contents were not scanned")
|
|
276
|
+
elif stems and not feather_counts:
|
|
277
|
+
report.add("warnings", "feather metadata scan failed; check pyarrow/numpy compatibility in this environment")
|
|
278
|
+
|
|
279
|
+
xyz_counts, xyz_elements = {}, {}
|
|
280
|
+
if needs_xyz:
|
|
281
|
+
if not xyz_dir or not os.path.isdir(xyz_dir):
|
|
282
|
+
report.add("errors", f"xyz_dir is required but not found: {xyz_dir}")
|
|
283
|
+
else:
|
|
284
|
+
stems = list_stems(xyz_dir, "*.xyz")
|
|
285
|
+
report.add("ok", f"found {len(stems)} xyz files")
|
|
286
|
+
xyz_counts, xyz_elements = xyz_atom_counts(xyz_dir)
|
|
287
|
+
|
|
288
|
+
if needs_log:
|
|
289
|
+
if not log_dir or not os.path.isdir(log_dir):
|
|
290
|
+
report.add("errors", f"log_dir is required for qm but not found: {log_dir}")
|
|
291
|
+
else:
|
|
292
|
+
logs = scan_logs(log_dir)
|
|
293
|
+
report.add("ok", f"found {len(logs)} Gaussian log/out files")
|
|
294
|
+
bad = [k for k, v in logs.items() if not v.get("normal_termination")]
|
|
295
|
+
if bad:
|
|
296
|
+
report.add("warnings", f"logs without Normal termination: {bad[:10]}")
|
|
297
|
+
if logs and not any(v.get("freq") for v in logs.values()):
|
|
298
|
+
report.add("warnings", "no frequency blocks found in logs; vibration features may be empty")
|
|
299
|
+
if logs and not any(v.get("dipole") for v in logs.values()):
|
|
300
|
+
report.add("warnings", "no dipole blocks found in logs")
|
|
301
|
+
|
|
302
|
+
if feather_counts and xyz_counts:
|
|
303
|
+
compare_name_sets(report, "feather", feather_counts, "xyz", xyz_counts)
|
|
304
|
+
shared = sorted(set(feather_elements) & set(xyz_elements))
|
|
305
|
+
mismatched = [s for s in shared if feather_elements[s] != xyz_elements[s]]
|
|
306
|
+
if mismatched:
|
|
307
|
+
report.add("warnings", f"atom element/order mismatch between feather and xyz for: {mismatched[:10]}")
|
|
308
|
+
elif shared:
|
|
309
|
+
report.add("ok", f"atom element/order matches for {len(shared)} shared molecules")
|
|
310
|
+
|
|
311
|
+
picked_max = max_picked_index(cfg)
|
|
312
|
+
if picked_max:
|
|
313
|
+
counts = feather_counts or xyz_counts
|
|
314
|
+
too_small = [name for name, n in counts.items() if n and picked_max > n]
|
|
315
|
+
if too_small:
|
|
316
|
+
report.add("errors", f"picked atom index {picked_max} exceeds atom count for: {too_small[:10]}")
|
|
317
|
+
elif counts:
|
|
318
|
+
report.add("ok", f"picked atom indices fit scanned molecules (max index {picked_max})")
|
|
319
|
+
|
|
320
|
+
full_atoms = engines.get("descripytor_full", {}).get("atoms", {})
|
|
321
|
+
if full_atoms.get("dipole") and feather_caps:
|
|
322
|
+
missing = [k for k, v in feather_caps.items() if not v.get("dipole")]
|
|
323
|
+
if missing:
|
|
324
|
+
report.add("warnings", f"dipole selected but dipole columns missing in feathers: {missing[:10]}")
|
|
325
|
+
if (full_atoms.get("stretching_vibration") or full_atoms.get("bending_vibration") or full_atoms.get("ring_vibration")) and feather_caps:
|
|
326
|
+
missing = [k for k, v in feather_caps.items() if not v.get("freq")]
|
|
327
|
+
if missing:
|
|
328
|
+
report.add("warnings", f"vibration selections present but Frequency column missing: {missing[:10]}")
|
|
329
|
+
|
|
330
|
+
for engine, packages in ENGINE_PACKAGES.items():
|
|
331
|
+
if engine not in engines:
|
|
332
|
+
continue
|
|
333
|
+
missing = [pkg for pkg in packages if not have(pkg)]
|
|
334
|
+
if missing:
|
|
335
|
+
report.add("warnings", f"{engine} enabled but missing import(s): {', '.join(missing)}")
|
|
336
|
+
else:
|
|
337
|
+
report.add("ok", f"{engine} optional imports available")
|
|
338
|
+
|
|
339
|
+
return report
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def print_install_report(cfg):
|
|
343
|
+
engines = enabled_engines(cfg)
|
|
344
|
+
needed = []
|
|
345
|
+
for name in engines:
|
|
346
|
+
needed.extend(ENGINE_PACKAGES.get(name, []))
|
|
347
|
+
missing = sorted({pkg for pkg in needed if not have(pkg)})
|
|
348
|
+
if not missing:
|
|
349
|
+
print("All optional imports for enabled engines are available.")
|
|
350
|
+
return
|
|
351
|
+
pip_names = {
|
|
352
|
+
"morfeus": "morfeus-ml",
|
|
353
|
+
}
|
|
354
|
+
print("Missing optional packages:")
|
|
355
|
+
for pkg in missing:
|
|
356
|
+
print(f" - {pkg}")
|
|
357
|
+
print("\nSuggested pip command:")
|
|
358
|
+
print(" pip install " + " ".join(pip_names.get(pkg, pkg) for pkg in missing))
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def main(argv=None):
|
|
362
|
+
ap = argparse.ArgumentParser(description="Preflight descriptor extraction config.")
|
|
363
|
+
ap.add_argument("--config", default="run_config.json", help="Path to run_config.json")
|
|
364
|
+
ap.add_argument("--install-report", action="store_true", help="Show missing optional packages for enabled engines")
|
|
365
|
+
args = ap.parse_args(argv)
|
|
366
|
+
|
|
367
|
+
if not os.path.exists(args.config):
|
|
368
|
+
print(f"[error] config not found: {args.config}")
|
|
369
|
+
return 2
|
|
370
|
+
cfg = load_config(args.config)
|
|
371
|
+
|
|
372
|
+
if args.install_report:
|
|
373
|
+
print_install_report(cfg)
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
report = run_checks(cfg)
|
|
377
|
+
report.print()
|
|
378
|
+
return report.exit_code
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if __name__ == "__main__":
|
|
382
|
+
raise SystemExit(main())
|