PostBOUND 0.19.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.
Files changed (67) hide show
  1. postbound/__init__.py +211 -0
  2. postbound/_base.py +6 -0
  3. postbound/_bench.py +1012 -0
  4. postbound/_core.py +1153 -0
  5. postbound/_hints.py +1373 -0
  6. postbound/_jointree.py +1079 -0
  7. postbound/_pipelines.py +1121 -0
  8. postbound/_qep.py +1986 -0
  9. postbound/_stages.py +876 -0
  10. postbound/_validation.py +734 -0
  11. postbound/db/__init__.py +72 -0
  12. postbound/db/_db.py +2348 -0
  13. postbound/db/_duckdb.py +785 -0
  14. postbound/db/mysql.py +1195 -0
  15. postbound/db/postgres.py +4216 -0
  16. postbound/experiments/__init__.py +12 -0
  17. postbound/experiments/analysis.py +674 -0
  18. postbound/experiments/benchmarking.py +54 -0
  19. postbound/experiments/ceb.py +877 -0
  20. postbound/experiments/interactive.py +105 -0
  21. postbound/experiments/querygen.py +334 -0
  22. postbound/experiments/workloads.py +980 -0
  23. postbound/optimizer/__init__.py +92 -0
  24. postbound/optimizer/__init__.pyi +73 -0
  25. postbound/optimizer/_cardinalities.py +369 -0
  26. postbound/optimizer/_joingraph.py +1150 -0
  27. postbound/optimizer/dynprog.py +1825 -0
  28. postbound/optimizer/enumeration.py +432 -0
  29. postbound/optimizer/native.py +539 -0
  30. postbound/optimizer/noopt.py +54 -0
  31. postbound/optimizer/presets.py +147 -0
  32. postbound/optimizer/randomized.py +650 -0
  33. postbound/optimizer/tonic.py +1479 -0
  34. postbound/optimizer/ues.py +1607 -0
  35. postbound/qal/__init__.py +343 -0
  36. postbound/qal/_qal.py +9678 -0
  37. postbound/qal/formatter.py +1089 -0
  38. postbound/qal/parser.py +2344 -0
  39. postbound/qal/relalg.py +4257 -0
  40. postbound/qal/transform.py +2184 -0
  41. postbound/shortcuts.py +70 -0
  42. postbound/util/__init__.py +46 -0
  43. postbound/util/_errors.py +33 -0
  44. postbound/util/collections.py +490 -0
  45. postbound/util/dataframe.py +71 -0
  46. postbound/util/dicts.py +330 -0
  47. postbound/util/jsonize.py +68 -0
  48. postbound/util/logging.py +106 -0
  49. postbound/util/misc.py +168 -0
  50. postbound/util/networkx.py +401 -0
  51. postbound/util/numbers.py +438 -0
  52. postbound/util/proc.py +107 -0
  53. postbound/util/stats.py +37 -0
  54. postbound/util/system.py +48 -0
  55. postbound/util/typing.py +35 -0
  56. postbound/vis/__init__.py +5 -0
  57. postbound/vis/fdl.py +69 -0
  58. postbound/vis/graphs.py +48 -0
  59. postbound/vis/optimizer.py +538 -0
  60. postbound/vis/plots.py +84 -0
  61. postbound/vis/tonic.py +70 -0
  62. postbound/vis/trees.py +105 -0
  63. postbound-0.19.0.dist-info/METADATA +355 -0
  64. postbound-0.19.0.dist-info/RECORD +67 -0
  65. postbound-0.19.0.dist-info/WHEEL +5 -0
  66. postbound-0.19.0.dist-info/licenses/LICENSE.txt +202 -0
  67. postbound-0.19.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,438 @@
