omlish 0.0.0.dev1__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.
Potentially problematic release.
This version of omlish might be problematic. Click here for more details.
- omlish/__about__.py +7 -0
- omlish/__init__.py +0 -0
- omlish/argparse.py +223 -0
- omlish/asyncs/__init__.py +17 -0
- omlish/asyncs/anyio.py +23 -0
- omlish/asyncs/asyncio.py +19 -0
- omlish/asyncs/asyncs.py +76 -0
- omlish/asyncs/futures.py +179 -0
- omlish/asyncs/trio.py +11 -0
- omlish/c3.py +173 -0
- omlish/cached.py +9 -0
- omlish/check.py +231 -0
- omlish/collections/__init__.py +63 -0
- omlish/collections/_abc.py +156 -0
- omlish/collections/_io_abc.py +78 -0
- omlish/collections/cache/__init__.py +11 -0
- omlish/collections/cache/descriptor.py +188 -0
- omlish/collections/cache/impl.py +485 -0
- omlish/collections/cache/types.py +37 -0
- omlish/collections/coerce.py +337 -0
- omlish/collections/frozen.py +148 -0
- omlish/collections/identity.py +106 -0
- omlish/collections/indexed.py +75 -0
- omlish/collections/mappings.py +127 -0
- omlish/collections/ordered.py +81 -0
- omlish/collections/persistent.py +36 -0
- omlish/collections/skiplist.py +193 -0
- omlish/collections/sorted.py +126 -0
- omlish/collections/treap.py +228 -0
- omlish/collections/treapmap.py +144 -0
- omlish/collections/unmodifiable.py +174 -0
- omlish/collections/utils.py +110 -0
- omlish/configs/__init__.py +0 -0
- omlish/configs/flattening.py +147 -0
- omlish/configs/props.py +64 -0
- omlish/dataclasses/__init__.py +83 -0
- omlish/dataclasses/impl/__init__.py +6 -0
- omlish/dataclasses/impl/api.py +260 -0
- omlish/dataclasses/impl/as_.py +76 -0
- omlish/dataclasses/impl/exceptions.py +2 -0
- omlish/dataclasses/impl/fields.py +148 -0
- omlish/dataclasses/impl/frozen.py +55 -0
- omlish/dataclasses/impl/hashing.py +85 -0
- omlish/dataclasses/impl/init.py +173 -0
- omlish/dataclasses/impl/internals.py +118 -0
- omlish/dataclasses/impl/main.py +150 -0
- omlish/dataclasses/impl/metaclass.py +126 -0
- omlish/dataclasses/impl/metadata.py +74 -0
- omlish/dataclasses/impl/order.py +47 -0
- omlish/dataclasses/impl/params.py +150 -0
- omlish/dataclasses/impl/processing.py +16 -0
- omlish/dataclasses/impl/reflect.py +173 -0
- omlish/dataclasses/impl/replace.py +40 -0
- omlish/dataclasses/impl/repr.py +34 -0
- omlish/dataclasses/impl/simple.py +92 -0
- omlish/dataclasses/impl/slots.py +80 -0
- omlish/dataclasses/impl/utils.py +167 -0
- omlish/defs.py +193 -0
- omlish/dispatch/__init__.py +3 -0
- omlish/dispatch/dispatch.py +137 -0
- omlish/dispatch/functions.py +52 -0
- omlish/dispatch/methods.py +162 -0
- omlish/docker.py +149 -0
- omlish/dynamic.py +220 -0
- omlish/graphs/__init__.py +0 -0
- omlish/graphs/dot/__init__.py +19 -0
- omlish/graphs/dot/items.py +162 -0
- omlish/graphs/dot/rendering.py +147 -0
- omlish/graphs/dot/utils.py +30 -0
- omlish/graphs/trees.py +249 -0
- omlish/http/__init__.py +0 -0
- omlish/http/consts.py +20 -0
- omlish/http/wsgi.py +34 -0
- omlish/inject/__init__.py +85 -0
- omlish/inject/binder.py +12 -0
- omlish/inject/bindings.py +49 -0
- omlish/inject/eagers.py +21 -0
- omlish/inject/elements.py +43 -0
- omlish/inject/exceptions.py +49 -0
- omlish/inject/impl/__init__.py +0 -0
- omlish/inject/impl/bindings.py +19 -0
- omlish/inject/impl/elements.py +154 -0
- omlish/inject/impl/injector.py +182 -0
- omlish/inject/impl/inspect.py +98 -0
- omlish/inject/impl/private.py +109 -0
- omlish/inject/impl/providers.py +132 -0
- omlish/inject/impl/scopes.py +198 -0
- omlish/inject/injector.py +40 -0
- omlish/inject/inspect.py +14 -0
- omlish/inject/keys.py +43 -0
- omlish/inject/managed.py +24 -0
- omlish/inject/overrides.py +18 -0
- omlish/inject/private.py +29 -0
- omlish/inject/providers.py +111 -0
- omlish/inject/proxy.py +48 -0
- omlish/inject/scopes.py +84 -0
- omlish/inject/types.py +21 -0
- omlish/iterators.py +184 -0
- omlish/json.py +194 -0
- omlish/lang/__init__.py +112 -0
- omlish/lang/cached.py +267 -0
- omlish/lang/classes/__init__.py +24 -0
- omlish/lang/classes/abstract.py +74 -0
- omlish/lang/classes/restrict.py +137 -0
- omlish/lang/classes/simple.py +120 -0
- omlish/lang/classes/test/__init__.py +0 -0
- omlish/lang/classes/test/test_abstract.py +89 -0
- omlish/lang/classes/test/test_restrict.py +71 -0
- omlish/lang/classes/test/test_simple.py +58 -0
- omlish/lang/classes/test/test_virtual.py +72 -0
- omlish/lang/classes/virtual.py +130 -0
- omlish/lang/clsdct.py +67 -0
- omlish/lang/cmp.py +63 -0
- omlish/lang/contextmanagers.py +249 -0
- omlish/lang/datetimes.py +67 -0
- omlish/lang/descriptors.py +52 -0
- omlish/lang/functions.py +126 -0
- omlish/lang/imports.py +153 -0
- omlish/lang/iterables.py +54 -0
- omlish/lang/maybes.py +136 -0
- omlish/lang/objects.py +103 -0
- omlish/lang/resolving.py +50 -0
- omlish/lang/strings.py +128 -0
- omlish/lang/typing.py +92 -0
- omlish/libc.py +532 -0
- omlish/logs/__init__.py +9 -0
- omlish/logs/_abc.py +247 -0
- omlish/logs/configs.py +62 -0
- omlish/logs/filters.py +9 -0
- omlish/logs/formatters.py +67 -0
- omlish/logs/utils.py +20 -0
- omlish/marshal/__init__.py +52 -0
- omlish/marshal/any.py +25 -0
- omlish/marshal/base.py +201 -0
- omlish/marshal/base64.py +25 -0
- omlish/marshal/dataclasses.py +115 -0
- omlish/marshal/datetimes.py +90 -0
- omlish/marshal/enums.py +43 -0
- omlish/marshal/exceptions.py +7 -0
- omlish/marshal/factories.py +129 -0
- omlish/marshal/global_.py +33 -0
- omlish/marshal/iterables.py +57 -0
- omlish/marshal/mappings.py +66 -0
- omlish/marshal/naming.py +17 -0
- omlish/marshal/objects.py +106 -0
- omlish/marshal/optionals.py +49 -0
- omlish/marshal/polymorphism.py +147 -0
- omlish/marshal/primitives.py +43 -0
- omlish/marshal/registries.py +57 -0
- omlish/marshal/standard.py +80 -0
- omlish/marshal/utils.py +23 -0
- omlish/marshal/uuids.py +29 -0
- omlish/marshal/values.py +30 -0
- omlish/math.py +184 -0
- omlish/os.py +32 -0
- omlish/reflect.py +359 -0
- omlish/replserver/__init__.py +5 -0
- omlish/replserver/__main__.py +4 -0
- omlish/replserver/console.py +247 -0
- omlish/replserver/server.py +146 -0
- omlish/runmodule.py +28 -0
- omlish/stats.py +342 -0
- omlish/term.py +222 -0
- omlish/testing/__init__.py +7 -0
- omlish/testing/pydevd.py +225 -0
- omlish/testing/pytest/__init__.py +8 -0
- omlish/testing/pytest/helpers.py +35 -0
- omlish/testing/pytest/inject/__init__.py +1 -0
- omlish/testing/pytest/inject/harness.py +159 -0
- omlish/testing/pytest/plugins/__init__.py +20 -0
- omlish/testing/pytest/plugins/_registry.py +6 -0
- omlish/testing/pytest/plugins/logging.py +13 -0
- omlish/testing/pytest/plugins/pycharm.py +54 -0
- omlish/testing/pytest/plugins/repeat.py +19 -0
- omlish/testing/pytest/plugins/skips.py +32 -0
- omlish/testing/pytest/plugins/spacing.py +19 -0
- omlish/testing/pytest/plugins/switches.py +70 -0
- omlish/testing/testing.py +102 -0
- omlish/text/__init__.py +0 -0
- omlish/text/delimit.py +171 -0
- omlish/text/indent.py +50 -0
- omlish/text/parts.py +265 -0
- omlish-0.0.0.dev1.dist-info/LICENSE +21 -0
- omlish-0.0.0.dev1.dist-info/METADATA +17 -0
- omlish-0.0.0.dev1.dist-info/RECORD +187 -0
- omlish-0.0.0.dev1.dist-info/WHEEL +5 -0
- omlish-0.0.0.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- !!! ANYIO !!!
|
|
4
|
+
- optional paramiko ssh-server
|
|
5
|
+
- optional ipython embed
|
|
6
|
+
|
|
7
|
+
lookit:
|
|
8
|
+
- https://github.com/vxgmichel/aioconsole/blob/e55f4b0601da3b3a40a88c965526d35ab38b5841/aioconsole/server.py
|
|
9
|
+
- https://github.com/nhoad/aiomanhole
|
|
10
|
+
- https://github.com/twisted/twisted/blob/00aa56f5257060304d41f09651c6ab58ee6104d6/src/twisted/conch/manhole.py
|
|
11
|
+
- https://github.com/Yelp/Tron/blob/4b864a73bd129b03e9890c134212972452bc6ab0/tron/manhole.py#L8
|
|
12
|
+
- https://github.com/ionelmc/python-manhole
|
|
13
|
+
- https://github.com/python/cpython/tree/15d48aea02099ffc5bdc5511cc53ced460cb31b9/Lib/_pyrepl
|
|
14
|
+
|
|
15
|
+
socat - UNIX-CONNECT:repl.sock
|
|
16
|
+
"""
|
|
17
|
+
import contextlib
|
|
18
|
+
import functools
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import socket as sock
|
|
22
|
+
import threading
|
|
23
|
+
import typing as ta
|
|
24
|
+
import weakref
|
|
25
|
+
|
|
26
|
+
from .. import check
|
|
27
|
+
from .. import dataclasses as dc
|
|
28
|
+
from .console import InteractiveSocketConsole
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ReplServer:
|
|
35
|
+
|
|
36
|
+
CONNECTION_THREAD_NAME = 'ReplServerConnection'
|
|
37
|
+
|
|
38
|
+
@dc.dataclass(frozen=True)
|
|
39
|
+
class Config:
|
|
40
|
+
path: str
|
|
41
|
+
file_mode: ta.Optional[int] = None
|
|
42
|
+
poll_interval: float = 0.5
|
|
43
|
+
exit_timeout: float = 10.0
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
config: Config,
|
|
48
|
+
) -> None:
|
|
49
|
+
super().__init__()
|
|
50
|
+
|
|
51
|
+
check.not_empty(config.path)
|
|
52
|
+
self._config = check.isinstance(config, ReplServer.Config)
|
|
53
|
+
|
|
54
|
+
self._socket: ta.Optional[sock.socket] = None
|
|
55
|
+
self._is_running = False
|
|
56
|
+
self._consoles_by_threads: ta.MutableMapping[threading.Thread, InteractiveSocketConsole] = \
|
|
57
|
+
weakref.WeakKeyDictionary() # noqa
|
|
58
|
+
self._is_shutdown = threading.Event()
|
|
59
|
+
self._should_shutdown = False
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def path(self) -> str:
|
|
63
|
+
return self._config.path
|
|
64
|
+
|
|
65
|
+
def __enter__(self):
|
|
66
|
+
check.state(not self._is_running)
|
|
67
|
+
check.state(not self._is_shutdown.is_set())
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
71
|
+
if not self._is_shutdown.is_set():
|
|
72
|
+
self.shutdown(True, self._config.exit_timeout)
|
|
73
|
+
|
|
74
|
+
def run(self) -> None:
|
|
75
|
+
check.state(not self._is_running)
|
|
76
|
+
check.state(not self._is_shutdown.is_set())
|
|
77
|
+
|
|
78
|
+
if os.path.exists(self._config.path):
|
|
79
|
+
os.unlink(self._config.path)
|
|
80
|
+
|
|
81
|
+
self._socket = sock.socket(sock.AF_UNIX, sock.SOCK_STREAM)
|
|
82
|
+
self._socket.settimeout(self._config.poll_interval)
|
|
83
|
+
self._socket.bind(self._config.path)
|
|
84
|
+
with contextlib.closing(self._socket):
|
|
85
|
+
self._socket.listen(1)
|
|
86
|
+
|
|
87
|
+
log.info(f'Repl server listening on file {self._config.path}')
|
|
88
|
+
|
|
89
|
+
self._is_running = True
|
|
90
|
+
try:
|
|
91
|
+
while not self._should_shutdown:
|
|
92
|
+
try:
|
|
93
|
+
conn, _ = self._socket.accept()
|
|
94
|
+
except sock.timeout:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
log.info(f'Got repl server connection on file {self._config.path}')
|
|
98
|
+
|
|
99
|
+
def run(conn):
|
|
100
|
+
with contextlib.closing(conn):
|
|
101
|
+
variables = globals().copy()
|
|
102
|
+
|
|
103
|
+
console = InteractiveSocketConsole(conn, variables)
|
|
104
|
+
variables['__console__'] = console
|
|
105
|
+
|
|
106
|
+
log.info(
|
|
107
|
+
f'Starting console {id(console)} repl server connection '
|
|
108
|
+
f'on file {self._config.path} '
|
|
109
|
+
f'on thread {threading.current_thread().ident}'
|
|
110
|
+
)
|
|
111
|
+
self._consoles_by_threads[threading.current_thread()] = console
|
|
112
|
+
console.interact()
|
|
113
|
+
|
|
114
|
+
thread = threading.Thread(
|
|
115
|
+
target=functools.partial(run, conn),
|
|
116
|
+
daemon=True,
|
|
117
|
+
name=self.CONNECTION_THREAD_NAME)
|
|
118
|
+
thread.start()
|
|
119
|
+
|
|
120
|
+
for thread, console in self._consoles_by_threads.items():
|
|
121
|
+
try:
|
|
122
|
+
console.conn.close()
|
|
123
|
+
except Exception:
|
|
124
|
+
log.exception('Error shutting down')
|
|
125
|
+
|
|
126
|
+
for thread in self._consoles_by_threads.keys():
|
|
127
|
+
try:
|
|
128
|
+
thread.join(self._config.exit_timeout)
|
|
129
|
+
except Exception:
|
|
130
|
+
log.exception('Error shutting down')
|
|
131
|
+
|
|
132
|
+
os.unlink(self._config.path)
|
|
133
|
+
|
|
134
|
+
finally:
|
|
135
|
+
self._is_shutdown.set()
|
|
136
|
+
self._is_running = False
|
|
137
|
+
|
|
138
|
+
def shutdown(self, block: bool = False, timeout: ta.Optional[float] = None) -> None:
|
|
139
|
+
self._should_shutdown = True
|
|
140
|
+
if block:
|
|
141
|
+
self._is_shutdown.wait(timeout=timeout)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def run():
|
|
145
|
+
with ReplServer(ReplServer.Config('repl.sock')) as repl_server:
|
|
146
|
+
repl_server.run()
|
omlish/runmodule.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Some environments refuse to support running modules rather than scripts, so this is just "python -m" functionality
|
|
4
|
+
exposed as a script.
|
|
5
|
+
"""
|
|
6
|
+
import runpy
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _main() -> int:
|
|
11
|
+
# Run the module specified as the next command line argument
|
|
12
|
+
if len(sys.argv) < 2:
|
|
13
|
+
print('No module specified for execution', file=sys.stderr)
|
|
14
|
+
return 1
|
|
15
|
+
|
|
16
|
+
if sys.argv[1] == '--wait':
|
|
17
|
+
import os
|
|
18
|
+
print(os.getpid())
|
|
19
|
+
input()
|
|
20
|
+
sys.argv.pop(1)
|
|
21
|
+
|
|
22
|
+
del sys.argv[0] # Make the requested module sys.argv[0]
|
|
23
|
+
runpy._run_module_as_main(sys.argv[0]) # type: ignore # noqa
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == '__main__':
|
|
28
|
+
sys.exit(_main())
|
omlish/stats.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO:
|
|
3
|
+
- reservoir
|
|
4
|
+
- dep tdigest?
|
|
5
|
+
"""
|
|
6
|
+
import bisect
|
|
7
|
+
import collections
|
|
8
|
+
import dataclasses as dc
|
|
9
|
+
import math
|
|
10
|
+
import operator
|
|
11
|
+
import random
|
|
12
|
+
import time
|
|
13
|
+
import typing as ta
|
|
14
|
+
|
|
15
|
+
from . import cached
|
|
16
|
+
from . import check
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_quantile(sorted_data: ta.Sequence[float], q: float) -> float:
|
|
23
|
+
q = float(q)
|
|
24
|
+
check.arg(0.0 <= q <= 1.0)
|
|
25
|
+
data, n = sorted_data, len(sorted_data)
|
|
26
|
+
idx = q / 1.0 * (n - 1)
|
|
27
|
+
idx_f, idx_c = math.floor(idx), math.ceil(idx)
|
|
28
|
+
if idx_f == idx_c:
|
|
29
|
+
return data[idx_f]
|
|
30
|
+
return (data[idx_f] * (idx_c - idx)) + (data[idx_c] * (idx - idx_f))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Stats(ta.Sequence[float]):
|
|
37
|
+
"""
|
|
38
|
+
~ https://github.com/mahmoud/boltons/blob/47e0c3bfcbd3291a1366f34069f23e43659717cd/boltons/statsutils.py
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
data: ta.Sequence[float],
|
|
44
|
+
*,
|
|
45
|
+
default: float = 0.,
|
|
46
|
+
eq: ta.Callable[[float, float], bool] = lambda a, b: a == b,
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
|
|
50
|
+
self._kwargs: ta.Any = dict(
|
|
51
|
+
default=default,
|
|
52
|
+
eq=eq,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self._data = data
|
|
56
|
+
self._default = default
|
|
57
|
+
self._eq = eq
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def data(self) -> ta.Sequence[float]:
|
|
61
|
+
return self._data
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def default(self) -> float:
|
|
65
|
+
return self._default
|
|
66
|
+
|
|
67
|
+
def __len__(self) -> int:
|
|
68
|
+
return len(self.data)
|
|
69
|
+
|
|
70
|
+
def __iter__(self) -> ta.Iterator[float]:
|
|
71
|
+
return iter(self.data)
|
|
72
|
+
|
|
73
|
+
def __getitem__(self, index: ta.Any) -> float: # type: ignore
|
|
74
|
+
return self._data[index]
|
|
75
|
+
|
|
76
|
+
@cached.property
|
|
77
|
+
def sorted(self) -> ta.Sequence[float]:
|
|
78
|
+
return sorted(self.data)
|
|
79
|
+
|
|
80
|
+
@cached.property
|
|
81
|
+
def mean(self) -> float:
|
|
82
|
+
return sum(self.data, 0.0) / len(self.data)
|
|
83
|
+
|
|
84
|
+
@cached.property
|
|
85
|
+
def max(self) -> float:
|
|
86
|
+
return max(self.data)
|
|
87
|
+
|
|
88
|
+
@cached.property
|
|
89
|
+
def min(self) -> float:
|
|
90
|
+
return min(self.data)
|
|
91
|
+
|
|
92
|
+
def get_quantile(self, q: float) -> float:
|
|
93
|
+
if not self.data:
|
|
94
|
+
return self.default
|
|
95
|
+
return get_quantile(self.sorted, q)
|
|
96
|
+
|
|
97
|
+
@cached.property
|
|
98
|
+
def median(self) -> float:
|
|
99
|
+
return self.get_quantile(0.5)
|
|
100
|
+
|
|
101
|
+
def get_pow_diffs(self, power: float) -> list[float]:
|
|
102
|
+
m = self.mean
|
|
103
|
+
return [(v - m) ** power for v in self.data]
|
|
104
|
+
|
|
105
|
+
@cached.property
|
|
106
|
+
def variance(self) -> float:
|
|
107
|
+
return Stats(self.get_pow_diffs(2)).mean
|
|
108
|
+
|
|
109
|
+
@cached.property
|
|
110
|
+
def std_dev(self) -> float:
|
|
111
|
+
return self.variance ** 0.5
|
|
112
|
+
|
|
113
|
+
@cached.property
|
|
114
|
+
def median_abs_dev(self) -> float:
|
|
115
|
+
x = self.median
|
|
116
|
+
return Stats([abs(x - v) for v in self.sorted]).median
|
|
117
|
+
|
|
118
|
+
@cached.property
|
|
119
|
+
def rel_std_dev(self) -> float:
|
|
120
|
+
abs_mean = abs(self.mean)
|
|
121
|
+
if abs_mean:
|
|
122
|
+
return self.std_dev / abs_mean
|
|
123
|
+
else:
|
|
124
|
+
return self.default
|
|
125
|
+
|
|
126
|
+
@cached.property
|
|
127
|
+
def skewness(self) -> float:
|
|
128
|
+
data, s_dev = self.data, self.std_dev
|
|
129
|
+
if len(data) > 1 and s_dev > 0:
|
|
130
|
+
return (sum(self.get_pow_diffs(3)) / float((len(data) - 1) * (s_dev ** 3)))
|
|
131
|
+
else:
|
|
132
|
+
return self.default
|
|
133
|
+
|
|
134
|
+
@cached.property
|
|
135
|
+
def kurtosis(self) -> float:
|
|
136
|
+
data, s_dev = self.data, self.std_dev
|
|
137
|
+
if len(data) > 1 and s_dev > 0:
|
|
138
|
+
return (sum(self.get_pow_diffs(4)) / float((len(data) - 1) * (s_dev ** 4)))
|
|
139
|
+
else:
|
|
140
|
+
return 0.0
|
|
141
|
+
|
|
142
|
+
@cached.property
|
|
143
|
+
def iqr(self) -> float:
|
|
144
|
+
return self.get_quantile(0.75) - self.get_quantile(0.25)
|
|
145
|
+
|
|
146
|
+
@cached.property
|
|
147
|
+
def trimean(self) -> float:
|
|
148
|
+
return (self.get_quantile(0.25) + (2 * self.get_quantile(0.5)) + self.get_quantile(0.75)) / 4.0
|
|
149
|
+
|
|
150
|
+
def get_zscore(self, value: float) -> float:
|
|
151
|
+
mean = self.mean
|
|
152
|
+
if self._eq(self.std_dev, 0.0):
|
|
153
|
+
if self._eq(value, mean):
|
|
154
|
+
return 0
|
|
155
|
+
if value > mean:
|
|
156
|
+
return float('inf')
|
|
157
|
+
if value < mean:
|
|
158
|
+
return float('-inf')
|
|
159
|
+
return (float(value) - mean) / self.std_dev
|
|
160
|
+
|
|
161
|
+
def trim_relative(self, amount: float = 0.15) -> 'Stats':
|
|
162
|
+
trim = float(amount)
|
|
163
|
+
check.arg(0.0 <= trim < 0.5)
|
|
164
|
+
size = len(self.data)
|
|
165
|
+
size_diff = int(size * trim)
|
|
166
|
+
if self._eq(size_diff, 0.0):
|
|
167
|
+
return self
|
|
168
|
+
return Stats(self.sorted[size_diff:-size_diff], **self._kwargs)
|
|
169
|
+
|
|
170
|
+
def get_bin_bounds(
|
|
171
|
+
self,
|
|
172
|
+
count: int | None = None,
|
|
173
|
+
with_max: bool = False,
|
|
174
|
+
) -> list[float]:
|
|
175
|
+
if not self.data:
|
|
176
|
+
return [0.0]
|
|
177
|
+
|
|
178
|
+
data = self.data
|
|
179
|
+
len_data, min_data, max_data = len(data), min(data), max(data)
|
|
180
|
+
|
|
181
|
+
if len_data < 4:
|
|
182
|
+
if not count:
|
|
183
|
+
count = len_data
|
|
184
|
+
dx = (max_data - min_data) / float(count)
|
|
185
|
+
bins = [min_data + (dx * i) for i in range(count)]
|
|
186
|
+
|
|
187
|
+
elif count is None:
|
|
188
|
+
# freedman algorithm for fixed-width bin selection
|
|
189
|
+
q25, q75 = self.get_quantile(0.25), self.get_quantile(0.75)
|
|
190
|
+
dx = 2 * (q75 - q25) / (len_data ** (1 / 3.0))
|
|
191
|
+
bin_count = max(1, int(math.ceil((max_data - min_data) / dx)))
|
|
192
|
+
bins = [min_data + (dx * i) for i in range(bin_count + 1)]
|
|
193
|
+
bins = [b for b in bins if b < max_data]
|
|
194
|
+
|
|
195
|
+
else:
|
|
196
|
+
dx = (max_data - min_data) / float(count)
|
|
197
|
+
bins = [min_data + (dx * i) for i in range(count)]
|
|
198
|
+
|
|
199
|
+
if with_max:
|
|
200
|
+
bins.append(float(max_data))
|
|
201
|
+
|
|
202
|
+
return bins
|
|
203
|
+
|
|
204
|
+
def get_histogram_counts(
|
|
205
|
+
self,
|
|
206
|
+
bins: list[float] | int | None = None,
|
|
207
|
+
*,
|
|
208
|
+
bin_digits: int = 1,
|
|
209
|
+
) -> list[tuple[float, int]]:
|
|
210
|
+
bin_digits = int(bin_digits)
|
|
211
|
+
if not bins:
|
|
212
|
+
bins = self.get_bin_bounds()
|
|
213
|
+
elif isinstance(bins, int):
|
|
214
|
+
bins = self.get_bin_bounds(bins)
|
|
215
|
+
else:
|
|
216
|
+
bins = [float(x) for x in bins]
|
|
217
|
+
if self.min < bins[0]:
|
|
218
|
+
bins = [self.min] + bins
|
|
219
|
+
|
|
220
|
+
round_factor = 10.0 ** bin_digits
|
|
221
|
+
bins = [math.floor(b * round_factor) / round_factor for b in bins]
|
|
222
|
+
bins = sorted(set(bins))
|
|
223
|
+
|
|
224
|
+
idxs = [bisect.bisect(bins, d) - 1 for d in self.data]
|
|
225
|
+
count_map = collections.Counter(idxs)
|
|
226
|
+
bin_counts = [(b, count_map.get(i, 0)) for i, b in enumerate(bins)]
|
|
227
|
+
return bin_counts
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
##
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class SamplingHistogram:
|
|
234
|
+
|
|
235
|
+
@dc.dataclass(frozen=True)
|
|
236
|
+
class Entry:
|
|
237
|
+
value: float
|
|
238
|
+
timestamp: float
|
|
239
|
+
|
|
240
|
+
@dc.dataclass(frozen=True)
|
|
241
|
+
class Percentile:
|
|
242
|
+
p: float
|
|
243
|
+
value: float
|
|
244
|
+
|
|
245
|
+
@dc.dataclass(frozen=True)
|
|
246
|
+
class Stats:
|
|
247
|
+
count: int
|
|
248
|
+
min: float
|
|
249
|
+
max: float
|
|
250
|
+
last_percentiles: list['SamplingHistogram.Percentile']
|
|
251
|
+
sample_percentiles: list['SamplingHistogram.Percentile']
|
|
252
|
+
|
|
253
|
+
DEFAULT_SIZE = 1000
|
|
254
|
+
DEFAULT_PERCENTILES = [0.5, 0.75, 0.9, 0.95, 0.99]
|
|
255
|
+
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
*,
|
|
259
|
+
size: int = DEFAULT_SIZE,
|
|
260
|
+
percentiles: ta.Iterable[float] | None = None,
|
|
261
|
+
) -> None:
|
|
262
|
+
check.arg(size > 0)
|
|
263
|
+
|
|
264
|
+
super().__init__()
|
|
265
|
+
|
|
266
|
+
self._size = size
|
|
267
|
+
self._percentiles = list(percentiles if percentiles is not None else self.DEFAULT_PERCENTILES)
|
|
268
|
+
|
|
269
|
+
self._count = 0
|
|
270
|
+
self._min = float('inf')
|
|
271
|
+
self._max = float('-inf')
|
|
272
|
+
|
|
273
|
+
self._percentile_pos_list = [self._calc_percentile_pos(p, self._size) for p in self._percentiles]
|
|
274
|
+
|
|
275
|
+
self._ring: list[ta.Optional[SamplingHistogram.Entry]] = [None] * size
|
|
276
|
+
self._ring_pos = 0
|
|
277
|
+
|
|
278
|
+
self._sample: list[ta.Optional[SamplingHistogram.Entry]] = [None] * size
|
|
279
|
+
self._sample_pos_queue = list(reversed(range(size)))
|
|
280
|
+
|
|
281
|
+
def add(self, value: float) -> None:
|
|
282
|
+
self._count += 1
|
|
283
|
+
self._min = min(self._min, value)
|
|
284
|
+
self._max = max(self._max, value)
|
|
285
|
+
|
|
286
|
+
entry = self.Entry(value, time.time())
|
|
287
|
+
|
|
288
|
+
self._ring[self._ring_pos] = entry
|
|
289
|
+
next_ring_pos = self._ring_pos + 1
|
|
290
|
+
self._ring_pos = 0 if next_ring_pos >= self._size else next_ring_pos
|
|
291
|
+
|
|
292
|
+
sample_pos = None
|
|
293
|
+
if self._sample_pos_queue:
|
|
294
|
+
try:
|
|
295
|
+
sample_pos = self._sample_pos_queue.pop()
|
|
296
|
+
except IndexError:
|
|
297
|
+
pass
|
|
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 int(round((p * sz) - 1))
|
|
305
|
+
|
|
306
|
+
def _calc_percentiles(self, entries: list[ta.Optional[Entry]]) -> 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)
|