omlish 0.0.0.dev295__py3-none-any.whl → 0.0.0.dev297__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.
- omlish/__about__.py +4 -2
- omlish/collections/__init__.py +5 -0
- omlish/collections/persistent/persistent.py +26 -5
- omlish/collections/persistent/treapmap.py +35 -12
- omlish/collections/sorted/skiplist.py +27 -19
- omlish/collections/sorted/sorted.py +49 -12
- omlish/dataclasses/generation/processor.py +5 -2
- omlish/dataclasses/processing/base.py +7 -2
- omlish/dataclasses/processing/driving.py +2 -2
- omlish/dom/__init__.py +26 -0
- omlish/dom/building.py +54 -0
- omlish/dom/content.py +129 -0
- omlish/dom/rendering.py +184 -0
- omlish/math/__init__.py +83 -0
- omlish/math/bits.py +0 -153
- omlish/math/c.py +13 -0
- omlish/math/fixed.py +390 -0
- omlish/math/floats.py +3 -0
- omlish/math/histogram.py +127 -0
- omlish/math/stats.py +0 -124
- omlish/sql/__init__.py +16 -0
- omlish/sql/api/__init__.py +7 -0
- omlish/text/indent.py +6 -2
- omlish/typedvalues/values.py +0 -4
- {omlish-0.0.0.dev295.dist-info → omlish-0.0.0.dev297.dist-info}/METADATA +3 -1
- {omlish-0.0.0.dev295.dist-info → omlish-0.0.0.dev297.dist-info}/RECORD +30 -23
- {omlish-0.0.0.dev295.dist-info → omlish-0.0.0.dev297.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev295.dist-info → omlish-0.0.0.dev297.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev295.dist-info → omlish-0.0.0.dev297.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev295.dist-info → omlish-0.0.0.dev297.dist-info}/top_level.txt +0 -0
omlish/math/fixed.py
ADDED
@@ -0,0 +1,390 @@
|
|
1
|
+
import enum
|
2
|
+
import functools
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from .. import check
|
6
|
+
from .. import lang
|
7
|
+
|
8
|
+
|
9
|
+
FixedWidthIntT = ta.TypeVar('FixedWidthIntT', bound='FixedWidthInt')
|
10
|
+
|
11
|
+
|
12
|
+
##
|
13
|
+
|
14
|
+
|
15
|
+
class CheckedFixedWidthIntError(ValueError):
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
class OverflowFixedWidthIntError(CheckedFixedWidthIntError):
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
class UnderflowFixedWidthIntError(CheckedFixedWidthIntError):
|
24
|
+
pass
|
25
|
+
|
26
|
+
|
27
|
+
##
|
28
|
+
|
29
|
+
|
30
|
+
def _get_exclusive_base_cls(cls: type, base_classes: ta.Iterable[type]) -> type:
|
31
|
+
return check.single(bcls for bcls in base_classes if issubclass(cls, bcls))
|
32
|
+
|
33
|
+
|
34
|
+
def _gen_scalar_proxy_method(name):
|
35
|
+
def inner(self, *args, **kwargs):
|
36
|
+
return self.__class__(orig(self, *args, **kwargs))
|
37
|
+
|
38
|
+
orig = getattr(int, name)
|
39
|
+
return functools.wraps(orig)(inner)
|
40
|
+
|
41
|
+
|
42
|
+
def _gen_tuple_proxy_method(name):
|
43
|
+
def inner(self, *args, **kwargs):
|
44
|
+
return tuple(map(self.__class__, orig(self, *args, **kwargs)))
|
45
|
+
|
46
|
+
orig = getattr(int, name)
|
47
|
+
return functools.wraps(orig)(inner)
|
48
|
+
|
49
|
+
|
50
|
+
class FixedWidthInt(int, lang.Abstract):
|
51
|
+
BITS: ta.ClassVar[int]
|
52
|
+
|
53
|
+
#
|
54
|
+
|
55
|
+
class Mode(enum.StrEnum):
|
56
|
+
CHECK = enum.auto()
|
57
|
+
CLAMP = enum.auto()
|
58
|
+
WRAP = enum.auto()
|
59
|
+
|
60
|
+
MODE: ta.ClassVar[Mode]
|
61
|
+
|
62
|
+
SIGNED: ta.ClassVar[bool]
|
63
|
+
|
64
|
+
#
|
65
|
+
|
66
|
+
MIN: ta.ClassVar[int]
|
67
|
+
MAX: ta.ClassVar[int]
|
68
|
+
|
69
|
+
MASK: ta.ClassVar[int]
|
70
|
+
|
71
|
+
#
|
72
|
+
|
73
|
+
def __init_subclass__(cls) -> None:
|
74
|
+
super().__init_subclass__()
|
75
|
+
|
76
|
+
if lang.is_abstract_class(cls):
|
77
|
+
return
|
78
|
+
|
79
|
+
#
|
80
|
+
|
81
|
+
bits = check.single({
|
82
|
+
check.isinstance(bbits, int)
|
83
|
+
for bcls in cls.__mro__
|
84
|
+
if (bbits := bcls.__dict__.get('BITS')) is not None
|
85
|
+
})
|
86
|
+
|
87
|
+
#
|
88
|
+
|
89
|
+
mode_base = _get_exclusive_base_cls(cls, _MODE_BASE_CLASSES)
|
90
|
+
|
91
|
+
if mode_base is CheckedInt:
|
92
|
+
cls.MODE = cls.Mode.CHECK
|
93
|
+
|
94
|
+
elif mode_base is ClampedInt:
|
95
|
+
cls.MODE = cls.Mode.CLAMP
|
96
|
+
|
97
|
+
elif mode_base is WrappedInt:
|
98
|
+
cls.MODE = cls.Mode.WRAP
|
99
|
+
|
100
|
+
else:
|
101
|
+
raise TypeError(cls)
|
102
|
+
|
103
|
+
#
|
104
|
+
|
105
|
+
sign_base = _get_exclusive_base_cls(cls, _SIGN_BASE_CLASSES)
|
106
|
+
|
107
|
+
if sign_base is SignedInt:
|
108
|
+
cls.SIGNED = True
|
109
|
+
cls.MIN = -(1 << (bits - 1))
|
110
|
+
cls.MAX = (1 << (bits - 1)) - 1
|
111
|
+
|
112
|
+
elif sign_base is UnsignedInt:
|
113
|
+
cls.SIGNED = False
|
114
|
+
cls.MIN = 0
|
115
|
+
cls.MAX = (1 << bits) - 1
|
116
|
+
|
117
|
+
else:
|
118
|
+
raise TypeError(cls)
|
119
|
+
|
120
|
+
#
|
121
|
+
|
122
|
+
cls.MASK = (1 << bits) - 1
|
123
|
+
|
124
|
+
#
|
125
|
+
|
126
|
+
@classmethod
|
127
|
+
def clamp(cls, value: int) -> int:
|
128
|
+
return max(min(value, cls.MAX), cls.MIN)
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def check(cls, value: int) -> int:
|
132
|
+
if value > cls.MAX:
|
133
|
+
raise OverflowFixedWidthIntError(value)
|
134
|
+
elif value < cls.MIN:
|
135
|
+
raise UnderflowFixedWidthIntError(value)
|
136
|
+
return value
|
137
|
+
|
138
|
+
@classmethod
|
139
|
+
def wrap(cls, value: int) -> int:
|
140
|
+
return ((value - cls.MIN) & cls.MASK) + cls.MIN
|
141
|
+
|
142
|
+
#
|
143
|
+
|
144
|
+
_SCALAR_PROXY_METHODS = frozenset([
|
145
|
+
'__abs__',
|
146
|
+
'__add__',
|
147
|
+
'__and__',
|
148
|
+
'__floordiv__',
|
149
|
+
'__invert__',
|
150
|
+
'__lshift__',
|
151
|
+
'__mod__',
|
152
|
+
'__mul__',
|
153
|
+
'__neg__',
|
154
|
+
'__or__',
|
155
|
+
'__pos__',
|
156
|
+
'__pow__',
|
157
|
+
'__radd__',
|
158
|
+
'__rand__',
|
159
|
+
'__rfloordiv__',
|
160
|
+
'__rlshift__',
|
161
|
+
'__rmod__',
|
162
|
+
'__rmul__',
|
163
|
+
'__ror__',
|
164
|
+
'__rpow__',
|
165
|
+
'__rrshift__',
|
166
|
+
'__rshift__',
|
167
|
+
'__rsub__',
|
168
|
+
'__rtruediv__',
|
169
|
+
'__rxor__',
|
170
|
+
'__sub__',
|
171
|
+
'__truediv__',
|
172
|
+
'__xor__',
|
173
|
+
])
|
174
|
+
|
175
|
+
_TUPLE_PROXY_METHODS = frozenset([
|
176
|
+
'__divmod__',
|
177
|
+
'__rdivmod__',
|
178
|
+
])
|
179
|
+
|
180
|
+
for _proxy_name in _SCALAR_PROXY_METHODS:
|
181
|
+
locals()[_proxy_name] = _gen_scalar_proxy_method(_proxy_name)
|
182
|
+
for _proxy_name in _TUPLE_PROXY_METHODS:
|
183
|
+
locals()[_proxy_name] = _gen_tuple_proxy_method(_proxy_name)
|
184
|
+
del _proxy_name
|
185
|
+
|
186
|
+
def __repr__(self) -> str:
|
187
|
+
return f'{self.__class__.__name__}({int(self)})'
|
188
|
+
|
189
|
+
|
190
|
+
##
|
191
|
+
|
192
|
+
|
193
|
+
class SignedInt(FixedWidthInt, lang.Abstract):
|
194
|
+
pass
|
195
|
+
|
196
|
+
|
197
|
+
class UnsignedInt(FixedWidthInt, lang.Abstract):
|
198
|
+
pass
|
199
|
+
|
200
|
+
|
201
|
+
_SIGN_BASE_CLASSES: tuple[type[FixedWidthInt], ...] = (
|
202
|
+
SignedInt,
|
203
|
+
UnsignedInt,
|
204
|
+
)
|
205
|
+
|
206
|
+
|
207
|
+
#
|
208
|
+
|
209
|
+
|
210
|
+
class CheckedInt(FixedWidthInt, lang.Abstract):
|
211
|
+
def __new__(cls: type[FixedWidthIntT], value: int) -> FixedWidthIntT:
|
212
|
+
return super().__new__(cls, cls.check(value)) # type: ignore[misc]
|
213
|
+
|
214
|
+
|
215
|
+
class ClampedInt(FixedWidthInt, lang.Abstract):
|
216
|
+
def __new__(cls: type[FixedWidthIntT], value: int) -> FixedWidthIntT:
|
217
|
+
return super().__new__(cls, cls.clamp(value)) # type: ignore[misc]
|
218
|
+
|
219
|
+
|
220
|
+
class WrappedInt(FixedWidthInt, lang.Abstract):
|
221
|
+
def __new__(cls: type[FixedWidthIntT], value: int) -> FixedWidthIntT:
|
222
|
+
return super().__new__(cls, cls.wrap(value)) # type: ignore[misc]
|
223
|
+
|
224
|
+
|
225
|
+
_MODE_BASE_CLASSES: tuple[type[FixedWidthInt], ...] = (
|
226
|
+
CheckedInt,
|
227
|
+
ClampedInt,
|
228
|
+
WrappedInt,
|
229
|
+
)
|
230
|
+
|
231
|
+
|
232
|
+
#
|
233
|
+
|
234
|
+
|
235
|
+
class AnyInt8(FixedWidthInt, lang.Abstract):
|
236
|
+
BITS = 8
|
237
|
+
|
238
|
+
|
239
|
+
class AnyInt16(FixedWidthInt, lang.Abstract):
|
240
|
+
BITS = 16
|
241
|
+
|
242
|
+
|
243
|
+
class AnyInt32(FixedWidthInt, lang.Abstract):
|
244
|
+
BITS = 32
|
245
|
+
|
246
|
+
|
247
|
+
class AnyInt64(FixedWidthInt, lang.Abstract):
|
248
|
+
BITS = 64
|
249
|
+
|
250
|
+
|
251
|
+
class AnyInt128(FixedWidthInt, lang.Abstract):
|
252
|
+
BITS = 128
|
253
|
+
|
254
|
+
|
255
|
+
##
|
256
|
+
|
257
|
+
|
258
|
+
class CheckedInt8(CheckedInt, SignedInt, AnyInt8):
|
259
|
+
pass
|
260
|
+
|
261
|
+
|
262
|
+
class CheckedInt16(CheckedInt, SignedInt, AnyInt16):
|
263
|
+
pass
|
264
|
+
|
265
|
+
|
266
|
+
class CheckedInt32(CheckedInt, SignedInt, AnyInt32):
|
267
|
+
pass
|
268
|
+
|
269
|
+
|
270
|
+
class CheckedInt64(CheckedInt, SignedInt, AnyInt64):
|
271
|
+
pass
|
272
|
+
|
273
|
+
|
274
|
+
class CheckedInt128(CheckedInt, SignedInt, AnyInt128):
|
275
|
+
pass
|
276
|
+
|
277
|
+
|
278
|
+
#
|
279
|
+
|
280
|
+
|
281
|
+
class CheckedUInt8(CheckedInt, UnsignedInt, AnyInt8):
|
282
|
+
pass
|
283
|
+
|
284
|
+
|
285
|
+
class CheckedUInt16(CheckedInt, UnsignedInt, AnyInt16):
|
286
|
+
pass
|
287
|
+
|
288
|
+
|
289
|
+
class CheckedUInt32(CheckedInt, UnsignedInt, AnyInt32):
|
290
|
+
pass
|
291
|
+
|
292
|
+
|
293
|
+
class CheckedUInt64(CheckedInt, UnsignedInt, AnyInt64):
|
294
|
+
pass
|
295
|
+
|
296
|
+
|
297
|
+
class CheckedUInt128(CheckedInt, UnsignedInt, AnyInt128):
|
298
|
+
pass
|
299
|
+
|
300
|
+
|
301
|
+
#
|
302
|
+
|
303
|
+
|
304
|
+
class ClampedInt8(ClampedInt, SignedInt, AnyInt8):
|
305
|
+
pass
|
306
|
+
|
307
|
+
|
308
|
+
class ClampedInt16(ClampedInt, SignedInt, AnyInt16):
|
309
|
+
pass
|
310
|
+
|
311
|
+
|
312
|
+
class ClampedInt32(ClampedInt, SignedInt, AnyInt32):
|
313
|
+
pass
|
314
|
+
|
315
|
+
|
316
|
+
class ClampedInt64(ClampedInt, SignedInt, AnyInt64):
|
317
|
+
pass
|
318
|
+
|
319
|
+
|
320
|
+
class ClampedInt128(ClampedInt, SignedInt, AnyInt128):
|
321
|
+
pass
|
322
|
+
|
323
|
+
|
324
|
+
#
|
325
|
+
|
326
|
+
|
327
|
+
class ClampedUInt8(ClampedInt, UnsignedInt, AnyInt8):
|
328
|
+
pass
|
329
|
+
|
330
|
+
|
331
|
+
class ClampedUInt16(ClampedInt, UnsignedInt, AnyInt16):
|
332
|
+
pass
|
333
|
+
|
334
|
+
|
335
|
+
class ClampedUInt32(ClampedInt, UnsignedInt, AnyInt32):
|
336
|
+
pass
|
337
|
+
|
338
|
+
|
339
|
+
class ClampedUInt64(ClampedInt, UnsignedInt, AnyInt64):
|
340
|
+
pass
|
341
|
+
|
342
|
+
|
343
|
+
class ClampedUInt128(ClampedInt, UnsignedInt, AnyInt128):
|
344
|
+
pass
|
345
|
+
|
346
|
+
|
347
|
+
#
|
348
|
+
|
349
|
+
|
350
|
+
class WrappedInt8(WrappedInt, SignedInt, AnyInt8):
|
351
|
+
pass
|
352
|
+
|
353
|
+
|
354
|
+
class WrappedInt16(WrappedInt, SignedInt, AnyInt16):
|
355
|
+
pass
|
356
|
+
|
357
|
+
|
358
|
+
class WrappedInt32(WrappedInt, SignedInt, AnyInt32):
|
359
|
+
pass
|
360
|
+
|
361
|
+
|
362
|
+
class WrappedInt64(WrappedInt, SignedInt, AnyInt64):
|
363
|
+
pass
|
364
|
+
|
365
|
+
|
366
|
+
class WrappedInt128(WrappedInt, SignedInt, AnyInt128):
|
367
|
+
pass
|
368
|
+
|
369
|
+
|
370
|
+
#
|
371
|
+
|
372
|
+
|
373
|
+
class WrappedUInt8(WrappedInt, UnsignedInt, AnyInt8):
|
374
|
+
pass
|
375
|
+
|
376
|
+
|
377
|
+
class WrappedUInt16(WrappedInt, UnsignedInt, AnyInt16):
|
378
|
+
pass
|
379
|
+
|
380
|
+
|
381
|
+
class WrappedUInt32(WrappedInt, UnsignedInt, AnyInt32):
|
382
|
+
pass
|
383
|
+
|
384
|
+
|
385
|
+
class WrappedUInt64(WrappedInt, UnsignedInt, AnyInt64):
|
386
|
+
pass
|
387
|
+
|
388
|
+
|
389
|
+
class WrappedUInt128(WrappedInt, UnsignedInt, AnyInt128):
|
390
|
+
pass
|
omlish/math/floats.py
CHANGED
omlish/math/histogram.py
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- reservoir
|
4
|
+
- dep tdigest?
|
5
|
+
- struct-of-arrays - array.array('f', ...) - backed SamplingHistogram
|
6
|
+
- https://docs.python.org/3/library/statistics.html
|
7
|
+
"""
|
8
|
+
import contextlib
|
9
|
+
import dataclasses as dc
|
10
|
+
import operator
|
11
|
+
import random
|
12
|
+
import time
|
13
|
+
import typing as ta
|
14
|
+
|
15
|
+
from .. import check
|
16
|
+
|
17
|
+
|
18
|
+
##
|
19
|
+
|
20
|
+
|
21
|
+
class SamplingHistogram:
|
22
|
+
@dc.dataclass(frozen=True)
|
23
|
+
class Entry:
|
24
|
+
value: float
|
25
|
+
timestamp: float
|
26
|
+
|
27
|
+
@dc.dataclass(frozen=True)
|
28
|
+
class Percentile:
|
29
|
+
p: float
|
30
|
+
value: float
|
31
|
+
|
32
|
+
@dc.dataclass(frozen=True)
|
33
|
+
class Stats:
|
34
|
+
count: int
|
35
|
+
min: float
|
36
|
+
max: float
|
37
|
+
last_percentiles: list['SamplingHistogram.Percentile']
|
38
|
+
sample_percentiles: list['SamplingHistogram.Percentile']
|
39
|
+
|
40
|
+
DEFAULT_SIZE = 1000
|
41
|
+
DEFAULT_PERCENTILES = (0.5, 0.75, 0.9, 0.95, 0.99)
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
*,
|
46
|
+
size: int = DEFAULT_SIZE,
|
47
|
+
percentiles: ta.Iterable[float] | None = None,
|
48
|
+
) -> None:
|
49
|
+
check.arg(size > 0)
|
50
|
+
|
51
|
+
super().__init__()
|
52
|
+
|
53
|
+
self._size = size
|
54
|
+
self._percentiles = list(percentiles if percentiles is not None else self.DEFAULT_PERCENTILES)
|
55
|
+
|
56
|
+
self._count = 0
|
57
|
+
self._min = float('inf')
|
58
|
+
self._max = float('-inf')
|
59
|
+
|
60
|
+
self._percentile_pos_list = [self._calc_percentile_pos(p, self._size) for p in self._percentiles]
|
61
|
+
|
62
|
+
self._ring: list[SamplingHistogram.Entry | None] = [None] * size
|
63
|
+
self._ring_pos = 0
|
64
|
+
|
65
|
+
self._sample: list[SamplingHistogram.Entry | None] = [None] * size
|
66
|
+
self._sample_pos_queue = list(reversed(range(size)))
|
67
|
+
|
68
|
+
def add(self, value: float) -> None:
|
69
|
+
self._count += 1
|
70
|
+
self._min = min(self._min, value)
|
71
|
+
self._max = max(self._max, value)
|
72
|
+
|
73
|
+
entry = self.Entry(value, time.time())
|
74
|
+
|
75
|
+
self._ring[self._ring_pos] = entry
|
76
|
+
next_ring_pos = self._ring_pos + 1
|
77
|
+
self._ring_pos = 0 if next_ring_pos >= self._size else next_ring_pos
|
78
|
+
|
79
|
+
sample_pos = None
|
80
|
+
if self._sample_pos_queue:
|
81
|
+
with contextlib.suppress(IndexError):
|
82
|
+
sample_pos = self._sample_pos_queue.pop()
|
83
|
+
if sample_pos is None:
|
84
|
+
sample_pos = random.randrange(0, self._size)
|
85
|
+
self._sample[sample_pos] = entry
|
86
|
+
|
87
|
+
@staticmethod
|
88
|
+
def _calc_percentile_pos(p: float, sz: int) -> int:
|
89
|
+
return round((p * sz) - 1)
|
90
|
+
|
91
|
+
def _calc_percentiles(self, entries: list[Entry | None]) -> list[Percentile]:
|
92
|
+
entries = list(filter(None, entries))
|
93
|
+
sz = len(entries)
|
94
|
+
if not sz:
|
95
|
+
return []
|
96
|
+
elif sz == self._size:
|
97
|
+
pos_list = self._percentile_pos_list
|
98
|
+
else:
|
99
|
+
pos_list = [self._calc_percentile_pos(p, sz) for p in self._percentiles]
|
100
|
+
entries.sort(key=operator.attrgetter('value'))
|
101
|
+
return [
|
102
|
+
self.Percentile(p, check.not_none(entries[pos]).value)
|
103
|
+
for p, pos in zip(self._percentiles, pos_list)
|
104
|
+
]
|
105
|
+
|
106
|
+
def get(self) -> Stats:
|
107
|
+
return self.Stats(
|
108
|
+
count=self._count,
|
109
|
+
min=self._min,
|
110
|
+
max=self._max,
|
111
|
+
last_percentiles=self._calc_percentiles(self._ring),
|
112
|
+
sample_percentiles=self._calc_percentiles(self._sample),
|
113
|
+
)
|
114
|
+
|
115
|
+
def get_filtered(self, entry_filter: ta.Callable[[Entry], bool]) -> Stats:
|
116
|
+
def filter_entries(l):
|
117
|
+
return [e for e in list(l) if e is not None and entry_filter(e)]
|
118
|
+
return self.Stats(
|
119
|
+
count=self._count,
|
120
|
+
min=self._min,
|
121
|
+
max=self._max,
|
122
|
+
last_percentiles=self._calc_percentiles(filter_entries(self._ring)),
|
123
|
+
sample_percentiles=self._calc_percentiles(filter_entries(self._sample)),
|
124
|
+
)
|
125
|
+
|
126
|
+
def get_since(self, min_timestamp: float) -> Stats:
|
127
|
+
return self.get_filtered(lambda e: e.timestamp >= min_timestamp)
|
omlish/math/stats.py
CHANGED
@@ -1,18 +1,6 @@
|
|
1
|
-
"""
|
2
|
-
TODO:
|
3
|
-
- reservoir
|
4
|
-
- dep tdigest?
|
5
|
-
- struct-of-arrays - array.array('f', ...) - backed SamplingHistogram
|
6
|
-
- https://docs.python.org/3/library/statistics.html
|
7
|
-
"""
|
8
1
|
import bisect
|
9
2
|
import collections
|
10
|
-
import contextlib
|
11
|
-
import dataclasses as dc
|
12
3
|
import math
|
13
|
-
import operator
|
14
|
-
import random
|
15
|
-
import time
|
16
4
|
import typing as ta
|
17
5
|
|
18
6
|
from .. import cached
|
@@ -228,115 +216,3 @@ class Stats(ta.Sequence[float]):
|
|
228
216
|
count_map = collections.Counter(idxs)
|
229
217
|
bin_counts = [(b, count_map.get(i, 0)) for i, b in enumerate(bins)]
|
230
218
|
return bin_counts
|
231
|
-
|
232
|
-
|
233
|
-
##
|
234
|
-
|
235
|
-
|
236
|
-
class SamplingHistogram:
|
237
|
-
@dc.dataclass(frozen=True)
|
238
|
-
class Entry:
|
239
|
-
value: float
|
240
|
-
timestamp: float
|
241
|
-
|
242
|
-
@dc.dataclass(frozen=True)
|
243
|
-
class Percentile:
|
244
|
-
p: float
|
245
|
-
value: float
|
246
|
-
|
247
|
-
@dc.dataclass(frozen=True)
|
248
|
-
class Stats:
|
249
|
-
count: int
|
250
|
-
min: float
|
251
|
-
max: float
|
252
|
-
last_percentiles: list['SamplingHistogram.Percentile']
|
253
|
-
sample_percentiles: list['SamplingHistogram.Percentile']
|
254
|
-
|
255
|
-
DEFAULT_SIZE = 1000
|
256
|
-
DEFAULT_PERCENTILES = (0.5, 0.75, 0.9, 0.95, 0.99)
|
257
|
-
|
258
|
-
def __init__(
|
259
|
-
self,
|
260
|
-
*,
|
261
|
-
size: int = DEFAULT_SIZE,
|
262
|
-
percentiles: ta.Iterable[float] | None = None,
|
263
|
-
) -> None:
|
264
|
-
check.arg(size > 0)
|
265
|
-
|
266
|
-
super().__init__()
|
267
|
-
|
268
|
-
self._size = size
|
269
|
-
self._percentiles = list(percentiles if percentiles is not None else self.DEFAULT_PERCENTILES)
|
270
|
-
|
271
|
-
self._count = 0
|
272
|
-
self._min = float('inf')
|
273
|
-
self._max = float('-inf')
|
274
|
-
|
275
|
-
self._percentile_pos_list = [self._calc_percentile_pos(p, self._size) for p in self._percentiles]
|
276
|
-
|
277
|
-
self._ring: list[SamplingHistogram.Entry | None] = [None] * size
|
278
|
-
self._ring_pos = 0
|
279
|
-
|
280
|
-
self._sample: list[SamplingHistogram.Entry | None] = [None] * size
|
281
|
-
self._sample_pos_queue = list(reversed(range(size)))
|
282
|
-
|
283
|
-
def add(self, value: float) -> None:
|
284
|
-
self._count += 1
|
285
|
-
self._min = min(self._min, value)
|
286
|
-
self._max = max(self._max, value)
|
287
|
-
|
288
|
-
entry = self.Entry(value, time.time())
|
289
|
-
|
290
|
-
self._ring[self._ring_pos] = entry
|
291
|
-
next_ring_pos = self._ring_pos + 1
|
292
|
-
self._ring_pos = 0 if next_ring_pos >= self._size else next_ring_pos
|
293
|
-
|
294
|
-
sample_pos = None
|
295
|
-
if self._sample_pos_queue:
|
296
|
-
with contextlib.suppress(IndexError):
|
297
|
-
sample_pos = self._sample_pos_queue.pop()
|
298
|
-
if sample_pos is None:
|
299
|
-
sample_pos = random.randrange(0, self._size)
|
300
|
-
self._sample[sample_pos] = entry
|
301
|
-
|
302
|
-
@staticmethod
|
303
|
-
def _calc_percentile_pos(p: float, sz: int) -> int:
|
304
|
-
return round((p * sz) - 1)
|
305
|
-
|
306
|
-
def _calc_percentiles(self, entries: list[Entry | None]) -> list[Percentile]:
|
307
|
-
entries = list(filter(None, entries))
|
308
|
-
sz = len(entries)
|
309
|
-
if not sz:
|
310
|
-
return []
|
311
|
-
elif sz == self._size:
|
312
|
-
pos_list = self._percentile_pos_list
|
313
|
-
else:
|
314
|
-
pos_list = [self._calc_percentile_pos(p, sz) for p in self._percentiles]
|
315
|
-
entries.sort(key=operator.attrgetter('value'))
|
316
|
-
return [
|
317
|
-
self.Percentile(p, check.not_none(entries[pos]).value)
|
318
|
-
for p, pos in zip(self._percentiles, pos_list)
|
319
|
-
]
|
320
|
-
|
321
|
-
def get(self) -> Stats:
|
322
|
-
return self.Stats(
|
323
|
-
count=self._count,
|
324
|
-
min=self._min,
|
325
|
-
max=self._max,
|
326
|
-
last_percentiles=self._calc_percentiles(self._ring),
|
327
|
-
sample_percentiles=self._calc_percentiles(self._sample),
|
328
|
-
)
|
329
|
-
|
330
|
-
def get_filtered(self, entry_filter: ta.Callable[[Entry], bool]) -> Stats:
|
331
|
-
def filter_entries(l):
|
332
|
-
return [e for e in list(l) if e is not None and entry_filter(e)]
|
333
|
-
return self.Stats(
|
334
|
-
count=self._count,
|
335
|
-
min=self._min,
|
336
|
-
max=self._max,
|
337
|
-
last_percentiles=self._calc_percentiles(filter_entries(self._ring)),
|
338
|
-
sample_percentiles=self._calc_percentiles(filter_entries(self._sample)),
|
339
|
-
)
|
340
|
-
|
341
|
-
def get_since(self, min_timestamp: float) -> Stats:
|
342
|
-
return self.get_filtered(lambda e: e.timestamp >= min_timestamp)
|
omlish/sql/__init__.py
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# ruff: noqa: I001
|
2
|
+
import typing as _ta
|
3
|
+
|
4
|
+
from .. import lang as _lang
|
5
|
+
|
6
|
+
|
7
|
+
if _ta.TYPE_CHECKING:
|
8
|
+
from . import api
|
9
|
+
else:
|
10
|
+
api = _lang.proxy_import('.api', __package__)
|
11
|
+
|
1
12
|
from .dbs import ( # noqa
|
2
13
|
DbLoc,
|
3
14
|
DbSpec,
|
@@ -11,3 +22,8 @@ from .qualifiedname import ( # noqa
|
|
11
22
|
QualifiedName,
|
12
23
|
qn,
|
13
24
|
)
|
25
|
+
|
26
|
+
if _ta.TYPE_CHECKING:
|
27
|
+
from . import queries
|
28
|
+
else:
|
29
|
+
queries = _lang.proxy_import('.queries', __package__)
|