labmate 0.10.5__py3-none-any.whl → 0.10.6__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.
Files changed (34) hide show
  1. labmate/__config__.py +1 -1
  2. labmate/acquisition/__init__.py +1 -1
  3. labmate/acquisition/acquisition_loop.py +11 -18
  4. labmate/acquisition/acquisition_manager.py +28 -48
  5. labmate/acquisition/analysis_data.py +15 -25
  6. labmate/acquisition/analysis_loop.py +9 -7
  7. labmate/acquisition/backend.py +2 -0
  8. labmate/acquisition/config_file.py +5 -3
  9. labmate/acquisition/custom_lint.py +2 -3
  10. labmate/acquisition_notebook/__init__.py +2 -2
  11. labmate/acquisition_notebook/acquisition_analysis_manager.py +24 -47
  12. labmate/acquisition_notebook/display_widget.py +9 -9
  13. labmate/attrdict/attrdict_class.py +4 -10
  14. labmate/display/__init__.py +2 -3
  15. labmate/display/buttons.py +1 -0
  16. labmate/display/links.py +2 -1
  17. labmate/display/main.py +3 -2
  18. labmate/display/platform_utils/__init__.py +3 -1
  19. labmate/display/platform_utils/windows_utils.py +3 -9
  20. labmate/parsing/__init__.py +3 -5
  21. labmate/parsing/parsed_value.py +30 -10
  22. labmate/parsing/saving.py +2 -6
  23. labmate/utils/async_utils.py +1 -0
  24. labmate/utils/autoreload.py +2 -0
  25. labmate/utils/file_read.py +12 -13
  26. labmate/utils/lint.py +9 -11
  27. labmate/utils/random_utils.py +1 -0
  28. labmate/utils/title_parsing.py +4 -14
  29. {labmate-0.10.5.dist-info → labmate-0.10.6.dist-info}/METADATA +1 -1
  30. labmate-0.10.6.dist-info/RECORD +41 -0
  31. {labmate-0.10.5.dist-info → labmate-0.10.6.dist-info}/WHEEL +1 -1
  32. labmate-0.10.5.dist-info/RECORD +0 -41
  33. {labmate-0.10.5.dist-info → labmate-0.10.6.dist-info}/licenses/LICENCE +0 -0
  34. {labmate-0.10.5.dist-info → labmate-0.10.6.dist-info}/top_level.txt +0 -0
labmate/__config__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """This file contains all package constants."""
2
2
 
3
- __version__ = "0.10.5"
3
+ __version__ = "0.10.6"
@@ -2,6 +2,6 @@
2
2
  from .acquisition_data import NotebookAcquisitionData
3
3
  from .acquisition_loop import AcquisitionLoop
4
4
  from .acquisition_manager import AcquisitionManager
5
- from .backend import AcquisitionBackend
6
5
  from .analysis_data import AnalysisData, FigureProtocol
7
6
  from .analysis_loop import AnalysisLoop
7
+ from .backend import AcquisitionBackend
@@ -96,9 +96,7 @@ class AcquisitionLoop(DH5):
96
96
  ) -> Iterator:
97
97
  """Return np.arange(start, stop, step) given a start, stop and step."""
98
98
 
