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.
Files changed (48) hide show
  1. damnit-0.1/LICENSE +29 -0
  2. damnit-0.1/PKG-INFO +54 -0
  3. damnit-0.1/README.md +7 -0
  4. damnit-0.1/damnit/__init__.py +5 -0
  5. damnit-0.1/damnit/api.py +376 -0
  6. damnit-0.1/damnit/backend/__init__.py +1 -0
  7. damnit-0.1/damnit/backend/db.py +428 -0
  8. damnit-0.1/damnit/backend/extract_data.py +392 -0
  9. damnit-0.1/damnit/backend/listener.py +181 -0
  10. damnit-0.1/damnit/backend/supervisord.conf +23 -0
  11. damnit-0.1/damnit/backend/supervisord.py +163 -0
  12. damnit-0.1/damnit/backend/test_listener.py +68 -0
  13. damnit-0.1/damnit/backend/user_variables.py +110 -0
  14. damnit-0.1/damnit/base_context_file.py +49 -0
  15. damnit-0.1/damnit/cli.py +268 -0
  16. damnit-0.1/damnit/context.py +13 -0
  17. damnit-0.1/damnit/ctxsupport/README.md +6 -0
  18. damnit-0.1/damnit/ctxsupport/ctxrunner.py +698 -0
  19. damnit-0.1/damnit/ctxsupport/damnit_ctx.py +71 -0
  20. damnit-0.1/damnit/definitions.py +9 -0
  21. damnit-0.1/damnit/gui/__init__.py +1 -0
  22. damnit-0.1/damnit/gui/editor.py +113 -0
  23. damnit-0.1/damnit/gui/ico/AMORE.png +0 -0
  24. damnit-0.1/damnit/gui/ico/closed-hover.png +0 -0
  25. damnit-0.1/damnit/gui/ico/closed.png +0 -0
  26. damnit-0.1/damnit/gui/ico/export.png +0 -0
  27. damnit-0.1/damnit/gui/ico/green_circle.svg +48 -0
  28. damnit-0.1/damnit/gui/ico/lock_closed_icon.png +0 -0
  29. damnit-0.1/damnit/gui/ico/lock_open_icon.png +0 -0
  30. damnit-0.1/damnit/gui/ico/lock_opening_icon.png +0 -0
  31. damnit-0.1/damnit/gui/ico/open-hover.png +0 -0
  32. damnit-0.1/damnit/gui/ico/open.png +0 -0
  33. damnit-0.1/damnit/gui/ico/red_circle.svg +47 -0
  34. damnit-0.1/damnit/gui/ico/search_icon.png +0 -0
  35. damnit-0.1/damnit/gui/ico/yellow_circle.svg +48 -0
  36. damnit-0.1/damnit/gui/kafka.py +79 -0
  37. damnit-0.1/damnit/gui/main_window.py +998 -0
  38. damnit-0.1/damnit/gui/open_dialog.py +92 -0
  39. damnit-0.1/damnit/gui/open_dialog.ui +181 -0
  40. damnit-0.1/damnit/gui/open_dialog_ui.py +76 -0
  41. damnit-0.1/damnit/gui/plot.py +689 -0
  42. damnit-0.1/damnit/gui/table.py +790 -0
  43. damnit-0.1/damnit/gui/user_variables.py +194 -0
  44. damnit-0.1/damnit/gui/widgets.py +78 -0
  45. damnit-0.1/damnit/gui/zulip_messenger.py +316 -0
  46. damnit-0.1/damnit/migrations.py +372 -0
  47. damnit-0.1/damnit/util.py +73 -0
  48. 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
+ [![Documentation Status](https://readthedocs.org/projects/damnit/badge/?version=latest)](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
+ [![Documentation Status](https://readthedocs.org/projects/damnit/badge/?version=latest)](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
@@ -0,0 +1,5 @@
1
+ """Prototype for extracting and showing metadata (AMORE project)"""
2
+
3
+ __version__ = '0.1'
4
+
5
+ from .api import Damnit, RunVariables, VariableData
@@ -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