rfclib 0.2.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.
- rfclib/__init__.py +3 -0
- rfclib/__main__.py +6 -0
- rfclib/cite.py +204 -0
- rfclib/cli.py +848 -0
- rfclib/config.py +119 -0
- rfclib/errata.py +203 -0
- rfclib/fsutil.py +82 -0
- rfclib/graph.py +213 -0
- rfclib/index/__init__.py +1 -0
- rfclib/index/chunker.py +268 -0
- rfclib/index/embed.py +166 -0
- rfclib/index/hybrid.py +305 -0
- rfclib/index/lexical.py +223 -0
- rfclib/index/rfcxml.py +673 -0
- rfclib/index/search.py +496 -0
- rfclib/index/txtseg.py +516 -0
- rfclib/library.py +285 -0
- rfclib/mcp/__init__.py +1 -0
- rfclib/mcp/server.py +327 -0
- rfclib/packs/__init__.py +1 -0
- rfclib/packs/builder.py +228 -0
- rfclib/packs/collections/all-current-txt.json +10 -0
- rfclib/packs/collections/essentials.json +19 -0
- rfclib/packs/collections/fyi-current.json +10 -0
- rfclib/packs/collections/standards-current.json +10 -0
- rfclib/packs/collections/year-2025.json +10 -0
- rfclib/packs/collections/year-2026.json +10 -0
- rfclib/packs/collections.py +163 -0
- rfclib/packs/docsetexport.py +163 -0
- rfclib/packs/epub.py +159 -0
- rfclib/packs/export.py +306 -0
- rfclib/packs/htmlexport.py +82 -0
- rfclib/packs/serve.py +143 -0
- rfclib/packs/zimexport.py +171 -0
- rfclib/query.py +466 -0
- rfclib/subjects.py +448 -0
- rfclib/sync/__init__.py +1 -0
- rfclib/sync/downloader.py +129 -0
- rfclib/sync/manifest.py +163 -0
- rfclib/sync/scanner.py +49 -0
- rfclib/sync/state.py +445 -0
- rfclib/views.py +1050 -0
- rfclib-0.2.0.dist-info/METADATA +511 -0
- rfclib-0.2.0.dist-info/RECORD +48 -0
- rfclib-0.2.0.dist-info/WHEEL +4 -0
- rfclib-0.2.0.dist-info/entry_points.txt +2 -0
- rfclib-0.2.0.dist-info/licenses/LICENSE +21 -0
- rfclib-0.2.0.dist-info/licenses/NOTICE +24 -0
rfclib/__init__.py
ADDED
rfclib/__main__.py
ADDED
rfclib/cite.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Citation primitives: reference the spec, do not restate it.
|
|
2
|
+
|
|
3
|
+
Why this module exists: prose written *about* an RFC inside a library is a
|
|
4
|
+
second, uncheckable copy of the spec. It drifts when the RFC is updated, it
|
|
5
|
+
drifts when someone misremembers it, and nothing ever notices. A citation —
|
|
6
|
+
document, section, canonical URL, hash of the exact span — stays checkable
|
|
7
|
+
forever, and check() re-verifies it in a lookup.
|
|
8
|
+
|
|
9
|
+
So the policy this pushes toward:
|
|
10
|
+
* cite the section for a rule (`RFC 2658 section 3.2`), never a summary of it;
|
|
11
|
+
* quote verbatim (quote()) only when the exact wording is load-bearing, and
|
|
12
|
+
carry the span hash so an upstream change becomes detectable;
|
|
13
|
+
* do not paraphrase normative text (MUST/SHOULD sentences, ABNF, wire
|
|
14
|
+
formats) into comments, docstrings or identifiers. That is the copy that
|
|
15
|
+
rots silently, so lint() flags near-verbatim prose and uncited normative
|
|
16
|
+
claims instead of pretending they are documentation.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
import sqlite3
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from . import query as q
|
|
27
|
+
from .index import search as _s
|
|
28
|
+
|
|
29
|
+
NORM_KEYWORDS = ("MUST NOT", "MUST", "SHALL NOT", "SHALL", "SHOULD NOT",
|
|
30
|
+
"SHOULD NOT", "SHOULD", "MAY", "REQUIRED")
|
|
31
|
+
_WORDS = re.compile(r"[A-Za-z0-9][A-Za-z0-9.'\-_]*")
|
|
32
|
+
_MONTHS = ["jan", "feb", "mar", "apr", "may", "jun",
|
|
33
|
+
"jul", "aug", "sep", "oct", "nov", "dec"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _now() -> str:
|
|
37
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def label(doc_id: str) -> str:
|
|
41
|
+
"""'RFC2167' -> 'RFC 2167' (how a human writes it); 'BCP14' -> 'BCP 14'."""
|
|
42
|
+
m = re.fullmatch(r"(RFC|BCP|STD|FYI)(\d+)", doc_id)
|
|
43
|
+
return f"{m.group(1)} {int(m.group(2))}" if m else doc_id
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def errata_note(conn: sqlite3.Connection, doc_id: str, sec: str = "") -> str:
|
|
47
|
+
"""Errata status for a doc or one section. Distinguishes 'none' from
|
|
48
|
+
'nobody looked' — asserting zero verified errata when the service was
|
|
49
|
+
never queried would be a false assurance."""
|
|
50
|
+
rows = conn.execute(
|
|
51
|
+
"SELECT status, section FROM errata WHERE doc_id=?",
|
|
52
|
+
(doc_id,)).fetchall()
|
|
53
|
+
if not rows:
|
|
54
|
+
from .errata import bulk_synced_at
|
|
55
|
+
at = bulk_synced_at(conn)
|
|
56
|
+
return (f"errata: none (as of {at[:10]})" if at
|
|
57
|
+
else f"errata: not cached (errata(ref='{doc_id}'))")
|
|
58
|
+
verified = [r for r in rows if r["status"] == "Verified"]
|
|
59
|
+
if not verified:
|
|
60
|
+
return f"errata: 0 verified ({len(rows)} on file)"
|
|
61
|
+
if sec:
|
|
62
|
+
want = _s.norm_sec(sec)
|
|
63
|
+
hit = [r for r in verified
|
|
64
|
+
if want and want in (r["section"] or "").replace("\n", " ")]
|
|
65
|
+
if hit:
|
|
66
|
+
# the exact call, so an agent that has not loaded the errata
|
|
67
|
+
# tool's schema copies the right argument name
|
|
68
|
+
return (f"errata: {len(hit)} VERIFIED on this section — read "
|
|
69
|
+
f"them before implementing: errata(ref='{doc_id}§{want}')")
|
|
70
|
+
return (f"errata: {len(verified)} verified elsewhere, "
|
|
71
|
+
f"none on this section")
|
|
72
|
+
return f"errata: {len(verified)} verified"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _span(conn: sqlite3.Connection, doc_id: str, section: str | None,
|
|
76
|
+
lines: str | None) -> dict:
|
|
77
|
+
"""Line span + anchor for a section reference, or an explicit 'a-b' line
|
|
78
|
+
range when the caller has no section number to offer."""
|
|
79
|
+
if section:
|
|
80
|
+
if not conn.execute("SELECT 1 FROM chunks WHERE doc_id=? LIMIT 1",
|
|
81
|
+
(doc_id,)).fetchone():
|
|
82
|
+
return {"ok": False, "reason": f"{doc_id} is not section-indexed "
|
|
83
|
+
"(run `rfclib index build`)"}
|
|
84
|
+
span = q._resolve_section_arg(conn, doc_id, section)
|
|
85
|
+
if not span.get("ok"):
|
|
86
|
+
return {"ok": False, "reason": span.get("reason"),
|
|
87
|
+
"suggestions": span.get("suggestions", []),
|
|
88
|
+
"outline": span.get("outline", [])}
|
|
89
|
+
return {"ok": True, "sec_num": span["sec_num"],
|
|
90
|
+
"sec_title": span.get("sec_title", ""),
|
|
91
|
+
"start_line": span["start_line"], "end_line": span["end_line"],
|
|
92
|
+
"frag": span.get("url_frag", "")}
|
|
93
|
+
if lines:
|
|
94
|
+
m = re.fullmatch(r"(\d+)\s*[-:]\s*(\d+)", lines.strip())
|
|
95
|
+
if not m:
|
|
96
|
+
return {"ok": False,
|
|
97
|
+
"reason": f"bad line range {lines!r} (want '1240-1288')"}
|
|
98
|
+
return {"ok": True, "sec_num": "", "sec_title": "",
|
|
99
|
+
"start_line": int(m.group(1)), "end_line": int(m.group(2)),
|
|
100
|
+
"frag": ""}
|
|
101
|
+
return {"ok": True, "sec_num": "", "sec_title": "", "start_line": 1,
|
|
102
|
+
"end_line": 0, "frag": ""}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def span_text(conn: sqlite3.Connection, doc_id: str, *, mirror_root: Path,
|
|
106
|
+
section: str | None = None, lines: str | None = None,
|
|
107
|
+
mode: str = "clean") -> tuple[str, dict]:
|
|
108
|
+
"""The exact text of a span plus its resolved anchor metadata.
|
|
109
|
+
|
|
110
|
+
mode='clean' (default) is the canonical form span_hash is defined over;
|
|
111
|
+
mode='raw' is byte-exact against the RFC Editor file.
|
|
112
|
+
"""
|
|
113
|
+
from .index.chunker import strip_page_furniture
|
|
114
|
+
text, served, _p = q._served_text(conn, doc_id, "txt", mirror_root)
|
|
115
|
+
sp = _span(conn, doc_id, section, lines)
|
|
116
|
+
if text is None:
|
|
117
|
+
return "", dict(sp, ok=False, reason="no local text copy")
|
|
118
|
+
if not sp.get("ok"):
|
|
119
|
+
return "", sp
|
|
120
|
+
ls = text.splitlines()
|
|
121
|
+
end = sp["end_line"] or len(ls)
|
|
122
|
+
body = "\n".join(ls[sp["start_line"] - 1:end])
|
|
123
|
+
if mode == "clean":
|
|
124
|
+
body = strip_page_furniture(body)
|
|
125
|
+
return body, dict(sp, ok=True, served_via=served, total_lines=len(ls))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _authors_meta(conn, meta) -> str:
|
|
129
|
+
return " and ".join(meta.get("authors") or [])
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cite(conn: sqlite3.Connection, doc_id: str, section: str | None = None,
|
|
133
|
+
lines: str | None = None, style: str = "verbose", *,
|
|
134
|
+
mirror_root: Path | None = None) -> str:
|
|
135
|
+
"""A canonical, checkable reference — the cheap substitute for pasting
|
|
136
|
+
spec text into code.
|
|
137
|
+
|
|
138
|
+
style: verbose (default) | short | markdown | comment | bibtex.
|
|
139
|
+
'verbose' and 'comment' carry the span hash when mirror_root is given, so
|
|
140
|
+
the citation can later be re-verified with verify().
|
|
141
|
+
"""
|
|
142
|
+
row = q.get_doc_row(conn, doc_id)
|
|
143
|
+
meta = q._row_to_meta(conn, row)
|
|
144
|
+
sp = _span(conn, doc_id, section, lines)
|
|
145
|
+
if not sp.get("ok") and style in ("verbose", "short", "comment",
|
|
146
|
+
"markdown"):
|
|
147
|
+
cands = ", ".join(sp.get("suggestions", [])) or "see get_toc"
|
|
148
|
+
return (f"CANNOT CITE {label(doc_id)} \u00a7{section}: {sp['reason']}\n"
|
|
149
|
+
f"candidates: {cands}")
|
|
150
|
+
sec = sp.get("sec_num", "")
|
|
151
|
+
title = meta.get("title") or ""
|
|
152
|
+
date = (meta.get("pub_date") or "")[:7]
|
|
153
|
+
url = q.anchor_url(meta, sp.get("frag") or "", q.resolve(conn, doc_id))
|
|
154
|
+
secpart = f" \u00a7{sec}" + (f' "{sp["sec_title"]}"'
|
|
155
|
+
if sp.get("sec_title") else "")
|
|
156
|
+
linepart = (f" lines {sp['start_line']}-{sp['end_line']}" if lines else "")
|
|
157
|
+
status = meta.get("current_status") or meta.get("status") or ""
|
|
158
|
+
successor = ", ".join(meta["relations"].get("obsoleted_by", []))
|
|
159
|
+
state = "current" if meta["is_current"] else \
|
|
160
|
+
f"OBSOLETE by {successor or 'unknown'}"
|
|
161
|
+
|
|
162
|
+
if style == "short":
|
|
163
|
+
return f"{label(doc_id)}{secpart}{linepart}"
|
|
164
|
+
if style == "markdown":
|
|
165
|
+
return f"[{label(doc_id)}{secpart}{linepart}]({url})"
|
|
166
|
+
if style == "bibtex":
|
|
167
|
+
num = re.sub(r"\D", "", doc_id)
|
|
168
|
+
series = ("Request for Comments" if doc_id.startswith("RFC")
|
|
169
|
+
else {"BCP": "Best Current Practice",
|
|
170
|
+
"STD": "Internet Standards",
|
|
171
|
+
"FYI": "FYI"}.get(doc_id[:3], "Request for Comments"))
|
|
172
|
+
month = (_MONTHS[int(date[5:7]) - 1]
|
|
173
|
+
if len(date) >= 7 and date[5:7].isdigit() else "")
|
|
174
|
+
rows = [f"@misc{{rfc{num},",
|
|
175
|
+
f" series = {{{series}}},",
|
|
176
|
+
f" number = {num},",
|
|
177
|
+
f" howpublished = {{{label(doc_id)}}},",
|
|
178
|
+
" publisher = {RFC Editor},",
|
|
179
|
+
f" doi = {{{meta.get('doi') or ''}}},",
|
|
180
|
+
f" url = {{{meta.get('canonical_url') or ''}}},",
|
|
181
|
+
f" author = {{{_authors_meta(conn, meta) or ''}}},",
|
|
182
|
+
f" title = {{{{{title}}}}},"]
|
|
183
|
+
if year := date[:4]:
|
|
184
|
+
rows.append(f" year = {year},")
|
|
185
|
+
if month:
|
|
186
|
+
rows.append(f" month = {month},")
|
|
187
|
+
rows.append("}")
|
|
188
|
+
return "\n".join(rows)
|
|
189
|
+
|
|
190
|
+
sha = ""
|
|
191
|
+
if mirror_root is not None and (sec or lines):
|
|
192
|
+
body, _ = span_text(conn, doc_id, mirror_root=mirror_root,
|
|
193
|
+
section=section, lines=lines)
|
|
194
|
+
sha = f" | span sha256:{q.span_hash(body)}"
|
|
195
|
+
tail = (f"{label(doc_id)}{secpart}{linepart} — {title} ({date}, "
|
|
196
|
+
f"{status}, {state})\n{url} | "
|
|
197
|
+
f"{errata_note(conn, doc_id, sec)}{sha}")
|
|
198
|
+
if style == "comment":
|
|
199
|
+
return "\n".join(("# " + ln) if ln else "#" for ln in tail.split("\n"))
|
|
200
|
+
if not meta["is_current"]:
|
|
201
|
+
tail += (f"\nWARNING: {label(doc_id)} is obsolete"
|
|
202
|
+
+ (f"; cite {successor} instead" if successor else ""))
|
|
203
|
+
return tail
|
|
204
|
+
|