omdev 0.0.0.dev196__py3-none-any.whl → 0.0.0.dev198__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.
- omdev/.manifests.json +12 -0
- omdev/tools/linehisto.py +335 -0
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/RECORD +8 -7
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/top_level.txt +0 -0
omdev/.manifests.json
CHANGED
@@ -302,6 +302,18 @@
|
|
302
302
|
}
|
303
303
|
}
|
304
304
|
},
|
305
|
+
{
|
306
|
+
"module": ".tools.linehisto",
|
307
|
+
"attr": "_CLI_MODULE",
|
308
|
+
"file": "omdev/tools/linehisto.py",
|
309
|
+
"line": 330,
|
310
|
+
"value": {
|
311
|
+
"$.cli.types.CliModule": {
|
312
|
+
"cmd_name": "linehisto",
|
313
|
+
"mod_name": "omdev.tools.linehisto"
|
314
|
+
}
|
315
|
+
}
|
316
|
+
},
|
305
317
|
{
|
306
318
|
"module": ".tools.mkrelimp",
|
307
319
|
"attr": "_CLI_MODULE",
|
omdev/tools/linehisto.py
ADDED
@@ -0,0 +1,335 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- cmd line options
|
4
|
+
- truncation
|
5
|
+
- mean/min/max/median/stddev
|
6
|
+
- delta mean/min/max/median/stddev
|
7
|
+
- heat - red rapidly changing blue stale
|
8
|
+
- paging
|
9
|
+
- find
|
10
|
+
- stdin/stdout redir (ttyname(0))
|
11
|
+
- graph
|
12
|
+
"""
|
13
|
+
import curses
|
14
|
+
import heapq
|
15
|
+
import operator
|
16
|
+
import sys
|
17
|
+
import time
|
18
|
+
import typing as ta
|
19
|
+
|
20
|
+
from ..cli import CliModule
|
21
|
+
|
22
|
+
|
23
|
+
##
|
24
|
+
|
25
|
+
|
26
|
+
def with_timer(interval=5.):
|
27
|
+
def outer(fn):
|
28
|
+
start = time.time()
|
29
|
+
last = start
|
30
|
+
|
31
|
+
def inner(*args, **kwargs):
|
32
|
+
nonlocal last
|
33
|
+
|
34
|
+
now = time.time()
|
35
|
+
if (now - last) < interval:
|
36
|
+
return None
|
37
|
+
|
38
|
+
last = now
|
39
|
+
return fn(*args, **kwargs)
|
40
|
+
|
41
|
+
return inner
|
42
|
+
|
43
|
+
return outer
|
44
|
+
|
45
|
+
|
46
|
+
def calc_percent(a: float, b: float) -> float:
|
47
|
+
if not a or not b:
|
48
|
+
return 0.
|
49
|
+
return ((a * 1e9) // (b * 1e5)) / 100.
|
50
|
+
|
51
|
+
|
52
|
+
##
|
53
|
+
|
54
|
+
|
55
|
+
class KeyedHisto:
|
56
|
+
def __init__(
|
57
|
+
self,
|
58
|
+
*,
|
59
|
+
max_entries: int | None = None,
|
60
|
+
) -> None:
|
61
|
+
super().__init__()
|
62
|
+
|
63
|
+
self.entries: dict[str, int] = {}
|
64
|
+
self.total_seen = 0
|
65
|
+
self.total_evicted = 0
|
66
|
+
|
67
|
+
self.max_entries = max_entries
|
68
|
+
if max_entries is not None:
|
69
|
+
self.eviction_len = int(max_entries * 2)
|
70
|
+
|
71
|
+
self.num_evicted = 0
|
72
|
+
|
73
|
+
def __len__(self) -> int:
|
74
|
+
return len(self.entries)
|
75
|
+
|
76
|
+
@property
|
77
|
+
def total_tracked(self) -> int:
|
78
|
+
return self.total_seen - self.total_evicted
|
79
|
+
|
80
|
+
def inc(self, key: str, n: int = 1) -> int:
|
81
|
+
if self.max_entries is not None and len(self) >= self.eviction_len:
|
82
|
+
self.evict(len(self) - self.max_entries)
|
83
|
+
|
84
|
+
self.total_seen += n
|
85
|
+
|
86
|
+
ct = self.entries.get(key, 0) + n
|
87
|
+
self.entries[key] = ct
|
88
|
+
|
89
|
+
return ct
|
90
|
+
|
91
|
+
@property
|
92
|
+
def items(self) -> ta.Iterable[tuple[str, int]]:
|
93
|
+
return self.entries.items()
|
94
|
+
|
95
|
+
@property
|
96
|
+
def sorted(self) -> list[tuple[str, int]]:
|
97
|
+
items = sorted(self.items, key=operator.itemgetter(1))
|
98
|
+
return items[::-1]
|
99
|
+
|
100
|
+
def evict(self, n: int = 1) -> None:
|
101
|
+
self.num_evicted += n
|
102
|
+
|
103
|
+
for key, ct in heapq.nsmallest(n, self.items, key=operator.itemgetter(1)):
|
104
|
+
self.total_evicted += ct
|
105
|
+
|
106
|
+
del self.entries[key]
|
107
|
+
|
108
|
+
def nlargest(self, n: int = 20) -> list[tuple[str, int]]:
|
109
|
+
return heapq.nlargest(n, self.items, key=operator.itemgetter(1))
|
110
|
+
|
111
|
+
def nsmallest(self, n: int = 20) -> list[tuple[str, int]]:
|
112
|
+
return heapq.nsmallest(n, self.items, key=operator.itemgetter(1))
|
113
|
+
|
114
|
+
|
115
|
+
class KeyedHistoRenderer:
|
116
|
+
def __init__(
|
117
|
+
self,
|
118
|
+
histo: KeyedHisto,
|
119
|
+
*,
|
120
|
+
max_lines: int | None = None,
|
121
|
+
) -> None:
|
122
|
+
super().__init__()
|
123
|
+
|
124
|
+
self.histo = histo
|
125
|
+
self.max_lines = max_lines
|
126
|
+
|
127
|
+
@property
|
128
|
+
def entries_to_render(self) -> list[tuple[str, int]]:
|
129
|
+
if self.max_lines is None:
|
130
|
+
return list(self.histo.sorted)
|
131
|
+
|
132
|
+
nlines = min(self.max_lines, len(self.histo))
|
133
|
+
|
134
|
+
return self.histo.nlargest(nlines)
|
135
|
+
|
136
|
+
def render_header(self, count_width: int) -> str:
|
137
|
+
header = 'count : % sen : % tkd : line'
|
138
|
+
|
139
|
+
if count_width > 5:
|
140
|
+
header = (' ' * (count_width - 5)) + header
|
141
|
+
|
142
|
+
return header
|
143
|
+
|
144
|
+
def render_entry(self, entry: tuple[str, int], count_width: int) -> str:
|
145
|
+
line, count = entry
|
146
|
+
|
147
|
+
line_fmt = '%' + str(count_width) + 'd : %5s : %5s : %s'
|
148
|
+
|
149
|
+
percent_seen_str = f'{calc_percent(count, self.histo.total_seen):3.2f}'
|
150
|
+
percent_tracked_str = f'{calc_percent(count, self.histo.total_tracked):3.2f}'
|
151
|
+
|
152
|
+
return line_fmt % (count, percent_seen_str, percent_tracked_str, line)
|
153
|
+
|
154
|
+
def render_entries(self, entries: ta.Iterable[tuple[str, int]], count_width: int) -> list[str]:
|
155
|
+
return [self.render_entry(entry, count_width) for entry in entries]
|
156
|
+
|
157
|
+
def render_status(self) -> str:
|
158
|
+
parts = [f'{self.histo.total_seen} total seen']
|
159
|
+
|
160
|
+
total_tracked_percent = calc_percent(self.histo.total_tracked, self.histo.total_seen)
|
161
|
+
parts.extend([
|
162
|
+
f'{len(self.histo)} tracked',
|
163
|
+
f'{self.histo.total_tracked} / {total_tracked_percent:.2f} % total tracked',
|
164
|
+
])
|
165
|
+
|
166
|
+
parts.extend([
|
167
|
+
f'{self.histo.num_evicted} evicted',
|
168
|
+
f'{self.histo.total_evicted} / {100.0 - total_tracked_percent:.2f} % total evicted',
|
169
|
+
])
|
170
|
+
|
171
|
+
duplicate_evictions = self.histo.total_evicted - self.histo.num_evicted
|
172
|
+
parts.append(
|
173
|
+
f'{duplicate_evictions} / {calc_percent(duplicate_evictions, self.histo.total_evicted):.2f} '
|
174
|
+
f'% duplicate evictions',
|
175
|
+
)
|
176
|
+
|
177
|
+
# tracked % + duplicate evicted %
|
178
|
+
parts.append(
|
179
|
+
f'{calc_percent(self.histo.total_tracked - duplicate_evictions, self.histo.total_seen):.2f} % correct',
|
180
|
+
)
|
181
|
+
|
182
|
+
return ', '.join(parts)
|
183
|
+
|
184
|
+
class Rendered(ta.NamedTuple):
|
185
|
+
status_line: str
|
186
|
+
header_line: str
|
187
|
+
entry_lines: list[str]
|
188
|
+
|
189
|
+
def render(self, entries: ta.Sequence[tuple[str, int]] | None = None) -> Rendered:
|
190
|
+
if entries is None:
|
191
|
+
entries = self.entries_to_render
|
192
|
+
|
193
|
+
max_count = entries[0][1] if entries else 0
|
194
|
+
count_width = len(str(max_count))
|
195
|
+
|
196
|
+
status_line = self.render_status()
|
197
|
+
header_line = self.render_header(count_width)
|
198
|
+
entry_lines = self.render_entries(entries, count_width)
|
199
|
+
|
200
|
+
return self.Rendered(
|
201
|
+
status_line,
|
202
|
+
header_line,
|
203
|
+
entry_lines,
|
204
|
+
)
|
205
|
+
|
206
|
+
def render_to_str(self) -> str:
|
207
|
+
status_line, header_line, entry_lines = self.render()
|
208
|
+
|
209
|
+
return '\n'.join([status_line, '', header_line, *entry_lines, ''])
|
210
|
+
|
211
|
+
|
212
|
+
class CursesKeyedHistoRenderer(KeyedHistoRenderer):
|
213
|
+
color_normal = 0
|
214
|
+
color_green = 1
|
215
|
+
color_yellow = 2
|
216
|
+
color_red = 3
|
217
|
+
|
218
|
+
def __init__(
|
219
|
+
self,
|
220
|
+
window,
|
221
|
+
histo: KeyedHisto,
|
222
|
+
*,
|
223
|
+
redraw_interval: float = .1,
|
224
|
+
) -> None:
|
225
|
+
self.window = window
|
226
|
+
self.redraw_interval = redraw_interval
|
227
|
+
|
228
|
+
h, w = self.window.getmaxyx()
|
229
|
+
max_lines = h - 3
|
230
|
+
|
231
|
+
self.max_line_len = w - 30
|
232
|
+
|
233
|
+
super().__init__(
|
234
|
+
histo,
|
235
|
+
max_lines=max_lines,
|
236
|
+
)
|
237
|
+
|
238
|
+
self.timed_redraw = with_timer(redraw_interval)(self.draw)
|
239
|
+
|
240
|
+
self.last_drawn_entries: list[tuple[str, int]] = []
|
241
|
+
|
242
|
+
curses.init_pair(self.color_green, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
243
|
+
curses.init_pair(self.color_yellow, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
244
|
+
curses.init_pair(self.color_red, curses.COLOR_RED, curses.COLOR_BLACK)
|
245
|
+
|
246
|
+
def get_entry_color(
|
247
|
+
self,
|
248
|
+
cur_pos: int,
|
249
|
+
last_pos: int | None,
|
250
|
+
) -> int:
|
251
|
+
if last_pos is None:
|
252
|
+
return self.color_green
|
253
|
+
elif last_pos > cur_pos:
|
254
|
+
return self.color_yellow
|
255
|
+
elif last_pos < cur_pos:
|
256
|
+
return self.color_red
|
257
|
+
else:
|
258
|
+
return self.color_normal
|
259
|
+
|
260
|
+
def get_entry_colors(self, entries: ta.Iterable[tuple[str, int]]) -> list[int]:
|
261
|
+
last_pos_map = {s: i for i, (s, _) in enumerate(self.last_drawn_entries)}
|
262
|
+
|
263
|
+
return [
|
264
|
+
self.get_entry_color(i, last_pos_map.get(key))
|
265
|
+
for i, (key, _) in enumerate(entries)
|
266
|
+
]
|
267
|
+
|
268
|
+
def draw(self) -> None:
|
269
|
+
entries = self.entries_to_render
|
270
|
+
|
271
|
+
status_line, header_line, entry_lines = self.render(entries)
|
272
|
+
entry_colors = self.get_entry_colors(entries)
|
273
|
+
|
274
|
+
self.last_drawn_entries = entries
|
275
|
+
|
276
|
+
self.window.clear()
|
277
|
+
self.window.addstr(0, 0, status_line)
|
278
|
+
self.window.addstr(2, 0, header_line)
|
279
|
+
|
280
|
+
for i, (line, color) in enumerate(zip(entry_lines, entry_colors)):
|
281
|
+
self.window.addstr(i + 3, 0, line[:120], curses.color_pair(color))
|
282
|
+
|
283
|
+
self.window.refresh()
|
284
|
+
|
285
|
+
|
286
|
+
def main() -> None:
|
287
|
+
screen = curses.initscr()
|
288
|
+
curses.start_color()
|
289
|
+
curses.curs_set(0)
|
290
|
+
|
291
|
+
histo = KeyedHisto(max_entries=10_000)
|
292
|
+
renderer = CursesKeyedHistoRenderer(screen, histo)
|
293
|
+
|
294
|
+
try:
|
295
|
+
while True:
|
296
|
+
try:
|
297
|
+
line = sys.stdin.readline()
|
298
|
+
except UnicodeDecodeError:
|
299
|
+
# FIXME
|
300
|
+
continue
|
301
|
+
|
302
|
+
if not line:
|
303
|
+
break
|
304
|
+
|
305
|
+
if len(line) > renderer.max_line_len:
|
306
|
+
line = line[renderer.max_line_len:]
|
307
|
+
|
308
|
+
line = line.strip()
|
309
|
+
|
310
|
+
if not line:
|
311
|
+
continue
|
312
|
+
|
313
|
+
histo.inc(line)
|
314
|
+
renderer.timed_redraw()
|
315
|
+
|
316
|
+
# screen.nodelay(1)
|
317
|
+
# ch = screen.getch()
|
318
|
+
# if ch != curses.ERR:
|
319
|
+
# histo.inc(ch, 100)
|
320
|
+
|
321
|
+
except (OSError, KeyboardInterrupt):
|
322
|
+
pass
|
323
|
+
|
324
|
+
finally:
|
325
|
+
curses.endwin()
|
326
|
+
|
327
|
+
sys.stdout.write(KeyedHistoRenderer(histo).render_to_str())
|
328
|
+
|
329
|
+
|
330
|
+
# @omlish-manifest
|
331
|
+
_CLI_MODULE = CliModule('linehisto', __name__)
|
332
|
+
|
333
|
+
|
334
|
+
if __name__ == '__main__':
|
335
|
+
main()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: omdev
|
3
|
-
Version: 0.0.0.
|
3
|
+
Version: 0.0.0.dev198
|
4
4
|
Summary: omdev
|
5
5
|
Author: wrmsr
|
6
6
|
License: BSD-3-Clause
|
@@ -12,7 +12,7 @@ Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Operating System :: POSIX
|
13
13
|
Requires-Python: >=3.12
|
14
14
|
License-File: LICENSE
|
15
|
-
Requires-Dist: omlish==0.0.0.
|
15
|
+
Requires-Dist: omlish==0.0.0.dev198
|
16
16
|
Provides-Extra: all
|
17
17
|
Requires-Dist: black~=24.10; extra == "all"
|
18
18
|
Requires-Dist: pycparser~=2.22; extra == "all"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
omdev/.manifests.json,sha256=
|
1
|
+
omdev/.manifests.json,sha256=u3arSHz2TkzWc1jB0u6MFRJVTmjXS0SRcBvslqSxTt8,8575
|
2
2
|
omdev/__about__.py,sha256=n5x-SO70OgbDQFzQ1d7sZDVMsnkQc4PxQZPFaIQFa0E,1281
|
3
3
|
omdev/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
4
|
omdev/bracepy.py,sha256=I8EdqtDvxzAi3I8TuMEW-RBfwXfqKbwp06CfOdj3L1o,2743
|
@@ -165,6 +165,7 @@ omdev/tools/doc.py,sha256=wvgGhv6aFaV-Zl-Qivejx37i-lKQ207rZ-4K2fPf-Ss,2547
|
|
165
165
|
omdev/tools/docker.py,sha256=KVFckA8eAdiapFUr8xkfMw9Uv3Qy4oNq0e70Lqt1F7I,7352
|
166
166
|
omdev/tools/git.py,sha256=gqbvt8FFiPsj3vrQ7TxUiq9FpYX2c9UZ54io2p3-x7w,7541
|
167
167
|
omdev/tools/importscan.py,sha256=nhJIhtjDY6eFVlReP7fegvv6L5ZjN-Z2VeyhsBonev4,4639
|
168
|
+
omdev/tools/linehisto.py,sha256=0ZNm34EuiZBE9Q2YC6KNLNNydNT8QPSOwvYzXiU9S2Q,8881
|
168
169
|
omdev/tools/mkrelimp.py,sha256=kyu_BbUakKHEEOxNEvYWk7tH1ixCfVb3NqqT8U-BozE,4066
|
169
170
|
omdev/tools/notebook.py,sha256=q1YMGwM1skHv-dPbtT_cM7UOGFNiMEAxjr6rr6rbobk,3494
|
170
171
|
omdev/tools/pip.py,sha256=eBD41hp-V3thGfhUBM3Erxl4CSG-5LG6Szo1sA76P2k,3459
|
@@ -182,9 +183,9 @@ omdev/tools/json/rendering.py,sha256=tMcjOW5edfozcMSTxxvF7WVTsbYLoe9bCKFh50qyaGw
|
|
182
183
|
omdev/tools/pawk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
183
184
|
omdev/tools/pawk/__main__.py,sha256=VCqeRVnqT1RPEoIrqHFSu4PXVMg4YEgF4qCQm90-eRI,66
|
184
185
|
omdev/tools/pawk/pawk.py,sha256=Eckymn22GfychCQcQi96BFqRo_LmiJ-EPhC8TTUJdB4,11446
|
185
|
-
omdev-0.0.0.
|
186
|
-
omdev-0.0.0.
|
187
|
-
omdev-0.0.0.
|
188
|
-
omdev-0.0.0.
|
189
|
-
omdev-0.0.0.
|
190
|
-
omdev-0.0.0.
|
186
|
+
omdev-0.0.0.dev198.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
187
|
+
omdev-0.0.0.dev198.dist-info/METADATA,sha256=vkKKIgZLZ2Zb_sdrluJx4URchzeBCHiu9AyvFJ-SjNY,1760
|
188
|
+
omdev-0.0.0.dev198.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
189
|
+
omdev-0.0.0.dev198.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
|
190
|
+
omdev-0.0.0.dev198.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
|
191
|
+
omdev-0.0.0.dev198.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|