damnit 0.1__tar.gz
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.
- damnit-0.1/LICENSE +29 -0
- damnit-0.1/PKG-INFO +54 -0
- damnit-0.1/README.md +7 -0
- damnit-0.1/damnit/__init__.py +5 -0
- damnit-0.1/damnit/api.py +376 -0
- damnit-0.1/damnit/backend/__init__.py +1 -0
- damnit-0.1/damnit/backend/db.py +428 -0
- damnit-0.1/damnit/backend/extract_data.py +392 -0
- damnit-0.1/damnit/backend/listener.py +181 -0
- damnit-0.1/damnit/backend/supervisord.conf +23 -0
- damnit-0.1/damnit/backend/supervisord.py +163 -0
- damnit-0.1/damnit/backend/test_listener.py +68 -0
- damnit-0.1/damnit/backend/user_variables.py +110 -0
- damnit-0.1/damnit/base_context_file.py +49 -0
- damnit-0.1/damnit/cli.py +268 -0
- damnit-0.1/damnit/context.py +13 -0
- damnit-0.1/damnit/ctxsupport/README.md +6 -0
- damnit-0.1/damnit/ctxsupport/ctxrunner.py +698 -0
- damnit-0.1/damnit/ctxsupport/damnit_ctx.py +71 -0
- damnit-0.1/damnit/definitions.py +9 -0
- damnit-0.1/damnit/gui/__init__.py +1 -0
- damnit-0.1/damnit/gui/editor.py +113 -0
- damnit-0.1/damnit/gui/ico/AMORE.png +0 -0
- damnit-0.1/damnit/gui/ico/closed-hover.png +0 -0
- damnit-0.1/damnit/gui/ico/closed.png +0 -0
- damnit-0.1/damnit/gui/ico/export.png +0 -0
- damnit-0.1/damnit/gui/ico/green_circle.svg +48 -0
- damnit-0.1/damnit/gui/ico/lock_closed_icon.png +0 -0
- damnit-0.1/damnit/gui/ico/lock_open_icon.png +0 -0
- damnit-0.1/damnit/gui/ico/lock_opening_icon.png +0 -0
- damnit-0.1/damnit/gui/ico/open-hover.png +0 -0
- damnit-0.1/damnit/gui/ico/open.png +0 -0
- damnit-0.1/damnit/gui/ico/red_circle.svg +47 -0
- damnit-0.1/damnit/gui/ico/search_icon.png +0 -0
- damnit-0.1/damnit/gui/ico/yellow_circle.svg +48 -0
- damnit-0.1/damnit/gui/kafka.py +79 -0
- damnit-0.1/damnit/gui/main_window.py +998 -0
- damnit-0.1/damnit/gui/open_dialog.py +92 -0
- damnit-0.1/damnit/gui/open_dialog.ui +181 -0
- damnit-0.1/damnit/gui/open_dialog_ui.py +76 -0
- damnit-0.1/damnit/gui/plot.py +689 -0
- damnit-0.1/damnit/gui/table.py +790 -0
- damnit-0.1/damnit/gui/user_variables.py +194 -0
- damnit-0.1/damnit/gui/widgets.py +78 -0
- damnit-0.1/damnit/gui/zulip_messenger.py +316 -0
- damnit-0.1/damnit/migrations.py +372 -0
- damnit-0.1/damnit/util.py +73 -0
- damnit-0.1/pyproject.toml +70 -0
damnit-0.1/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022, European X-Ray Free-Electron Laser Facility GmbH
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
damnit-0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: damnit
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: The Data And Metadata iNspection Interactive Thing
|
|
5
|
+
Author-email: Thomas Kluyver <thomas.kluyver@xfel.eu>, Luca Gelisio <luca.gelisio@xfel.eu>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
9
|
+
Requires-Dist: h5netcdf
|
|
10
|
+
Requires-Dist: h5py
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: xarray
|
|
13
|
+
Requires-Dist: EXtra-data ; extra == "backend"
|
|
14
|
+
Requires-Dist: ipython ; extra == "backend"
|
|
15
|
+
Requires-Dist: kafka-python ; extra == "backend"
|
|
16
|
+
Requires-Dist: matplotlib ; extra == "backend"
|
|
17
|
+
Requires-Dist: numpy ; extra == "backend"
|
|
18
|
+
Requires-Dist: pyyaml ; extra == "backend"
|
|
19
|
+
Requires-Dist: requests ; extra == "backend"
|
|
20
|
+
Requires-Dist: scipy ; extra == "backend"
|
|
21
|
+
Requires-Dist: supervisor ; extra == "backend"
|
|
22
|
+
Requires-Dist: termcolor ; extra == "backend"
|
|
23
|
+
Requires-Dist: mkdocs ; extra == "docs"
|
|
24
|
+
Requires-Dist: mkdocs-material ; extra == "docs"
|
|
25
|
+
Requires-Dist: mkdocstrings ; extra == "docs"
|
|
26
|
+
Requires-Dist: mkdocstrings-python ; extra == "docs"
|
|
27
|
+
Requires-Dist: pymdown-extensions ; extra == "docs"
|
|
28
|
+
Requires-Dist: adeqt ; extra == "gui"
|
|
29
|
+
Requires-Dist: mplcursors ; extra == "gui"
|
|
30
|
+
Requires-Dist: mpl-pan-zoom ; extra == "gui"
|
|
31
|
+
Requires-Dist: openpyxl ; extra == "gui"
|
|
32
|
+
Requires-Dist: PyQt5 ; extra == "gui"
|
|
33
|
+
Requires-Dist: pyflakes ; extra == "gui"
|
|
34
|
+
Requires-Dist: QScintilla==2.13 ; extra == "gui"
|
|
35
|
+
Requires-Dist: tabulate ; extra == "gui"
|
|
36
|
+
Requires-Dist: pytest ; extra == "test"
|
|
37
|
+
Requires-Dist: pytest-qt ; extra == "test"
|
|
38
|
+
Requires-Dist: pytest-xvfb ; extra == "test"
|
|
39
|
+
Requires-Dist: pytest-timeout ; extra == "test"
|
|
40
|
+
Requires-Dist: pytest-virtualenv ; extra == "test"
|
|
41
|
+
Project-URL: Home, https://github.com/European-XFEL/DAMNIT
|
|
42
|
+
Provides-Extra: backend
|
|
43
|
+
Provides-Extra: docs
|
|
44
|
+
Provides-Extra: gui
|
|
45
|
+
Provides-Extra: test
|
|
46
|
+
|
|
47
|
+
# DAMNIT
|
|
48
|
+
|
|
49
|
+
[](https://damnit.readthedocs.io/en/latest/?badge=latest)
|
|
50
|
+
|
|
51
|
+
DAMNIT is a tool developed at the European XFEL to help users create an
|
|
52
|
+
automated overview of their experiment. Check out the documentation for more
|
|
53
|
+
information: https://damnit.rtfd.io
|
|
54
|
+
|
damnit-0.1/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# DAMNIT
|
|
2
|
+
|
|
3
|
+
[](https://damnit.readthedocs.io/en/latest/?badge=latest)
|
|
4
|
+
|
|
5
|
+
DAMNIT is a tool developed at the European XFEL to help users create an
|
|
6
|
+
automated overview of their experiment. Check out the documentation for more
|
|
7
|
+
information: https://damnit.rtfd.io
|
damnit-0.1/damnit/api.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from glob import iglob
|
|
3
|
+
import os.path as osp
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
|
|
8
|
+
import h5py
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import xarray as xr
|
|
11
|
+
|
|
12
|
+
from .backend.db import DamnitDB, BlobTypes
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# This is a copy of damnit.ctxsupport.ctxrunner.DataType, purely so that we can
|
|
16
|
+
# avoid the dependencies of the runner in the API (namely requests and pyyaml).
|
|
17
|
+
class DataType(Enum):
|
|
18
|
+
DataArray = "dataarray"
|
|
19
|
+
Dataset = "dataset"
|
|
20
|
+
Image = "image"
|
|
21
|
+
Timestamp = "timestamp"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DATA_ROOT_DIR = os.environ.get('EXTRA_DATA_DATA_ROOT', '/gpfs/exfel/exp')
|
|
25
|
+
|
|
26
|
+
# Also copied, this time from extra_data.read_machinery
|
|
27
|
+
def find_proposal(propno):
|
|
28
|
+
"""Find the proposal directory for a given proposal on Maxwell"""
|
|
29
|
+
if '/' in propno:
|
|
30
|
+
# Already passed a proposal directory
|
|
31
|
+
return propno
|
|
32
|
+
|
|
33
|
+
for d in iglob(osp.join(DATA_ROOT_DIR, '*/*/{}'.format(propno))):
|
|
34
|
+
return d
|
|
35
|
+
|
|
36
|
+
raise FileNotFoundError("Couldn't find proposal dir for {!r}".format(propno))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VariableData:
|
|
40
|
+
"""Represents a variable for a single run.
|
|
41
|
+
|
|
42
|
+
Don't create this object yourself, index a [Damnit][damnit.api.Damnit] or
|
|
43
|
+
[RunVariables][damnit.api.RunVariables] object instead.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, name: str, title: str,
|
|
47
|
+
proposal: int, run: int,
|
|
48
|
+
h5_path: Path, data_format_version: int,
|
|
49
|
+
db: DamnitDB, db_only: bool):
|
|
50
|
+
self._name = name
|
|
51
|
+
self._title = title
|
|
52
|
+
self._proposal = proposal
|
|
53
|
+
self._run = run
|
|
54
|
+
self._h5_path = h5_path
|
|
55
|
+
self._data_format_version = data_format_version
|
|
56
|
+
self._db = db
|
|
57
|
+
self._db_only = db_only
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def name(self) -> str:
|
|
61
|
+
"""The variable name."""
|
|
62
|
+
return self._name
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def title(self) -> str:
|
|
66
|
+
"""The variable title (defaults to the name if not set explicitly)."""
|
|
67
|
+
return self._title
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def proposal(self) -> int:
|
|
71
|
+
"""The proposal to which the variable belongs."""
|
|
72
|
+
return self._proposal
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def run(self) -> int:
|
|
76
|
+
"""The run to which the variable belongs."""
|
|
77
|
+
return self._run
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def file(self) -> Path:
|
|
81
|
+
"""The path to the HDF5 file for the run.
|
|
82
|
+
|
|
83
|
+
Note that the data for user-editable variables will not be stored in the
|
|
84
|
+
HDF5 files.
|
|
85
|
+
"""
|
|
86
|
+
return self._h5_path
|
|
87
|
+
|
|
88
|
+
@contextmanager
|
|
89
|
+
def _open_h5_group(self):
|
|
90
|
+
with h5py.File(self._h5_path) as f:
|
|
91
|
+
yield f[self.name]
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _type_hint(group):
|
|
95
|
+
hint_s = group.attrs.get('_damnit_objtype', '')
|
|
96
|
+
if hint_s:
|
|
97
|
+
return DataType(hint_s)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def _read_netcdf(self, one_array=False):
|
|
101
|
+
load = xr.load_dataarray if one_array else xr.load_dataset
|
|
102
|
+
obj = load(self._h5_path, group=self.name, engine="h5netcdf")
|
|
103
|
+
# Remove internal attributes from loaded object
|
|
104
|
+
obj.attrs = {k: v for (k, v) in obj.attrs.items()
|
|
105
|
+
if not k.startswith('_damnit_')}
|
|
106
|
+
return obj
|
|
107
|
+
|
|
108
|
+
def read(self):
|
|
109
|
+
"""Read the data for the variable."""
|
|
110
|
+
if self._db_only:
|
|
111
|
+
return self.summary()
|
|
112
|
+
|
|
113
|
+
with self._open_h5_group() as group:
|
|
114
|
+
type_hint = self._type_hint(group)
|
|
115
|
+
if type_hint is DataType.Dataset:
|
|
116
|
+
return self._read_netcdf()
|
|
117
|
+
elif type_hint is DataType.DataArray:
|
|
118
|
+
return self._read_netcdf(one_array=True)
|
|
119
|
+
|
|
120
|
+
dset = group["data"]
|
|
121
|
+
if h5py.check_string_dtype(dset.dtype) is not None:
|
|
122
|
+
# Strings. Scalar/non-scalar strings need to be read differently.
|
|
123
|
+
if dset.ndim == 0:
|
|
124
|
+
return dset[()].decode("utf-8", "surrogateescape")
|
|
125
|
+
else:
|
|
126
|
+
return dset.asstr("utf-8", "surrogateescape")[0]
|
|
127
|
+
elif dset.ndim == 0:
|
|
128
|
+
# Scalars
|
|
129
|
+
return dset[()]
|
|
130
|
+
else:
|
|
131
|
+
# Otherwise, return a Numpy array
|
|
132
|
+
return group["data"][()]
|
|
133
|
+
|
|
134
|
+
def summary(self):
|
|
135
|
+
"""Read the summary data for a variable.
|
|
136
|
+
|
|
137
|
+
For user-editable variables like comments, this will be the same as
|
|
138
|
+
[VariableData.read()][damnit.api.VariableData.read].
|
|
139
|
+
"""
|
|
140
|
+
result = self._db.conn.execute("""
|
|
141
|
+
SELECT value, max(version) FROM run_variables
|
|
142
|
+
WHERE proposal=? AND run=? AND name=?
|
|
143
|
+
""", (self.proposal, self.run, self.name)).fetchone()
|
|
144
|
+
|
|
145
|
+
if result is None:
|
|
146
|
+
# This should never be reached unless the variable is deleted
|
|
147
|
+
# after creating the VariableData object.
|
|
148
|
+
raise RuntimeError(f"Could not find value for '{self.name}' in p{self.proposal}, r{self.name}")
|
|
149
|
+
else:
|
|
150
|
+
return result[0]
|
|
151
|
+
|
|
152
|
+
def __repr__(self):
|
|
153
|
+
return f"<VariableData for '{self.name}' in p{self.proposal}, r{self.run}>"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class RunVariables:
|
|
157
|
+
"""Represents the variables for a single run.
|
|
158
|
+
|
|
159
|
+
Don't create this object yourself, index a [Damnit][damnit.api.Damnit]
|
|
160
|
+
object instead.
|
|
161
|
+
|
|
162
|
+
Indexing this by either a variable name or title will return a
|
|
163
|
+
[VariableData][damnit.api.VariableData] object:
|
|
164
|
+
```python
|
|
165
|
+
db = Damnit(1234)
|
|
166
|
+
run_vars = db[100]
|
|
167
|
+
myvar = run_vars["myvar"] # Alternatively by title, `run_vars["My Variable"]`
|
|
168
|
+
```
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self, db_dir, run):
|
|
172
|
+
self._db = DamnitDB.from_dir(db_dir)
|
|
173
|
+
self._proposal = self._db.metameta["proposal"]
|
|
174
|
+
self._run = run
|
|
175
|
+
self._data_format_version = self._db.metameta["data_format_version"]
|
|
176
|
+
self._h5_path = Path(db_dir) / f"extracted_data/p{self._proposal}_r{self._run}.h5"
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def proposal(self) -> int:
|
|
180
|
+
"""The proposal of the run."""
|
|
181
|
+
return self._proposal
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def run(self) -> int:
|
|
185
|
+
"""The run number."""
|
|
186
|
+
return self._run
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def file(self) -> Path:
|
|
190
|
+
"""The path to the HDF5 file for the run."""
|
|
191
|
+
return self._h5_path
|
|
192
|
+
|
|
193
|
+
def __getitem__(self, name):
|
|
194
|
+
key_locs = self._key_locations()
|
|
195
|
+
names_to_titles = self._var_titles()
|
|
196
|
+
titles_to_names = { title: name for name, title in names_to_titles.items() }
|
|
197
|
+
|
|
198
|
+
if name not in key_locs and name not in titles_to_names:
|
|
199
|
+
raise KeyError(f"Variable data for '{name!r}' not found for p{self.proposal}, r{self.run}")
|
|
200
|
+
|
|
201
|
+
if name in titles_to_names:
|
|
202
|
+
name = titles_to_names[name]
|
|
203
|
+
|
|
204
|
+
return VariableData(name, names_to_titles[name],
|
|
205
|
+
self.proposal, self.run,
|
|
206
|
+
self._h5_path, self._data_format_version,
|
|
207
|
+
self._db, key_locs[name])
|
|
208
|
+
|
|
209
|
+
def _key_locations(self):
|
|
210
|
+
# Read keys from the HDF5 file
|
|
211
|
+
with h5py.File(self.file) as f:
|
|
212
|
+
all_keys = { name: False for name in f.keys() }
|
|
213
|
+
del all_keys[".reduced"]
|
|
214
|
+
|
|
215
|
+
# And the keys from the database
|
|
216
|
+
user_vars = list(self._db.get_user_variables().keys())
|
|
217
|
+
user_vars.append("comment")
|
|
218
|
+
|
|
219
|
+
for var_name in user_vars:
|
|
220
|
+
result = self._db.conn.execute("""
|
|
221
|
+
SELECT name, value, max(version) FROM run_variables
|
|
222
|
+
WHERE proposal=? AND run=? AND name=?
|
|
223
|
+
""", (self.proposal, self.run, var_name)).fetchone()
|
|
224
|
+
if result is not None and result[1] is not None:
|
|
225
|
+
all_keys[var_name] = True
|
|
226
|
+
|
|
227
|
+
return all_keys
|
|
228
|
+
|
|
229
|
+
def keys(self) -> list:
|
|
230
|
+
"""The names of the available variables.
|
|
231
|
+
|
|
232
|
+
Note that a variable will not appear in the list if there is no data for
|
|
233
|
+
it.
|
|
234
|
+
"""
|
|
235
|
+
return sorted(self._key_locations().keys())
|
|
236
|
+
|
|
237
|
+
def _var_titles(self):
|
|
238
|
+
result = self._db.conn.execute("SELECT name, title FROM variables").fetchall()
|
|
239
|
+
available_vars = self.keys()
|
|
240
|
+
return { row[0]: row[1] if row[1] is not None else row[0] for row in result
|
|
241
|
+
if row[0] in available_vars }
|
|
242
|
+
|
|
243
|
+
def titles(self) -> list:
|
|
244
|
+
"""The titles of available variables.
|
|
245
|
+
|
|
246
|
+
As with [RunVariables.keys()][damnit.api.RunVariables.keys], only
|
|
247
|
+
variables that have data for the run will be included.
|
|
248
|
+
"""
|
|
249
|
+
return sorted(list(self._var_titles().values()))
|
|
250
|
+
|
|
251
|
+
def _ipython_key_completions_(self):
|
|
252
|
+
# This makes autocompleting the variable names work with ipython
|
|
253
|
+
return self.keys()
|
|
254
|
+
|
|
255
|
+
def __repr__(self):
|
|
256
|
+
return f"<RunVariables for p{self.proposal}, r{self.run} with {len(self.keys())} variables>"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class Damnit:
|
|
260
|
+
"""Represents a DAMNIT database.
|
|
261
|
+
|
|
262
|
+
Indexing this will return either a [RunVariables][damnit.api.RunVariables]
|
|
263
|
+
or [VariableData][damnit.api.VariableData] object:
|
|
264
|
+
```python
|
|
265
|
+
db = Damnit(1234)
|
|
266
|
+
|
|
267
|
+
# Index by run number to get a RunVariables object
|
|
268
|
+
run_vars = db[100]
|
|
269
|
+
# Or by run number and variable name/title to get a VariableData object
|
|
270
|
+
myvar = db[100, "myvar"]
|
|
271
|
+
```
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
def __init__(self, location):
|
|
275
|
+
"""
|
|
276
|
+
This is the entrypoint for inspecting data stored by DAMNIT.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
location (int or str or Path): This can be either a proposal number or
|
|
280
|
+
a path to a database directory.
|
|
281
|
+
"""
|
|
282
|
+
if isinstance(location, int):
|
|
283
|
+
proposal_path = find_proposal(f"p{location:06}")
|
|
284
|
+
self._db_dir = Path(proposal_path) / "usr/Shared/amore"
|
|
285
|
+
elif isinstance(location, (Path, str)):
|
|
286
|
+
self._db_dir = Path(location)
|
|
287
|
+
else:
|
|
288
|
+
raise TypeError(f"Unsupported location: {location}")
|
|
289
|
+
|
|
290
|
+
if not self._db_dir.is_dir():
|
|
291
|
+
raise FileNotFoundError(f"DAMNIT directory does not exist: {self._db_dir}")
|
|
292
|
+
|
|
293
|
+
self._db_path = self._db_dir / "runs.sqlite"
|
|
294
|
+
if not self._db_path.is_file():
|
|
295
|
+
raise FileNotFoundError(f"DAMNIT database does not exist: {self._db_path}")
|
|
296
|
+
|
|
297
|
+
self._db = DamnitDB(self._db_path)
|
|
298
|
+
|
|
299
|
+
def __getitem__(self, obj):
|
|
300
|
+
if isinstance(obj, int):
|
|
301
|
+
run, variable = obj, None
|
|
302
|
+
elif isinstance(obj, tuple) and len(obj) == 2:
|
|
303
|
+
run, variable = obj
|
|
304
|
+
else:
|
|
305
|
+
raise TypeError(f"Unrecognised key type: {type(obj)}")
|
|
306
|
+
|
|
307
|
+
if run not in self.runs():
|
|
308
|
+
raise KeyError(f"Unknown run number for p{self.proposal}")
|
|
309
|
+
|
|
310
|
+
run_vars = RunVariables(self._db_dir, run)
|
|
311
|
+
return run_vars[variable] if variable is not None else run_vars
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def proposal(self) -> int:
|
|
315
|
+
"""The currently active proposal of the database."""
|
|
316
|
+
return self._db.metameta["proposal"]
|
|
317
|
+
|
|
318
|
+
def runs(self) -> list:
|
|
319
|
+
"""A list of all existing runs.
|
|
320
|
+
|
|
321
|
+
Note that this does not include runs that were pre-created through the
|
|
322
|
+
GUI but were never taken by the DAQ.
|
|
323
|
+
"""
|
|
324
|
+
result = self._db.conn.execute("SELECT run FROM run_info WHERE start_time IS NOT NULL").fetchall()
|
|
325
|
+
return [row[0] for row in result]
|
|
326
|
+
|
|
327
|
+
def table(self, with_titles=False) -> pd.DataFrame:
|
|
328
|
+
"""Retrieve the run table as a [DataFrame][pandas.DataFrame].
|
|
329
|
+
|
|
330
|
+
There are a few differences compared to what you'll see in the table
|
|
331
|
+
displayed in the GUI:
|
|
332
|
+
|
|
333
|
+
- Images will be replaced with an `<image>` string.
|
|
334
|
+
- Runs that were pre-created through the GUI but never taken by the DAQ
|
|
335
|
+
will not be included.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
with_titles (bool): Whether to use variable titles instead of names
|
|
339
|
+
for the columns in the dataframe.
|
|
340
|
+
"""
|
|
341
|
+
df = pd.read_sql_query("SELECT * FROM runs", self._db.conn)
|
|
342
|
+
|
|
343
|
+
# Convert the start_time into a datetime column
|
|
344
|
+
start_time = pd.to_datetime(df["start_time"], unit="s", utc=True)
|
|
345
|
+
df["start_time"] = start_time.dt.tz_convert("Europe/Berlin")
|
|
346
|
+
|
|
347
|
+
# Delete added_at, this is internal
|
|
348
|
+
del df["added_at"]
|
|
349
|
+
|
|
350
|
+
# Ensure that there's always a comment column for consistency, it may
|
|
351
|
+
# not be present if no comments were made.
|
|
352
|
+
if "comment" not in df:
|
|
353
|
+
df.insert(3, "comment", None)
|
|
354
|
+
|
|
355
|
+
# Convert PNG blobs into a string
|
|
356
|
+
def image2str(value):
|
|
357
|
+
if isinstance(value, bytes) and BlobTypes.identify(value) is BlobTypes.png:
|
|
358
|
+
return "<image>"
|
|
359
|
+
else:
|
|
360
|
+
return value
|
|
361
|
+
df = df.applymap(image2str)
|
|
362
|
+
|
|
363
|
+
# Use the full variable titles
|
|
364
|
+
if with_titles:
|
|
365
|
+
results = self._db.conn.execute("SELECT name, title FROM variables").fetchall()
|
|
366
|
+
renames = { row[0]: row[1] for row in results }
|
|
367
|
+
renames["proposal"] = "Proposal"
|
|
368
|
+
renames["run"] = "Run"
|
|
369
|
+
renames["start_time"] = "Timestamp"
|
|
370
|
+
|
|
371
|
+
df.rename(columns=renames, inplace=True)
|
|
372
|
+
|
|
373
|
+
return df
|
|
374
|
+
|
|
375
|
+
def __repr__(self):
|
|
376
|
+
return f"<Damnit database for p{self.proposal}>"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .supervisord import initialize_and_start_backend, backend_is_running
|