pymodaq_utils 0.0.2__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.
@@ -0,0 +1,379 @@
1
+ from abc import abstractproperty
2
+ from collections.abc import Iterable
3
+
4
+ from os import environ
5
+ import sys
6
+ import datetime
7
+ from pathlib import Path
8
+ from typing import Union, Dict, TypeVar, Any, List, TYPE_CHECKING
9
+ from typing import Iterable as IterableType
10
+
11
+ import toml
12
+ import logging
13
+ if TYPE_CHECKING:
14
+ from pyqtgraph import Parameter
15
+
16
+
17
+ try:
18
+ USER = environ['USERNAME'] if sys.platform == 'win32' else environ['USER']
19
+ except:
20
+ USER = 'unknown_user'
21
+
22
+ CONFIG_BASE_PATH = Path(environ['PROGRAMDATA']) if sys.platform == 'win32' else \
23
+ Path('Library/Application Support') if sys.platform == 'darwin' else Path('/etc')
24
+
25
+
26
+ KeyType = TypeVar('KeyType')
27
+
28
+
29
+ def deep_update(mapping: Dict[KeyType, Any], *updating_mappings: Dict[KeyType, Any]) -> Dict[KeyType, Any]:
30
+ """ Make sure a dictionary is updated using another dict in any nested level
31
+ Taken from Pydantic v1
32
+ """
33
+ updated_mapping = mapping.copy()
34
+ for updating_mapping in updating_mappings:
35
+ for k, v in updating_mapping.items():
36
+ if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict):
37
+ updated_mapping[k] = deep_update(updated_mapping[k], v)
38
+ else:
39
+ updated_mapping[k] = v
40
+ return updated_mapping
41
+
42
+
43
+ def replace_file_extension(filename: str, ext: str):
44
+ """Replace the extension of a file by the specified one, without the dot"""
45
+ file_name = Path(filename).stem # remove eventual extensions
46
+ if ext[0] == '.':
47
+ ext = ext[1:]
48
+ file_name += '.' + ext
49
+ return file_name
50
+
51
+
52
+ def getitem_recursive(dic, *args, ndepth=0, create_if_missing=False):
53
+ """Will scan recursively a dictionary in order to get the item defined by the iterable args
54
+
55
+ Parameters
56
+ ----------
57
+ dic: dict
58
+ the dictionary to scan
59
+ args: an iterable of str
60
+ keys of the dict
61
+ ndepth: int
62
+ by default (0) get the last element defined by args. 1 would mean it get the parent dict, 2 the parent of the
63
+ parent...
64
+ create_if_missing: bool
65
+ if the entry is not present, create it assigning the 'none' default value (as a lower case string)
66
+ Returns
67
+ -------
68
+ object or dict
69
+ """
70
+ args = list(args)
71
+ while len(args) > ndepth:
72
+ try:
73
+ arg = args.pop(0)
74
+ dic = dic[arg]
75
+ except KeyError as e:
76
+ if create_if_missing:
77
+ if len(args) > 0:
78
+ dic[arg] = {}
79
+ dic = dic[arg]
80
+ else:
81
+ dic[arg] = 'none'
82
+ dic = 'none'
83
+ else:
84
+ raise e
85
+ return dic
86
+
87
+
88
+ def recursive_iterable_flattening(aniterable: IterableType):
89
+ flatten_iter = []
90
+ for elt in aniterable:
91
+ if not isinstance(elt, str) and isinstance(elt, Iterable):
92
+ flatten_iter.extend(recursive_iterable_flattening(elt))
93
+ else:
94
+ flatten_iter.append(elt)
95
+ return flatten_iter
96
+
97
+
98
+ def get_set_path(a_base_path: Path, dir_name: str) -> Path:
99
+ path_to_get = a_base_path.joinpath(dir_name)
100
+ if not path_to_get.is_dir():
101
+ try:
102
+ path_to_get.mkdir()
103
+ except PermissionError as e:
104
+ logging.warning(f"Cannot create local config folder at this location: {path_to_get}"
105
+ f", try using admin rights. "
106
+ f"Changing the not permitted path to a user "
107
+ f"one: {Path.home().joinpath(dir_name)}.")
108
+ path_to_get = Path.home().joinpath(dir_name)
109
+ if not path_to_get.is_dir():
110
+ path_to_get.mkdir()
111
+ return path_to_get
112
+
113
+
114
+ def get_set_local_dir(user=False) -> Path:
115
+ """Defines, creates and returns a local folder where configuration files will be saved
116
+
117
+ Depending on the os the configurations files will be stored in CONFIG_BASE_PATH, then
118
+ each user will have another one created that could override the default and system-wide base folder
119
+
120
+ Parameters
121
+ ----------
122
+ user: bool
123
+ if False get the system-wide folder, otherwise the user folder
124
+
125
+ Returns
126
+ -------
127
+ Path: the local path
128
+ """
129
+ if user:
130
+ local_path = get_set_path(Path.home(), '.pymodaq')
131
+ else:
132
+ local_path = get_set_path(CONFIG_BASE_PATH, '.pymodaq')
133
+ return local_path
134
+
135
+
136
+ def get_config_file(config_file_name: str, user=False):
137
+ return get_set_local_dir(user).joinpath(replace_file_extension(config_file_name, 'toml'))
138
+
139
+
140
+ def get_set_config_dir(config_name='config', user=False):
141
+ """Creates a folder in the local config directory to store specific configuration files
142
+
143
+ Parameters
144
+ ----------
145
+ config_name: (str) name of the configuration folder
146
+ user: bool
147
+ if False get the system-wide folder, otherwise the user folder
148
+
149
+ Returns
150
+ -------
151
+ Path
152
+
153
+ See Also
154
+ --------
155
+ get_set_local_dir
156
+ """
157
+ return get_set_path(get_set_local_dir(user=user), config_name)
158
+
159
+
160
+ def get_set_log_path():
161
+ """ creates and return the config folder path for log files
162
+ """
163
+ return get_set_config_dir('log')
164
+
165
+
166
+ def create_toml_from_dict(mydict: dict, dest_path: Path):
167
+ """Create a Toml file at a given path from a dictionnary"""
168
+ dest_path.write_text(toml.dumps(mydict))
169
+
170
+
171
+ def check_config(config_base: dict, config_local: dict):
172
+ """Compare two configuration dictionaries. Adding missing keys
173
+
174
+ Parameters
175
+ ----------
176
+ config_base: dict
177
+ The base dictionaries with possible new keys
178
+ config_local: dict
179
+ a dict from a local config file potentially missing keys
180
+
181
+ Returns
182
+ -------
183
+ bool: True if keys where missing else False
184
+ """
185
+ status = False
186
+ for key in config_base:
187
+ if key in config_local:
188
+ if isinstance(config_base[key], dict):
189
+ status = status or check_config(config_base[key], config_local[key])
190
+ else:
191
+ config_local[key] = config_base[key]
192
+ status = True
193
+ return status
194
+
195
+
196
+ def copy_template_config(config_file_name: str = 'config', source_path: Union[Path, str] = None,
197
+ dest_path: Union[Path, str] = None):
198
+ """Get a toml file path and copy it
199
+
200
+ the destination is made of a given folder path (or the system-wide local path by default) and the config_file_name
201
+ appended by the suffix '.toml'
202
+
203
+ The source file (or pymodaq config template path by default) is read and dumped in this destination file
204
+
205
+ Parameters
206
+ ----------
207
+ config_file_name: str
208
+ the name of the destination config file
209
+ source_path: Path or str
210
+ the path of the toml source to be copied
211
+ dest_path: Path or str
212
+ the destination path of the copied config
213
+
214
+ Returns
215
+ -------
216
+ Path: the path of the copied file
217
+ """
218
+ if dest_path is None:
219
+ dest_path = get_set_local_dir()
220
+
221
+ file_name = Path(config_file_name).stem # remove eventual extensions
222
+ file_name += '.toml'
223
+ dest_path_with_filename = dest_path.joinpath(file_name)
224
+
225
+ if source_path is None:
226
+ config_template_dict = {}
227
+ else:
228
+ config_template_dict = toml.load(Path(source_path))
229
+
230
+ create_toml_from_dict(config_template_dict, dest_path_with_filename)
231
+ return dest_path_with_filename
232
+
233
+
234
+ def load_system_config_and_update_from_user(config_file_name: str):
235
+ """load from a system-wide config file, update it from the user config file
236
+
237
+ Parameters
238
+ ----------
239
+ config_file_name: str
240
+ The config file to be loaded
241
+ Returns
242
+ -------
243
+ dict: contains the toml system-wide file update with the user file
244
+ """
245
+ config_dict = dict([])
246
+ toml_base_path = get_config_file(config_file_name, user=False)
247
+ if toml_base_path.is_file():
248
+ config_dict = toml.load(toml_base_path)
249
+ toml_user_path = get_config_file(config_file_name, user=True)
250
+ if toml_user_path.is_file():
251
+ config_dict = deep_update(config_dict, toml.load(toml_user_path))
252
+ return config_dict
253
+
254
+
255
+ class ConfigError(Exception):
256
+ pass
257
+
258
+
259
+ class BaseConfig:
260
+ """Base class to manage configuration files
261
+
262
+ Should be subclassed with proper class attributes for each configuration file you need with pymodaq
263
+
264
+ Attributes
265
+ ----------
266
+ config_name: str
267
+ The name with which the configuration will be saved
268
+ config_template_path: Path
269
+ The Path of the template from which the config is constructed
270
+
271
+ """
272
+ config_template_path: Path = abstractproperty()
273
+ config_name: str = abstractproperty()
274
+
275
+ def __init__(self):
276
+ self._config = self.load_config(self.config_name, self.config_template_path)
277
+
278
+ def __repr__(self):
279
+ return f'{self.config_name} configuration file'
280
+
281
+ def __call__(self, *args):
282
+ try:
283
+ ret = getitem_recursive(self._config, *args)
284
+ except KeyError as e:
285
+ raise ConfigError(f'the path {args} does not exist in your configuration toml'
286
+ f' file, check your config folder')
287
+ return ret
288
+
289
+ def to_dict(self):
290
+ return self._config
291
+
292
+ def __getitem__(self, item):
293
+ """for backcompatibility when it was a dictionnary"""
294
+ if isinstance(item, tuple):
295
+ return getitem_recursive(self._config, *item)
296
+ else:
297
+ return self._config[item]
298
+
299
+ # def __setitem__(self, key, value):
300
+ # if isinstance(key, tuple):
301
+ # dic = getitem_recursive(self._config, *key, ndepth=1, create_if_missing=False)
302
+ # dic[key[-1]] = value
303
+ # else:
304
+ # self._config[key] = value
305
+
306
+ def __setitem__(self, key, value):
307
+ if isinstance(key, tuple):
308
+ dic = getitem_recursive(self._config, *key, ndepth=1, create_if_missing=True)
309
+ if value is None: # means the setting is a group
310
+ value = {}
311
+ dic[key[-1]] = value
312
+ else:
313
+ self._config[key] = value
314
+
315
+ def load_config(self, config_file_name, template_path: Path):
316
+ """Load a configuration file from both system-wide and user file
317
+
318
+ check also if missing entries in the configuration file compared to the template"""
319
+ toml_base_path = get_config_file(config_file_name, user=False)
320
+ toml_user_path = get_config_file(config_file_name, user=True)
321
+ if toml_base_path.is_file():
322
+ config = toml.load(toml_base_path)
323
+ if template_path is not None:
324
+ config_template = toml.load(template_path)
325
+ else:
326
+ config_template = {}
327
+ if check_config(config_template, config): # check if all fields from template are there
328
+ # (could have been modified by some commits)
329
+ create_toml_from_dict(config, toml_base_path)
330
+
331
+ else:
332
+ copy_template_config(config_file_name, template_path, toml_base_path.parent)
333
+
334
+ if not toml_user_path.is_file():
335
+ # create the author from environment variable
336
+ config_dict = self.dict_to_add_to_user()
337
+ if config_dict is not None:
338
+ create_toml_from_dict(config_dict, toml_user_path)
339
+
340
+ config_dict = load_system_config_and_update_from_user(config_file_name)
341
+ return config_dict
342
+
343
+ def dict_to_add_to_user(self):
344
+ """To subclass"""
345
+ return dict([])
346
+
347
+ @property
348
+ def config_path(self):
349
+ """Get the user config path"""
350
+ return get_config_file(self.config_name, user=True)
351
+
352
+ @property
353
+ def system_config_path(self):
354
+ """Get the system_wide config path"""
355
+ return get_config_file(self.config_name, user=False)
356
+
357
+ def save(self):
358
+ """Save the current Config object into the user toml file"""
359
+ self.config_path.write_text(toml.dumps(self.to_dict()))
360
+
361
+ def get_children(self, *path: IterableType[str]):
362
+ """ Get the list of config entries at a given path within the configulation toml file
363
+
364
+ new in 4.3.0
365
+ """
366
+ return list(getitem_recursive(self._config, *path).keys())
367
+
368
+
369
+ class Config(BaseConfig):
370
+ """Main class to deal with configuration values for PyMoDAQ"""
371
+ config_template_path = Path(__file__).parent.joinpath('resources/config_template.toml')
372
+ config_name = 'config_pymodaq'
373
+
374
+ def dict_to_add_to_user(self):
375
+ """To subclass"""
376
+ return dict(user=dict(name=USER))
377
+
378
+
379
+
pymodaq_utils/enums.py ADDED
@@ -0,0 +1,72 @@
1
+ from enum import Enum
2
+ from typing import List, Union
3
+
4
+
5
+ class BaseEnum(Enum):
6
+ """Enum to be used within pymodaq with some utility methods"""
7
+
8
+ @classmethod
9
+ def names(cls) -> List[str]:
10
+ """Returns all the names of the enum"""
11
+ return list(cls.__members__.keys())
12
+
13
+ @classmethod
14
+ def values(cls) -> List[str]:
15
+ """Returns all the names of the enum"""
16
+ return [cls[name].value for name in cls.names()]
17
+
18
+ @classmethod
19
+ def to_dict(cls):
20
+ """ Returns the enum in form of a dict with names os keys
21
+
22
+ New in 4.0.2
23
+ """
24
+ return {name: cls[name].value for name in cls.names()}
25
+
26
+ @classmethod
27
+ def to_dict_value(cls):
28
+ """ Returns the enum in form of a dict with values os keys
29
+
30
+ New in 4.0.2
31
+ """
32
+ return {cls[name].value: name for name in cls.names()}
33
+
34
+ def __eq__(self, other: Union[str, Enum]):
35
+ """testing for equality using the enum name"""
36
+ if isinstance(other, str):
37
+ if other == self.name:
38
+ return True
39
+ return super().__eq__(other)
40
+
41
+
42
+ def enum_checker(enum: BaseEnum, item: Union[BaseEnum, str]):
43
+ """Check if the item parameter is a valid enum or at least one valid string name of the enum
44
+
45
+ If a string, transforms it to a valid enum (case not important)
46
+
47
+ Parameters
48
+ ----------
49
+ enum: BaseEnum class or one of its derivated class
50
+
51
+ item: str or BaseEnum instance
52
+
53
+ Returns
54
+ -------
55
+ BaseEnum class or one of its derivated class
56
+ """
57
+
58
+ if not isinstance(item, enum):
59
+ if not isinstance(item, str):
60
+ raise ValueError(f'{item} is an invalid {enum}. Should be a {enum} enum or '
61
+ f'a string in {enum.names()}')
62
+ for ind, name in enumerate(enum.names()):
63
+ if item.lower() == name.lower():
64
+ item = enum[name]
65
+ break
66
+ else:
67
+ raise ValueError(f'{item} is an invalid {enum}. Should be a {enum} enum or '
68
+ f'a string in {enum.names()}')
69
+ return item
70
+
71
+
72
+
@@ -0,0 +1,82 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created the 04/11/2022
4
+
5
+ @author: Sebastien Weber
6
+ """
7
+ from typing import Callable, Union
8
+ from abc import ABCMeta, abstractmethod
9
+
10
+ from pymodaq_utils.logger import set_logger, get_module_name
11
+
12
+ logger = set_logger(get_module_name(__file__))
13
+
14
+
15
+ class BuilderBase(ABCMeta):
16
+ """Abstract class defining an object/service builder with a callable interface accepting some arguments
17
+
18
+ See https://realpython.com/factory-method-python/ for some details
19
+
20
+ See Also
21
+ --------
22
+ pymodaq.post_treatment.process_1d_to_scalar
23
+ """
24
+ @abstractmethod
25
+ def __call__(self, *args, **kwargs):
26
+ pass
27
+
28
+
29
+ class ObjectFactory(metaclass=ABCMeta):
30
+ """Generic ObjectFactory with a decorator register to add object builders to the factory with a
31
+ unique key identifier
32
+
33
+ See https://realpython.com/factory-method-python/ for some details
34
+
35
+ Examples
36
+ --------
37
+ @ObjectFactory.register('custom')
38
+ def my_custom_builder():
39
+ pass
40
+
41
+ See Also
42
+ --------
43
+ pymodaq.post_treatment.process_1d_to_scalar.Data1DProcessorFactory
44
+ """
45
+ _builders = {}
46
+
47
+ @classmethod
48
+ def register(cls, key: str) -> Callable:
49
+ def inner_wrapper(wrapped_class: Union[BuilderBase, Callable]) -> Callable:
50
+ if cls.__name__ not in cls._builders:
51
+ cls._builders[cls.__name__] = {}
52
+ if key not in cls._builders[cls.__name__]:
53
+ cls._builders[cls.__name__][key] = wrapped_class
54
+ else:
55
+ logger.warning(f'The {cls.__name__}/{key} builder is already registered. Replacing it')
56
+ return wrapped_class
57
+ return inner_wrapper
58
+
59
+ @property
60
+ def builders(self):
61
+ return self._builders
62
+
63
+ def keys_function(self, do_sort=True):
64
+ if do_sort:
65
+ return self.keys
66
+ else:
67
+ return list(self.builders[self.__class__.__name__].keys())
68
+
69
+ @property
70
+ def keys(self):
71
+ return sorted(list(self.builders[self.__class__.__name__].keys()))
72
+
73
+ @classmethod
74
+ def create(cls, key, **kwargs):
75
+ builder = cls._builders[cls.__name__].get(key)
76
+ if not builder:
77
+ raise ValueError(key)
78
+ return builder(**kwargs)
79
+
80
+ @classmethod
81
+ def get_class(cls, key):
82
+ return cls._builders[cls.__name__].get(key)
pymodaq_utils/icon.ico ADDED
Binary file
@@ -0,0 +1,78 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created the 27/10/2022
4
+
5
+ @author: Sebastien Weber
6
+ """
7
+ import logging
8
+ import warnings
9
+ from logging.handlers import TimedRotatingFileHandler
10
+ from pathlib import Path
11
+
12
+ from pymodaq_utils.config import get_set_config_dir, Config
13
+
14
+ config = Config()
15
+
16
+
17
+ def set_logger(logger_name, add_handler=False, base_logger=False, add_to_console=False, log_level=None,
18
+ logger_base_name='pymodaq') -> logging.Logger:
19
+ """defines a logger of a given name and eventually add an handler to it
20
+
21
+ Parameters
22
+ ----------
23
+ logger_name: (str) the name of the logger (usually it is the module name as returned by get_module_name
24
+ add_handler (bool) if True adds a TimedRotatingFileHandler to the logger instance (should be True if logger set from
25
+ main app
26
+ base_logger: (bool) specify if this is the parent logger (usually where one defines the handler)
27
+
28
+ Returns
29
+ -------
30
+ logger: (logging.Logger) logger instance
31
+ See Also
32
+ --------
33
+ get_module_name, logging.handlers.TimedRotatingFileHandler
34
+ """
35
+ if not base_logger:
36
+ logger_name = f'{logger_base_name}.{logger_name}'
37
+
38
+ logger = logging.getLogger(logger_name)
39
+ log_path = get_set_config_dir('log', user=True)
40
+ if log_level is None:
41
+ log_level = config('general', 'debug_level')
42
+ logger.setLevel(log_level)
43
+ if add_handler:
44
+ log_file_path = log_path.joinpath(f'{logger_base_name}.log')
45
+ if not log_file_path.is_file():
46
+ log_file_path.touch(mode=0o777)
47
+ handler = TimedRotatingFileHandler(log_file_path, when='midnight')
48
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
49
+ handler.setFormatter(formatter)
50
+ logger.addHandler(handler)
51
+
52
+ logging.captureWarnings(True)
53
+ # only catch DeprecationWarning in DEBUG level
54
+ if log_level == 'DEBUG':
55
+ warnings.filterwarnings('default', category=DeprecationWarning)
56
+ else:
57
+ warnings.filterwarnings('ignore', category=DeprecationWarning)
58
+ warnings_logger = logging.getLogger("py.warnings")
59
+ warnings_logger.addHandler(handler)
60
+
61
+ if add_to_console:
62
+ console_handler = logging.StreamHandler()
63
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
64
+ console_handler.setFormatter(formatter)
65
+ logger.addHandler(console_handler)
66
+ return logger
67
+
68
+
69
+ def get_base_logger(logger: logging.Logger) -> logging.Logger:
70
+ while logger.name != 'pymodaq':
71
+ logger = logger.parent
72
+ return logger
73
+
74
+
75
+ def get_module_name(module__file__path):
76
+ """from the full path of a module extract its name"""
77
+ path = Path(module__file__path)
78
+ return path.stem