99
- def __call__(
100
- self, *args, iterable: Optional[Iterable] = None, **kwds
101
- ) -> Optional[Iterator]:
99
+ def __call__(self, *args, iterable: Optional[Iterable] = None, **kwds) -> Optional[Iterator]:
102
100
  """Append_data or arange.
103
101
 
104
102
  If kwds are provided then is same as calling append_data(kwds),
@@ -115,10 +113,11 @@ class AcquisitionLoop(DH5):
115
113
  return None
116
114
 
117
115
  if iterable is None:
118
- if isinstance(args[0], (int, float, np.int_, np.floating)): # type: ignore
119
- iterable = np.arange(*args)
120
- else:
121
- iterable = args[0]
116
+ iterable = (
117
+ np.arange(*args)
118
+ if isinstance(args[0], (int, float, np.int_, np.floating)) # type: ignore
119
+ else args[0]
120
+ )
122
121
 
123
122
  if iterable is None:
124
123
  raise ValueError("You should provide an iterable")
@@ -166,8 +165,8 @@ class AcquisitionLoop(DH5):
166
165
  if len(key_shape) < len(self[key].shape):
167
166
  raise ValueError(
168
167
  f"Object {key} hasn't the same shape as before. Now it's"
169
- f" {key_shape[len(shape):]},"
170
- f" but before it was {self[key].shape[len(shape):]}."
168
+ f" {key_shape[len(shape) :]},"
169
+ f" but before it was {self[key].shape[len(shape) :]}."
171
170
  )
172
171
  if len(key_shape) > len(self[key].shape):
173
172
  raise ValueError(
@@ -178,9 +177,7 @@ class AcquisitionLoop(DH5):
178
177
  self[key] = SyncNp(
179
178
  np.pad(
180
179
  self[key],
181
- pad_width=tuple(
182
- (0, i - j) for i, j in zip(key_shape, self[key].shape)
183
- ),
180
+ pad_width=tuple((0, i - j) for i, j in zip(key_shape, self[key].shape)),
184
181
  )
185
182
  )
186
183
  self[key][iteration] = value
@@ -201,9 +198,7 @@ class AcquisitionLoop(DH5):
201
198
  ):
202
199
  if length is None:
203
200
  if not hasattr(iterable, "__len__"):
204
- raise TypeError(
205
- "Iterable should has __len__ method or length should be provided"
206
- )
201
+ raise TypeError("Iterable should has __len__ method or length should be provided")
207
202
  length = len(iterable) # type: ignore
208
203
 
209
204
  def loop_iter(array, length):
@@ -283,9 +278,7 @@ class AcquisitionLoop(DH5):
283
278
  """
284
279
  if key is None:
285
280
  if not self._save_indexes:
286
- raise ValueError(
287
- "As indexes are not saved with the Loop, key should be provided."
288
- )
281
+ raise ValueError("As indexes are not saved with the Loop, key should be provided.")
289
282
  key = f"__index_{self._level}__"
290
283
 
291
284
  iteration = tuple(self._iteration[: self._level])
@@ -2,13 +2,14 @@ import os
2
2
  from concurrent.futures import Future, ThreadPoolExecutor
3
3
  from typing import (
4
4
  TYPE_CHECKING,
5
+ Any,
5
6
  Dict,
7
+ Iterable,
6
8
  List,
7
9
  NamedTuple,
8
10
  Optional,
9
11
  Tuple,
10
12
  Union,
11
- Iterable,
12
13
  )
13
14
 
14
15
  from dh5 import jsn
@@ -19,6 +20,7 @@ from ..utils import get_timestamp
19
20
  from ..utils.file_read import read_file, read_files
20
21
  from .acquisition_data import NotebookAcquisitionData
21
22
 
23
+
22
24
  if TYPE_CHECKING:
23
25
  from .backend import AcquisitionBackend
24
26
 
@@ -57,14 +59,12 @@ class AcquisitionManager:
57
59
 
58
60
  def __init__(
59
61
  self,
60
- data_directory: Optional[Union[str, Path]] = None,
62
+ data_directory: Optional[Union[str, Path, Any]] = None,
61
63
  *,
62
64
  config_files: Optional[List[str]] = None,
63
65
  save_files: Optional[bool] = None,
64
66
  save_on_edit: Optional[bool] = None,
65
- backend: Optional[
66
- Union["AcquisitionBackend", Iterable["AcquisitionBackend"]]
67
- ] = None,
67
+ backend: Optional[Union["AcquisitionBackend", Iterable["AcquisitionBackend"]]] = None,
68
68
  ):
69
69
  if save_files is not None:
70
70
  self._save_files = save_files
@@ -89,7 +89,11 @@ class AcquisitionManager:
89
89
  self._configs_last_modified = []
90
90
 
91
91
  if data_directory is not None:
92
- self.data_directory = Path(data_directory)
92
+ self.data_directory = (
93
+ Path(str(data_directory))
94
+ if not isinstance(data_directory, Path)
95
+ else data_directory
96
+ )
93
97
  elif "ACQUISITION_DIR" in os.environ:
94
98
  self.data_directory = Path(os.environ["ACQUISITION_DIR"])
95
99
  else:
@@ -134,13 +138,9 @@ class AcquisitionManager:
134
138
  configs: dict of configurations files to save
