omdev 0.0.0.dev196__py3-none-any.whl → 0.0.0.dev198__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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",
@@ -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.dev196
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.dev196
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=8HWjsS08zA6hAkjrOyjgda5T0iAOd8Oey9N-SCjhidM,8309
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.dev196.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
186
- omdev-0.0.0.dev196.dist-info/METADATA,sha256=LTWF1yuSoAFnLCyQOMzXDMvJEiTACchM_48sTMCznuA,1760
187
- omdev-0.0.0.dev196.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
188
- omdev-0.0.0.dev196.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
189
- omdev-0.0.0.dev196.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
190
- omdev-0.0.0.dev196.dist-info/RECORD,,
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,,