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 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,,