135
139
  directory: directory where the data is stored
136
140
  """
137
- acquisition_tmp_data = self._acquisition_tmp_data or self.get_temp_data(
138
- self.temp_file_path
139
- )
141
+ acquisition_tmp_data = self._acquisition_tmp_data or self.get_temp_data(self.temp_file_path)
140
142
  if acquisition_tmp_data is None:
141
- raise ValueError(
142
- "You should create a new acquisition. It will create temp.json file."
143
- )
143
+ raise ValueError("You should create a new acquisition. It will create temp.json file.")
144
144
  return acquisition_tmp_data
145
145
 
146
146
  @acquisition_tmp_data.setter
@@ -162,12 +162,10 @@ class AcquisitionManager:
162
162
  filename = [str(filename)]
163
163
 
164
164
  self.config_files = list(filename)
165
- self._config_files_names_to_path = {
166
- os.path.basename(file): file for file in self.config_files
167
- }
165
+ self._config_files_names_to_path = {Path(file).name: file for file in self.config_files}
168
166
 
169
167
  for config_file in self.config_files:
170
- if not os.path.exists(config_file):
168
+ if not Path(config_file).exists():
171
169
  raise ValueError(f"Configuration file at {config_file} does not exist")
172
170
 
173
171
  return self
@@ -177,7 +175,7 @@ class AcquisitionManager:
177
175
  raise ValueError(
178
176
  "Configuration file should be specified before with set_config_file function"
179
177
  )
180
- self.config_files_eval[os.path.basename(file)] = module
178
+ self.config_files_eval[Path(file).name] = module
181
179
 
182
180
  def set_init_analyse_file(self, filename: Union[str, Path]) -> None:
183
181
  if not isinstance(filename, Path):
@@ -194,22 +192,18 @@ class AcquisitionManager:
194
192
  if not experiment_path.exists():
195
193
  experiment_path.makedirs()
196
194
  # Copy init_analyses code to the experiment directory if it does not exist
197
- if self._init_code and not os.path.exists(experiment_path / "init_analyse.py"):
198
- with open(
199
- experiment_path / "init_analyse.py", "w", encoding="utf-8"
200
- ) as file:
195
+ if self._init_code and not (experiment_path / "init_analyse.py").exists():
196
+ with open(experiment_path / "init_analyse.py", "w", encoding="utf-8") as file:
201
197
  file.write(self._init_code)
202
198
 
203
- filepath_original = filepath = (
204
- experiment_path / f"{dic.time_stamp}__{dic.experiment_name}"
205
- )
199
+ filepath_original = filepath = experiment_path / f"{dic.time_stamp}__{dic.experiment_name}"
206
200
 
207
201
  # If ignore existence is True, no check is required
208
202
  if ignore_existence:
209
203
  return filepath
210
204
  # If the file already exists, add a suffix to the name
211
205
  index = 1
212
- while os.path.exists(filepath + ".h5"):
206
+ while (filepath + ".h5").exists():
213
207
  filepath = filepath_original + f"__{index}"
214
208
  index += 1
215
209
 
@@ -217,16 +211,14 @@ class AcquisitionManager:
217
211
 
218
212
  @staticmethod
219
213
  def get_temp_data(path: Path) -> Optional[AcquisitionTmpData]:
220
- if not os.path.exists(path):
214
+ if not Path(path).exists():
221
215
  return None
222
216
  return AcquisitionTmpData(**jsn.read(path))
223
217
 
224
218
  def _get_configs_last_modified(self) -> List[float]:
225
- return [os.path.getmtime(file) for file in self.config_files]
219
+ return [Path(file).stat().st_mtime for file in self.config_files]
226
220
 
227
- def _schedule_backend_save(
228
- self, acquisition: NotebookAcquisitionData
229
- ) -> Optional[Future]:
221
+ def _schedule_backend_save(self, acquisition: NotebookAcquisitionData) -> Optional[Future]:
230
222
  if self._backend is None:
231
223
  return None
232
224
 
@@ -245,9 +237,7 @@ class AcquisitionManager:
245
237
  future.add_done_callback(shutdown_executor)
246
238
  return future
247
239
 
248
- def _schedule_backend_load(
249
- self, acquisition: NotebookAcquisitionData
250
- ) -> Optional[Future]:
240
+ def _schedule_backend_load(self, acquisition: NotebookAcquisitionData) -> Optional[Future]:
251
241
  if self._backend is None:
252
242
  return None
253
243
 
@@ -277,9 +267,7 @@ class AcquisitionManager:
277
267
  self._configs_last_modified = self._get_configs_last_modified()
278
268
 
279
269
  if self.config_files_eval:
280
- configs = append_values_from_modules_to_files(
281
- configs, self.config_files_eval
282
- )
270
+ configs = append_values_from_modules_to_files(configs, self.config_files_eval)
283
271
 
284
272
  dic = AcquisitionTmpData(
285
273
  experiment_name=name,
@@ -290,9 +278,7 @@ class AcquisitionManager:
290
278
 
291
279
  self.acquisition_tmp_data = dic
292
280
 
293
- self._current_acquisition = self.get_acquisition(
294
- replace=True, save_on_edit=save_on_edit
295
- )
281
+ self._current_acquisition = self.get_acquisition(replace=True, save_on_edit=save_on_edit)
296
282
 
297
283
  return self.current_acquisition
298
284
 
@@ -306,9 +292,7 @@ class AcquisitionManager:
306
292
  configs = read_files(self.config_files)
307
293
 
308
294
  if self.config_files_eval:
309
- configs = append_values_from_modules_to_files(
310
- configs, self.config_files_eval
311
- )
295
+ configs = append_values_from_modules_to_files(configs, self.config_files_eval)
312
296
 
313
297
  if name is None:
314
298
  name = self.current_experiment_name + "_item"
@@ -356,17 +340,13 @@ class AcquisitionManager:
356
340
 
357
341
  @property
358
342
  def current_experiment_name(self) -> str:
359
- return (
360
- self.acquisition_tmp_data.experiment_name
361
- ) # self.current_acquisition.name
343
+ return self.acquisition_tmp_data.experiment_name # self.current_acquisition.name
362
344
 
363
345
  def get_acquisition(
364
346
  self, replace: Optional[bool] = False, save_on_edit: Optional[bool] = None
365
347
  ) -> NotebookAcquisitionData:
366
348
  acquisition_tmp_data = self.acquisition_tmp_data
367
- filepath = self.create_path_from_tmp_data(
368
- acquisition_tmp_data, ignore_existence=True
369
- )
349
+ filepath = self.create_path_from_tmp_data(acquisition_tmp_data, ignore_existence=True)
370
350
  configs = acquisition_tmp_data.configs
371
351
  configs = configs if configs else None
372
352
  cell = self.cell
@@ -12,6 +12,7 @@ from ..logger import logger
12
12
  from .analysis_loop import AnalysisLoop
13
13
  from .config_file import ConfigFile
14
14
 
15
+
15
16
  _T = TypeVar("_T", bound="AnalysisData")
16
17
 
17
18
 
@@ -115,7 +116,7 @@ class AnalysisData(DH5):
115
116
  self._save_files = save_files
116
117
  self._save_fig_inside_h5 = save_fig_inside_h5
117
118
 
118
- self._default_config_files: Tuple[str, ...] = tuple()
119
+ self._default_config_files: Tuple[str, ...] = ()
119
120
  if "info" in self and "default_config_files" in self["info"]:
120
121
  self._default_config_files = tuple(self["info"]["default_config_files"])
121
122
 
@@ -158,9 +159,9 @@ class AnalysisData(DH5):
158
159
  logger.warning("Analysis cell is not set. Nothing to save")
159
160
  return self
160
161
 
161
- self.unlock_data(cell_name_key).update({cell_name_key: code}).lock_data(
162
- cell_name_key
163
- ).save([cell_name_key])
162
+ self.unlock_data(cell_name_key).update({cell_name_key: code}).lock_data(cell_name_key).save(
163
+ [cell_name_key]
164
+ )
164
165
 
165
166
  if self._save_files:
166
167
  assert self.filepath, "You must set self.filepath before saving"
@@ -198,18 +199,14 @@ class AnalysisData(DH5):
198
199
  "inside_h5", False
199
200
  ):
200
201
  try:
201
- raise NotImplementedError(
202
- "save_fig_inside_h5 is not implemented for the moment."
203
- )
202
+ raise NotImplementedError("save_fig_inside_h5 is not implemented for the moment.")
204
203
  # import pltsave
205
204
 
206
205
  # data = pltsave.dumps(fig).to_json()
207
206
  # self[f"figures/{fig_name}"] = data
208
207
  # self.save([f"figures/{fig_name}"])
209
208
  except Exception as error:
210
- logger.exception(
211
- "Failed to save the figure inside h5 file due to %s", error
212
- )
209
+ logger.exception("Failed to save the figure inside h5 file due to %s", error)
213
210
  if tight_layout and hasattr(fig, "tight_layout"):
214
211
  fig.tight_layout() # type: ignore
215
212
  if metadata is None:
@@ -261,14 +258,12 @@ class AnalysisData(DH5):
261
258
  name = str(name)
262
259
  if not name.isnumeric() and name[0] != "_":
263
260
  name = "_" + name
264
- if os.path.splitext(name)[-1] == "":
261
+ if Path(name).suffix == "": # check if name has no extension
265
262
  name = f"{name}.{extensions}"
266
263
 
267
264
  return "FIG" + name
268
265
 
269
- def parse_config(
270
- self, config_files: Optional[Tuple[str, ...]] = None
271
- ) -> "ConfigFile":
266
+ def parse_config(self, config_files: Optional[Tuple[str, ...]] = None) -> "ConfigFile":
272
267
  """Parse config files. If `config_files` are not provided takes `default_config_files`."""
273
268
 
274
269
  config_files = config_files or self._default_config_files
@@ -303,9 +298,7 @@ class AnalysisData(DH5):
303
298
  if key_value == "filename" or key_value == "file" or key_value == "f":
304
299
  filename = os.path.split(self.filepath)[-1]
305
300
  keys_with_values.append(
306
- utils.title_parsing.ValueForPrint(
307
- key_value, filename, key_units, key_format
308
- )
301
+ utils.title_parsing.ValueForPrint(key_value, filename, key_units, key_format)
309
302
  )
310
303
  elif key_value in self:
311
304
  keys_with_values.append(
@@ -358,9 +351,7 @@ class AnalysisData(DH5):
358
351
  )
359
352
 
360
353
  if config_file_name in self._parsed_configs:
361
- self._parsed_configs[original_config_name] = self._parsed_configs[
362
- config_file_name
363
- ]
354
+ self._parsed_configs[original_config_name] = self._parsed_configs[config_file_name]
364
355
  return self._parsed_configs[config_file_name]
365
356
 
366
357
  else:
@@ -393,16 +384,14 @@ class AnalysisData(DH5):
393
384
  code: Optional[dict] = self.get("analysis_cells")
394
385
  if code is None:
395
386
  raise ValueError(
396
- f"There is no field 'analysis_cells' inside the data file. "
387
+ "There is no field 'analysis_cells' inside the data file. "
397
388
  f"Possible keys are {tuple(self.keys())}."
398
389
  )
399
390
 
400
391
  # if isinstance(code, bytes):
401
392
  # code = code.decode()
402
393
  if name not in code:
403
- raise KeyError(
404
- f"Cannot get cell '{name}'. Possible cells are: {tuple(code.keys())}"
405
- )
394
+ raise KeyError(f"Cannot get cell '{name}'. Possible cells are: {tuple(code.keys())}")
406
395
 
407
396
  code_str: str = code[name]
408
397
  if update_code:
@@ -426,7 +415,8 @@ class AnalysisData(DH5):
426
415
  return figures
427
416
 
428
417
  # raise NotImplementedError(
429
- # "Not implemented for the moment. If you want to open an old figure. Use open_old_figs function")
418
+ # "Not implemented for the moment. If you want to open an old figure.
419
+ # Use open_old_figs function" )
430
420
 
431
421
  def pull(self, force_pull: bool = False):
432
422
  self._reset_attrs()
@@ -2,7 +2,7 @@
2
2
  It has mainly __iter__ method and __getitem__ method for slicing.
3
3
  """
