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 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
neterse/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """``python -m neterse`` — same entry point as the ``neterse`` console script."""
2
+ import sys
3
+
4
+ from .audit import main
5
+
6
+ sys.exit(main())