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.
- postbound/__init__.py +211 -0
- postbound/_base.py +6 -0
- postbound/_bench.py +1012 -0
- postbound/_core.py +1153 -0
- postbound/_hints.py +1373 -0
- postbound/_jointree.py +1079 -0
- postbound/_pipelines.py +1121 -0
- postbound/_qep.py +1986 -0
- postbound/_stages.py +876 -0
- postbound/_validation.py +734 -0
- postbound/db/__init__.py +72 -0
- postbound/db/_db.py +2348 -0
- postbound/db/_duckdb.py +785 -0
- postbound/db/mysql.py +1195 -0
- postbound/db/postgres.py +4216 -0
- postbound/experiments/__init__.py +12 -0
- postbound/experiments/analysis.py +674 -0
- postbound/experiments/benchmarking.py +54 -0
- postbound/experiments/ceb.py +877 -0
- postbound/experiments/interactive.py +105 -0
- postbound/experiments/querygen.py +334 -0
- postbound/experiments/workloads.py +980 -0
- postbound/optimizer/__init__.py +92 -0
- postbound/optimizer/__init__.pyi +73 -0
- postbound/optimizer/_cardinalities.py +369 -0
- postbound/optimizer/_joingraph.py +1150 -0
- postbound/optimizer/dynprog.py +1825 -0
- postbound/optimizer/enumeration.py +432 -0
- postbound/optimizer/native.py +539 -0
- postbound/optimizer/noopt.py +54 -0
- postbound/optimizer/presets.py +147 -0
- postbound/optimizer/randomized.py +650 -0
- postbound/optimizer/tonic.py +1479 -0
- postbound/optimizer/ues.py +1607 -0
- postbound/qal/__init__.py +343 -0
- postbound/qal/_qal.py +9678 -0
- postbound/qal/formatter.py +1089 -0
- postbound/qal/parser.py +2344 -0
- postbound/qal/relalg.py +4257 -0
- postbound/qal/transform.py +2184 -0
- postbound/shortcuts.py +70 -0
- postbound/util/__init__.py +46 -0
- postbound/util/_errors.py +33 -0
- postbound/util/collections.py +490 -0
- postbound/util/dataframe.py +71 -0
- postbound/util/dicts.py +330 -0
- postbound/util/jsonize.py +68 -0
- postbound/util/logging.py +106 -0
- postbound/util/misc.py +168 -0
- postbound/util/networkx.py +401 -0
- postbound/util/numbers.py +438 -0
- postbound/util/proc.py +107 -0
- postbound/util/stats.py +37 -0
- postbound/util/system.py +48 -0
- postbound/util/typing.py +35 -0
- postbound/vis/__init__.py +5 -0
- postbound/vis/fdl.py +69 -0
- postbound/vis/graphs.py +48 -0
- postbound/vis/optimizer.py +538 -0
- postbound/vis/plots.py +84 -0
- postbound/vis/tonic.py +70 -0
- postbound/vis/trees.py +105 -0
- postbound-0.19.0.dist-info/METADATA +355 -0
- postbound-0.19.0.dist-info/RECORD +67 -0
- postbound-0.19.0.dist-info/WHEEL +5 -0
- postbound-0.19.0.dist-info/licenses/LICENSE.txt +202 -0
- 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)
|
postbound/util/stats.py
ADDED
|
@@ -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
|
postbound/util/system.py
ADDED
|
@@ -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()
|
postbound/util/typing.py
ADDED
|
@@ -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."""
|