4
4
 
5
- from typing import Any, Iterable, List, Optional, Tuple, Union
5
+ from typing import Any, List, Optional, Sequence, Tuple, Union
6
6
 
7
7
  from dh5 import DH5
8
8
 
@@ -39,16 +39,14 @@ class AnalysisLoop(DH5):
39
39
 
40
40
  """
41
41
 
42
- def __init__(
43
- self, data: Optional[dict] = None, loop_shape: Optional[List[int]] = None
44
- ):
42
+ def __init__(self, data: Optional[dict] = None, loop_shape: Optional[List[int]] = None):
45
43
  """Initialize an AnalysisLoop object.
46
44
 
47
45
  Args:
48
46
  data (Optional[dict]): A dictionary containing data to initialize the object with.
49
- loop_shape (Optional[List[int]]): A list of integers representing the shape of the analysis loop.
47
+ loop_shape (list[int] | None): A list of ints representing the shape of the analysis loop.
50
48
  If not provided, the shape is retrieved from the object's '__loop_shape__' attribute.
51
- """
49
+ """ # noqa: E501
52
50
  super().__init__(data=data)
53
51
  if loop_shape is None:
54
52
  loop_shape = self.get("__loop_shape__")
@@ -86,7 +84,11 @@ class AnalysisLoop(DH5):
86
84
  child_kwds[key] = value[index]
