codetool-shell 0.1.1__py3-none-win_amd64.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.
Files changed (72) hide show
  1. codetool_shell/__init__.py +11 -0
  2. codetool_shell/api.py +59 -0
  3. codetool_shell/bin/windows-x86_64/codetool-shell-rust.exe +0 -0
  4. codetool_shell/filters/__init__.py +14 -0
  5. codetool_shell/filters/build_compiler/__init__.py +7 -0
  6. codetool_shell/filters/build_compiler/detector.py +412 -0
  7. codetool_shell/filters/build_compiler/reducer.py +166 -0
  8. codetool_shell/filters/build_compiler/summary.py +617 -0
  9. codetool_shell/filters/ci_job_log/__init__.py +7 -0
  10. codetool_shell/filters/ci_job_log/detector.py +64 -0
  11. codetool_shell/filters/ci_job_log/reducer.py +99 -0
  12. codetool_shell/filters/ci_job_log/summary.py +243 -0
  13. codetool_shell/filters/diff/__init__.py +7 -0
  14. codetool_shell/filters/diff/detector.py +136 -0
  15. codetool_shell/filters/diff/reducer.py +308 -0
  16. codetool_shell/filters/generic_log/__init__.py +7 -0
  17. codetool_shell/filters/generic_log/detector.py +175 -0
  18. codetool_shell/filters/generic_log/reducer.py +99 -0
  19. codetool_shell/filters/generic_log/summary.py +161 -0
  20. codetool_shell/filters/git.py +514 -0
  21. codetool_shell/filters/html_cleanup/__init__.py +7 -0
  22. codetool_shell/filters/html_cleanup/detector.py +136 -0
  23. codetool_shell/filters/html_cleanup/reducer.py +27 -0
  24. codetool_shell/filters/html_cleanup/summary.py +422 -0
  25. codetool_shell/filters/json_payload/__init__.py +7 -0
  26. codetool_shell/filters/json_payload/detector.py +62 -0
  27. codetool_shell/filters/json_payload/reducer.py +81 -0
  28. codetool_shell/filters/json_payload/summary.py +233 -0
  29. codetool_shell/filters/listing/__init__.py +7 -0
  30. codetool_shell/filters/listing/detector.py +294 -0
  31. codetool_shell/filters/listing/reducer.py +30 -0
  32. codetool_shell/filters/log_template/__init__.py +7 -0
  33. codetool_shell/filters/log_template/constants.py +76 -0
  34. codetool_shell/filters/log_template/detector.py +331 -0
  35. codetool_shell/filters/log_template/reducer.py +78 -0
  36. codetool_shell/filters/log_template/template.py +280 -0
  37. codetool_shell/filters/log_template/types.py +21 -0
  38. codetool_shell/filters/opaque_payload/__init__.py +7 -0
  39. codetool_shell/filters/opaque_payload/detector.py +563 -0
  40. codetool_shell/filters/opaque_payload/reducer.py +142 -0
  41. codetool_shell/filters/opaque_payload/summary.py +61 -0
  42. codetool_shell/filters/package_manager/__init__.py +7 -0
  43. codetool_shell/filters/package_manager/detector.py +220 -0
  44. codetool_shell/filters/package_manager/reducer.py +110 -0
  45. codetool_shell/filters/package_manager/summary.py +172 -0
  46. codetool_shell/filters/pipeline.py +65 -0
  47. codetool_shell/filters/rg.py +250 -0
  48. codetool_shell/filters/system_output/__init__.py +7 -0
  49. codetool_shell/filters/system_output/detector.py +600 -0
  50. codetool_shell/filters/system_output/reducer.py +331 -0
  51. codetool_shell/filters/system_output/summary.py +164 -0
  52. codetool_shell/filters/table/__init__.py +7 -0
  53. codetool_shell/filters/table/detector.py +244 -0
  54. codetool_shell/filters/table/reducer.py +57 -0
  55. codetool_shell/filters/table/summary.py +37 -0
  56. codetool_shell/filters/test_runner/__init__.py +7 -0
  57. codetool_shell/filters/test_runner/ansi.py +80 -0
  58. codetool_shell/filters/test_runner/detector.py +409 -0
  59. codetool_shell/filters/test_runner/reducer.py +288 -0
  60. codetool_shell/filters/test_runner/summary.py +449 -0
  61. codetool_shell/filters/text.py +38 -0
  62. codetool_shell/filters/traceback/__init__.py +7 -0
  63. codetool_shell/filters/traceback/detector.py +209 -0
  64. codetool_shell/filters/traceback/reducer.py +141 -0
  65. codetool_shell/filters/traceback/summary.py +122 -0
  66. codetool_shell/filters/tree.py +59 -0
  67. codetool_shell/py.typed +0 -0
  68. codetool_shell/python_backend.py +38 -0
  69. codetool_shell/rust_backend.py +254 -0
  70. codetool_shell-0.1.1.dist-info/METADATA +152 -0
  71. codetool_shell-0.1.1.dist-info/RECORD +72 -0
  72. codetool_shell-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,244 @@
