cgse-common 2024.1.1__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.
egse/config.py ADDED
@@ -0,0 +1,289 @@
1
+ """
2
+ This module provides convenience functions to properly configure the Common-EGSE
3
+ and to find paths and resources.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import fnmatch
8
+ import logging
9
+ import os
10
+ from pathlib import Path
11
+ from pathlib import PurePath
12
+ from typing import List
13
+ from typing import Optional
14
+ from typing import Tuple
15
+ from typing import Union
16
+
17
+ _HERE = Path(__file__).parent.resolve()
18
+ _LOGGER = logging.getLogger(__name__)
19
+
20
+
21
+ def find_first_occurrence_of_dir(pattern: str, root: Path | str = None) -> Optional[Path]:
22
+ """
23
+ Returns the full path of the directory that first matches the pattern. The directory hierarchy is
24
+ traversed in alphabetical order. The pattern is matched first against all directories in the root
25
+ folder, if there is no match, the first folder in root is traversed until a match is found. If no
26
+ match is found, the second folder in root is traversed.
27
+
28
+ Note that the pattern may contain parent directories, like `/egse/data/icons` or `egse/*/icons`,
29
+ in which case the full pattern is matched.
30
+
31
+ Args:
32
+ pattern: a filename pattern
33
+ root: the root folder to start the hierarchical search
34
+
35
+ Returns:
36
+ The full path of the matched pattern or None if no match could be found.
37
+ """
38
+ import fnmatch
39
+
40
+ root = Path(root).resolve() if root else _HERE
41
+ if not root.is_dir():
42
+ root = root.parent
43
+
44
+ parts = pattern.rsplit("/", maxsplit=1)
45
+ if len(parts) == 2:
46
+ first_part = parts[0]
47
+ last_part = parts[1]
48
+ else:
49
+ first_part = ""
50
+ last_part = parts[0]
51
+
52
+ dirs = sorted([entry.name for entry in root.iterdir() if entry.is_dir()])
53
+
54
+ if root.match(f"*{first_part}") and (matches := fnmatch.filter(dirs, last_part)):
55
+ return root / matches[0]
56
+
57
+ for d in dirs:
58
+ if match := find_first_occurrence_of_dir(pattern, root / d):
59
+ return match
60
+
61
+ return None
62
+
63
+
64
+ def find_dir(pattern: str, root: str = None) -> Optional[Path]:
65
+ """
66
+ Find the first folder that matches the given pattern.
67
+
68
+ Note that if there are more folders that match the pattern in the distribution,
69
+ this function only returns the first occurrence that is found, which might
70
+ not be what you want. To be sure only one folder is returned, use the
71
+ `find_dirs()` function and check if there is just one item returned in the list.
72
+
73
+ Args:
74
+ pattern (str): pattern to match (use * for wildcard)
75
+ root (str): the top level folder to search [default=common-egse-root]
76
+
77
+ Returns:
78
+ the first occurrence of the directory pattern or None when not found.
79
+ """
80
+ for folder in find_dirs(pattern, root):
81
+ return folder
82
+
83
+ return None
84
+
85
+
86
+ def find_dirs(pattern: str, root: str = None):
87
+ """
88
+ Generator for returning directory paths from a walk started at `root` and matching pattern.
89
+
90
+ The pattern can contain the asterisk '*' as a wildcard.
91
+
92
+ The pattern can contain a directory separator '/' which means
93
+ the last part of the path needs to match these folders.
94
+
95
+ Examples:
96
+ >>> for folder in find_dirs("/egse/images"):
97
+ ... assert folder.match('*/egse/images')
98
+
99
+ >>> folders = list(find_dirs("/egse/images"))
100
+ >>> assert len(folders)
101
+
102
+ Args:
103
+ pattern (str): pattern to match (use * for wildcard)
104
+ root (str): the top level folder to search [default=common-egse-root]
105
+
106
+ Returns:
107
+ Paths of folders matching pattern, from root.
108
+ """
109
+ root = Path(root).resolve() if root else get_common_egse_root()
110
+ if not root.is_dir():
111
+ root = root.parent
112
+
113
+ parts = pattern.rsplit("/", maxsplit=1)
114
+ if len(parts) == 2:
115
+ first_part = parts[0]
116
+ last_part = parts[1]
117
+ else:
118
+ first_part = ""
119
+ last_part = parts[0]
120
+
121
+ for path, folders, files in os.walk(root):
122
+ for name in fnmatch.filter(folders, last_part):
123
+ if path.endswith(first_part):
124
+ yield Path(path) / name
125
+
126
+
127
+ def find_files(pattern: str, root: str = None, in_dir: str = None):
128
+ """
129
+ Generator for returning file paths from a top folder, matching the pattern.
130
+
131
+ The top folder can be specified as e.g. `__file__` in which case the parent of that file
132
+ will be used as the top root folder. Note that when you specify '.' as the root argument
133
+ the current working directory will be taken as the root folder, which is probably not what
134
+ you intended.
135
+
136
+ When the file shall be in a specific directory, use the `in_dir` keyword. This requires
137
+ that the path ends with the given string in `in_dir`.
138
+
139
+ >>> file_pattern = 'EtherSpaceLink*.dylib'
140
+ >>> in_dir = 'lib/CentOS-7'
141
+ >>> for file in find_files(file_pattern, in_dir=in_dir):
142
+ ... assert file.match("*lib/CentOS-7/EtherSpaceLink*")
143
+
144
+ Args:
145
+ pattern (str) : sorting pattern (use * for wildcard)
146
+ root (str): the top level folder to search [default=common-egse-root]
147
+ in_dir (str): the 'leaf' directory in which the file shall be
148
+
149
+ Returns:
150
+ Paths of files matching pattern, from root.
151
+ """
152
+ root = Path(root).resolve() if root else get_common_egse_root()
153
+ if not root.is_dir():
154
+ root = root.parent
155
+
156
+ exclude_dirs = ("venv", "venv38", ".git", ".idea", ".DS_Store")
157
+
158
+ for path, folders, files in os.walk(root):
159
+ folders[:] = list(filter(lambda x: x not in exclude_dirs, folders))
160
+ if in_dir and not path.endswith(in_dir):
161
+ continue
162
+ for name in fnmatch.filter(files, pattern):
163
+ yield Path(path) / name
164
+
165
+
166
+ def find_file(name: str, root: str = None, in_dir: str = None) -> Optional[Path]:
167
+ """
168
+ Find the path to the given file starting from the root directory of the
169
+ distribution.
170
+
171
+ Note that if there are more files with the given name found in the distribution,
172
+ this function only returns the first file that is found, which might not be
173
+ what you want. To be sure only one file is returned, use the `find_files()`
174
+ function and check if there is just one file returned in the list.
175
+
176
+ When the file shall be in a specific directory, use the `in_dir` keyword.
177
+ This requires that the path ends with the given string in `in_dir`.
178
+
179
+ >>> file_pattern = 'EtherSpaceLink*.dylib'
180
+ >>> in_dir = 'lib/CentOS-7'
181
+ >>> file = find_file(file_pattern, in_dir=in_dir)
182
+ >>> assert file.match("*/lib/CentOS-7/EtherSpace*")
183
+
184
+ Args:
185
+ name (str): the name of the file
186
+ root (str): the top level folder to search [default=common-egse-root]
187
+ in_dir (str): the 'leaf' directory in which the file shall be
188
+
189
+ Returns:
190
+ the first occurrence of the file or None when not found.
191
+ """
192
+ for file_ in find_files(name, root, in_dir):
193
+ return file_
194
+
195
+ return None
196
+
197
+
198
+ def find_root(
199
+ path: Union[str, PurePath], tests: Tuple[str, ...] = (), default: str = None
200
+ ) -> Union[PurePath, None]:
201
+ """
202
+ Find the root folder based on the files in ``tests``.
203
+
204
+ The algorithm crawls backward over the directory structure until one of the
205
+ items in ``tests`` is matched. and it will return that directory as a ``Path``.
206
+
207
+ When no root folder can be determined, the ``default``
208
+ parameter is returned as a Path (or None).
209
+
210
+ When nothing is provided in ``tests``, all matches will
211
+ fail and the ``default`` parameter will be returned.
212
+
213
+ Args:
214
+ path: folder from which the search is started
215
+ tests: names (files or dirs) to test for existence
216
+ default: returned when no root is found
217
+
218
+ Returns:
219
+ a Path which is the root folder.
220
+ """
221
+
222
+ if path is None:
223
+ return None
224
+ if not Path(path).exists():
225
+ return None
226
+
227
+ prev, test = None, Path(path)
228
+ while prev != test:
229
+ if any(test.joinpath(file_).exists() for file_ in tests):
230
+ return test.resolve()
231
+ prev, test = test, test.parent
232
+
233
+ return Path(default) if default is not None else None
234
+
235
+
236
+ def set_logger_levels(logger_levels: List[Tuple] = None):
237
+ """
238
+ Set the logging level for the given loggers.
239
+
240
+ """
241
+ logger_levels = logger_levels or []
242
+
243
+ for name, level in logger_levels:
244
+ a_logger = logging.getLogger(name)
245
+ a_logger.setLevel(level)
246
+
247
+
248
+ class WorkingDirectory:
249
+ """
250
+ WorkingDirectory is a context manager to temporarily change the working directory while
251
+ executing some code.
252
+
253
+ This context manager has a property `path` which returns the absolute path of the
254
+ current directory.
255
+
256
+ Examples:
257
+ >>> with WorkingDirectory(find_dir("/egse/images")) as wdir:
258
+ ... for file in wdir.path.glob('*'):
259
+ ... assert file.exists() # do something with the image files
260
+
261
+ """
262
+
263
+ def __init__(self, path):
264
+ """
265
+ Args:
266
+ path (str, Path): the folder to change to within this context
267
+ Raises:
268
+ ValueError when the given path doesn't exist.
269
+ """
270
+ self._temporary_path = Path(path)
271
+ if not self._temporary_path.exists():
272
+ raise ValueError(f"The given path ({path}) doesn't exist.")
273
+ self._current_dir = None
274
+
275
+ def __enter__(self):
276
+ self._current_dir = os.getcwd()
277
+ os.chdir(self._temporary_path)
278
+ return self
279
+
280
+ def __exit__(self, exc_type, exc_val, exc_tb):
281
+ try:
282
+ os.chdir(self._current_dir)
283
+ except OSError as exc:
284
+ _LOGGER.warning(f"Change back to previous directory failed: {exc}")
285
+
286
+ @property
287
+ def path(self):
288
+ """Resolve and return the current Path of the context."""
289
+ return self._temporary_path.resolve()