labmate 0.8.4__py3-none-any.whl → 0.10.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.
- labmate/__config__.py +1 -1
- labmate/acquisition/acquisition_data.py +1 -1
- labmate/acquisition/acquisition_manager.py +17 -6
- labmate/acquisition/analysis_data.py +31 -3
- labmate/acquisition_notebook/acquisition_analysis_manager.py +93 -19
- labmate/acquisition_notebook/display_widget.py +26 -31
- labmate/display/__init__.py +10 -3
- labmate/display/buttons.py +4 -0
- labmate/display/html_output.py +48 -0
- labmate/display/main.py +19 -3
- labmate/logger/__init__.py +79 -0
- labmate/utils/__init__.py +2 -1
- labmate/utils/file_read.py +48 -1
- labmate/utils/title_parsing.py +23 -6
- {labmate-0.8.4.dist-info → labmate-0.10.0.dist-info}/METADATA +1 -1
- {labmate-0.8.4.dist-info → labmate-0.10.0.dist-info}/RECORD +19 -19
- {labmate-0.8.4.dist-info → labmate-0.10.0.dist-info}/WHEEL +1 -1
- labmate/acquisition/logger_setup.py +0 -5
- {labmate-0.8.4.dist-info → labmate-0.10.0.dist-info}/LICENCE +0 -0
- {labmate-0.8.4.dist-info → labmate-0.10.0.dist-info}/top_level.txt +0 -0
labmate/__config__.py
CHANGED
|
@@ -26,15 +26,17 @@ class AcquisitionManager:
|
|
|
26
26
|
"""AcquisitionManager."""
|
|
27
27
|
|
|
28
28
|
_data_directory: Path
|
|
29
|
-
config_files = []
|
|
30
|
-
config_files_eval = {}
|
|
29
|
+
config_files: List[str] = []
|
|
30
|
+
config_files_eval: Dict[str, str] = {}
|
|
31
|
+
_configs_last_modified: List[float] = []
|
|
31
32
|
|
|
32
|
-
_current_acquisition = None
|
|
33
|
-
_current_filepath = None
|
|
33
|
+
_current_acquisition: Optional[NotebookAcquisitionData] = None
|
|
34
|
+
_current_filepath: Optional[str] = None
|
|
34
35
|
|
|
35
|
-
_save_files = False
|
|
36
|
-
_save_on_edit = True
|
|
36
|
+
_save_files: bool = False
|
|
37
|
+
_save_on_edit: bool = True
|
|
37
38
|
_init_code = None
|
|
39
|
+
_once_saved: bool
|
|
38
40
|
|
|
39
41
|
cell: Optional[str] = None
|
|
40
42
|
|
|
@@ -54,9 +56,11 @@ class AcquisitionManager:
|
|
|
54
56
|
|
|
55
57
|
self._current_acquisition = None
|
|
56
58
|
self._acquisition_tmp_data = None
|
|
59
|
+
self._once_saved = False
|
|
57
60
|
|
|
58
61
|
self.config_files = []
|
|
59
62
|
self.config_files_eval = {}
|
|
63
|
+
self._configs_last_modified = []
|
|
60
64
|
|
|
61
65
|
if data_directory is not None:
|
|
62
66
|
self.data_directory = Path(data_directory)
|
|
@@ -187,13 +191,19 @@ class AcquisitionManager:
|
|
|
187
191
|
return None
|
|
188
192
|
return AcquisitionTmpData(**jsn.read(path))
|
|
189
193
|
|
|
194
|
+
def _get_configs_last_modified(self) -> List[float]:
|
|
195
|
+
return [os.path.getmtime(file) for file in self.config_files]
|
|
196
|
+
|
|
190
197
|
def new_acquisition(
|
|
191
198
|
self, name: str, cell: Optional[str] = None, save_on_edit: Optional[bool] = None
|
|
192
199
|
) -> NotebookAcquisitionData:
|
|
193
200
|
"""Create a new acquisition with the given experiment name."""
|
|
194
201
|
self._current_acquisition = None
|
|
202
|
+
self._once_saved = False
|
|
195
203
|
self.cell = cell
|
|
196
204
|
configs = read_files(self.config_files)
|
|
205
|
+
self._configs_last_modified = self._get_configs_last_modified()
|
|
206
|
+
|
|
197
207
|
if self.config_files_eval:
|
|
198
208
|
configs = append_values_from_modules_to_files(
|
|
199
209
|
configs, self.config_files_eval
|
|
@@ -310,4 +320,5 @@ class AcquisitionManager:
|
|
|
310
320
|
acq_data.save_additional_info()
|
|
311
321
|
if acq_data.save_on_edit is False:
|
|
312
322
|
acq_data.save()
|
|
323
|
+
self._once_saved = True
|
|
313
324
|
return self
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"""AnalysisData class."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
|
-
from typing import List, Literal, Optional, Protocol, Tuple, TypeVar, Union
|
|
5
|
+
from typing import List, Literal, Optional, Protocol, Tuple, TypedDict, TypeVar, Union
|
|
5
6
|
|
|
6
7
|
from dh5 import DH5
|
|
7
8
|
from dh5.path import Path
|
|
8
9
|
|
|
9
10
|
from .. import utils
|
|
11
|
+
from ..logger import logger
|
|
10
12
|
from .analysis_loop import AnalysisLoop
|
|
11
13
|
from .config_file import ConfigFile
|
|
12
|
-
from .logger_setup import logger
|
|
13
14
|
|
|
14
15
|
_T = TypeVar("_T", bound="AnalysisData")
|
|
15
16
|
|
|
@@ -21,6 +22,13 @@ class FigureProtocol(Protocol):
|
|
|
21
22
|
"""Save the figure to a file."""
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
class PdfMetadataDict(TypedDict, total=False):
|
|
26
|
+
"""Metadata for the pdf file."""
|
|
27
|
+
|
|
28
|
+
Subject: Union[str, dict]
|
|
29
|
+
Keywords: Union[str, dict]
|
|
30
|
+
|
|
31
|
+
|
|
24
32
|
class AnalysisData(DH5):
|
|
25
33
|
"""A subclass of DH5 that provides additional functionality for analyzing data.
|
|
26
34
|
|
|
@@ -167,6 +175,7 @@ class AnalysisData(DH5):
|
|
|
167
175
|
name: Optional[Union[str, int]] = None,
|
|
168
176
|
extensions: Optional[str] = None,
|
|
169
177
|
tight_layout: bool = True,
|
|
178
|
+
metadata: Optional[PdfMetadataDict] = None,
|
|
170
179
|
**kwargs,
|
|
171
180
|
) -> _T:
|
|
172
181
|
"""Save the figure with the filename (...)_FIG_name.
|
|
@@ -201,7 +210,26 @@ class AnalysisData(DH5):
|
|
|
201
210
|
)
|
|
202
211
|
if tight_layout and hasattr(fig, "tight_layout"):
|
|
203
212
|
fig.tight_layout() # type: ignore
|
|
204
|
-
|
|
213
|
+
if metadata is None:
|
|
214
|
+
fig.savefig(full_fig_name, **kwargs)
|
|
215
|
+
else:
|
|
216
|
+
if not full_fig_name.endswith(".pdf"):
|
|
217
|
+
raise ValueError("Metadata can be added only to pdf files.")
|
|
218
|
+
|
|
219
|
+
from matplotlib.backends.backend_pdf import PdfPages
|
|
220
|
+
|
|
221
|
+
pdf_fig = PdfPages(full_fig_name)
|
|
222
|
+
fig.savefig(pdf_fig, format="pdf", **kwargs) # type: ignore
|
|
223
|
+
metadata = metadata or {}
|
|
224
|
+
if not isinstance(metadata.get("Subject", ""), str):
|
|
225
|
+
metadata["Subject"] = json.dumps(metadata.get("Subject"))
|
|
226
|
+
|
|
227
|
+
if not isinstance(metadata.get("Keywords", ""), str):
|
|
228
|
+
metadata["Keywords"] = json.dumps(metadata.get("Keywords"))
|
|
229
|
+
|
|
230
|
+
pdf_metadata = pdf_fig.infodict()
|
|
231
|
+
pdf_metadata.update(metadata)
|
|
232
|
+
pdf_fig.close()
|
|
205
233
|
|
|
206
234
|
self._figure_saved = True
|
|
207
235
|
|
|
@@ -16,6 +16,7 @@ from typing import (
|
|
|
16
16
|
|
|
17
17
|
from .. import display, utils
|
|
18
18
|
from ..acquisition import AcquisitionManager, AnalysisData
|
|
19
|
+
from ..logger import logger
|
|
19
20
|
from . import display_widget
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
@@ -24,19 +25,17 @@ if TYPE_CHECKING:
|
|
|
24
25
|
from ..acquisition import FigureProtocol
|
|
25
26
|
from ..acquisition.config_file import ConfigFile
|
|
26
27
|
|
|
28
|
+
# from ..logger import Logger
|
|
29
|
+
|
|
27
30
|
|
|
28
31
|
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO)
|
|
29
|
-
logger = logging.getLogger(__name__)
|
|
30
|
-
handler = logging.StreamHandler()
|
|
31
|
-
formatter = logging.Formatter("%(levelname)s:%(message)s")
|
|
32
|
-
handler.setFormatter(formatter)
|
|
33
|
-
logger.addHandler(handler)
|
|
34
|
-
logger.setLevel(logging.INFO)
|
|
35
|
-
logger.propagate = False
|
|
36
32
|
|
|
37
33
|
_CallableWithNoArgs = Callable[[], Any]
|
|
38
34
|
|
|
39
35
|
|
|
36
|
+
CATCH_PRINT = True
|
|
37
|
+
|
|
38
|
+
|
|
40
39
|
class AcquisitionAnalysisManager(AcquisitionManager):
|
|
41
40
|
"""AcquisitionAnalysisManager.
|
|
42
41
|
|
|
@@ -126,6 +125,7 @@ class AcquisitionAnalysisManager(AcquisitionManager):
|
|
|
126
125
|
self._save_on_edit_analysis = save_on_edit_analysis
|
|
127
126
|
self._save_fig_inside_h5 = save_fig_inside_h5
|
|
128
127
|
|
|
128
|
+
self._logger = logger
|
|
129
129
|
super().__init__(
|
|
130
130
|
data_directory=str(data_directory),
|
|
131
131
|
config_files=config_files,
|
|
@@ -133,6 +133,10 @@ class AcquisitionAnalysisManager(AcquisitionManager):
|
|
|
133
133
|
save_on_edit=save_on_edit,
|
|
134
134
|
)
|
|
135
135
|
|
|
136
|
+
@property
|
|
137
|
+
def logger(self):
|
|
138
|
+
return self._logger
|
|
139
|
+
|
|
136
140
|
@property
|
|
137
141
|
def current_acquisition(self):
|
|
138
142
|
"""Return current acquisition if it's not an old data analyses."""
|
|
@@ -240,13 +244,19 @@ class AcquisitionAnalysisManager(AcquisitionManager):
|
|
|
240
244
|
|
|
241
245
|
def save_acquisition(self, **kwds) -> "AcquisitionAnalysisManager":
|
|
242
246
|
acquisition_finished = time.time()
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
247
|
+
if not self._once_saved:
|
|
248
|
+
additional_info: Dict[str, Any] = {
|
|
249
|
+
"acquisition_duration": acquisition_finished
|
|
250
|
+
- self._acquisition_started,
|
|
251
|
+
"logs": self.logger.getvalue(),
|
|
252
|
+
"prints": self.logger.get_stdout(),
|
|
253
|
+
}
|
|
254
|
+
if self._default_config_files:
|
|
255
|
+
additional_info.update(
|
|
256
|
+
{"default_config_files": self._default_config_files}
|
|
257
|
+
)
|
|
248
258
|
|
|
249
|
-
|
|
259
|
+
kwds.update({"info": additional_info})
|
|
250
260
|
|
|
251
261
|
super().save_acquisition(**kwds)
|
|
252
262
|
self._load_analysis_data()
|
|
@@ -309,12 +319,24 @@ class AcquisitionAnalysisManager(AcquisitionManager):
|
|
|
309
319
|
self._current_acquisition.current_step = step
|
|
310
320
|
self._current_acquisition.set_cell(cell, step=step)
|
|
311
321
|
self._current_acquisition.save_cell(cell, suffix=str(step))
|
|
322
|
+
configs_modified = self._get_configs_last_modified()
|
|
323
|
+
if configs_modified != self._configs_last_modified:
|
|
324
|
+
raise ValueError(
|
|
325
|
+
"Config files were modified since the previous acquisition step. "
|
|
326
|
+
"Please rerun the acquisition from the first step."
|
|
327
|
+
)
|
|
328
|
+
self.logger.stdout_flush()
|
|
312
329
|
else:
|
|
330
|
+
self.logger.reset()
|
|
313
331
|
self.new_acquisition(name=name, cell=cell, save_on_edit=save_on_edit)
|
|
314
332
|
|
|
315
|
-
logger.info(
|
|
333
|
+
self.logger.info( # pylint: disable=W1203
|
|
334
|
+
f"{step}:{self.current_filepath.basename}"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if step == 1:
|
|
338
|
+
utils.run_functions(self._acquisition_cell_prerun_hook)
|
|
316
339
|
|
|
317
|
-
utils.run_functions(self._acquisition_cell_prerun_hook)
|
|
318
340
|
utils.run_functions(prerun)
|
|
319
341
|
|
|
320
342
|
return self
|
|
@@ -366,7 +388,7 @@ class AcquisitionAnalysisManager(AcquisitionManager):
|
|
|
366
388
|
)
|
|
367
389
|
|
|
368
390
|
filename = str(self.current_filepath) # without h5
|
|
369
|
-
logger.info(os.path.basename(filename))
|
|
391
|
+
self.logger.info(os.path.basename(filename))
|
|
370
392
|
|
|
371
393
|
if (
|
|
372
394
|
(not self._is_old_data)
|
|
@@ -406,11 +428,12 @@ class AcquisitionAnalysisManager(AcquisitionManager):
|
|
|
406
428
|
run_on_call=custom_lint.on_call_functions,
|
|
407
429
|
)
|
|
408
430
|
for var in lint_result.external_vars:
|
|
409
|
-
logger.warning(
|
|
431
|
+
self.logger.warning(
|
|
410
432
|
"External variable used inside the analysis code: %s", var
|
|
411
433
|
)
|
|
412
434
|
for error in lint_result.errors:
|
|
413
|
-
logger.warning(error)
|
|
435
|
+
self.logger.warning(error)
|
|
436
|
+
|
|
414
437
|
utils.run_functions(self._analysis_cell_prerun_hook)
|
|
415
438
|
utils.run_functions(prerun)
|
|
416
439
|
|
|
@@ -535,7 +558,7 @@ class AcquisitionAnalysisManager(AcquisitionManager):
|
|
|
535
558
|
|
|
536
559
|
res = self.find_param_in_config(param_text)
|
|
537
560
|
if res is None:
|
|
538
|
-
logger.warning(
|
|
561
|
+
self.logger.warning(
|
|
539
562
|
"Parameter '%s' cannot be found in default config files.", param
|
|
540
563
|
)
|
|
541
564
|
continue
|
|
@@ -545,6 +568,57 @@ class AcquisitionAnalysisManager(AcquisitionManager):
|
|
|
545
568
|
links += link + "<br/>"
|
|
546
569
|
return display.display_html(links)
|
|
547
570
|
|
|
571
|
+
def display_cfg_link(
|
|
572
|
+
self,
|
|
573
|
+
parameters: Dict[str, Any],
|
|
574
|
+
):
|
|
575
|
+
from labmate.display import html_output
|
|
576
|
+
|
|
577
|
+
links = []
|
|
578
|
+
for param, value in parameters.items():
|
|
579
|
+
param_eq = f"{param.strip()} = "
|
|
580
|
+
res = self.find_param_in_config(param_eq)
|
|
581
|
+
if res is None:
|
|
582
|
+
self.logger.warning(
|
|
583
|
+
"Parameter '%s' cannot be found in default config files.", param
|
|
584
|
+
)
|
|
585
|
+
continue
|
|
586
|
+
file, line_no = res
|
|
587
|
+
file = self._config_files_names_to_path.get(file, file)
|
|
588
|
+
|
|
589
|
+
def update_value(param, value):
|
|
590
|
+
self.update_config_params_on_disk({param: value})
|
|
591
|
+
|
|
592
|
+
buttons = [
|
|
593
|
+
display.buttons.create_button(update_value, param, value, name="Update")
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
link = html_output.create_link_row(
|
|
597
|
+
link_text=f"{param} = ",
|
|
598
|
+
link_url=f"{file}:{line_no}",
|
|
599
|
+
text=str(value),
|
|
600
|
+
buttons=buttons, # type: ignore
|
|
601
|
+
)
|
|
602
|
+
links.append(link)
|
|
603
|
+
return display.display_widgets_vertically(links, class_="labmate-params")
|
|
604
|
+
|
|
605
|
+
def update_config_params_on_disk(self, params: Dict[str, Any]):
|
|
606
|
+
# params_per_files = {}
|
|
607
|
+
# for param, value in params.items():
|
|
608
|
+
# res = self.find_param_in_data_config(param)
|
|
609
|
+
# if res is None:
|
|
610
|
+
# raise ValueError(
|
|
611
|
+
# f"Parameter '{param}' cannot be found in default config files."
|
|
612
|
+
# )
|
|
613
|
+
# file, _ = res
|
|
614
|
+
# params_per_files.setdefault(file, {})[param] = value
|
|
615
|
+
|
|
616
|
+
for file in self.config_files:
|
|
617
|
+
file = self._config_files_names_to_path.get(file, file)
|
|
618
|
+
utils.file_read.update_file_variable(file, params)
|
|
619
|
+
|
|
620
|
+
return self
|
|
621
|
+
|
|
548
622
|
def connect_default_widget(
|
|
549
623
|
self,
|
|
550
624
|
objs: Union[
|
|
@@ -1,20 +1,38 @@
|
|
|
1
|
-
"""Module with widgets that can be displayed with AcquisitionAnalysisManager."""
|
|
2
|
-
|
|
3
1
|
import os
|
|
4
2
|
from typing import TYPE_CHECKING, List, Optional, Protocol, TypeVar
|
|
5
3
|
|
|
6
4
|
from .. import display as lm_display
|
|
7
5
|
from ..display import platform_utils
|
|
8
6
|
|
|
9
|
-
# import abc
|
|
10
|
-
|
|
11
|
-
|
|
12
7
|
if TYPE_CHECKING:
|
|
13
8
|
from labmate.acquisition_notebook import AcquisitionAnalysisManager
|
|
14
9
|
|
|
15
10
|
_T = TypeVar("_T")
|
|
16
11
|
|
|
17
12
|
|
|
13
|
+
def _get_filepath(aqm: "AcquisitionAnalysisManager") -> Optional[str]:
|
|
14
|
+
filepath = aqm.current_analysis or aqm.current_acquisition
|
|
15
|
+
return filepath.filepath if filepath else None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _create_file_link(aqm: "AcquisitionAnalysisManager", level_up) -> str:
|
|
19
|
+
filepath = _get_filepath(aqm)
|
|
20
|
+
if filepath is None:
|
|
21
|
+
return ""
|
|
22
|
+
link_name = os.path.basename(filepath)
|
|
23
|
+
link = "/".join(
|
|
24
|
+
os.path.abspath(filepath).replace("\\", "/").split("/")[-level_up:]
|
|
25
|
+
).replace(" ", "%20")
|
|
26
|
+
link = f"[{link_name}](//kyrylo-gr.github.io/h5viewer/open?url={link})"
|
|
27
|
+
return link
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def display_widgets(objs: List["WidgetProtocol"], *args, **kwargs):
|
|
31
|
+
"""Create (with *args, **kwargs) and display a list of widgets."""
|
|
32
|
+
widgets = [obj.create(*args, **kwargs) for obj in objs]
|
|
33
|
+
lm_display.display_widgets(widgets) # type: ignore
|
|
34
|
+
|
|
35
|
+
|
|
18
36
|
class WidgetProtocol(Protocol):
|
|
19
37
|
"""Protocol for widgets that can be displayed with AcquisitionAnalysisManager.
|
|
20
38
|
|
|
@@ -73,7 +91,7 @@ class BaseWidget:
|
|
|
73
91
|
raise NotImplementedError("This method is not implemented for the base class.")
|
|
74
92
|
|
|
75
93
|
|
|
76
|
-
class
|
|
94
|
+
class CopyFileURLPathButton(BaseWidget):
|
|
77
95
|
"""Create button to copy file path to clipboard.
|
|
78
96
|
|
|
79
97
|
Examples:
|
|
@@ -180,32 +198,9 @@ class OpenFinderButton(BaseWidget):
|
|
|
180
198
|
path = path.replace("/", "\\")
|
|
181
199
|
subprocess.run(["explorer", "/select,", path], shell=True)
|
|
182
200
|
elif sys.platform == "darwin":
|
|
183
|
-
subprocess.Popen(["open", "-R", path])
|
|
201
|
+
subprocess.Popen(["open", "-R", os.path.dirname(path)])
|
|
184
202
|
else:
|
|
185
|
-
subprocess.Popen(["nautilus", "--select", path])
|
|
203
|
+
subprocess.Popen(["nautilus", "--select", os.path.dirname(path)])
|
|
186
204
|
|
|
187
205
|
self.widget = lm_display.buttons.create_button(open_finder, name="Open finder")
|
|
188
206
|
return self.widget
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def _get_filepath(aqm: "AcquisitionAnalysisManager") -> Optional[str]:
|
|
192
|
-
filepath = aqm.current_analysis or aqm.current_acquisition
|
|
193
|
-
return filepath.filepath if filepath else None
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def _create_file_link(aqm: "AcquisitionAnalysisManager", level_up) -> str:
|
|
197
|
-
filepath = _get_filepath(aqm)
|
|
198
|
-
if filepath is None:
|
|
199
|
-
return ""
|
|
200
|
-
link_name = os.path.basename(filepath)
|
|
201
|
-
link = "/".join(
|
|
202
|
-
os.path.abspath(filepath).replace("\\", "/").split("/")[-level_up:]
|
|
203
|
-
).replace(" ", "%20")
|
|
204
|
-
link = f"[{link_name}](//kyrylo-gr.github.io/h5viewer/open?url={link})"
|
|
205
|
-
return link
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def display_widgets(objs: List["WidgetProtocol"], *args, **kwargs):
|
|
209
|
-
"""Create (with *args, **kwargs) and display a list of widgets."""
|
|
210
|
-
widgets = [obj.create(*args, **kwargs) for obj in objs]
|
|
211
|
-
lm_display.display_widgets(widgets) # type: ignore
|
labmate/display/__init__.py
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
import importlib
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
|
-
from .main import
|
|
5
|
+
from .main import (
|
|
6
|
+
display,
|
|
7
|
+
display_html,
|
|
8
|
+
display_widgets,
|
|
9
|
+
display_widgets_vertically,
|
|
10
|
+
logger,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
|
-
__all__ = ["links", "buttons", "logger"]
|
|
13
|
+
__all__ = ["links", "buttons", "logger", "html_output"]
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
class _LazyModule:
|
|
@@ -28,7 +34,8 @@ class _LazyModule:
|
|
|
28
34
|
|
|
29
35
|
links = _LazyModule("links")
|
|
30
36
|
buttons = _LazyModule("buttons")
|
|
37
|
+
html_output = _LazyModule("html_output")
|
|
31
38
|
|
|
32
39
|
|
|
33
40
|
if TYPE_CHECKING:
|
|
34
|
-
from . import buttons, links
|
|
41
|
+
from . import buttons, html_output, links
|
labmate/display/buttons.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""This submodule contains functions that create Button widgets."""
|
|
2
|
+
|
|
2
3
|
from typing import TypeVar
|
|
4
|
+
|
|
3
5
|
from .main import display, widgets
|
|
4
6
|
|
|
5
7
|
# from functools import wraps
|
|
@@ -69,6 +71,8 @@ def create_button(func, *args, name=None, **kwargs) -> "DisplayingButton":
|
|
|
69
71
|
def on_button_click(_):
|
|
70
72
|
func(*args, **kwargs)
|
|
71
73
|
|
|
74
|
+
button.style.button_color = "transparent" # type: ignore
|
|
75
|
+
|
|
72
76
|
button.on_click(on_button_click)
|
|
73
77
|
return button
|
|
74
78
|
|
labmate/display/html_output.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"""This submodule contains functions that create different html."""
|
|
2
2
|
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from .main import widgets
|
|
6
|
+
|
|
3
7
|
|
|
4
8
|
def display_warning(text: str):
|
|
5
9
|
"""Display div warning block with `text`.
|
|
@@ -13,3 +17,47 @@ def display_warning(text: str):
|
|
|
13
17
|
>{text}</div>"""
|
|
14
18
|
|
|
15
19
|
display_html(str(html))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_link_row(
|
|
23
|
+
link_text: str,
|
|
24
|
+
link_url: str,
|
|
25
|
+
text: str,
|
|
26
|
+
buttons: Optional[List[widgets.Button]] = None,
|
|
27
|
+
):
|
|
28
|
+
|
|
29
|
+
# Create the HTML link with custom styling
|
|
30
|
+
link_widget = widgets.HTML(
|
|
31
|
+
value=f'<a href="{link_url}" target="_blank" onclick="return false;">{link_text}</a>'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Create the text with custom styling
|
|
35
|
+
text_widget = widgets.HTML(
|
|
36
|
+
value=f'<span style="padding: 0 10px;">{text}</span>',
|
|
37
|
+
layout=widgets.Layout(background_color="transparent"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
custom_css = """
|
|
41
|
+
<style>
|
|
42
|
+
.cell-output-ipywidget-background:has(.labmate-params) {
|
|
43
|
+
background: transparent !important;
|
|
44
|
+
}
|
|
45
|
+
.labmate-params, .labmate-params * {
|
|
46
|
+
color: inherit !important;
|
|
47
|
+
}
|
|
48
|
+
.labmate-params a {
|
|
49
|
+
text-decoration: underline;
|
|
50
|
+
color: blue;
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
</style>
|
|
55
|
+
"""
|
|
56
|
+
buttons = buttons or []
|
|
57
|
+
|
|
58
|
+
hbox = widgets.HBox(
|
|
59
|
+
[widgets.HTML(custom_css), link_widget, text_widget, *buttons],
|
|
60
|
+
layout=widgets.Layout(background_color="transparent"),
|
|
61
|
+
)
|
|
62
|
+
hbox.add_class("labmate-params")
|
|
63
|
+
return hbox
|
labmate/display/main.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""This submodule contains functions that help to display content in IPython."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
4
|
import sys
|
|
4
5
|
from typing import Callable, List
|
|
5
6
|
|
|
6
|
-
|
|
7
7
|
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.WARNING)
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
9
9
|
handler = logging.StreamHandler()
|
|
@@ -18,9 +18,9 @@ try:
|
|
|
18
18
|
if "pytest" in sys.modules:
|
|
19
19
|
raise ImportError
|
|
20
20
|
|
|
21
|
-
from IPython.core.display import HTML # type: ignore
|
|
22
|
-
from IPython.core import display # type: ignore
|
|
23
21
|
import ipywidgets as widgets # pylint: disable=W0611 # type: ignore
|
|
22
|
+
from IPython.core import display # type: ignore
|
|
23
|
+
from IPython.core.display import HTML # type: ignore
|
|
24
24
|
|
|
25
25
|
display = display.display_functions.display
|
|
26
26
|
|
|
@@ -49,9 +49,17 @@ except ImportError:
|
|
|
49
49
|
def __init__(self, lst: list) -> None: # noqa: D107
|
|
50
50
|
pass
|
|
51
51
|
|
|
52
|
+
class VBox: # noqa: D106
|
|
53
|
+
def __init__(self, lst: list) -> None: # noqa: D107
|
|
54
|
+
pass
|
|
55
|
+
|
|
52
56
|
class CoreWidget: # noqa: D106
|
|
53
57
|
pass
|
|
54
58
|
|
|
59
|
+
class Layout:
|
|
60
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
61
|
+
del args, kwargs
|
|
62
|
+
|
|
55
63
|
# pylint: enable=C0115, C0103, R0903
|
|
56
64
|
|
|
57
65
|
|
|
@@ -64,3 +72,11 @@ def display_widgets(objs: List[widgets.CoreWidget]):
|
|
|
64
72
|
"""Display the given list of widgets in a HBox ."""
|
|
65
73
|
button_row = widgets.HBox(objs)
|
|
66
74
|
display(button_row)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def display_widgets_vertically(objs: List[widgets.CoreWidget], class_: str = ""):
|
|
78
|
+
"""Display the given list of widgets in a VBox ."""
|
|
79
|
+
button_row = widgets.VBox(objs)
|
|
80
|
+
if class_:
|
|
81
|
+
button_row.add_class(class_) # type: ignore
|
|
82
|
+
display(button_row)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BufferCatcher(io.StringIO):
|
|
7
|
+
_last_value = None
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def last_value(self):
|
|
11
|
+
if self._last_value is None:
|
|
12
|
+
return self.getvalue()
|
|
13
|
+
return self._last_value
|
|
14
|
+
|
|
15
|
+
def close(self) -> None:
|
|
16
|
+
self._last_value = self.getvalue()
|
|
17
|
+
return super().close()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StreamHandler(logging.StreamHandler):
|
|
21
|
+
stream: io.StringIO
|
|
22
|
+
|
|
23
|
+
def __init__(self, stream=None):
|
|
24
|
+
if stream is None:
|
|
25
|
+
stream = io.StringIO()
|
|
26
|
+
super().__init__(stream)
|
|
27
|
+
|
|
28
|
+
def reset(self):
|
|
29
|
+
self.stream.close()
|
|
30
|
+
stream = io.StringIO()
|
|
31
|
+
self.setStream(stream)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Logger(logging.Logger):
|
|
35
|
+
stdout_message: str
|
|
36
|
+
|
|
37
|
+
def __init__(self, name, level=logging.NOTSET):
|
|
38
|
+
super().__init__(name, level)
|
|
39
|
+
|
|
40
|
+
self.logger_handler = StreamHandler()
|
|
41
|
+
logger_formatter = logging.Formatter(
|
|
42
|
+
"%(name)s:%(filename)s:%(asctime)s:\n%(levelname)s:%(message)s"
|
|
43
|
+
)
|
|
44
|
+
self.logger_handler.setFormatter(logger_formatter)
|
|
45
|
+
self.logger_handler.setLevel(logging.DEBUG)
|
|
46
|
+
self.addHandler(self.logger_handler)
|
|
47
|
+
|
|
48
|
+
logger_handler_short = logging.StreamHandler()
|
|
49
|
+
logger_formatter_short = logging.Formatter("%(levelname)s:%(message)s")
|
|
50
|
+
logger_handler_short.setFormatter(logger_formatter_short)
|
|
51
|
+
logger_handler_short.setLevel(logging.INFO)
|
|
52
|
+
self.addHandler(logger_handler_short)
|
|
53
|
+
|
|
54
|
+
self.propagate = False
|
|
55
|
+
self.stdout_buffer = BufferCatcher()
|
|
56
|
+
self.stdout_message = ""
|
|
57
|
+
|
|
58
|
+
def reset(self):
|
|
59
|
+
self.logger_handler.reset()
|
|
60
|
+
|
|
61
|
+
self.stdout_setup()
|
|
62
|
+
self.stdout_message = ""
|
|
63
|
+
|
|
64
|
+
def stdout_flush(self):
|
|
65
|
+
self.stdout_message += f"\n{self.stdout_buffer.last_value}"
|
|
66
|
+
self.stdout_setup()
|
|
67
|
+
|
|
68
|
+
def stdout_setup(self):
|
|
69
|
+
self.stdout_buffer = BufferCatcher()
|
|
70
|
+
sys.stdout._buffer = self.stdout_buffer # type: ignore # pylint: disable=protected-access
|
|
71
|
+
|
|
72
|
+
def getvalue(self):
|
|
73
|
+
return self.logger_handler.stream.getvalue()
|
|
74
|
+
|
|
75
|
+
def get_stdout(self):
|
|
76
|
+
return self.stdout_message + f"\n{self.stdout_buffer.last_value}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
logger = Logger("Labmate")
|
labmate/utils/__init__.py
CHANGED
labmate/utils/file_read.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"""Different method to read files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
2
4
|
import os
|
|
3
|
-
from typing import Dict, List
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from ..parsing.brackets_score import BracketsScore
|
|
4
8
|
|
|
5
9
|
|
|
6
10
|
def read_file(file: str, /) -> str:
|
|
@@ -47,3 +51,46 @@ def read_files(files: List[str], /) -> Dict[str, str]:
|
|
|
47
51
|
)
|
|
48
52
|
configs[config_file_name] = read_file(config_file)
|
|
49
53
|
return configs
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def update_file_variable(file, params: Dict[str, Any]):
|
|
57
|
+
"""
|
|
58
|
+
Update the variables in a file with the given parameters.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
file (str): The path to the file to update.
|
|
62
|
+
params (Dict[str, Any]): The parameters to update the file with.
|
|
63
|
+
"""
|
|
64
|
+
with open(file, "r", encoding="utf-8") as file_opened:
|
|
65
|
+
lines = file_opened.readlines()
|
|
66
|
+
brackets = BracketsScore()
|
|
67
|
+
current_param: Optional[str] = None
|
|
68
|
+
# print(lines)
|
|
69
|
+
for line in lines:
|
|
70
|
+
# print(line)
|
|
71
|
+
if len(line) == 0:
|
|
72
|
+
continue
|
|
73
|
+
if brackets.is_zero() and "=" in line:
|
|
74
|
+
param = line.split("=")[0].strip()
|
|
75
|
+
# print("param", param)
|
|
76
|
+
if param in params:
|
|
77
|
+
current_param = param
|
|
78
|
+
start_line = lines.index(line)
|
|
79
|
+
brackets.update_from_str(line)
|
|
80
|
+
if brackets.is_zero() and current_param is not None:
|
|
81
|
+
end_line = lines.index(line)
|
|
82
|
+
end_comment = line.split("#")[-1].strip() if "#" in line else None
|
|
83
|
+
del lines[start_line : end_line + 1]
|
|
84
|
+
value_str = json.JSONEncoder().encode(params[current_param])
|
|
85
|
+
lines.insert(
|
|
86
|
+
start_line,
|
|
87
|
+
(
|
|
88
|
+
f"{current_param} = {value_str}"
|
|
89
|
+
+ (f" # {end_comment}" if end_comment is not None else "")
|
|
90
|
+
+ "\n"
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
current_param = None
|
|
94
|
+
|
|
95
|
+
with open(file, "w", encoding="utf-8") as file_opened:
|
|
96
|
+
file_opened.writelines(lines)
|
labmate/utils/title_parsing.py
CHANGED
|
@@ -17,7 +17,11 @@ def parse_get_format(key: str) -> Tuple[str, Optional[str], Optional[str]]:
|
|
|
17
17
|
args = key.split("__")
|
|
18
18
|
if len(args) >= 3:
|
|
19
19
|
return args[0], args[1], args[2]
|
|
20
|
-
elif
|
|
20
|
+
elif (
|
|
21
|
+
len(args) == 2
|
|
22
|
+
and len(args[1]) > 0
|
|
23
|
+
and (args[1][0].isdigit() or args[1][0] in (".", "_"))
|
|
24
|
+
):
|
|
21
25
|
return args[0], None, args[1]
|
|
22
26
|
elif len(args) == 2:
|
|
23
27
|
return args[0], args[1], None
|
|
@@ -32,6 +36,23 @@ class ValueForPrint(NamedTuple):
|
|
|
32
36
|
units: Optional[str] = None
|
|
33
37
|
format: Optional[str] = None
|
|
34
38
|
|
|
39
|
+
def format_value(self, format_spec: Optional[str] = None) -> str:
|
|
40
|
+
format_spec = format_spec or self.format
|
|
41
|
+
if not format_spec:
|
|
42
|
+
return str(self.value)
|
|
43
|
+
if format_spec.endswith("p"):
|
|
44
|
+
format_spec = format_spec[:-1] + "e"
|
|
45
|
+
value_str = format(self.value, format_spec)
|
|
46
|
+
number, power = value_str.split("e")
|
|
47
|
+
number = number.rstrip("0_").rstrip(".") if "." in number else number
|
|
48
|
+
power = (
|
|
49
|
+
(power[0].lstrip("+0") + power[1:].lstrip("+0"))
|
|
50
|
+
if len(power) > 1
|
|
51
|
+
else power
|
|
52
|
+
)
|
|
53
|
+
return f"{number}e{power}"
|
|
54
|
+
return format(self.value, format_spec)
|
|
55
|
+
|
|
35
56
|
|
|
36
57
|
def format_title(values: List[ValueForPrint], max_length: Optional[int] = None) -> str:
|
|
37
58
|
"""Create title out of a list of valuesForPrint.
|
|
@@ -47,11 +68,7 @@ def format_title(values: List[ValueForPrint], max_length: Optional[int] = None)
|
|
|
47
68
|
last_line_len = 0
|
|
48
69
|
for value in values:
|
|
49
70
|
units = f" ({value.units})" if value.units is not None else ""
|
|
50
|
-
value_str = (
|
|
51
|
-
value.value
|
|
52
|
-
if value.format is None
|
|
53
|
-
else value.value.__format__(f".{value.format}")
|
|
54
|
-
)
|
|
71
|
+
value_str = value.format_value()
|
|
55
72
|
new_txt = f"{value.key} = {value_str}{units}"
|
|
56
73
|
if not max_length or (
|
|
57
74
|
(last_line_len + len(new_txt) < max_length) or last_line_len == 0
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
labmate/__config__.py,sha256=
|
|
1
|
+
labmate/__config__.py,sha256=klTEHuAm-AhAQg9qYq-W6YyefUJiHBGfIPWnRQr-eT0,72
|
|
2
2
|
labmate/__init__.py,sha256=aHQiPLldCXIJqz6wwcfyFU9sGLofUR3W5sXBIRzK2n4,182
|
|
3
3
|
labmate/acquisition/__init__.py,sha256=8q3dy18lL32A9y_Du8GggpLgJqDMFcFKddHrySMavrM,269
|
|
4
|
-
labmate/acquisition/acquisition_data.py,sha256=
|
|
4
|
+
labmate/acquisition/acquisition_data.py,sha256=UcpUaT1SkmnqGMyzP8cgH2JjLRtWZZdTk5S0JHf-v9c,6681
|
|
5
5
|
labmate/acquisition/acquisition_loop.py,sha256=fiiseV21GB7pczHSEJrlLPiTSQ2u9y5VMDyCO7Hn0vY,11106
|
|
6
|
-
labmate/acquisition/acquisition_manager.py,sha256=
|
|
7
|
-
labmate/acquisition/analysis_data.py,sha256=
|
|
6
|
+
labmate/acquisition/acquisition_manager.py,sha256=D94ahyQuzsEZdJzEUHJcN2aRwGoVYu4YDMKDCa7JnCU,10992
|
|
7
|
+
labmate/acquisition/analysis_data.py,sha256=svpkOp12iMhBlR7nBgCFQXiOzQtwiQ3v2wXrZNpYdmk,15711
|
|
8
8
|
labmate/acquisition/analysis_loop.py,sha256=1Y8lyPkTCNwskM8DkwrMXSOt0hNmBHcWJaQjdVZ81Hs,5075
|
|
9
9
|
labmate/acquisition/config_file.py,sha256=1WwqaKTM-R5xZQHqqqGUi2QCF0PC1ag2mFgPOJuEdWI,2212
|
|
10
10
|
labmate/acquisition/custom_lint.py,sha256=x4vNoOnbH3A4Odu2DQVtBsuSPo5JfvRpo8_EP0EOmgM,1005
|
|
11
|
-
labmate/acquisition/logger_setup.py,sha256=udTp-0S4cqhGdUGQlk3G3Eg51wePEGraNG-69P9fTOo,129
|
|
12
11
|
labmate/acquisition_notebook/__init__.py,sha256=ZtOGQtmPqEM1IRrL-_JYo4xYA87lFQ5JY5GmKcZz9z0,251
|
|
13
|
-
labmate/acquisition_notebook/acquisition_analysis_manager.py,sha256=
|
|
14
|
-
labmate/acquisition_notebook/display_widget.py,sha256=
|
|
12
|
+
labmate/acquisition_notebook/acquisition_analysis_manager.py,sha256=cnsaIxgJc0yU1odPbqFY1VCmVVZT9Lwyyt6KWdkC4_U,22615
|
|
13
|
+
labmate/acquisition_notebook/display_widget.py,sha256=yS7KeZ362djhHYFldQMpz8keVT7G2MgHsCmGMlXT81s,6858
|
|
15
14
|
labmate/attrdict/__init__.py,sha256=MvuZVe7j4a0HxGMxim_K2cv-dhqZOfzdeMiTX-SRgDg,58
|
|
16
15
|
labmate/attrdict/attrdict_class.py,sha256=4lKXe7oZo_lLHefmf5vAOKhibWgGDffJcxMhaWLvGs4,4047
|
|
17
|
-
labmate/display/__init__.py,sha256=
|
|
18
|
-
labmate/display/buttons.py,sha256=
|
|
19
|
-
labmate/display/html_output.py,sha256=
|
|
16
|
+
labmate/display/__init__.py,sha256=u6HL38LpkeiGDkcIqE4GVvsEGF8OgZ776cSOHG5dmLA,966
|
|
17
|
+
labmate/display/buttons.py,sha256=ReFLn5KD-KOlxjZPCoT32lt1jNMN8Xka3K35oh17ZLo,2926
|
|
18
|
+
labmate/display/html_output.py,sha256=aBDVzaR39PfFxchKcY9W5OpiYjIzB6t3WzNjzjubjzg,1591
|
|
20
19
|
labmate/display/links.py,sha256=YgCNxowca-oHxqozGWbe4oGLzwltnZ-C0v3E5fQnK0U,746
|
|
21
|
-
labmate/display/main.py,sha256=
|
|
20
|
+
labmate/display/main.py,sha256=2bjLbRmK5H1PYhtoiyZHzisu6CyQ6AmzlFtEhFZkStg,2354
|
|
22
21
|
labmate/display/platform_utils/__init__.py,sha256=GOWB9vXF-wxGZfdHQJlQWy-hy2V2KXUP3JwxgN91Fq4,1136
|
|
23
22
|
labmate/display/platform_utils/windows_utils.py,sha256=4Z_avuJIZ_KoXkuRZOH2667t2wEljzNBMP6fbNDknuk,3268
|
|
23
|
+
labmate/logger/__init__.py,sha256=Ks4bUNO_rNTFsw0Sh3JAfK1tZq8mqHoqXHP89xpexp0,2177
|
|
24
24
|
labmate/parsing/__init__.py,sha256=AHNB502jlm6PGd49_PJjvSxt97fxJeXnIfXYh8HV5x0,1312
|
|
25
25
|
labmate/parsing/brackets_score.py,sha256=zzup7z6o57YUGeMr5FOSTo3nz9Z62s2omxqFV3M9MmI,988
|
|
26
26
|
labmate/parsing/parsed_value.py,sha256=UYB_gCkV3DiFQRjcprnQUPw--FqYPqb3pii-PQsbHf0,5261
|
|
27
27
|
labmate/parsing/saving.py,sha256=pwCdYI9shrpKyFceRPNbPcbEfJbNQ7Xj0AMsDOr5qLA,2548
|
|
28
|
-
labmate/utils/__init__.py,sha256=
|
|
28
|
+
labmate/utils/__init__.py,sha256=g9LSaSVDFC4Uo7hbdd66aT7QewuXNRheZ8PZlMrcsNw,104
|
|
29
29
|
labmate/utils/async_utils.py,sha256=mSfmpF7I3M5KePkPtoS-OcuoCkFDHPKjf-RVF0P3R48,118
|
|
30
30
|
labmate/utils/autoreload.py,sha256=wKi1GgWyRu1h101OguVRpO3zQXZ8qsFj-K-1P8PKuq8,305
|
|
31
31
|
labmate/utils/errors.py,sha256=ly7-JQStTKmPiMuT0w3eXFw1O8-1kpTsqZT2jebpJ-I,140
|
|
32
|
-
labmate/utils/file_read.py,sha256=
|
|
32
|
+
labmate/utils/file_read.py,sha256=mRHRIyqejtDlfLuPyafEYcfKfb4bac7zHbt-DW091Vg,3155
|
|
33
33
|
labmate/utils/lint.py,sha256=7llJbZUAM-ikEpmU_ZzraqOwGUuJPgk1wAf3aYMJdxg,9312
|
|
34
34
|
labmate/utils/random_utils.py,sha256=ZA3gK9P-eTcd_a3BTS_ZeJI5A0GM_GXL7X3yUqnPTO4,690
|
|
35
|
-
labmate/utils/title_parsing.py,sha256=
|
|
36
|
-
labmate-0.
|
|
37
|
-
labmate-0.
|
|
38
|
-
labmate-0.
|
|
39
|
-
labmate-0.
|
|
40
|
-
labmate-0.
|
|
35
|
+
labmate/utils/title_parsing.py,sha256=5csdqiD6w6pzyqRon38V2WeGA00CifSrMMtoWZmk0Ok,2644
|
|
36
|
+
labmate-0.10.0.dist-info/LICENCE,sha256=J9XIxdJExlWYZuxhhKtk4oYILvUz8-JM0y_leRQCKUE,7488
|
|
37
|
+
labmate-0.10.0.dist-info/METADATA,sha256=_8Qsw4NxSoayjiPoPvdunyV-a_ovkwjxYaqThycQvVQ,3211
|
|
38
|
+
labmate-0.10.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
|
39
|
+
labmate-0.10.0.dist-info/top_level.txt,sha256=WWAn6t2zNWsp02gRq6f5cSsGebcs-4L6HBFk0XrcY0o,8
|
|
40
|
+
labmate-0.10.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|