1
+ """Conservative parsers for structured table output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class StructuredTable:
11
+ """Parsed table lines ready for row reduction."""
12
+
13
+ header_lines: tuple[str, ...]
14
+ rows: tuple[str, ...]
15
+ footer_lines: tuple[str, ...] = ()
16
+
17
+
18
+ _MIN_DATA_ROWS = 9
19
+ _PSQL_FOOTER_RE = re.compile(r"^\(\d+ rows?\)$")
20
+ _PATH_LOCATION_RE = re.compile(r"^(.+?):([1-9]\d*)(?::([1-9]\d*))?:(.*)$")
21
+
22
+
23
+ def parse_structured_table(lines: list[str]) -> StructuredTable | None:
24
+ """Return a parsed safe table, or ``None`` if the shape is uncertain."""
25
+
26
+ if _has_blocking_context(lines):
27
+ return None
28
+
29
+ for parser in (
30
+ _parse_mysql_box_table,
31
+ _parse_psql_table,
32
+ _parse_markdown_pipe_table,
33
+ _parse_sqlite_column_table,
34
+ ):
35
+ parsed = parser(lines)
36
+ if parsed is not None and len(parsed.rows) >= _MIN_DATA_ROWS:
37
+ return parsed
38
+ return None
39
+
40
+
41
+ def _parse_psql_table(lines: list[str]) -> StructuredTable | None:
42
+ if len(lines) < _MIN_DATA_ROWS + 2:
43
+ return None
44
+
45
+ footer_lines: tuple[str, ...] = ()
46
+ body = lines
47
+ if lines and _PSQL_FOOTER_RE.match(lines[-1].strip()):
48
+ footer_lines = (lines[-1],)
49
+ body = lines[:-1]
50
+
51
+ if len(body) < _MIN_DATA_ROWS + 2:
52
+ return None
53
+
54
+ header, separator = body[0], body[1]
55
+ if "|" not in header or not _is_psql_separator(separator):
56
+ return None
57
+
58
+ column_count = len(separator.strip().split("+"))
59
+ if column_count < 2 or len(header.split("|")) != column_count:
60
+ return None
61
+
62
+ rows = body[2:]
63
+ if any(row.count("|") != column_count - 1 for row in rows):
64
+ return None
65
+
66
+ return StructuredTable((header, separator), tuple(rows), footer_lines)
67
+
68
+
69
+ def _parse_sqlite_column_table(lines: list[str]) -> StructuredTable | None:
70
+ if len(lines) < _MIN_DATA_ROWS + 2:
71
+ return None
72
+
73
+ header, separator = lines[0], lines[1]
74
+ if "|" in header or "|" in separator or "+" in separator:
75
+ return None
76
+ if not _is_sqlite_separator(separator):
77
+ return None
78
+
79
+ header_columns = _split_columns(header)
80
+ separator_columns = _split_columns(separator)
81
+ if len(header_columns) != len(separator_columns) or len(header_columns) < 2:
82
+ return None
83
+ if any(not _is_safe_header_cell(column) for column in header_columns):
84
+ return None
85
+
86
+ rows = lines[2:]
87
+ valid_rows = 0
88
+ for row in rows:
89
+ if "|" in row or _looks_like_path_location_line(row) or _looks_like_log_line(row):
90
+ return None
91
+ if len(_split_columns(row)) >= 2:
92
+ valid_rows += 1
93
+
94
+ if valid_rows * 4 < len(rows) * 3:
95
+ return None
96
+
97
+ return StructuredTable((header, separator), tuple(rows))
98
+
99
+
100
+ def _parse_mysql_box_table(lines: list[str]) -> StructuredTable | None:
101
+ if len(lines) < _MIN_DATA_ROWS + 4:
102
+ return None
103
+ if not (
104
+ _is_mysql_border(lines[0])
105
+ and _is_mysql_row(lines[1])
106
+ and _is_mysql_border(lines[2])
107
+ ):
108
+ return None
109
+
110
+ column_count = lines[0].count("+") - 1
111
+ if column_count < 2 or lines[1].count("|") != column_count + 1:
112
+ return None
113
+
114
+ row_end: int | None = None
115
+ for index in range(3, len(lines)):
116
+ line = lines[index]
117
+ if _is_mysql_border(line):
118
+ row_end = index
119
+ break
120
+ if not _is_mysql_row(line) or line.count("|") != column_count + 1:
121
+ return None
122
+
123
+ if row_end is None:
124
+ return None
125
+
126
+ rows = lines[3:row_end]
127
+ footer_lines = lines[row_end:]
128
+ if len(footer_lines) > 1 and any(
129
+ not _is_mysql_footer_line(line) for line in footer_lines[1:]
130
+ ):
131
+ return None
132
+
133
+ return StructuredTable(tuple(lines[:3]), tuple(rows), tuple(footer_lines))
134
+
135
+
136
+ def _parse_markdown_pipe_table(lines: list[str]) -> StructuredTable | None:
137
+ if len(lines) < _MIN_DATA_ROWS + 2:
138
+ return None
139
+ if not (_is_markdown_row(lines[0]) and _is_markdown_separator(lines[1])):
140
+ return None
141
+
142
+ rows = lines[2:]
143
+ if any(not _is_markdown_row(row) for row in rows):
144
+ return None
145
+
146
+ return StructuredTable((lines[0], lines[1]), tuple(rows))
147
+
148
+
149
+ def _has_blocking_context(lines: list[str]) -> bool:
150
+ if not lines or any(not line.strip() for line in lines):
151
+ return True
152
+ if any(line.lstrip().startswith(("```", "~~~")) for line in lines):
153
+ return True
154
+ if any(_looks_like_traceback_line(line) for line in lines):
155
+ return True
156
+ return _looks_like_path_location_output(lines)
157
+
158
+
159
+ def _is_psql_separator(line: str) -> bool:
160
+ parts = line.strip().split("+")
161
+ return len(parts) >= 2 and all(
162
+ len(part.strip()) >= 2 and set(part.strip()) == {"-"} for part in parts
163
+ )
164
+
165
+
166
+ def _is_sqlite_separator(line: str) -> bool:
167
+ columns = _split_columns(line)
168
+ return len(columns) >= 2 and all(
169
+ len(column) >= 2 and set(column) == {"-"} for column in columns
170
+ )
171
+
172
+
173
+ def _split_columns(line: str) -> list[str]:
174
+ return [column.strip() for column in re.split(r"\s{2,}", line.strip()) if column.strip()]
175
+
176
+
177
+ def _is_safe_header_cell(cell: str) -> bool:
178
+ return bool(re.fullmatch(r"[A-Za-z0-9_.#()/ -]{1,80}", cell))
179
+
180
+
181
+ def _is_mysql_border(line: str) -> bool:
182
+ return (
183
+ line.startswith("+")
184
+ and line.endswith("+")
185
+ and line.count("+") >= 3
186
+ and all(char in "+-" for char in line)
187
+ )
188
+
189
+
190
+ def _is_mysql_row(line: str) -> bool:
191
+ return line.startswith("|") and line.endswith("|") and line.count("|") >= 3
192
+
193
+
194
+ def _is_mysql_footer_line(line: str) -> bool:
195
+ stripped = line.strip()
196
+ return " row in set" in stripped or " rows in set" in stripped
197
+
198
+
199
+ def _is_markdown_row(line: str) -> bool:
200
+ stripped = line.strip()
201
+ return stripped.startswith("|") and stripped.endswith("|") and stripped.count("|") >= 3
202
+
203
+
204
+ def _is_markdown_separator(line: str) -> bool:
205
+ if not _is_markdown_row(line):
206
+ return False
207
+ cells = [cell.strip().replace(" ", "") for cell in line.strip().strip("|").split("|")]
208
+ if len(cells) < 2:
209
+ return False
210
+ for cell in cells:
211
+ if not re.fullmatch(r":?-{3,}:?", cell):
212
+ return False
213
+ return True
214
+
215
+
216
+ def _looks_like_traceback_line(line: str) -> bool:
217
+ stripped = line.lstrip()
218
+ return stripped.startswith("Traceback ") or stripped.startswith('File "')
219
+
220
+
221
+ def _looks_like_log_line(line: str) -> bool:
222
+ stripped = line.lstrip()
223
+ return stripped.startswith(("INFO ", "DEBUG ", "TRACE ", "WARN ", "ERROR "))
224
+
225
+
226
+ def _looks_like_path_location_output(lines: list[str]) -> bool:
227
+ matches = sum(1 for line in lines if _looks_like_path_location_line(line))
228
+ return matches >= 2 and matches * 2 >= len(lines)
229
+
230
+
231
+ def _looks_like_path_location_line(line: str) -> bool:
232
+ match = _PATH_LOCATION_RE.match(line)
233
+ if match is None:
234
+ return False
235
+ return _looks_like_path(match.group(1))
236
+
237
+
238
+ def _looks_like_path(value: str) -> bool:
239
+ return (
240
+ "/" in value
241
+ or "\\" in value
242
+ or "." in value.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
243
+ or value.startswith(("~", "/"))
244
+ )
@@ -0,0 +1,57 @@
1
+ """Reduce long structured tables while preserving diagnostic rows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..text import join_preserving_final_newline, score, split_preserving_final_newline
6
+ from .detector import StructuredTable, parse_structured_table
7
+ from .summary import is_interesting_row
8
+
9
+ _HEAD_ROWS = 3
10
+ _TAIL_ROWS = 3
11
+
12
+
13
+ def compress_table_output(text: str) -> str:
14
+ """Compress clear long table output, otherwise return ``text`` unchanged."""
15
+
16
+ lines, final_newline = split_preserving_final_newline(text)
17
+ parsed = parse_structured_table(lines)
18
+ if parsed is None:
19
+ return text
20
+
21
+ candidate_lines = _reduce_table(parsed)
22
+ candidate = join_preserving_final_newline(candidate_lines, final_newline)
23
+ if score(candidate) < score(text):
24
+ return candidate
25
+ return text
26
+
27
+
28
+ def _reduce_table(table: StructuredTable) -> list[str]:
29
+ selected_indices = _select_row_indices(table.rows)
30
+ selected = [*table.header_lines]
31
+ cursor = 0
32
+
33
+ for index in selected_indices:
34
+ omitted = index - cursor
35
+ if omitted > 0:
36
+ selected.append(_omitted_line(omitted))
37
+ selected.append(table.rows[index])
38
+ cursor = index + 1
39
+
40
+ if cursor < len(table.rows):
41
+ selected.append(_omitted_line(len(table.rows) - cursor))
42
+
43
+ selected.extend(table.footer_lines)
44
+ return selected
45
+
46
+
47
+ def _select_row_indices(rows: tuple[str, ...]) -> list[int]:
48
+ total = len(rows)
49
+ selected = set(range(min(_HEAD_ROWS, total)))
50
+ selected.update(range(max(_HEAD_ROWS, total - _TAIL_ROWS), total))
51
+ selected.update(index for index, row in enumerate(rows) if is_interesting_row(row))
52
+ return sorted(selected)
53
+
54
+
55
+ def _omitted_line(count: int) -> str:
56
+ plural = "" if count == 1 else "s"
57
+ return f"… {count} table row{plural} omitted"
@@ -0,0 +1,37 @@
1
+ """Row importance helpers for structured table compression."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ _ALERT_RE = re.compile(
8
+ r"\b(?:error|errors|warn|warning|warnings|fail|failed|failure|fatal|critical|exception|traceback)\b",
9
+ re.IGNORECASE,
10
+ )
11
+ _URL_RE = re.compile(r"\b[A-Za-z][A-Za-z0-9+.-]*://\S+")
12
+ _PATH_RE = re.compile(
13
+ r"(^|[\s|])(?:\.{0,2}/|/|~/|[A-Za-z]:[\\/]|[A-Za-z0-9_.@+-]+/[A-Za-z0-9_.@+/\-]+)"
14
+ )
15
+ _UUID_RE = re.compile(
16
+ r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b"
17
+ )
18
+ _LONG_HEX_RE = re.compile(r"\b[0-9a-fA-F]{12,}\b")
19
+ _ISSUE_ID_RE = re.compile(r"\b[A-Z]{2,}-\d{2,}\b")
20
+ _NAMED_ID_RE = re.compile(r"\b(?:id|uuid|hash|sha)[=: ]+[A-Za-z0-9_-]{8,}\b", re.IGNORECASE)
21
+
22
+
23
+ def is_interesting_row(row: str) -> bool:
24
+ """Return whether a row should be preserved even from the middle."""
25
+
26
+ return any(
27
+ pattern.search(row)
28
+ for pattern in (
29
+ _ALERT_RE,
30
+ _URL_RE,
31
+ _PATH_RE,
32
+ _UUID_RE,
33
+ _LONG_HEX_RE,
34
+ _ISSUE_ID_RE,
35
+ _NAMED_ID_RE,
36
+ )
37
+ )
@@ -0,0 +1,7 @@
1
+ """Test-runner shellion filter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .reducer import compress_test_runner_output
6
+
7
+ __all__ = ["compress_test_runner_output"]
@@ -0,0 +1,80 @@
1
+ """ANSI/control decoration stripping for test-runner output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ ESC = "\x1b"
6
+
7
+
8
+ def strip_ansi_control_sequences(text: str) -> str:
9
+ """Remove known terminal decorations while preserving unknown escapes."""
10
+
11
+ output: list[str] = []
12
+ index = 0
13
+ length = len(text)
14
+
15
+ while index < length:
16
+ char = text[index]
17
+
18
+ if char == ESC:
19
+ sequence_end = _known_escape_sequence_end(text, index)
20
+ if sequence_end is not None:
21
+ index = sequence_end
22
+ continue
23
+ output.append(char)
24
+ index += 1
25
+ continue
26
+
27
+ if _is_removable_control_char(char):
28
+ index += 1
29
+ continue
30
+
31
+ output.append(char)
32
+ index += 1
33
+
34
+ return "".join(output)
35
+
36
+
37
+ def _known_escape_sequence_end(text: str, index: int) -> int | None:
38
+ csi_end = _known_csi_sequence_end(text, index)
39
+ if csi_end is not None:
40
+ return csi_end
41
+
42
+ if index + 2 < len(text) and text[index + 1] in {"(", ")", "*", "+"}:
43
+ final = ord(text[index + 2])
44
+ if 0x20 <= final <= 0x7E:
45
+ return index + 3
46
+
47
+ return None
48
+
49
+
50
+ def _known_csi_sequence_end(text: str, index: int) -> int | None:
51
+ start = index + 2
52
+ if start > len(text) or text[index + 1 : start] != "[":
53
+ return None
54
+
55
+ cursor = start
56
+ while cursor < len(text):
57
+ char = text[cursor]
58
+ codepoint = ord(char)
59
+ if 0x40 <= codepoint <= 0x7E:
60
+ params = text[start:cursor]
61
+ if _should_strip_csi(params, char):
62
+ return cursor + 1
63
+ return None
64
+ if not 0x20 <= codepoint <= 0x3F:
65
+ return None
66
+ cursor += 1
67
+
68
+ return None
69
+
70
+
71
+ def _should_strip_csi(params: str, final: str) -> bool:
72
+ if final in {"m", "K", "J", "A", "B", "C", "D", "E", "F", "G"}:
73
+ return True
74
+ return final in {"h", "l"} and params.startswith("?25")
75
+
76
+
77
+ def _is_removable_control_char(char: str) -> bool:
78
+ return char not in {"\n", "\t"} and (
79
+ "\x00" <= char <= "\x08" or "\x0b" <= char <= "\x1f" or char == "\x7f"
80
+ )