87
85
 
88
86
  val = child_kwds[key]
89
- if isinstance(val, (Iterable)) and len(val) == 1 and not isinstance(val[0], (Iterable)): # type: ignore
87
+ if (
88
+ isinstance(val, (Sequence))
89
+ and len(val) == 1
90
+ and not isinstance(val[0], (Sequence))
91
+ ): # type: ignore
90
92
  child_kwds[key] = val[0] # type: ignore
91
93
 
92
94
  if len(self._loop_shape) > 1:
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
+
2
3
  from typing import TYPE_CHECKING
3
4
 
5
+
4
6
  if TYPE_CHECKING: # pragma: no cover - imported for typing only
5
7
  from .acquisition_data import NotebookAcquisitionData
6
8
 
@@ -1,5 +1,7 @@
1
1
  """ConfigFile class."""
2
+
2
3
  from typing import Any
4
+
3
5
  from .. import attrdict
4
6
 
5
7
 
@@ -43,8 +45,8 @@ class ConfigFile(attrdict.AttrDict):
43
45
  raise ValueError("Content is not defined")
44
46
 
45
47
  module = type(attrdict)("config_module")
46
- cc = compile(self.content, "<string>", "exec") # noqa: DUO110
47
- eval(cc, module.__dict__) # pylint: disable=W0123 # noqa: DUO104
48
+ cc = compile(self.content, "<string>", "exec")
49
+ eval(cc, module.__dict__)
48
50
  return module
