nbs-bl 0.2.0__py3-none-any.whl → 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.
nbs_bl/beamline.py CHANGED
@@ -20,6 +20,7 @@ class BeamlineModel:
20
20
  "mirrors",
21
21
  "controllers",
22
22
  "vacuum",
23
+ "source",
23
24
  "misc",
24
25
  ]
25
26
 
@@ -31,6 +32,7 @@ class BeamlineModel:
31
32
  "intensity_detector",
32
33
  "primary_sampleholder",
33
34
  "reference_sampleholder",
35
+ "mode",
34
36
  "slits",
35
37
  ]
36
38
 
@@ -228,7 +230,8 @@ class BeamlineModel:
228
230
  If loading the device fails
229
231
  """
230
232
  if device_name not in self._deferred_devices:
231
- raise KeyError(f"Device {device_name} is not in deferred devices")
233
+ print(f"Device {device_name} is not in deferred devices")
234
+ return
232
235
 
233
236
  # If it's an alias, get and load the root device
234
237
  config = self._deferred_config.get(device_name, {})
nbs_bl/configuration.py CHANGED
@@ -50,8 +50,7 @@ def load_and_configure_everything(startup_dir=None, initializer=None):
50
50
  ip.user_ns["request_update"] = request_update
51
51
 
52
52
  if initializer is None:
53
- initializer = BeamlineInitializer(GLOBAL_BEAMLINE)
54
- ip.user_ns["beamline_initializer"] = initializer
53
+ initializer = get_default_initializer()
55
54
  initializer.initialize(startup_dir, ip.user_ns)
56
55
 
57
56
 
nbs_bl/plans/xas.py CHANGED
@@ -120,4 +120,4 @@ def load_xas(filename):
120
120
  user_ns[key] = xas_func
121
121
 
122
122
  # Return the generated plans dictionary in case it's needed
123
- return generated_plans
123
+ return list(generated_plans.keys())
@@ -0,0 +1,384 @@
1
+ try:
2
+ import tomllib
3
+ except ModuleNotFoundError:
4
+ import tomli as tomllib
5
+
6
+ from copy import deepcopy
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from qtpy.QtCore import QObject, Signal
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ConfigIssue:
16
+ """A validation issue found in a device configuration.
17
+
18
+ Parameters
19
+ ----------
20
+ device_key : str
21
+ The device table name in the TOML file.
22
+ message : str
23
+ A human-readable description of the issue.
24
+ """
25
+
26
+ device_key: str
27
+ message: str
28
+
29
+
30
+ class ConfigModel(QObject):
31
+ """Model for editing NBS-BL device TOML configuration files.
32
+
33
+ This model is responsible for:
34
+ - Loading/parsing TOML into a Python mapping
35
+ - Serializing the mapping back to TOML
36
+ - Providing a small validation layer for NBS-BL special keys
37
+ - Translating special keys to/from human-readable GUI labels
38
+
39
+ Notes
40
+ -----
41
+ This model does not attempt to preserve TOML comments. Saving will
42
+ rewrite the file content.
43
+ """
44
+
45
+ changed = Signal()
46
+ issues_changed = Signal(list)
47
+ file_path_changed = Signal(object)
48
+ dirty_changed = Signal(bool)
49
+
50
+ SPECIAL_KEY_LABELS = {
51
+ "_target": "Target Class",
52
+ "_group": "Group",
53
+ "_role": "Role",
54
+ "_defer_loading": "Defer Loading",
55
+ "_add_to_ns": "Add to Namespace",
56
+ "_load_order": "Load Order",
57
+ "_baseline": "Baseline",
58
+ "_modes": "Modes",
59
+ "_alias": "Alias",
60
+ }
61
+
62
+ SPECIAL_KEYS = set(SPECIAL_KEY_LABELS.keys())
63
+
64
+ def __init__(self, parent: QObject | None = None):
65
+ super().__init__(parent=parent)
66
+ self._devices: dict[str, dict[str, Any]] = {}
67
+ self._saved_devices: dict[str, dict[str, Any]] = {}
68
+ self._file_path: Path | None = None
69
+ self._issues: list[ConfigIssue] = []
70
+ self._label_to_key = {v: k for k, v in self.SPECIAL_KEY_LABELS.items()}
71
+ self._dirty = False
72
+
73
+ @property
74
+ def file_path(self) -> Path | None:
75
+ """Path to the currently loaded file, if any."""
76
+
77
+ return self._file_path
78
+
79
+ @property
80
+ def devices(self) -> dict[str, dict[str, Any]]:
81
+ """Return the device configuration mapping."""
82
+
83
+ return self._devices
84
+
85
+ @property
86
+ def issues(self) -> list[ConfigIssue]:
87
+ """Return current validation issues."""
88
+
89
+ return self._issues
90
+
91
+ @property
92
+ def dirty(self) -> bool:
93
+ """Whether the configuration has unsaved changes."""
94
+
95
+ return self._dirty
96
+
97
+ def is_device_dirty(self, device_key: str) -> bool:
98
+ """Return whether a device has been modified since last load/save.
99
+
100
+ Parameters
101
+ ----------
102
+ device_key : str
103
+ Device key to check.
104
+
105
+ Returns
106
+ -------
107
+ bool
108
+ True if the device differs from the last saved snapshot.
109
+ """
110
+
111
+ return self._devices.get(device_key, {}) != self._saved_devices.get(device_key, {})
112
+
113
+ def clear(self) -> None:
114
+ """Clear all devices and reset file path."""
115
+
116
+ self._devices = {}
117
+ self._saved_devices = deepcopy(self._devices)
118
+ self._file_path = None
119
+ self._revalidate()
120
+ self._update_dirty()
121
+ self.file_path_changed.emit(self._file_path)
122
+ self.changed.emit()
123
+
124
+ def load_from_file(self, file_path: str | Path) -> None:
125
+ """Load devices configuration from a TOML file.
126
+
127
+ Parameters
128
+ ----------
129
+ file_path : str or pathlib.Path
130
+ Path to a TOML file containing device tables.
131
+ """
132
+
133
+ path = Path(file_path)
134
+ with path.open("rb") as f:
135
+ data = tomllib.load(f)
136
+ if not isinstance(data, dict):
137
+ raise ValueError("Device config TOML must be a table of device tables")
138
+ devices: dict[str, dict[str, Any]] = {}
139
+ for key, value in data.items():
140
+ if isinstance(value, dict):
141
+ devices[str(key)] = dict(value)
142
+ else:
143
+ raise ValueError(f"Device '{key}' must be a table")
144
+ self._devices = devices
145
+ self._saved_devices = deepcopy(self._devices)
146
+ self._file_path = path
147
+ self._revalidate()
148
+ self._update_dirty()
149
+ self.file_path_changed.emit(self._file_path)
150
+ self.changed.emit()
151
+
152
+ def save_to_file(self, file_path: str | Path | None = None) -> None:
153
+ """Save devices configuration to a TOML file.
154
+
155
+ Parameters
156
+ ----------
157
+ file_path : str or pathlib.Path or None, optional
158
+ Output path. If None, uses the currently loaded file path.
159
+ """
160
+
161
+ path = Path(file_path) if file_path is not None else self._file_path
162
+ if path is None:
163
+ raise ValueError("No file path provided for saving")
164
+
165
+ text = self.dumps()
166
+ path.parent.mkdir(parents=True, exist_ok=True)
167
+ path.write_text(text, encoding="utf-8")
168
+ self._file_path = path
169
+ self._saved_devices = deepcopy(self._devices)
170
+ self._update_dirty()
171
+ self.file_path_changed.emit(self._file_path)
172
+
173
+ def dumps(self) -> str:
174
+ """Serialize current devices mapping to TOML text.
175
+
176
+ Returns
177
+ -------
178
+ str
179
+ TOML representation of the devices mapping.
180
+ """
181
+
182
+ lines: list[str] = []
183
+ for device_key, config in self._devices.items():
184
+ lines.append(self._format_table_header(device_key))
185
+ for key, value in config.items():
186
+ lines.append(f"{self._format_key(key)} = {self._format_value(value)}")
187
+ lines.append("")
188
+ return "\n".join(lines).rstrip() + "\n"
189
+
190
+ def device_keys(self) -> list[str]:
191
+ """Return device keys in display order."""
192
+
193
+ return list(self._devices.keys())
194
+
195
+ def get_device_config(self, device_key: str) -> dict[str, Any]:
196
+ """Get a shallow copy of a device configuration."""
197
+
198
+ return dict(self._devices.get(device_key, {}))
199
+
200
+ def set_device_config(self, device_key: str, config: dict[str, Any]) -> None:
201
+ """Replace a device configuration entirely."""
202
+
203
+ self._devices[device_key] = dict(config)
204
+ self._revalidate()
205
+ self._update_dirty()
206
+ self.changed.emit()
207
+
208
+ def add_device(self, device_key: str) -> None:
209
+ """Add a new device with an empty configuration."""
210
+
211
+ if device_key in self._devices:
212
+ raise ValueError(f"Device '{device_key}' already exists")
213
+ self._devices[device_key] = {}
214
+ self._revalidate()
215
+ self._update_dirty()
216
+ self.changed.emit()
217
+
218
+ def remove_device(self, device_key: str) -> None:
219
+ """Remove a device from the configuration."""
220
+
221
+ self._devices.pop(device_key, None)
222
+ self._revalidate()
223
+ self._update_dirty()
224
+ self.changed.emit()
225
+
226
+ def set_value(self, device_key: str, key: str, value: Any) -> None:
227
+ """Set a configuration key on a device."""
228
+
229
+ if device_key not in self._devices:
230
+ raise KeyError(device_key)
231
+ self._devices[device_key][key] = value
232
+ self._revalidate()
233
+ self._update_dirty()
234
+ self.changed.emit()
235
+
236
+ def unset_value(self, device_key: str, key: str) -> None:
237
+ """Remove a configuration key from a device."""
238
+
239
+ if device_key not in self._devices:
240
+ raise KeyError(device_key)
241
+ self._devices[device_key].pop(key, None)
242
+ self._revalidate()
243
+ self._update_dirty()
244
+ self.changed.emit()
245
+
246
+ def special_key_to_label(self, key: str) -> str:
247
+ """Convert a special TOML key to a GUI label."""
248
+
249
+ return self.SPECIAL_KEY_LABELS.get(key, key)
250
+
251
+ def label_to_special_key(self, label: str) -> str:
252
+ """Convert a GUI label back to a TOML special key."""
253
+
254
+ return self._label_to_key.get(label, label)
255
+
256
+ def parse_value_text(self, text: str) -> Any:
257
+ """Parse a user-provided value string into a TOML-compatible value.
258
+
259
+ Parameters
260
+ ----------
261
+ text : str
262
+ Text representing a TOML value.
263
+
264
+ Returns
265
+ -------
266
+ Any
267
+ Parsed value. If parsing fails, the original text is returned.
268
+ """
269
+
270
+ stripped = text.strip()
271
+ if stripped == "":
272
+ return ""
273
+ try:
274
+ parsed = tomllib.loads(f"v = {stripped}\n")
275
+ return parsed["v"]
276
+ except Exception:
277
+ return text
278
+
279
+ def _revalidate(self) -> None:
280
+ issues = self.validate()
281
+ self._issues = issues
282
+ self.issues_changed.emit(issues)
283
+
284
+ def _update_dirty(self) -> None:
285
+ dirty = self._devices != self._saved_devices
286
+ if dirty != self._dirty:
287
+ self._dirty = dirty
288
+ self.dirty_changed.emit(dirty)
289
+
290
+ def validate(self) -> list[ConfigIssue]:
291
+ """Validate device configurations for known special keys and types.
292
+
293
+ Returns
294
+ -------
295
+ list[ConfigIssue]
296
+ A list of issues found in the current configuration.
297
+ """
298
+
299
+ issues: list[ConfigIssue] = []
300
+ for device_key, config in self._devices.items():
301
+ if not isinstance(config, dict):
302
+ issues.append(ConfigIssue(device_key, "Device table must be a mapping"))
303
+ continue
304
+
305
+ special_keys = [k for k in config.keys() if isinstance(k, str) and k.startswith("_")]
306
+ for k in special_keys:
307
+ if k not in self.SPECIAL_KEYS:
308
+ issues.append(ConfigIssue(device_key, f"Unknown special key: {k}"))
309
+
310
+ has_target = "_target" in config
311
+ has_alias = "_alias" in config
312
+ if not has_target and not has_alias:
313
+ issues.append(ConfigIssue(device_key, "Missing Target Class or Alias"))
314
+ if has_target and not isinstance(config.get("_target"), str):
315
+ issues.append(ConfigIssue(device_key, "Target Class must be a string"))
316
+ if has_alias and not isinstance(config.get("_alias"), str):
317
+ issues.append(ConfigIssue(device_key, "Alias must be a string"))
318
+
319
+ if "_role" in config and not isinstance(config.get("_role"), str):
320
+ issues.append(ConfigIssue(device_key, "Role must be a string"))
321
+
322
+ if "_group" in config:
323
+ group_val = config.get("_group")
324
+ if isinstance(group_val, str):
325
+ pass
326
+ elif isinstance(group_val, list) and all(isinstance(x, str) for x in group_val):
327
+ pass
328
+ else:
329
+ issues.append(ConfigIssue(device_key, "Group must be a string or list of strings"))
330
+
331
+ if "_modes" in config:
332
+ modes_val = config.get("_modes")
333
+ if not (isinstance(modes_val, list) and all(isinstance(x, str) for x in modes_val)):
334
+ issues.append(ConfigIssue(device_key, "Modes must be a list of strings"))
335
+
336
+ if "_load_order" in config and not isinstance(config.get("_load_order"), int):
337
+ issues.append(ConfigIssue(device_key, "Load Order must be an integer"))
338
+
339
+ for bool_key, label in (
340
+ ("_defer_loading", "Defer Loading"),
341
+ ("_add_to_ns", "Add to Namespace"),
342
+ ("_baseline", "Baseline"),
343
+ ):
344
+ if bool_key in config and not isinstance(config.get(bool_key), bool):
345
+ issues.append(ConfigIssue(device_key, f"{label} must be a boolean"))
346
+
347
+ return issues
348
+
349
+ def _format_table_header(self, name: str) -> str:
350
+ if self._is_bare_key(name):
351
+ return f"[{name}]"
352
+ return f'[{self._format_string(name)}]'
353
+
354
+ def _format_key(self, key: str) -> str:
355
+ if self._is_bare_key(key):
356
+ return key
357
+ return self._format_string(key)
358
+
359
+ def _format_value(self, value: Any) -> str:
360
+ if isinstance(value, bool):
361
+ return "true" if value else "false"
362
+ if isinstance(value, int):
363
+ return str(value)
364
+ if isinstance(value, float):
365
+ return repr(value)
366
+ if isinstance(value, str):
367
+ return self._format_string(value)
368
+ if isinstance(value, list):
369
+ inner = ", ".join(self._format_value(v) for v in value)
370
+ return f"[{inner}]"
371
+ raise TypeError(f"Unsupported value type: {type(value).__name__}")
372
+
373
+ def _format_string(self, s: str) -> str:
374
+ escaped = s.replace("\\", "\\\\").replace('"', '\\"')
375
+ return f'"{escaped}"'
376
+
377
+ def _is_bare_key(self, key: str) -> bool:
378
+ if key == "":
379
+ return False
380
+ for ch in key:
381
+ if not (ch.isalnum() or ch in ("_", "-", ".")):
382
+ return False
383
+ return True
384
+
@@ -0,0 +1,770 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from qtpy.QtCore import Qt
6
+ from qtpy.QtWidgets import (
7
+ QAbstractItemView,
8
+ QApplication,
9
+ QCheckBox,
10
+ QComboBox,
11
+ QFileDialog,
12
+ QFormLayout,
13
+ QGroupBox,
14
+ QHBoxLayout,
15
+ QInputDialog,
16
+ QLabel,
17
+ QLineEdit,
18
+ QListWidget,
19
+ QMainWindow,
20
+ QMessageBox,
21
+ QPushButton,
22
+ QSpinBox,
23
+ QSplitter,
24
+ QTableWidget,
25
+ QTableWidgetItem,
26
+ QVBoxLayout,
27
+ QWidget,
28
+ )
29
+
30
+ from nbs_bl.qt.models.config_model import ConfigIssue, ConfigModel
31
+
32
+
33
+ class ConfigEditorWidget(QWidget):
34
+ """Widget for editing NBS-BL `devices.toml` files."""
35
+
36
+ def __init__(self, parent: QWidget | None = None):
37
+ super().__init__(parent=parent)
38
+ self.model = ConfigModel(parent=self)
39
+ self._current_device_key: str | None = None
40
+ self._updating = False
41
+ self._device_type_overrides: dict[str, str] = {}
42
+ self._device_type_values: dict[str, dict[str, str]] = {}
43
+ self._special_bool_defaults = {
44
+ "_defer_loading": False,
45
+ "_add_to_ns": True,
46
+ "_baseline": True,
47
+ }
48
+
49
+ self._build_ui()
50
+ self._connect_signals()
51
+ self._refresh_device_list()
52
+ self._set_current_device(None)
53
+
54
+ def _build_ui(self) -> None:
55
+ root = QVBoxLayout()
56
+
57
+ self.file_label = QLabel("")
58
+ self.file_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
59
+ root.addWidget(self.file_label)
60
+
61
+ toolbar = QHBoxLayout()
62
+ self.new_button = QPushButton("New")
63
+ self.open_button = QPushButton("Open")
64
+ self.save_button = QPushButton("Save")
65
+ self.save_as_button = QPushButton("Save As")
66
+ toolbar.addWidget(self.new_button)
67
+ toolbar.addWidget(self.open_button)
68
+ toolbar.addWidget(self.save_button)
69
+ toolbar.addWidget(self.save_as_button)
70
+ toolbar.addStretch(1)
71
+ root.addLayout(toolbar)
72
+
73
+ splitter = QSplitter()
74
+
75
+ left = QWidget()
76
+ left_layout = QVBoxLayout()
77
+ left.setLayout(left_layout)
78
+ self.device_list = QListWidget()
79
+ self.device_list.setSelectionMode(QAbstractItemView.SingleSelection)
80
+ left_layout.addWidget(self.device_list)
81
+
82
+ left_buttons = QHBoxLayout()
83
+ self.add_device_button = QPushButton("Add Device")
84
+ self.remove_device_button = QPushButton("Remove Device")
85
+ left_buttons.addWidget(self.add_device_button)
86
+ left_buttons.addWidget(self.remove_device_button)
87
+ left_layout.addLayout(left_buttons)
88
+
89
+ splitter.addWidget(left)
90
+
91
+ right = QWidget()
92
+ right_layout = QVBoxLayout()
93
+ right.setLayout(right_layout)
94
+
95
+ self.device_key_label = QLabel("")
96
+ self.device_key_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
97
+ right_layout.addWidget(self.device_key_label)
98
+
99
+ self.special_group = QGroupBox("Special Keys")
100
+ special_form = QFormLayout()
101
+ self.special_group.setLayout(special_form)
102
+
103
+ self.device_type = QComboBox()
104
+ self.device_type.addItems(["Target Class", "Alias"])
105
+
106
+ self.type_value = QLineEdit()
107
+ type_row = QWidget()
108
+ type_row_layout = QHBoxLayout()
109
+ type_row_layout.setContentsMargins(0, 0, 0, 0)
110
+ type_row.setLayout(type_row_layout)
111
+ type_row_layout.addWidget(self.device_type)
112
+ type_row_layout.addWidget(self.type_value)
113
+
114
+ self.group = QLineEdit()
115
+ self.role = QLineEdit()
116
+ self.modes = QLineEdit()
117
+
118
+ self.defer_loading = QCheckBox()
119
+ self.add_to_namespace = QCheckBox()
120
+ self.baseline = QCheckBox()
121
+
122
+ self.load_order = QSpinBox()
123
+ self.load_order.setMinimum(-999999)
124
+ self.load_order.setMaximum(999999)
125
+
126
+ special_form.addRow("Type", type_row)
127
+ special_form.addRow("Group", self.group)
128
+ special_form.addRow("Role", self.role)
129
+ special_form.addRow("Modes", self.modes)
130
+ special_form.addRow("Defer Loading", self.defer_loading)
131
+ special_form.addRow("Add to Namespace", self.add_to_namespace)
132
+ special_form.addRow("Baseline", self.baseline)
133
+ special_form.addRow("Load Order", self.load_order)
134
+
135
+ right_layout.addWidget(self.special_group)
136
+
137
+ self.normal_group = QGroupBox("Normal Keys")
138
+ normal_layout = QVBoxLayout()
139
+ self.normal_group.setLayout(normal_layout)
140
+
141
+ normal_form = QFormLayout()
142
+ self.ophyd_name = QLineEdit()
143
+ self.prefix = QLineEdit()
144
+ normal_form.addRow("Name", self.ophyd_name)
145
+ normal_form.addRow("Prefix", self.prefix)
146
+ normal_layout.addLayout(normal_form)
147
+
148
+ self.extra_table = QTableWidget()
149
+ self.extra_table.setColumnCount(2)
150
+ self.extra_table.setHorizontalHeaderLabels(["Key", "Value"])
151
+ self.extra_table.setSelectionBehavior(QAbstractItemView.SelectRows)
152
+ self.extra_table.setSelectionMode(QAbstractItemView.SingleSelection)
153
+ self.extra_table.verticalHeader().setVisible(False)
154
+ self.extra_table.horizontalHeader().setStretchLastSection(True)
155
+ normal_layout.addWidget(self.extra_table)
156
+
157
+ extra_buttons = QHBoxLayout()
158
+ self.add_key_button = QPushButton("Add Key")
159
+ self.remove_key_button = QPushButton("Remove Key")
160
+ extra_buttons.addWidget(self.add_key_button)
161
+ extra_buttons.addWidget(self.remove_key_button)
162
+ extra_buttons.addStretch(1)
163
+ normal_layout.addLayout(extra_buttons)
164
+
165
+ right_layout.addWidget(self.normal_group)
166
+
167
+ self.validation_group = QGroupBox("Validation")
168
+ validation_layout = QVBoxLayout()
169
+ self.validation_group.setLayout(validation_layout)
170
+ self.validation_list = QListWidget()
171
+ validation_layout.addWidget(self.validation_list)
172
+ self.remove_unknown_button = QPushButton("Remove Unknown Special Keys")
173
+ validation_layout.addWidget(self.remove_unknown_button)
174
+ right_layout.addWidget(self.validation_group)
175
+
176
+ right_layout.addStretch(1)
177
+ splitter.addWidget(right)
178
+
179
+ splitter.setStretchFactor(0, 0)
180
+ splitter.setStretchFactor(1, 1)
181
+
182
+ root.addWidget(splitter)
183
+ self.setLayout(root)
184
+
185
+ self._update_type_placeholder()
186
+ self._update_file_label()
187
+
188
+ def _connect_signals(self) -> None:
189
+ self.new_button.clicked.connect(self._new_file)
190
+ self.open_button.clicked.connect(self._open_file)
191
+ self.save_button.clicked.connect(self._save_file)
192
+ self.save_as_button.clicked.connect(self._save_file_as)
193
+
194
+ self.add_device_button.clicked.connect(self._add_device)
195
+ self.remove_device_button.clicked.connect(self._remove_device)
196
+ self.device_list.currentItemChanged.connect(self._on_device_item_changed)
197
+
198
+ self.device_type.currentTextChanged.connect(self._on_type_changed)
199
+ self.type_value.editingFinished.connect(self._on_special_edited)
200
+ self.group.editingFinished.connect(self._on_special_edited)
201
+ self.role.editingFinished.connect(self._on_special_edited)
202
+ self.modes.editingFinished.connect(self._on_special_edited)
203
+ self.defer_loading.stateChanged.connect(self._on_special_edited)
204
+ self.add_to_namespace.stateChanged.connect(self._on_special_edited)
205
+ self.baseline.stateChanged.connect(self._on_special_edited)
206
+ self.load_order.valueChanged.connect(self._on_special_edited)
207
+
208
+ self.ophyd_name.editingFinished.connect(self._on_normal_edited)
209
+ self.prefix.editingFinished.connect(self._on_normal_edited)
210
+ self.add_key_button.clicked.connect(self._add_extra_key)
211
+ self.remove_key_button.clicked.connect(self._remove_extra_key)
212
+ self.extra_table.itemChanged.connect(self._on_extra_item_changed)
213
+ self.remove_unknown_button.clicked.connect(self._remove_unknown_special_keys)
214
+
215
+ self.model.changed.connect(self._refresh_device_list)
216
+ self.model.issues_changed.connect(self._refresh_validation)
217
+ self.model.file_path_changed.connect(self._update_file_label)
218
+ self.model.dirty_changed.connect(self._update_file_label)
219
+
220
+ def _update_file_label(self) -> None:
221
+ suffix = " (modified)" if self.model.dirty else ""
222
+ if self.model.file_path is None:
223
+ self.file_label.setText(f"File: (unsaved){suffix}")
224
+ else:
225
+ self.file_label.setText(f"File: {self.model.file_path}{suffix}")
226
+
227
+ def _refresh_device_list(self) -> None:
228
+ current = self._current_device_key
229
+ self.device_list.blockSignals(True)
230
+ self.device_list.clear()
231
+ for key in self.model.device_keys():
232
+ text = f"{key} *" if self.model.is_device_dirty(key) else key
233
+ self.device_list.addItem(text)
234
+ self.device_list.item(self.device_list.count() - 1).setData(Qt.UserRole, key)
235
+ self.device_list.blockSignals(False)
236
+
237
+ if current in self.model.devices:
238
+ for i in range(self.device_list.count()):
239
+ item = self.device_list.item(i)
240
+ if item is not None and item.data(Qt.UserRole) == current:
241
+ self.device_list.setCurrentItem(item)
242
+ break
243
+ elif self.model.device_keys():
244
+ self.device_list.setCurrentRow(0)
245
+ else:
246
+ self._set_current_device(None)
247
+
248
+ def _on_device_item_changed(self, current, previous) -> None:
249
+ if current is None:
250
+ self._set_current_device(None)
251
+ return
252
+ key = current.data(Qt.UserRole)
253
+ self._set_current_device(str(key) if key is not None else current.text())
254
+
255
+ def _set_current_device(self, device_key: str | None) -> None:
256
+ self._current_device_key = device_key if device_key else None
257
+ if self._current_device_key is None:
258
+ self.device_key_label.setText("No device selected")
259
+ self._set_editor_enabled(False)
260
+ self._clear_editor()
261
+ return
262
+ self.device_key_label.setText(f"Device Key: {self._current_device_key}")
263
+ self._set_editor_enabled(True)
264
+ self._load_device_to_editor(self._current_device_key)
265
+
266
+ def _set_editor_enabled(self, enabled: bool) -> None:
267
+ self.special_group.setEnabled(enabled)
268
+ self.normal_group.setEnabled(enabled)
269
+
270
+ def _clear_editor(self) -> None:
271
+ self._updating = True
272
+ try:
273
+ self.device_type.setCurrentText("Target Class")
274
+ self.type_value.setText("")
275
+ self.group.setText("")
276
+ self.role.setText("")
277
+ self.modes.setText("")
278
+ self.defer_loading.setChecked(bool(self._special_bool_defaults["_defer_loading"]))
279
+ self.add_to_namespace.setChecked(bool(self._special_bool_defaults["_add_to_ns"]))
280
+ self.baseline.setChecked(bool(self._special_bool_defaults["_baseline"]))
281
+ self.load_order.setValue(0)
282
+ self.ophyd_name.setText("")
283
+ self.prefix.setText("")
284
+ self.extra_table.setRowCount(0)
285
+ self.validation_list.clear()
286
+ self._update_type_placeholder()
287
+ self._update_normal_keys_enabled()
288
+ finally:
289
+ self._updating = False
290
+
291
+ def _load_device_to_editor(self, device_key: str) -> None:
292
+ config = self.model.get_device_config(device_key)
293
+ self._updating = True
294
+ try:
295
+ if device_key not in self._device_type_values:
296
+ self._device_type_values[device_key] = {"Target Class": "", "Alias": ""}
297
+ if "_target" in config and isinstance(config.get("_target"), str):
298
+ self._device_type_values[device_key]["Target Class"] = str(config.get("_target"))
299
+ if "_alias" in config and isinstance(config.get("_alias"), str):
300
+ self._device_type_values[device_key]["Alias"] = str(config.get("_alias"))
301
+
302
+ if "_alias" in config and "_target" not in config:
303
+ selected_type = "Alias"
304
+ elif "_target" in config and "_alias" not in config:
305
+ selected_type = "Target Class"
306
+ else:
307
+ selected_type = self._device_type_overrides.get(device_key, "Target Class")
308
+
309
+ self.device_type.setCurrentText(selected_type)
310
+ self.type_value.setText(self._device_type_values.get(device_key, {}).get(selected_type, ""))
311
+
312
+ group_val = config.get("_group", "")
313
+ if isinstance(group_val, list):
314
+ self.group.setText(", ".join(group_val))
315
+ else:
316
+ self.group.setText(str(group_val) if group_val is not None else "")
317
+
318
+ self.role.setText(str(config.get("_role", "")) if "_role" in config else "")
319
+
320
+ modes_val = config.get("_modes", "")
321
+ if isinstance(modes_val, list):
322
+ self.modes.setText(", ".join(modes_val))
323
+ else:
324
+ self.modes.setText("")
325
+
326
+ self.defer_loading.setChecked(self._get_bool_with_default(config, "_defer_loading"))
327
+ self.add_to_namespace.setChecked(self._get_bool_with_default(config, "_add_to_ns"))
328
+ self.baseline.setChecked(self._get_bool_with_default(config, "_baseline"))
329
+
330
+ load_order_val = config.get("_load_order", 0)
331
+ self.load_order.setValue(int(load_order_val) if isinstance(load_order_val, int) else 0)
332
+
333
+ self.ophyd_name.setText(str(config.get("name", "")) if "name" in config else "")
334
+ self.prefix.setText(str(config.get("prefix", "")) if "prefix" in config else "")
335
+
336
+ self._populate_extra_table(config)
337
+ self._update_type_placeholder()
338
+ self._update_normal_keys_enabled()
339
+ finally:
340
+ self._updating = False
341
+
342
+ self._refresh_validation(self.model.issues)
343
+
344
+ def _populate_extra_table(self, config: dict) -> None:
345
+ excluded = set(self.model.SPECIAL_KEYS) | {"name", "prefix"}
346
+ extra_items = [
347
+ (k, v)
348
+ for k, v in config.items()
349
+ if k not in excluded and not (isinstance(k, str) and k.startswith("_"))
350
+ ]
351
+ self.extra_table.blockSignals(True)
352
+ self.extra_table.setRowCount(0)
353
+ for key, value in extra_items:
354
+ row = self.extra_table.rowCount()
355
+ self.extra_table.insertRow(row)
356
+ key_item = QTableWidgetItem(str(key))
357
+ key_item.setData(Qt.UserRole, str(key))
358
+ val_item = QTableWidgetItem(self._value_to_text(value))
359
+ self.extra_table.setItem(row, 0, key_item)
360
+ self.extra_table.setItem(row, 1, val_item)
361
+ self.extra_table.blockSignals(False)
362
+
363
+ def _refresh_validation(self, issues: list[ConfigIssue]) -> None:
364
+ self.validation_list.clear()
365
+ if not issues:
366
+ self.validation_list.addItem("No issues")
367
+ return
368
+ for issue in issues:
369
+ self.validation_list.addItem(f"{issue.device_key}: {issue.message}")
370
+
371
+ def _value_to_text(self, value: object) -> str:
372
+ if isinstance(value, str):
373
+ return value
374
+ if isinstance(value, bool):
375
+ return "true" if value else "false"
376
+ if isinstance(value, (int, float)):
377
+ return str(value)
378
+ if isinstance(value, list):
379
+ parts = []
380
+ for v in value:
381
+ if isinstance(v, str):
382
+ parts.append(f'"{v}"')
383
+ else:
384
+ parts.append(str(v))
385
+ return f"[{', '.join(parts)}]"
386
+ return str(value)
387
+
388
+ def _update_type_placeholder(self) -> None:
389
+ if self.device_type.currentText() == "Alias":
390
+ self.type_value.setPlaceholderText("Alias")
391
+ else:
392
+ self.type_value.setPlaceholderText("Target class path")
393
+
394
+ def _update_normal_keys_enabled(self) -> None:
395
+ enabled = self._current_device_key is not None and self.device_type.currentText() != "Alias"
396
+ self.normal_group.setEnabled(enabled)
397
+
398
+ def _get_bool_with_default(self, config: dict, key: str) -> bool:
399
+ default = bool(self._special_bool_defaults[key])
400
+ if key not in config:
401
+ return default
402
+ value = config.get(key)
403
+ return value if isinstance(value, bool) else default
404
+
405
+ def _on_type_changed(self) -> None:
406
+ if self._current_device_key is not None:
407
+ key = self._current_device_key
408
+ self._device_type_overrides[key] = self.device_type.currentText()
409
+ if key not in self._device_type_values:
410
+ self._device_type_values[key] = {"Target Class": "", "Alias": ""}
411
+ self._update_type_placeholder()
412
+ self._update_normal_keys_enabled()
413
+ if self._current_device_key is None:
414
+ return
415
+ key = self._current_device_key
416
+ current_type = self.device_type.currentText()
417
+ self._updating = True
418
+ try:
419
+ self.type_value.setText(self._device_type_values.get(key, {}).get(current_type, ""))
420
+ finally:
421
+ self._updating = False
422
+ self._apply_type_selection_to_config()
423
+
424
+ def _apply_type_selection_to_config(self) -> None:
425
+ if self._current_device_key is None:
426
+ return
427
+ key = self._current_device_key
428
+ config = self.model.get_device_config(key)
429
+ selected_type = self.device_type.currentText()
430
+
431
+ if key not in self._device_type_values:
432
+ self._device_type_values[key] = {"Target Class": "", "Alias": ""}
433
+
434
+ if selected_type == "Alias":
435
+ config.pop("_target", None)
436
+ alias_text = self._device_type_values[key].get("Alias", "").strip()
437
+ if alias_text:
438
+ config["_alias"] = alias_text
439
+ else:
440
+ config.pop("_alias", None)
441
+ for k in list(config.keys()):
442
+ if isinstance(k, str) and not k.startswith("_"):
443
+ config.pop(k, None)
444
+ else:
445
+ config.pop("_alias", None)
446
+ target_text = self._device_type_values[key].get("Target Class", "").strip()
447
+ if target_text:
448
+ config["_target"] = target_text
449
+ else:
450
+ config.pop("_target", None)
451
+
452
+ self.model.set_device_config(key, config)
453
+
454
+ def _on_special_edited(self) -> None:
455
+ if self._updating or self._current_device_key is None:
456
+ return
457
+
458
+ key = self._current_device_key
459
+ config = self.model.get_device_config(key)
460
+
461
+ if key not in self._device_type_values:
462
+ self._device_type_values[key] = {"Target Class": "", "Alias": ""}
463
+
464
+ if self.device_type.currentText() == "Alias":
465
+ alias_text = self.type_value.text().strip()
466
+ self._device_type_values[key]["Alias"] = alias_text
467
+ config.pop("_target", None)
468
+ if alias_text:
469
+ config["_alias"] = alias_text
470
+ else:
471
+ config.pop("_alias", None)
472
+ for k in list(config.keys()):
473
+ if isinstance(k, str) and not k.startswith("_"):
474
+ config.pop(k, None)
475
+ else:
476
+ target_text = self.type_value.text().strip()
477
+ self._device_type_values[key]["Target Class"] = target_text
478
+ config.pop("_alias", None)
479
+ if target_text:
480
+ config["_target"] = target_text
481
+ else:
482
+ config.pop("_target", None)
483
+
484
+ group_text = self.group.text().strip()
485
+ if group_text:
486
+ parts = [p.strip() for p in group_text.split(",") if p.strip()]
487
+ if len(parts) == 1:
488
+ config["_group"] = parts[0]
489
+ else:
490
+ config["_group"] = parts
491
+ else:
492
+ config.pop("_group", None)
493
+
494
+ role_text = self.role.text().strip()
495
+ if role_text:
496
+ config["_role"] = role_text
497
+ else:
498
+ config.pop("_role", None)
499
+
500
+ modes_text = self.modes.text().strip()
501
+ if modes_text:
502
+ parts = [p.strip() for p in modes_text.split(",") if p.strip()]
503
+ config["_modes"] = parts
504
+ else:
505
+ config.pop("_modes", None)
506
+
507
+ for key_name, widget in (
508
+ ("_defer_loading", self.defer_loading),
509
+ ("_add_to_ns", self.add_to_namespace),
510
+ ("_baseline", self.baseline),
511
+ ):
512
+ default = bool(self._special_bool_defaults[key_name])
513
+ value = bool(widget.isChecked())
514
+ if value == default:
515
+ config.pop(key_name, None)
516
+ else:
517
+ config[key_name] = value
518
+
519
+ load_order_val = int(self.load_order.value())
520
+ if load_order_val == 0:
521
+ config.pop("_load_order", None)
522
+ else:
523
+ config["_load_order"] = load_order_val
524
+
525
+ self.model.set_device_config(key, config)
526
+ self._update_normal_keys_enabled()
527
+
528
+ def _on_normal_edited(self) -> None:
529
+ if self._updating or self._current_device_key is None:
530
+ return
531
+
532
+ key = self._current_device_key
533
+ config = self.model.get_device_config(key)
534
+
535
+ name_text = self.ophyd_name.text()
536
+ prefix_text = self.prefix.text()
537
+
538
+ if name_text.strip() == "":
539
+ config.pop("name", None)
540
+ else:
541
+ config["name"] = name_text
542
+
543
+ if prefix_text.strip() == "":
544
+ config.pop("prefix", None)
545
+ else:
546
+ config["prefix"] = prefix_text
547
+
548
+ self.model.set_device_config(key, config)
549
+
550
+ def _add_extra_key(self) -> None:
551
+ if self._current_device_key is None:
552
+ return
553
+ row = self.extra_table.rowCount()
554
+ self.extra_table.insertRow(row)
555
+ key_item = QTableWidgetItem("")
556
+ key_item.setData(Qt.UserRole, "")
557
+ val_item = QTableWidgetItem("")
558
+ self.extra_table.setItem(row, 0, key_item)
559
+ self.extra_table.setItem(row, 1, val_item)
560
+ self.extra_table.setCurrentCell(row, 0)
561
+
562
+ def _remove_extra_key(self) -> None:
563
+ if self._current_device_key is None:
564
+ return
565
+ rows = self.extra_table.selectionModel().selectedRows()
566
+ if not rows:
567
+ return
568
+ row = rows[0].row()
569
+ key_item = self.extra_table.item(row, 0)
570
+ prev_key = key_item.data(Qt.UserRole) if key_item is not None else ""
571
+ if prev_key:
572
+ self.model.unset_value(self._current_device_key, str(prev_key))
573
+ self.extra_table.removeRow(row)
574
+
575
+ def _on_extra_item_changed(self, item: QTableWidgetItem) -> None:
576
+ if self._updating or self._current_device_key is None:
577
+ return
578
+ row = item.row()
579
+ key_item = self.extra_table.item(row, 0)
580
+ val_item = self.extra_table.item(row, 1)
581
+ if key_item is None or val_item is None:
582
+ return
583
+
584
+ new_key = key_item.text().strip()
585
+ prev_key = str(key_item.data(Qt.UserRole) or "").strip()
586
+ value_text = val_item.text()
587
+
588
+ if prev_key and prev_key != new_key:
589
+ self.model.unset_value(self._current_device_key, prev_key)
590
+
591
+ if new_key == "":
592
+ key_item.setData(Qt.UserRole, "")
593
+ return
594
+
595
+ if new_key.startswith("_"):
596
+ QMessageBox.warning(
597
+ self,
598
+ "Reserved Key",
599
+ "Keys beginning with '_' are special keys and are edited in the Special Keys section.",
600
+ )
601
+ self._updating = True
602
+ try:
603
+ key_item.setText(prev_key)
604
+ finally:
605
+ self._updating = False
606
+ return
607
+
608
+ value = self.model.parse_value_text(value_text)
609
+ self.model.set_value(self._current_device_key, new_key, value)
610
+ key_item.setData(Qt.UserRole, new_key)
611
+
612
+ def _add_device(self) -> None:
613
+ device_key, ok = QInputDialog.getText(self, "Add Device", "Device key")
614
+ if not ok:
615
+ return
616
+ key = device_key.strip()
617
+ if key == "":
618
+ return
619
+ try:
620
+ self.model.add_device(key)
621
+ items = self.device_list.findItems(key, Qt.MatchExactly)
622
+ if items:
623
+ self.device_list.setCurrentItem(items[0])
624
+ except Exception as exc:
625
+ QMessageBox.critical(self, "Add Device Failed", str(exc))
626
+
627
+ def _remove_device(self) -> None:
628
+ if self._current_device_key is None:
629
+ return
630
+ key = self._current_device_key
631
+ ret = QMessageBox.question(
632
+ self,
633
+ "Remove Device",
634
+ f"Remove device '{key}'?",
635
+ QMessageBox.Yes | QMessageBox.No,
636
+ QMessageBox.No,
637
+ )
638
+ if ret != QMessageBox.Yes:
639
+ return
640
+ self._device_type_overrides.pop(key, None)
641
+ self._device_type_values.pop(key, None)
642
+ self.model.remove_device(key)
643
+ self._set_current_device(None)
644
+
645
+ def _new_file(self) -> None:
646
+ ret = QMessageBox.question(
647
+ self,
648
+ "New File",
649
+ "Create a new configuration? Unsaved changes will be lost.",
650
+ QMessageBox.Yes | QMessageBox.No,
651
+ QMessageBox.No,
652
+ )
653
+ if ret != QMessageBox.Yes:
654
+ return
655
+ self.model.clear()
656
+ self._refresh_device_list()
657
+
658
+ def _open_file(self) -> None:
659
+ path, _ = QFileDialog.getOpenFileName(self, "Open TOML", "", "TOML Files (*.toml)")
660
+ if not path:
661
+ return
662
+ try:
663
+ self.model.load_from_file(path)
664
+ except Exception as exc:
665
+ QMessageBox.critical(self, "Open Failed", str(exc))
666
+ return
667
+ self._refresh_device_list()
668
+
669
+ def _save_file(self) -> None:
670
+ if self.model.file_path is None:
671
+ self._save_file_as()
672
+ return
673
+ self._save_with_warning(self.model.file_path)
674
+
675
+ def _save_file_as(self) -> None:
676
+ path, _ = QFileDialog.getSaveFileName(self, "Save TOML", "", "TOML Files (*.toml)")
677
+ if not path:
678
+ return
679
+ self._save_with_warning(Path(path))
680
+
681
+ def _save_with_warning(self, path: Path) -> None:
682
+ if self.model.file_path is None or Path(self.model.file_path) != Path(path):
683
+ msg = "Saving will rewrite the file and will not preserve TOML comments."
684
+ else:
685
+ msg = "Saving will rewrite the file and will not preserve TOML comments."
686
+ ret = QMessageBox.question(
687
+ self,
688
+ "Save",
689
+ msg,
690
+ QMessageBox.Yes | QMessageBox.No,
691
+ QMessageBox.Yes,
692
+ )
693
+ if ret != QMessageBox.Yes:
694
+ return
695
+ try:
696
+ self.model.save_to_file(path)
697
+ except Exception as exc:
698
+ QMessageBox.critical(self, "Save Failed", str(exc))
699
+
700
+ def _remove_unknown_special_keys(self) -> None:
701
+ if self._current_device_key is None:
702
+ return
703
+ key = self._current_device_key
704
+ config = self.model.get_device_config(key)
705
+ unknown = [
706
+ k
707
+ for k in list(config.keys())
708
+ if isinstance(k, str) and k.startswith("_") and k not in self.model.SPECIAL_KEYS
709
+ ]
710
+ if not unknown:
711
+ QMessageBox.information(self, "Remove Unknown Special Keys", "No unknown special keys found.")
712
+ return
713
+ ret = QMessageBox.question(
714
+ self,
715
+ "Remove Unknown Special Keys",
716
+ "Remove unknown special keys from this device?\n\n" + "\n".join(unknown),
717
+ QMessageBox.Yes | QMessageBox.No,
718
+ QMessageBox.No,
719
+ )
720
+ if ret != QMessageBox.Yes:
721
+ return
722
+ for k in unknown:
723
+ config.pop(k, None)
724
+ self.model.set_device_config(key, config)
725
+
726
+
727
+ class ConfigEditorMainWindow(QMainWindow):
728
+ """Standalone window wrapper for the configuration editor."""
729
+
730
+ def __init__(self, parent: QWidget | None = None):
731
+ super().__init__(parent=parent)
732
+ self.setWindowTitle("NBS-BL Config Editor")
733
+ self.editor = ConfigEditorWidget(self)
734
+ self.setCentralWidget(self.editor)
735
+ self._build_menu()
736
+
737
+ def _build_menu(self) -> None:
738
+ file_menu = self.menuBar().addMenu("File")
739
+
740
+ new_action = file_menu.addAction("New")
741
+ open_action = file_menu.addAction("Open")
742
+ save_action = file_menu.addAction("Save")
743
+ save_as_action = file_menu.addAction("Save As")
744
+ file_menu.addSeparator()
745
+ exit_action = file_menu.addAction("Exit")
746
+
747
+ new_action.triggered.connect(self.editor._new_file)
748
+ open_action.triggered.connect(self.editor._open_file)
749
+ save_action.triggered.connect(self.editor._save_file)
750
+ save_as_action.triggered.connect(self.editor._save_file_as)
751
+ exit_action.triggered.connect(self.close)
752
+
753
+
754
+ def main() -> None:
755
+ """Launch the configuration editor as a standalone application."""
756
+
757
+ app = QApplication.instance()
758
+ owns_app = app is None
759
+ if app is None:
760
+ app = QApplication([])
761
+ win = ConfigEditorMainWindow()
762
+ win.resize(1100, 750)
763
+ win.show()
764
+ if owns_app:
765
+ app.exec_()
766
+
767
+
768
+ if __name__ == "__main__":
769
+ main()
770
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nbs-bl
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: NBS Beamline Framework
5
5
  Project-URL: homepage, https://github.com/xraygui/nbs-bl
6
6
  Author-email: Charles Titus <ctitus@bnl.gov>
@@ -12,6 +12,7 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Requires-Python: >=3.9
14
14
  Requires-Dist: bluesky
15
+ Requires-Dist: bluesky-live
15
16
  Requires-Dist: bluesky-queueserver
16
17
  Requires-Dist: databroker>=2.0.0b67
17
18
  Requires-Dist: ipython
@@ -1,6 +1,6 @@
1
1
  nbs_bl/__init__.py,sha256=L6YdoI4Fn6jg6fWl76pNXkWJ3_pu-msrFH3rxY4y5AY,329
2
- nbs_bl/beamline.py,sha256=1fGrJLOl7ZJgEBOPk9Zye2KJLrSOpJCCbQ4TNwvtGZc,14836
3
- nbs_bl/configuration.py,sha256=h6NnLp4xvDTbhL_N8wwr5ldKY4ITC9iDnz3wmZUA7A4,26610
2
+ nbs_bl/beamline.py,sha256=cT7RGKzIdu5UcnuiRUBlxPBV_GumNdAniNWVXAoTHqc,14880
3
+ nbs_bl/configuration.py,sha256=9c0eyYPo1JTGYJ3EQ-bdtPpnk91sj0RyrT3LT-W5-hE,26542
4
4
  nbs_bl/detectors.py,sha256=ID2LW3lN1GOejCFBlHo9Q4T9aQb_FK15oCnpJJcj2Rw,2159
5
5
  nbs_bl/gGrEqns.py,sha256=N56e_KTJIZkyno9Zecn2ofD3tnb1xZ9mcBHTiSJx5lU,7206
6
6
  nbs_bl/help.py,sha256=xDAHH1spoL2hfZUhytKLha45EZ7KvJSbgKbWWjOzcMU,3746
@@ -44,9 +44,11 @@ nbs_bl/plans/scan_decorators.py,sha256=ehmzbyoU4Ld2Kmx-DBHR0GJg_koK7ElCLu9TYCR4A
44
44
  nbs_bl/plans/scans.py,sha256=iErg7xckjbqUI3DlWBd2QCFthxyCe2Wzrb3N70dhrWM,4332
45
45
  nbs_bl/plans/suspenders.py,sha256=JQWt07o8R8wFVzjkKtYhkrrJXujs0Rtdvy3Er-OYEPs,2885
46
46
  nbs_bl/plans/time_estimation.py,sha256=u40I6P51ySSaXdb-mqdl3UMdlZ2PDhlXWC7QHq_FLmY,4931
47
- nbs_bl/plans/xas.py,sha256=i31uhpoJ3P4Y_pnyait0VWpIooSg6khbiIxWY7rY8sY,3818
47
+ nbs_bl/plans/xas.py,sha256=2rqh-eTi4HZpa1Rm76PyRueUFsrKdsL6f_inVv_k6nU,3831
48
48
  nbs_bl/qt/models/beamline.py,sha256=PUYXUkShx3XS6kYrdFZzzccsFjSit5RBP3I-JYCU3ws,410
49
+ nbs_bl/qt/models/config_model.py,sha256=1TthS1UeGk_9AfpqgCw9tMC9NH1s_cdgk3UvwA0FRVE,12745
49
50
  nbs_bl/qt/models/energy.py,sha256=z3_g8UXGw4Xj1r9b2554Bh94k0uqOT5u2iSlAOkqsZg,1460
51
+ nbs_bl/qt/widgets/config_editor.py,sha256=anqt1RKSJAG5Thzy0v1zvb7GH5JWbgiFouhlG1e4yUw,29035
50
52
  nbs_bl/qt/widgets/energy.py,sha256=YeQXBxp29TS_YVLgmHKpAr-Jc7RDAEFS1HJndcIGlJQ,7760
51
53
  nbs_bl/sim/__init__.py,sha256=UZ9yiwTvtx0Rv5Ypqu_RoR5soNI65nBQxtA7tLEGgUs,64
52
54
  nbs_bl/sim/energy.py,sha256=-Mw7HHrjDMyOH-hxgA1bz4vCjdNvzGmP-8yfeK8m5lc,13356
@@ -57,8 +59,8 @@ nbs_bl/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
59
  nbs_bl/tests/modify_regions.py,sha256=OoY0cndFuffAM3oicL6hcH4UADlYIyJ0sku08BBOq_4,4490
58
60
  nbs_bl/tests/test_frames.py,sha256=p0RpAL7T7_Q7DQnlc5P2TQ7SLbiJWijsHzGSkchWsWg,3036
59
61
  nbs_bl/tests/test_panels.py,sha256=66Xhfonwhr8-O9LQneDricg71X5lBMZ5f1WmQaD_xrY,2465
60
- nbs_bl-0.2.0.dist-info/METADATA,sha256=_sMLQbCw2_qjEGAXOTlFt-lwbD51sxY2jUJ7n1Cd1zc,1928
61
- nbs_bl-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
62
- nbs_bl-0.2.0.dist-info/entry_points.txt,sha256=H1M93MsVKucXf6mBFiSnyFetkGERq2Uiti6cGz8ohLk,54
63
- nbs_bl-0.2.0.dist-info/licenses/LICENSE,sha256=ICdbdLm_mtXdM6ItExNyqZRhmz1yUXwWdG4uvumFhHc,1879
64
- nbs_bl-0.2.0.dist-info/RECORD,,
62
+ nbs_bl-0.2.1.dist-info/METADATA,sha256=UtOdRihS3X3UjVKjLLXOCPMX9-Jw1uXBXdtuA40Ry0A,1956
63
+ nbs_bl-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
64
+ nbs_bl-0.2.1.dist-info/entry_points.txt,sha256=T3mrRIIjujIy1Gsxr68-teFrNR3w32YuAhZ0PJr3zfc,133
65
+ nbs_bl-0.2.1.dist-info/licenses/LICENSE,sha256=ICdbdLm_mtXdM6ItExNyqZRhmz1yUXwWdG4uvumFhHc,1879
66
+ nbs_bl-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ nbs-bl-config-editor = nbs_bl.qt.widgets.config_editor:main
3
+
4
+ [nbs_bl.plan_loaders]
5
+ xas = nbs_bl.plans.xas:load_xas
@@ -1,2 +0,0 @@
1
- [nbs_bl.plan_loaders]
2
- xas = nbs_bl.plans.xas:load_xas
File without changes