neterse 0.1.0__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.
- neterse/__init__.py +222 -0
- neterse/__main__.py +6 -0
- neterse/_compressors.py +500 -0
- neterse/audit.py +288 -0
- neterse/engine.py +261 -0
- neterse/parsed.py +165 -0
- neterse/py.typed +0 -0
- neterse/registry.py +205 -0
- neterse/specs.py +419 -0
- neterse-0.1.0.dist-info/METADATA +180 -0
- neterse-0.1.0.dist-info/RECORD +15 -0
- neterse-0.1.0.dist-info/WHEEL +5 -0
- neterse-0.1.0.dist-info/entry_points.txt +2 -0
- neterse-0.1.0.dist-info/licenses/LICENSE +190 -0
- neterse-0.1.0.dist-info/top_level.txt +1 -0
neterse/__init__.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""neterse — the ``| brief`` the vendor never shipped.
|
|
2
|
+
|
|
3
|
+
Turns verbose network CLI output into the minimum-token representation
|
|
4
|
+
for LLM context.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
|
|
8
|
+
* :func:`render` — produce every shrinking :class:`Candidate` for a
|
|
9
|
+
command's output. The library never picks a winner; policy (e.g.
|
|
10
|
+
"smallest wins"), metrics, and caching belong to the consumer.
|
|
11
|
+
* :func:`optimize` — convenience wrapper preserving the historical
|
|
12
|
+
behavior byte-for-byte: smallest candidate's text, or the raw output
|
|
13
|
+
unchanged when nothing shrinks it.
|
|
14
|
+
* :func:`register` — plugin escape hatch: bind your own compressor to a
|
|
15
|
+
command regex, optionally scoped by ``platforms`` and carrying a
|
|
16
|
+
``dropped_fields`` manifest.
|
|
17
|
+
* :func:`iter_compressors` / :func:`iter_entries` — registry views
|
|
18
|
+
(pattern/function pairs, or full :class:`Entry` metadata).
|
|
19
|
+
|
|
20
|
+
``render`` parameters:
|
|
21
|
+
|
|
22
|
+
* ``platform`` — a platform string such as
|
|
23
|
+
``"cisco_nxos"``. Entries that declare a platform scope and don't match
|
|
24
|
+
are skipped; entries with no declared scope are always tried. The
|
|
25
|
+
filter can only skip work, never force a match — omitting it is always
|
|
26
|
+
safe and reproduces the historical behavior exactly.
|
|
27
|
+
* ``parsed`` — rows another parser (Genie,
|
|
28
|
+
ntc-templates, TTP, NAPALM, gNMI) already produced for this output: a
|
|
29
|
+
list of dicts (or a single dict — one row). The parsed tier re-encodes
|
|
30
|
+
them header-once (``parsed:csv`` and ``parsed:toon`` candidates,
|
|
31
|
+
``method="parsed"``), independent of any registry match. Malformed or
|
|
32
|
+
non-row-shaped input yields no parsed candidates — fail-open, like
|
|
33
|
+
everything else.
|
|
34
|
+
* ``profile`` — a named, opt-in, DECLARED-lossy
|
|
35
|
+
projection (e.g. ``"updown"``: interface liveness only). Entries and
|
|
36
|
+
parsed encodings that know the profile emit only the kept fields and
|
|
37
|
+
append an inline omission marker naming what was withheld; everything
|
|
38
|
+
else falls back to its complete default rendering. ``"default"`` (and
|
|
39
|
+
its alias ``"full"``, the spelling the marker suggests for re-query)
|
|
40
|
+
is the complete-but-compact path — byte-identical to omitting the
|
|
41
|
+
argument. An unknown profile name never loses data.
|
|
42
|
+
|
|
43
|
+
Invariants — enforced here, relied on by every consumer:
|
|
44
|
+
|
|
45
|
+
1. Fail-open: a compressor that raises, returns a non-string, returns
|
|
46
|
+
empty, or fails to shrink simply produces no candidate. Raw data is
|
|
47
|
+
never lost and never enlarged.
|
|
48
|
+
2. Zero runtime dependencies: standard library only.
|
|
49
|
+
3. Candidates, not policy: this package never decides which
|
|
50
|
+
representation a consumer must use.
|
|
51
|
+
4. Declared lossiness: a rendering that omits data-bearing fields says so
|
|
52
|
+
on ``Candidate.dropped_fields`` (``None`` = predates declaration), and
|
|
53
|
+
non-default profiles additionally say so INLINE in the rendering
|
|
54
|
+
itself, so the model knows data was withheld and how to recover it.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import logging
|
|
60
|
+
from dataclasses import dataclass
|
|
61
|
+
from typing import Any, Optional, Tuple
|
|
62
|
+
|
|
63
|
+
from . import parsed as _parsed_tier
|
|
64
|
+
from .registry import ( # noqa: F401 (re-exported)
|
|
65
|
+
Entry,
|
|
66
|
+
REGISTRY,
|
|
67
|
+
iter_compressors,
|
|
68
|
+
iter_entries,
|
|
69
|
+
register,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
__version__ = "0.1.0"
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"Candidate",
|
|
76
|
+
"Entry",
|
|
77
|
+
"iter_compressors",
|
|
78
|
+
"iter_entries",
|
|
79
|
+
"optimize",
|
|
80
|
+
"register",
|
|
81
|
+
"render",
|
|
82
|
+
"__version__",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
logger = logging.getLogger("neterse")
|
|
86
|
+
logger.addHandler(logging.NullHandler())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class Candidate:
|
|
91
|
+
"""One possible compact rendering of a command's output.
|
|
92
|
+
|
|
93
|
+
``method`` is the producing tier: ``"toon"`` = raw-text tier,
|
|
94
|
+
``"parsed"`` = parsed tier. ``dropped_fields`` is the lossiness
|
|
95
|
+
manifest: ``None`` means the compressor predates declaration
|
|
96
|
+
("undeclared", not "nothing dropped"); ``()`` means declared
|
|
97
|
+
lossless; names list the data-bearing fields this rendering omits.
|
|
98
|
+
``profile`` names the projection actually applied — ``"default"``
|
|
99
|
+
unless a requested non-default profile took effect, in which case the
|
|
100
|
+
text also carries the inline omission marker.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
text: str
|
|
104
|
+
method: str
|
|
105
|
+
source: str # producing entry/encoder
|
|
106
|
+
dropped_fields: Optional[Tuple[str, ...]] = None
|
|
107
|
+
profile: str = "default"
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def est_chars(self) -> int:
|
|
111
|
+
return len(self.text)
|
|
112
|
+
|
|
113
|
+
def est_tokens(self, chars_per_token: float = 4.0) -> int:
|
|
114
|
+
"""Cheap token estimate (chars/4 by default). Real tokenizers are
|
|
115
|
+
a CI concern, never a runtime dependency."""
|
|
116
|
+
return int(len(self.text) / chars_per_token) if chars_per_token else 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _profile_key(profile: Optional[str]) -> str:
|
|
120
|
+
"""Normalize the requested profile; ``full`` is an alias of the
|
|
121
|
+
complete ``default`` (it's the spelling the omission marker tells the
|
|
122
|
+
model to re-query with)."""
|
|
123
|
+
key = (profile or "default").lower()
|
|
124
|
+
return "default" if key == "full" else key
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def render(
|
|
128
|
+
raw_output: str,
|
|
129
|
+
*,
|
|
130
|
+
command: str,
|
|
131
|
+
platform: Optional[str] = None,
|
|
132
|
+
parsed: Optional[Any] = None,
|
|
133
|
+
profile: str = "default",
|
|
134
|
+
) -> list:
|
|
135
|
+
"""Return every candidate that strictly shrinks *raw_output*.
|
|
136
|
+
|
|
137
|
+
Raw-text-tier candidates appear first, in registry order, then
|
|
138
|
+
parsed-tier candidates; ``min(..., key=lambda c: len(c.text))``
|
|
139
|
+
therefore reproduces the historical "first smallest wins" selection
|
|
140
|
+
exactly on the default path. An empty list means nothing shrank —
|
|
141
|
+
send the raw output.
|
|
142
|
+
"""
|
|
143
|
+
if not raw_output:
|
|
144
|
+
return []
|
|
145
|
+
platform_key = platform.lower() if platform else None
|
|
146
|
+
profile_key = _profile_key(profile)
|
|
147
|
+
candidates: list = []
|
|
148
|
+
for entry in REGISTRY:
|
|
149
|
+
if not entry.pattern.search(command):
|
|
150
|
+
continue
|
|
151
|
+
if platform_key and entry.platforms and not entry.platforms.search(platform_key):
|
|
152
|
+
continue
|
|
153
|
+
fn, drops, applied = entry.fn, entry.dropped_fields, "default"
|
|
154
|
+
if profile_key != "default" and entry.profiles and profile_key in entry.profiles:
|
|
155
|
+
variant = entry.profiles[profile_key]
|
|
156
|
+
fn, drops, applied = variant.fn, variant.dropped_fields, profile_key
|
|
157
|
+
try:
|
|
158
|
+
result = fn(raw_output)
|
|
159
|
+
except Exception:
|
|
160
|
+
logger.debug(
|
|
161
|
+
"neterse compressor %s failed for '%s', skipping",
|
|
162
|
+
entry.name, command, exc_info=True,
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
if isinstance(result, str) and 0 < len(result) < len(raw_output):
|
|
166
|
+
candidates.append(
|
|
167
|
+
Candidate(
|
|
168
|
+
text=result,
|
|
169
|
+
method="toon",
|
|
170
|
+
source=entry.name,
|
|
171
|
+
dropped_fields=drops,
|
|
172
|
+
profile=applied,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
if parsed is not None:
|
|
176
|
+
try:
|
|
177
|
+
encoded = _parsed_tier.encode(parsed, profile_key)
|
|
178
|
+
except Exception:
|
|
179
|
+
logger.debug(
|
|
180
|
+
"neterse parsed tier failed for '%s', skipping",
|
|
181
|
+
command, exc_info=True,
|
|
182
|
+
)
|
|
183
|
+
encoded = []
|
|
184
|
+
for text, source, drops, applied in encoded:
|
|
185
|
+
if isinstance(text, str) and 0 < len(text) < len(raw_output):
|
|
186
|
+
candidates.append(
|
|
187
|
+
Candidate(
|
|
188
|
+
text=text,
|
|
189
|
+
method="parsed",
|
|
190
|
+
source=source,
|
|
191
|
+
dropped_fields=drops,
|
|
192
|
+
profile=applied,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
return candidates
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def optimize(command: str, raw_output: str) -> str:
|
|
199
|
+
"""Compress *raw_output* for the given CLI *command*.
|
|
200
|
+
|
|
201
|
+
Every compressor whose pattern matches the command is tried, and the
|
|
202
|
+
SMALLEST result that actually shrinks the output wins. Trying all matches
|
|
203
|
+
(rather than stopping at the first) means a broad, older pattern can no
|
|
204
|
+
longer shadow a newer, command-specific compressor — a real problem when
|
|
205
|
+
IOS-shaped patterns overlap NX-OS command names. Each compressor is
|
|
206
|
+
fail-open: an exception or a non-shrinking result simply drops it from the
|
|
207
|
+
running, so the original output is never lost.
|
|
208
|
+
"""
|
|
209
|
+
if not raw_output:
|
|
210
|
+
return raw_output
|
|
211
|
+
best: Optional[Candidate] = None
|
|
212
|
+
for candidate in render(raw_output, command=command):
|
|
213
|
+
if best is None or len(candidate.text) < len(best.text):
|
|
214
|
+
best = candidate
|
|
215
|
+
if best is None:
|
|
216
|
+
return raw_output
|
|
217
|
+
saved = 100 * (1 - len(best.text) / len(raw_output))
|
|
218
|
+
logger.debug(
|
|
219
|
+
"TOON compressed '%s' via %s: %d -> %d chars (%.0f%% reduction)",
|
|
220
|
+
command, best.source, len(raw_output), len(best.text), saved,
|
|
221
|
+
)
|
|
222
|
+
return best.text
|