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.

Files changed (187) hide show
  1. omlish/__about__.py +7 -0
  2. omlish/__init__.py +0 -0
  3. omlish/argparse.py +223 -0
  4. omlish/asyncs/__init__.py +17 -0
  5. omlish/asyncs/anyio.py +23 -0
  6. omlish/asyncs/asyncio.py +19 -0
  7. omlish/asyncs/asyncs.py +76 -0
  8. omlish/asyncs/futures.py +179 -0
  9. omlish/asyncs/trio.py +11 -0
  10. omlish/c3.py +173 -0
  11. omlish/cached.py +9 -0
  12. omlish/check.py +231 -0
  13. omlish/collections/__init__.py +63 -0
  14. omlish/collections/_abc.py +156 -0
  15. omlish/collections/_io_abc.py +78 -0
  16. omlish/collections/cache/__init__.py +11 -0
  17. omlish/collections/cache/descriptor.py +188 -0
  18. omlish/collections/cache/impl.py +485 -0
  19. omlish/collections/cache/types.py +37 -0
  20. omlish/collections/coerce.py +337 -0
  21. omlish/collections/frozen.py +148 -0
  22. omlish/collections/identity.py +106 -0
  23. omlish/collections/indexed.py +75 -0
  24. omlish/collections/mappings.py +127 -0
  25. omlish/collections/ordered.py +81 -0
  26. omlish/collections/persistent.py +36 -0
  27. omlish/collections/skiplist.py +193 -0
  28. omlish/collections/sorted.py +126 -0
  29. omlish/collections/treap.py +228 -0
  30. omlish/collections/treapmap.py +144 -0
  31. omlish/collections/unmodifiable.py +174 -0
  32. omlish/collections/utils.py +110 -0
  33. omlish/configs/__init__.py +0 -0
  34. omlish/configs/flattening.py +147 -0
  35. omlish/configs/props.py +64 -0
  36. omlish/dataclasses/__init__.py +83 -0
  37. omlish/dataclasses/impl/__init__.py +6 -0
  38. omlish/dataclasses/impl/api.py +260 -0
  39. omlish/dataclasses/impl/as_.py +76 -0
  40. omlish/dataclasses/impl/exceptions.py +2 -0
  41. omlish/dataclasses/impl/fields.py +148 -0
  42. omlish/dataclasses/impl/frozen.py +55 -0
  43. omlish/dataclasses/impl/hashing.py +85 -0
  44. omlish/dataclasses/impl/init.py +173 -0
  45. omlish/dataclasses/impl/internals.py +118 -0
  46. omlish/dataclasses/impl/main.py +150 -0
  47. omlish/dataclasses/impl/metaclass.py +126 -0
  48. omlish/dataclasses/impl/metadata.py +74 -0
  49. omlish/dataclasses/impl/order.py +47 -0
  50. omlish/dataclasses/impl/params.py +150 -0
  51. omlish/dataclasses/impl/processing.py +16 -0
  52. omlish/dataclasses/impl/reflect.py +173 -0
  53. omlish/dataclasses/impl/replace.py +40 -0
  54. omlish/dataclasses/impl/repr.py +34 -0
  55. omlish/dataclasses/impl/simple.py +92 -0
  56. omlish/dataclasses/impl/slots.py +80 -0
  57. omlish/dataclasses/impl/utils.py +167 -0
  58. omlish/defs.py +193 -0
  59. omlish/dispatch/__init__.py +3 -0
  60. omlish/dispatch/dispatch.py +137 -0
  61. omlish/dispatch/functions.py +52 -0
  62. omlish/dispatch/methods.py +162 -0
  63. omlish/docker.py +149 -0
  64. omlish/dynamic.py +220 -0
  65. omlish/graphs/__init__.py +0 -0
  66. omlish/graphs/dot/__init__.py +19 -0
  67. omlish/graphs/dot/items.py +162 -0
  68. omlish/graphs/dot/rendering.py +147 -0
  69. omlish/graphs/dot/utils.py +30 -0
  70. omlish/graphs/trees.py +249 -0
  71. omlish/http/__init__.py +0 -0
  72. omlish/http/consts.py +20 -0
  73. omlish/http/wsgi.py +34 -0
  74. omlish/inject/__init__.py +85 -0
  75. omlish/inject/binder.py +12 -0
  76. omlish/inject/bindings.py +49 -0
  77. omlish/inject/eagers.py +21 -0
  78. omlish/inject/elements.py +43 -0
  79. omlish/inject/exceptions.py +49 -0
  80. omlish/inject/impl/__init__.py +0 -0
  81. omlish/inject/impl/bindings.py +19 -0
  82. omlish/inject/impl/elements.py +154 -0
  83. omlish/inject/impl/injector.py +182 -0
  84. omlish/inject/impl/inspect.py +98 -0
  85. omlish/inject/impl/private.py +109 -0
  86. omlish/inject/impl/providers.py +132 -0
  87. omlish/inject/impl/scopes.py +198 -0
  88. omlish/inject/injector.py +40 -0
  89. omlish/inject/inspect.py +14 -0
  90. omlish/inject/keys.py +43 -0
  91. omlish/inject/managed.py +24 -0
  92. omlish/inject/overrides.py +18 -0
  93. omlish/inject/private.py +29 -0
  94. omlish/inject/providers.py +111 -0
  95. omlish/inject/proxy.py +48 -0
  96. omlish/inject/scopes.py +84 -0
  97. omlish/inject/types.py +21 -0
  98. omlish/iterators.py +184 -0
  99. omlish/json.py +194 -0
  100. omlish/lang/__init__.py +112 -0
  101. omlish/lang/cached.py +267 -0
  102. omlish/lang/classes/__init__.py +24 -0
  103. omlish/lang/classes/abstract.py +74 -0
  104. omlish/lang/classes/restrict.py +137 -0
  105. omlish/lang/classes/simple.py +120 -0
  106. omlish/lang/classes/test/__init__.py +0 -0
  107. omlish/lang/classes/test/test_abstract.py +89 -0
  108. omlish/lang/classes/test/test_restrict.py +71 -0
  109. omlish/lang/classes/test/test_simple.py +58 -0
  110. omlish/lang/classes/test/test_virtual.py +72 -0
  111. omlish/lang/classes/virtual.py +130 -0
  112. omlish/lang/clsdct.py +67 -0
  113. omlish/lang/cmp.py +63 -0
  114. omlish/lang/contextmanagers.py +249 -0
  115. omlish/lang/datetimes.py +67 -0
  116. omlish/lang/descriptors.py +52 -0
  117. omlish/lang/functions.py +126 -0
  118. omlish/lang/imports.py +153 -0
  119. omlish/lang/iterables.py +54 -0
  120. omlish/lang/maybes.py +136 -0
  121. omlish/lang/objects.py +103 -0
  122. omlish/lang/resolving.py +50 -0
  123. omlish/lang/strings.py +128 -0
  124. omlish/lang/typing.py +92 -0
  125. omlish/libc.py +532 -0
  126. omlish/logs/__init__.py +9 -0
  127. omlish/logs/_abc.py +247 -0
  128. omlish/logs/configs.py +62 -0
  129. omlish/logs/filters.py +9 -0
  130. omlish/logs/formatters.py +67 -0
  131. omlish/logs/utils.py +20 -0
  132. omlish/marshal/__init__.py +52 -0
  133. omlish/marshal/any.py +25 -0
  134. omlish/marshal/base.py +201 -0
  135. omlish/marshal/base64.py +25 -0
  136. omlish/marshal/dataclasses.py +115 -0
  137. omlish/marshal/datetimes.py +90 -0
  138. omlish/marshal/enums.py +43 -0
  139. omlish/marshal/exceptions.py +7 -0
  140. omlish/marshal/factories.py +129 -0
  141. omlish/marshal/global_.py +33 -0
  142. omlish/marshal/iterables.py +57 -0
  143. omlish/marshal/mappings.py +66 -0
  144. omlish/marshal/naming.py +17 -0
  145. omlish/marshal/objects.py +106 -0
  146. omlish/marshal/optionals.py +49 -0
  147. omlish/marshal/polymorphism.py +147 -0
  148. omlish/marshal/primitives.py +43 -0
  149. omlish/marshal/registries.py +57 -0
  150. omlish/marshal/standard.py +80 -0
  151. omlish/marshal/utils.py +23 -0
  152. omlish/marshal/uuids.py +29 -0
  153. omlish/marshal/values.py +30 -0
  154. omlish/math.py +184 -0
  155. omlish/os.py +32 -0
  156. omlish/reflect.py +359 -0
  157. omlish/replserver/__init__.py +5 -0
  158. omlish/replserver/__main__.py +4 -0
  159. omlish/replserver/console.py +247 -0
  160. omlish/replserver/server.py +146 -0
  161. omlish/runmodule.py +28 -0
  162. omlish/stats.py +342 -0
  163. omlish/term.py +222 -0
  164. omlish/testing/__init__.py +7 -0
  165. omlish/testing/pydevd.py +225 -0
  166. omlish/testing/pytest/__init__.py +8 -0
  167. omlish/testing/pytest/helpers.py +35 -0
  168. omlish/testing/pytest/inject/__init__.py +1 -0
  169. omlish/testing/pytest/inject/harness.py +159 -0
  170. omlish/testing/pytest/plugins/__init__.py +20 -0
  171. omlish/testing/pytest/plugins/_registry.py +6 -0
  172. omlish/testing/pytest/plugins/logging.py +13 -0
  173. omlish/testing/pytest/plugins/pycharm.py +54 -0
  174. omlish/testing/pytest/plugins/repeat.py +19 -0
  175. omlish/testing/pytest/plugins/skips.py +32 -0
  176. omlish/testing/pytest/plugins/spacing.py +19 -0
  177. omlish/testing/pytest/plugins/switches.py +70 -0
  178. omlish/testing/testing.py +102 -0
  179. omlish/text/__init__.py +0 -0
  180. omlish/text/delimit.py +171 -0
  181. omlish/text/indent.py +50 -0
  182. omlish/text/parts.py +265 -0
  183. omlish-0.0.0.dev1.dist-info/LICENSE +21 -0
  184. omlish-0.0.0.dev1.dist-info/METADATA +17 -0
  185. omlish-0.0.0.dev1.dist-info/RECORD +187 -0
  186. omlish-0.0.0.dev1.dist-info/WHEEL +5 -0
  187. 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)