1
+ """Utilities centered around numbers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import numbers
7
+ import threading
8
+ from typing import Any, Union
9
+
10
+
11
+ def represents_number(val: str) -> bool:
12
+ """Checks, whether `val` can be cast into an integer/float value."""
13
+ try:
14
+ float(val)
15
+ except (TypeError, ValueError):
16
+ return False
17
+ return True
18
+
19
+
20
+ class AtomicInt(numbers.Integral):
21
+ """An atomic int allows for multi-threaded access to the integer value."""
22
+
23
+ def __init__(self, value: int = 0):
24
+ self._value = value
25
+ self._lock = threading.Lock()
26
+
27
+ def increment(self, by: int = 1) -> None:
28
+ with self._lock:
29
+ self._value += by
30
+
31
+ def reset(self) -> None:
32
+ with self._lock:
33
+ self._value = 0
34
+
35
+ def _get_value(self) -> int:
36
+ with self._lock:
37
+ return self._value
38
+
39
+ def _set_value(self, value: int) -> None:
40
+ with self._lock:
41
+ self._value = value
42
+
43
+ def _assert_integral(self, other: Any):
44
+ if not isinstance(other, numbers.Integral):
45
+ raise TypeError(
46
+ f"Cannot add argument of type {type(other)} to object of type AtomicInt"
47
+ )
48
+
49
+ def _unwrap_atomic(self, other: Any):
50
+ return other._value if isinstance(other, AtomicInt) else other
51
+
52
+ value = property(_get_value, _set_value)
53
+
54
+ def __abs__(self) -> int:
55
+ with self._lock:
56
+ return abs(self._value)
57
+
58
+ def __add__(self, other: Any) -> AtomicInt:
59
+ self._assert_integral(other)
60
+ other = self._unwrap_atomic(other)
61
+ with self._lock:
62
+ return AtomicInt(self._value + other)
63
+
64
+ def __and__(self, other: Any) -> Any:
65
+ other = self._unwrap_atomic(other)
66
+ with self._lock:
67
+ return self._value & other
68
+
69
+ def __ceil__(self) -> int:
70
+ with self._lock:
71
+ return math.ceil(self._value)
72
+
73
+ def __eq__(self, other: object) -> bool:
74
+ other = self._unwrap_atomic(other)
75
+ with self._lock:
76
+ return self._value == other
77
+
78
+ def __floor__(self) -> int:
79
+ with self._lock:
80
+ return math.floor(self._value)
81
+
82
+ def __floordiv__(self, other: Any) -> AtomicInt:
83
+ other = self._unwrap_atomic(other)
84
+ with self._lock:
85
+ return AtomicInt(self._value // other)
86
+
87
+ def __int__(self) -> int:
88
+ with self._lock:
89
+ return int(self._value)
90
+
91
+ def __invert__(self) -> Any:
92
+ with self._lock:
93
+ return ~self._value
94
+
95
+ def __le__(self, other: Any) -> bool:
96
+ other = self._unwrap_atomic(other)
97
+ with self._lock:
98
+ return self._value <= other
99
+
100
+ def __lshift__(self, other: Any) -> Any:
101
+ other = self._unwrap_atomic(other)
102
+ with self._lock:
103
+ return self._value << other
104
+
105
+ def __lt__(self, other: Any) -> bool:
106
+ other = self._unwrap_atomic(other)
107
+ with self._lock:
108
+ return self._value < other
109
+
110
+ def __mod__(self, other: Any) -> Any:
111
+ other = self._unwrap_atomic(other)
112
+ with self._lock:
113
+ return self._value % other
114
+
115
+ def __mul__(self, other: Any) -> AtomicInt:
116
+ self._assert_integral(other)
117
+ other = self._unwrap_atomic(other)
118
+ with self._lock:
119
+ return AtomicInt(self._value * other)
120
+
121
+ def __neg__(self) -> AtomicInt:
122
+ with self._lock:
123
+ return AtomicInt(-self._value)
124
+
125
+ def __or__(self, other: Any) -> Any:
126
+ other = self._unwrap_atomic(other)
127
+ with self._lock:
128
+ return self._value | other
129
+
130
+ def __pos__(self) -> Any:
131
+ with self._lock:
132
+ return +self.value
133
+
134
+ def __pow__(self, exponent: Any, modulus: Any | None = ...) -> AtomicInt:
135
+ with self._lock:
136
+ res = self._value**exponent
137
+ if res != int(res):
138
+ raise ValueError(
139
+ f"Power not supported for type AtomicInt with argument {exponent}"
140
+ )
141
+ return AtomicInt(res)
142
+
143
+ def __radd__(self, other: Any) -> Any:
144
+ other = self._unwrap_atomic(other)
145
+ with self._lock:
146
+ return other + self._value
147
+
148
+ def __rand__(self, other: Any) -> Any:
149
+ other = self._unwrap_atomic(other)
150
+ with self._lock:
151
+ return other + self._value
152
+
153
+ def __rfloordiv__(self, other: Any) -> Any:
154
+ other = self._unwrap_atomic(other)
155
+ with self._lock:
156
+ return other // self._value
157
+
158
+ def __rlshift__(self, other: Any) -> Any:
159
+ other = self._unwrap_atomic(other)
160
+ with self._lock:
161
+ return other << self._value
162
+
163
+ def __rmod__(self, other: Any) -> Any:
164
+ other = self._unwrap_atomic(other)
165
+ with self._lock:
166
+ return other % self._value
167
+
168
+ def __rmul__(self, other: Any) -> Any:
169
+ other = self._unwrap_atomic(other)
170
+ with self._lock:
171
+ return other * self._value
172
+
173
+ def __ror__(self, other: Any) -> Any:
174
+ other = self._unwrap_atomic(other)
175
+ with self._lock:
176
+ return other | self._value
177
+
178
+ def __round__(self, ndigits: Union[int, None] = None) -> int:
179
+ with self._lock:
180
+ return self._value
181
+
182
+ def __rpow__(self, base: Any) -> Any:
183
+ base = self._unwrap_atomic(base)
184
+ with self._lock:
185
+ return base**self._value
186
+
187
+ def __rrshift__(self, other: Any) -> Any:
188
+ other = self._unwrap_atomic(other)
189
+ with self._lock:
190
+ return other >> self._value
191
+
192
+ def __rshift__(self, other: Any) -> Any:
193
+ other = self._unwrap_atomic(other)
194
+ with self._lock:
195
+ return self._value >> other
196
+
197
+ def __rtruediv__(self, other: Any) -> Any:
198
+ other = self._unwrap_atomic(other)
199
+ with self._lock:
200
+ return other / self._value
201
+
202
+ def __rxor__(self, other: Any) -> Any:
203
+ other = self._unwrap_atomic(other)
204
+ with self._lock:
205
+ return other ^ self._value
206
+
207
+ def __truediv__(self, other: Any) -> Any:
208
+ other = self._unwrap_atomic(other)
209
+ with self._lock:
210
+ return self._value / other
211
+
212
+ def __trunc__(self) -> int:
213
+ with self._lock:
214
+ return math.trunc(self._value)
215
+
216
+ def __xor__(self, other: Any) -> Any:
217
+ other = self._unwrap_atomic(other)
218
+ with self._lock:
219
+ return self._value ^ other
220
+
221
+ def __hash__(self) -> int:
222
+ with self._lock:
223
+ return hash(self._value)
224
+
225
+ def __repr__(self) -> str:
226
+ with self._lock:
227
+ return f"AtomicInt({self._value})"
228
+
229
+ def __str__(self) -> str:
230
+ with self._lock:
231
+ return str(self._value)
232
+
233
+
234
+ class BoundedInt(numbers.Integral):
235
+ """A bounded int cannot become larger and/or smaller than a specified interval.
236
+
237
+ If the bounded integer does leave the allowed interval, it will be snapped back to the minimum/maximum allowed
238
+ number, respectively.
239
+ """
240
+
241
+ @staticmethod
242
+ def non_neg(value: int, *, allowed_max: Union[int, None] = None) -> BoundedInt:
243
+ return BoundedInt(value, allowed_min=0, allowed_max=allowed_max)
244
+
245
+ def __init__(
246
+ self,
247
+ value: int,
248
+ *,
249
+ allowed_min: Union[int, None] = None,
250
+ allowed_max: Union[int, None] = None,
251
+ ):
252
+ if not isinstance(value, int):
253
+ raise TypeError(f"Only integer values allowed, but {type(value)} given!")
254
+ if (
255
+ allowed_min is not None
256
+ and allowed_max is not None
257
+ and allowed_min > allowed_max
258
+ ):
259
+ raise ValueError("Allowed minimum may not be larger than allowed maximum!")
260
+
261
+ self._value = value
262
+ self._allowed_min = allowed_min
263
+ self._allowed_max = allowed_max
264
+
265
+ # don't forget the first update!
266
+ self._snap_to_min_max()
267
+
268
+ def _snap_to_min_max(self) -> None:
269
+ if self._allowed_min is not None and self._value < self._allowed_min:
270
+ self._value = self._allowed_min
271
+ if self._allowed_max is not None and self._value > self._allowed_max:
272
+ self._value = self._allowed_max
273
+
274
+ def _unwrap_atomic(self, value: Any) -> int:
275
+ return value._value if isinstance(value, BoundedInt) else value
276
+
277
+ def _get_value(self) -> int:
278
+ return self._value
279
+
280
+ def _set_value(self, value: int) -> None:
281
+ if not isinstance(value, int):
282
+ raise TypeError(f"Only integer values allowed, but {type(value)} given!")
283
+ self._value = value
284
+ self._snap_to_min_max()
285
+
286
+ value = property(_get_value, _set_value)
287
+
288
+ def __abs__(self) -> int:
289
+ return abs(self._value)
290
+
291
+ def __add__(self, other: int | BoundedInt) -> BoundedInt:
292
+ other_value = self._unwrap_atomic(other)
293
+ return BoundedInt(
294
+ self._value + other_value,
295
+ allowed_min=self._allowed_min,
296
+ allowed_max=self._allowed_max,
297
+ )
298
+
299
+ def __and__(self, other: Any) -> Any:
300
+ other_value = self._unwrap_atomic(other)
301
+ return self._value & other_value
302
+
303
+ def __ceil__(self) -> int:
304
+ return self._value
305
+
306
+ def __eq__(self, other: object) -> bool:
307
+ other_value = self._unwrap_atomic(other)
308
+ return self._value == other_value
309
+
310
+ def __floor__(self) -> int:
311
+ return self._value
312
+
313
+ def __floordiv__(self, other: Any) -> int:
314
+ other_value = self._unwrap_atomic(other)
315
+ return self._value // other_value
316
+
317
+ def __int__(self) -> int:
318
+ return self._value
319
+
320
+ def __invert__(self) -> Any:
321
+ return ~self._value
322
+
323
+ def __le__(self, other: Any) -> bool:
324
+ other_value = self._unwrap_atomic(other)
325
+ return self._value <= other_value
326
+
327
+ def __lshift__(self, other: Any) -> Any:
328
+ other_value = self._unwrap_atomic(other)
329
+ return self.value << other_value
330
+
331
+ def __lt__(self, other: Any) -> bool:
332
+ other_value = self._unwrap_atomic(other)
333
+ return self._value < other_value
334
+
335
+ def __mod__(self, other: Any) -> Any:
336
+ other_value = self._unwrap_atomic(other)
337
+ return self._value % other_value
338
+
339
+ def __mul__(self, other: Any) -> BoundedInt:
340
+ other_value = self._unwrap_atomic(other)
341
+ return BoundedInt(
342
+ self._value * other_value,
343
+ allowed_min=self._allowed_min,
344
+ allowed_max=self._allowed_max,
345
+ )
346
+
347
+ def __neg__(self) -> BoundedInt:
348
+ return BoundedInt(
349
+ -self._value, allowed_min=self._allowed_min, allowed_max=self._allowed_max
350
+ )
351
+
352
+ def __or__(self, other: Any) -> Any:
353
+ other_value = self._unwrap_atomic(other)
354
+ return self._value | other_value
355
+
356
+ def __pos__(self) -> Any:
357
+ return +self._value
358
+
359
+ def __pow__(self, exponent: Any, modulus: Union[Any, None] = ...) -> BoundedInt:
360
+ res = self._value**exponent
361
+ if res != int(res):
362
+ raise ValueError(
363
+ f"Power not support for type BoundedInt with argument {exponent}"
364
+ )
365
+ return BoundedInt(
366
+ res, allowed_min=self._allowed_min, allowed_max=self._allowed_max
367
+ )
368
+
369
+ def __radd__(self, other: Any) -> Any:
370
+ other_value = self._unwrap_atomic(other)
371
+ return other_value + self._value
372
+
373
+ def __rand__(self, other: Any) -> Any:
374
+ other_value = self._unwrap_atomic(other)
375
+ return other_value & self._value
376
+
377
+ def __rfloordiv__(self, other: Any) -> Any:
378
+ other_value = self._unwrap_atomic(other)
379
+ return other_value // self._value
380
+
381
+ def __rlshift__(self, other: Any) -> Any:
382
+ other_value = self._unwrap_atomic(other)
383
+ return other_value << self._value
384
+
385
+ def __rmod__(self, other: Any) -> Any:
386
+ other_value = self._unwrap_atomic(other)
387
+ return other_value % self._value
388
+
389
+ def __rmul__(self, other: Any) -> Any:
390
+ other_value = self._unwrap_atomic(other)
391
+ return other_value * self._value
392
+
393
+ def __ror__(self, other: Any) -> Any:
394
+ other_value = self._unwrap_atomic(other)
395
+ return other_value | self._value
396
+
397
+ def __round__(self, ndigits: Union[int, None] = None) -> int:
398
+ return self._value
399
+
400
+ def __rpow__(self, base: Any) -> Any:
401
+ other_value = self._unwrap_atomic(base)
402
+ return other_value**self._value
403
+
404
+ def __rrshift__(self, other: Any) -> Any:
405
+ other_value = self._unwrap_atomic(other)
406
+ return other_value >> self._value
407
+
408
+ def __rshift__(self, other: Any) -> Any:
409
+ other_value = self._unwrap_atomic(other)
410
+ return self._value >> other_value
411
+
412
+ def __rtruediv__(self, other: Any) -> Any:
413
+ other_value = self._unwrap_atomic(other)
414
+ return other_value / self._value
415
+
416
+ def __rxor__(self, other: Any) -> Any:
417
+ other_value = self._unwrap_atomic(other)
418
+ return other_value ^ self._value
419
+
420
+ def __truediv__(self, other: Any) -> Any:
421
+ other_value = self._unwrap_atomic(other)
422
+ return self._value / other_value
423
+
424
+ def __trunc__(self) -> int:
425
+ return math.trunc(self._value)
426
+
427
+ def __xor__(self, other: Any) -> Any:
428
+ other_value = self._unwrap_atomic(other)
429
+ return self._value ^ other_value
430
+
431
+ def __hash__(self) -> int:
432
+ return hash(self._value)
433
+
434
+ def __repr__(self) -> str:
435
+ return f"BoundedInt({self._value}; min={self._allowed_min}, max={self._allowed_max})"
436
+
437
+ def __str__(self) -> str:
438
+ return str(self._value)
postbound/util/proc.py ADDED
@@ -0,0 +1,107 @@
1
+ """Provides utilities to interact with outside processes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import pathlib
7
+ import subprocess
8
+ from collections.abc import Iterable
9
+ from typing import Any, Optional
10
+
11
+
12
+ class ProcResult(str):
13
+ """Wrapper for the result of an external process.
14
+
15
+ In contrast to `CompletedProcess` provided by the `subprocess` module, this class is designed for more convenient usage.
16
+ More specifically, it can be used directly as a substitute for the *stdout* of the process (hence the subclassing of
17
+ `str`). Furthermore, bool checks ensure that the process exited with a zero exit code.
18
+
19
+ All output is provided in dedicated attributes.
20
+
21
+ Parameters
22
+ ----------
23
+ out_data : str
24
+ The stdout of the process.
25
+ err_data : str
26
+ The stderr of the process.
27
+ exit_code : int
28
+ The exit code of the process.
29
+ """
30
+
31
+ def __init__(self, out_data: str, err_data: str, exit_code: int) -> None:
32
+ self.out_data = out_data
33
+ self.err_data = err_data
34
+ self.exit_code = exit_code
35
+
36
+ def __new__(cls, out_data: str, err_data: str, exit_code: int):
37
+ return str.__new__(cls, out_data)
38
+
39
+ def echo(self) -> None:
40
+ """Provides the contents of stdout and stderr in a format for debugging by humans."""
41
+ print("stdout:")
42
+ print(self.out_data)
43
+ print("stderr:")
44
+ print(self.err_data)
45
+
46
+ def raise_if_error(self) -> None:
47
+ """Raises an exception if the process exited with a non-zero exit code."""
48
+ if self.exit_code != 0:
49
+ raise RuntimeError(
50
+ f"Process exited with code {self.exit_code}: '{self.err_data}'"
51
+ )
52
+
53
+ def __bool__(self) -> bool:
54
+ return self.exit_code == 0
55
+
56
+ def __repr__(self) -> str:
57
+ return f"ProcResult(exit_code={self.exit_code}, stdout={repr(self.out_data)}, stderr={repr(self.err_data)})"
58
+
59
+ def __str__(self) -> str:
60
+ return self.out_data
61
+
62
+
63
+ def run_cmd(
64
+ cmd: str | Iterable[Any],
65
+ *args,
66
+ work_dir: Optional[str | pathlib.Path] = None,
67
+ **kwargs,
68
+ ) -> ProcResult:
69
+ """Executes an arbitrary external command.
70
+
71
+ The command can be executed in an different working directory. After execution the working directory is restored.
72
+
73
+ This function delegates to `subprocess.run`. Therefore, most arguments accepted by this function follow the same rules
74
+ as the `run` function.
75
+
76
+ Parameters
77
+ ----------
78
+ cmd : str | Iterable[Any]
79
+ The program to execute. Can be either a single invocation, or a list of the program name and its arguments.
80
+ work_dir : Optional[str | pathlib.Path], optional
81
+ The working directory where the process should be executed. If `None`, the current working directory is used.
82
+ Otherwise, the current working directory is changed to the desired directory for the duration of the process execution
83
+ and restored afterwards.
84
+ *args
85
+ Additional arguments to be passed to the command.
86
+ **kwargs
87
+ Additional arguments to customize the subprocess invocation.
88
+
89
+ Returns
90
+ -------
91
+ ProcResult
92
+ The result of the process execution. If the command can be executed but fails, the `exit_code` will be non-zero. On the
93
+ other hand, if the command cannot be executed at all (e.g. because it is not found or the user does not have the
94
+ required permissions), an error is raised.
95
+ """
96
+ work_dir = os.getcwd() if work_dir is None else str(work_dir)
97
+ current_dir = os.getcwd()
98
+
99
+ if isinstance(cmd, Iterable) and not isinstance(cmd, str):
100
+ cmd, args = str(cmd[0]), cmd[1:] + list(args)
101
+ invocation = [cmd] + [str(arg) for arg in args]
102
+
103
+ os.chdir(work_dir)
104
+ res = subprocess.run(invocation, capture_output=True, text=True, **kwargs)
105
+ os.chdir(current_dir)
106
+
107
+ return ProcResult(res.stdout, res.stderr, res.returncode)
@@ -0,0 +1,37 @@
1
+ """Different mathematical and statistical formulas and utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import numbers
7
+ import typing
8
+ from collections.abc import Callable, Iterable
9
+
10
+ import numpy as np
11
+
12
+
13
+ def catalan_number(n: int) -> int:
14
+ """Computes the n-th catalan number. See https://en.wikipedia.org/wiki/Catalan_number."""
15
+ return round(math.comb(2 * n, n) / (n + 1))
16
+
17
+
18
+ def jaccard(a: set | frozenset, b: set | frozenset) -> float:
19
+ """Jaccard coefficient between a and b. Defined as |a ∩ b| / |a ∪ b|"""
20
+ return len(a & b) / len(a | b)
21
+
22
+
23
+ T = typing.TypeVar("T")
24
+
25
+
26
+ def score_matrix(
27
+ elems: Iterable[T], scoring: Callable[[T, T], numbers.Number]
28
+ ) -> np.ndarray:
29
+ elems = list(elems)
30
+ n = len(elems)
31
+
32
+ matrix = np.ones((n, n))
33
+ for i, elem_i in enumerate(elems):
34
+ for j, elem_j in enumerate(elems):
35
+ matrix[i, j] = scoring(elem_i, elem_j)
36
+
37
+ return matrix
@@ -0,0 +1,48 @@
1
+ """Provides utilities to access (operating system) related information."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ import warnings
8
+ from typing import Optional
9
+
10
+ from . import proc
11
+
12
+
13
+ def open_files(pid: Optional[int] = None) -> list[str]:
14
+ """Provides all files (e.g. text files and shared objects) opened by the given process/PID.
15
+
16
+ Parameters
17
+ ----------
18
+ pid : Optional[int], optional
19
+ The PID of the process to query. Defaults to the current process.
20
+
21
+ Returns
22
+ -------
23
+ list[str]
24
+ All opened files
25
+ """
26
+ if not os.name == "posix":
27
+ warnings.warn("Can only check for open files on POSIX systems.")
28
+ return []
29
+
30
+ pid = os.getpid() if pid is None else pid
31
+ ext = ".dylib" if sys.platform == "darwin" else ".so"
32
+
33
+ # lsof -p produces some "weird" (or rather impractical) output from time to time (and depending on the lsof version)
34
+ # we do the following:
35
+ # lsof -Fn -p gives the names of all opened files for a specific PID
36
+ # But: it prefixes those names with a "n" to distinguish from other files (e.g. sockets)
37
+ # Hence, we grep for ^n to only get real files
38
+ # Afterwards, we remove the n prefix with cut
39
+ # Still, some files are weird because lsof adds a suffix like (path dev=...) to the output. As of right now, I don't know
40
+ # how to interpret this output nor how to get rid of it. The second cut removes this suffix.
41
+ # Lastly, the final grep filters for shared objects. Notice that we don't grep for '.so$' in order to keep files like
42
+ # loibc.so.6
43
+ res = proc.run_cmd(
44
+ f"lsof -Fn -p {pid} | grep '^n' | cut -c2- | cut -d' ' -f1 | grep '{ext}'",
45
+ shell=True,
46
+ )
47
+
48
+ return res.splitlines()
@@ -0,0 +1,35 @@
1
+ """Provides additional type hints, type decorators, ..."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import warnings
7
+ from typing import Callable
8
+
9
+ from .._base import T
10
+
11
+
12
+ def deprecated(func: Callable) -> Callable:
13
+ """Indicates that the given function or class should no longer be used."""
14
+
15
+ @functools.wraps(func)
16
+ def deprecation_wrapper(*args, **kwargs) -> Callable:
17
+ warnings.warn(f"Usage of {func.__name__} is deprecated")
18
+ return func(*args, **kwargs)
19
+
20
+ return deprecation_wrapper
21
+
22
+
23
+ def module_local(func: Callable) -> Callable:
24
+ """
25
+ Marker decorator to show that a seemingly private method of a class is intended to be used by other objects from
26
+ the same module.
27
+ """
28
+ return func
29
+
30
+
31
+ Lazy = None
32
+ """A placeholder to indicate that a value is not yet computed, but will be computed lazily."""
33
+
34
+ LazyVal = T | Lazy
35
+ """Type hint for a value that is computed lazily."""
@@ -0,0 +1,5 @@
1
+ """Contains utilities to visualize different PostBOUND objects."""
2
+
3
+ from . import fdl, graphs, optimizer as opt, plots, tonic, trees
4
+
5
+ __all__ = ["fdl", "graphs", "opt", "plots", "tonic", "trees"]