49
51
 
50
52
  def eval_key(self, key) -> Any:
@@ -59,5 +61,5 @@ class ConfigFile(attrdict.AttrDict):
59
61
  """
60
62
  val = self.get(key)
61
63
  if val and val.value:
62
- return eval(val.value) # pylint: disable=W0123 # noqa: DUO104
64
+ return eval(val.value)
63
65
  return None
@@ -5,6 +5,7 @@ from typing import Any, Dict
5
5
 
6
6
  from ..utils.lint import get_all_args_from_call
7
7
 
8
+
8
9
  SPECIAL_FUNCTIONS_LIST = []
9
10
 
10
11
 
@@ -25,9 +26,7 @@ def on_call_functions(node: ast.Call, db: Dict[str, Any]):
25
26
  h = hash(tuple(get_all_args_from_call(node)))
26
27
  data = db.setdefault("save_fig", [])
27
28
  if h in data:
28
- errors.append(
29
- "save_fig with the save arguments is used more then ones."
30
- )
29
+ errors.append("save_fig with the save arguments is used more then ones.")
31
30
 
32
31
  data.append(h)
33
32
  return errors
@@ -1,7 +1,7 @@
1
1
  # flake8: noqa: F401
2
+ from ..acquisition.acquisition_loop import AcquisitionLoop
3
+ from ..acquisition.analysis_data import AnalysisData
2
4
  from .acquisition_analysis_manager import (
3
5
  AcquisitionAnalysisManager,
4
6
  AcquisitionAnalysisManagerDataOnly,
5
7
  )
6
- from ..acquisition.acquisition_loop import AcquisitionLoop
7
- from ..acquisition.analysis_data import AnalysisData