primarysource 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.
- primarysource/__init__.py +5 -0
- primarysource/bbf/__init__.py +56 -0
- primarysource/bbf/_xml.py +63 -0
- primarysource/bbf/catalogue.json +777 -0
- primarysource/bbf/catalogue.py +137 -0
- primarysource/bbf/cli.py +79 -0
- primarysource/bbf/datamodel.py +281 -0
- primarysource/bbf/importer.py +151 -0
- primarysource/bbf/library.py +380 -0
- primarysource/bbf/server.py +72 -0
- primarysource/budget.py +63 -0
- primarysource/c2sp/__init__.py +53 -0
- primarysource/c2sp/_fetch.py +5 -0
- primarysource/c2sp/catalogue.json +231 -0
- primarysource/c2sp/catalogue.py +95 -0
- primarysource/c2sp/cli.py +62 -0
- primarysource/c2sp/importer.py +90 -0
- primarysource/c2sp/library.py +309 -0
- primarysource/c2sp/markdown.py +31 -0
- primarysource/c2sp/server.py +68 -0
- primarysource/cavp/__init__.py +42 -0
- primarysource/cavp/cli.py +84 -0
- primarysource/cavp/data/vectors.toml +47 -0
- primarysource/cavp/importer.py +156 -0
- primarysource/cavp/model.py +279 -0
- primarysource/cavp/parse.py +269 -0
- primarysource/cavp/paths.py +17 -0
- primarysource/cavp/server.py +67 -0
- primarysource/cavp/tools.py +169 -0
- primarysource/cli.py +107 -0
- primarysource/cursor.py +82 -0
- primarysource/cve/__init__.py +5 -0
- primarysource/cve/__main__.py +6 -0
- primarysource/cve/cli.py +144 -0
- primarysource/cve/collection.py +20 -0
- primarysource/cve/config.py +58 -0
- primarysource/cve/errors.py +16 -0
- primarysource/cve/kernel.py +232 -0
- primarysource/cve/mcp.py +139 -0
- primarysource/cve/parse.py +318 -0
- primarysource/cve/py.typed +0 -0
- primarysource/cve/service.py +760 -0
- primarysource/cve/store.py +214 -0
- primarysource/cve/sync.py +438 -0
- primarysource/cve/views.py +304 -0
- primarysource/debpolicy/__init__.py +16 -0
- primarysource/debpolicy/catalogue.py +263 -0
- primarysource/debpolicy/cli.py +12 -0
- primarysource/debpolicy/server.py +11 -0
- primarysource/draft/__init__.py +11 -0
- primarysource/draft/catalogue.py +117 -0
- primarysource/draft/cli.py +149 -0
- primarysource/draft/collection.py +21 -0
- primarysource/draft/datatracker.py +158 -0
- primarysource/draft/importer.py +163 -0
- primarysource/draft/licence.py +89 -0
- primarysource/draft/names.py +64 -0
- primarysource/draft/net.py +133 -0
- primarysource/draft/segment.py +192 -0
- primarysource/draft/server.py +91 -0
- primarysource/draft/status.py +146 -0
- primarysource/draft/store.py +199 -0
- primarysource/draft/tracked.json +104 -0
- primarysource/draft/views.py +430 -0
- primarysource/draft/xmlmeta.py +74 -0
- primarysource/ecma/__init__.py +52 -0
- primarysource/ecma/_pdf.py +143 -0
- primarysource/ecma/catalogue.json +22 -0
- primarysource/ecma/catalogue.py +100 -0
- primarysource/ecma/cli.py +67 -0
- primarysource/ecma/identify.py +25 -0
- primarysource/ecma/importer.py +108 -0
- primarysource/ecma/library.py +240 -0
- primarysource/ecma/server.py +67 -0
- primarysource/errors.py +29 -0
- primarysource/fetch.py +360 -0
- primarysource/fsutil.py +150 -0
- primarysource/gitdocs/__init__.py +15 -0
- primarysource/gitdocs/catalogue.py +116 -0
- primarysource/gitdocs/cli.py +12 -0
- primarysource/gitdocs/server.py +8 -0
- primarysource/home.py +65 -0
- primarysource/iana/__init__.py +32 -0
- primarysource/iana/catalogue.json +244 -0
- primarysource/iana/catalogue.py +69 -0
- primarysource/iana/cli.py +69 -0
- primarysource/iana/importer.py +157 -0
- primarysource/iana/model.py +325 -0
- primarysource/iana/reader.py +315 -0
- primarysource/iana/server.py +67 -0
- primarysource/iana/store.py +83 -0
- primarysource/iana/xmlsafe.py +47 -0
- primarysource/identify.py +186 -0
- primarysource/ieee/__init__.py +58 -0
- primarysource/ieee/_html.py +49 -0
- primarysource/ieee/catalogue.json +74 -0
- primarysource/ieee/catalogue.py +123 -0
- primarysource/ieee/cli.py +79 -0
- primarysource/ieee/identify.py +24 -0
- primarysource/ieee/ieee802.py +47 -0
- primarysource/ieee/importer.py +305 -0
- primarysource/ieee/posix.py +233 -0
- primarysource/ieee/server.py +79 -0
- primarysource/index.py +446 -0
- primarysource/iso/__init__.py +51 -0
- primarysource/iso/cli.py +98 -0
- primarysource/iso/designation.py +95 -0
- primarysource/iso/identify.py +100 -0
- primarysource/iso/importer.py +224 -0
- primarysource/iso/library.py +249 -0
- primarysource/iso/server.py +69 -0
- primarysource/itu/__init__.py +41 -0
- primarysource/itu/__main__.py +8 -0
- primarysource/itu/asn1mod.py +478 -0
- primarysource/itu/catalog.py +472 -0
- primarysource/itu/cli.py +445 -0
- primarysource/itu/collection.py +20 -0
- primarysource/itu/corpus.py +118 -0
- primarysource/itu/errata.py +16 -0
- primarysource/itu/formal.py +565 -0
- primarysource/itu/identify.py +26 -0
- primarysource/itu/index.py +490 -0
- primarysource/itu/mcp.py +474 -0
- primarysource/itu/py.typed +0 -0
- primarysource/itu/sources.py +600 -0
- primarysource/itu/text.py +609 -0
- primarysource/kconfig/__init__.py +16 -0
- primarysource/kconfig/catalogue.py +98 -0
- primarysource/kconfig/cli.py +12 -0
- primarysource/kconfig/server.py +8 -0
- primarysource/model.py +63 -0
- primarysource/nist/__init__.py +5 -0
- primarysource/nist/collection.py +46 -0
- primarysource/nist/data/acquired-nist.json +1657 -0
- primarysource/nist/data/nist-catalog.json +12090 -0
- primarysource/nist/data/sources.toml +1996 -0
- primarysource/nist/identify.py +41 -0
- primarysource/nist/ingest/__init__.py +1 -0
- primarysource/nist/ingest/acquire_nist.py +128 -0
- primarysource/nist/ingest/controls.py +82 -0
- primarysource/nist/ingest/discover_nist.py +218 -0
- primarysource/nist/ingest/extract.py +48 -0
- primarysource/nist/ingest/fetch.py +211 -0
- primarysource/nist/ingest/index.py +14 -0
- primarysource/nist/ingest/oscal.py +480 -0
- primarysource/nist/ingest/prose_layouts.py +236 -0
- primarysource/nist/ingest/sources.py +94 -0
- primarysource/nist/ingest/status.py +255 -0
- primarysource/nist/models.py +7 -0
- primarysource/nist/paths.py +33 -0
- primarysource/nist/server/__init__.py +3 -0
- primarysource/nist/server/corpus.py +38 -0
- primarysource/nist/server/mcp.py +19 -0
- primarysource/nist/server/tools.py +35 -0
- primarysource/nist/spec.py +38 -0
- primarysource/pdfcorpus/__init__.py +16 -0
- primarysource/pdfcorpus/corpus.py +159 -0
- primarysource/pdfcorpus/fts.py +175 -0
- primarysource/pdfcorpus/index.py +70 -0
- primarysource/pdfcorpus/models.py +30 -0
- primarysource/pdfcorpus/server.py +96 -0
- primarysource/pdfcorpus/spec.py +37 -0
- primarysource/pdfcorpus/split.py +391 -0
- primarysource/pdfcorpus/tools.py +244 -0
- primarysource/pep/__init__.py +15 -0
- primarysource/pep/catalogue.py +191 -0
- primarysource/pep/cli.py +12 -0
- primarysource/pep/server.py +8 -0
- primarysource/pkgdocs/__init__.py +19 -0
- primarysource/pkgdocs/fetch.py +356 -0
- primarysource/pkgdocs/index.py +89 -0
- primarysource/pkgdocs/model.py +76 -0
- primarysource/pkgdocs/readers/__init__.py +1 -0
- primarysource/pkgdocs/readers/_common.py +134 -0
- primarysource/pkgdocs/readers/asciidoc.py +160 -0
- primarysource/pkgdocs/readers/latex.py +315 -0
- primarysource/pkgdocs/readers/markdown.py +173 -0
- primarysource/pkgdocs/readers/pod.py +132 -0
- primarysource/pkgdocs/readers/rst.py +239 -0
- primarysource/pkgdocs/readers/scdoc.py +108 -0
- primarysource/pkgdocs/runtime.py +552 -0
- primarysource/pkgdocs/serving.py +158 -0
- primarysource/pkgdocs/store.py +167 -0
- primarysource/pms/__init__.py +16 -0
- primarysource/pms/catalogue.py +88 -0
- primarysource/pms/cli.py +12 -0
- primarysource/pms/server.py +8 -0
- primarysource/py.typed +0 -0
- primarysource/readers/__init__.py +10 -0
- primarysource/readers/bookmarks.py +192 -0
- primarysource/readers/html.py +1312 -0
- primarysource/readers/markdown.py +189 -0
- primarysource/readers/pdf.py +187 -0
- primarysource/readers/xml.py +230 -0
- primarysource/refs.py +87 -0
- primarysource/registry.py +161 -0
- primarysource/render.py +49 -0
- primarysource/rfc/__init__.py +3 -0
- primarysource/rfc/__main__.py +6 -0
- primarysource/rfc/cite.py +204 -0
- primarysource/rfc/cli.py +903 -0
- primarysource/rfc/collection.py +19 -0
- primarysource/rfc/config.py +138 -0
- primarysource/rfc/errata.py +203 -0
- primarysource/rfc/fsutil.py +82 -0
- primarysource/rfc/graph.py +213 -0
- primarysource/rfc/index/__init__.py +1 -0
- primarysource/rfc/index/chunker.py +281 -0
- primarysource/rfc/index/embed.py +166 -0
- primarysource/rfc/index/hybrid.py +305 -0
- primarysource/rfc/index/lexical.py +223 -0
- primarysource/rfc/index/rfcxml.py +673 -0
- primarysource/rfc/index/search.py +508 -0
- primarysource/rfc/index/txtseg.py +585 -0
- primarysource/rfc/library.py +294 -0
- primarysource/rfc/mcp/__init__.py +1 -0
- primarysource/rfc/mcp/server.py +381 -0
- primarysource/rfc/packs/__init__.py +1 -0
- primarysource/rfc/packs/builder.py +228 -0
- primarysource/rfc/packs/collections/all-current-txt.json +10 -0
- primarysource/rfc/packs/collections/essentials.json +19 -0
- primarysource/rfc/packs/collections/fyi-current.json +10 -0
- primarysource/rfc/packs/collections/standards-current.json +10 -0
- primarysource/rfc/packs/collections/year-2025.json +10 -0
- primarysource/rfc/packs/collections/year-2026.json +10 -0
- primarysource/rfc/packs/collections.py +163 -0
- primarysource/rfc/packs/docsetexport.py +163 -0
- primarysource/rfc/packs/epub.py +159 -0
- primarysource/rfc/packs/export.py +306 -0
- primarysource/rfc/packs/htmlexport.py +82 -0
- primarysource/rfc/packs/serve.py +143 -0
- primarysource/rfc/packs/zimexport.py +171 -0
- primarysource/rfc/query.py +466 -0
- primarysource/rfc/subjects.py +457 -0
- primarysource/rfc/sync/__init__.py +1 -0
- primarysource/rfc/sync/downloader.py +132 -0
- primarysource/rfc/sync/manifest.py +162 -0
- primarysource/rfc/sync/scanner.py +49 -0
- primarysource/rfc/sync/state.py +464 -0
- primarysource/rfc/views.py +1067 -0
- primarysource/rpmdocs/__init__.py +15 -0
- primarysource/rpmdocs/catalogue.py +117 -0
- primarysource/rpmdocs/cli.py +12 -0
- primarysource/rpmdocs/server.py +8 -0
- primarysource/scaffold.py +223 -0
- primarysource/serve.py +75 -0
- primarysource/settings.py +152 -0
- primarysource/usb/__init__.py +5 -0
- primarysource/usb/collection.py +37 -0
- primarysource/usb/data/acquired.json +2025 -0
- primarysource/usb/data/sources-usb.toml +852 -0
- primarysource/usb/data/sources.toml +10 -0
- primarysource/usb/data/usb-catalog.json +1663 -0
- primarysource/usb/data/usb-classification.json +1986 -0
- primarysource/usb/data/usb-corpus-build.json +608 -0
- primarysource/usb/data/usb-pdf-choice.json +839 -0
- primarysource/usb/data/usb-split-measure.json +1803 -0
- primarysource/usb/ingest/__init__.py +1 -0
- primarysource/usb/ingest/acquire.py +130 -0
- primarysource/usb/ingest/discover_usb.py +88 -0
- primarysource/usb/ingest/extract.py +48 -0
- primarysource/usb/ingest/fetch.py +197 -0
- primarysource/usb/ingest/index.py +14 -0
- primarysource/usb/ingest/sources.py +85 -0
- primarysource/usb/models.py +7 -0
- primarysource/usb/paths.py +33 -0
- primarysource/usb/server/__init__.py +3 -0
- primarysource/usb/server/corpus.py +38 -0
- primarysource/usb/server/mcp.py +19 -0
- primarysource/usb/server/tools.py +35 -0
- primarysource/usb/spec.py +20 -0
- primarysource/usbids/__init__.py +56 -0
- primarysource/usbids/cli.py +65 -0
- primarysource/usbids/importer.py +272 -0
- primarysource/usbids/parse.py +283 -0
- primarysource/usbids/server.py +55 -0
- primarysource/usbids/store.py +80 -0
- primarysource/usbids/tools.py +174 -0
- primarysource/zim/__init__.py +5 -0
- primarysource/zim/__main__.py +8 -0
- primarysource/zim/_libzim.py +37 -0
- primarysource/zim/catalog.py +594 -0
- primarysource/zim/cli.py +555 -0
- primarysource/zim/collection.py +41 -0
- primarysource/zim/errors.py +100 -0
- primarysource/zim/groups.py +30 -0
- primarysource/zim/library.py +668 -0
- primarysource/zim/manage.py +636 -0
- primarysource/zim/markdown.py +1247 -0
- primarysource/zim/mcp.py +201 -0
- primarysource/zim/purl.py +130 -0
- primarysource/zim/py.typed +0 -0
- primarysource/zim/refs.py +90 -0
- primarysource/zim/service.py +1182 -0
- primarysource/zim/snapshot.py +197 -0
- primarysource/zim/sync.py +269 -0
- primarysource/zim/views.py +230 -0
- primarysource-0.1.0.dist-info/METADATA +118 -0
- primarysource-0.1.0.dist-info/RECORD +303 -0
- primarysource-0.1.0.dist-info/WHEEL +4 -0
- primarysource-0.1.0.dist-info/entry_points.txt +2 -0
- primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
- primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
primarysource/refs.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""One ref grammar, parameterised by what a document id looks like.
|
|
2
|
+
|
|
3
|
+
The three libraries that have a ref string agree on its shape and differ only in the document id:
|
|
4
|
+
|
|
5
|
+
RFC9000§9.6¶2 X.690§11.6¶2 devdocs_en_flask§blueprints/index#why¶3
|
|
6
|
+
|
|
7
|
+
So the grammar is `<doc> § <section> [#anchor] [¶ n]`, or `<doc> : first-last` for a line range. `::` is the
|
|
8
|
+
ASCII spelling of `§`. A library supplies the pattern for its document ids and, if its sections are not dotted
|
|
9
|
+
numbers, the pattern for those.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Protocol
|
|
17
|
+
|
|
18
|
+
from primarysource.errors import LibError
|
|
19
|
+
|
|
20
|
+
DOTTED_SECTION = r"[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class Ref:
|
|
25
|
+
"""A parsed ref. Every part but one may be None; `lines` is (first, last), 1-based and inclusive."""
|
|
26
|
+
|
|
27
|
+
doc: str | None = None
|
|
28
|
+
section: str | None = None
|
|
29
|
+
para: int | None = None
|
|
30
|
+
anchor: str | None = None
|
|
31
|
+
lines: tuple[int, int] | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RefParser(Protocol):
|
|
35
|
+
"""What a collection's refs need: text to `Ref` and back. `RefGrammar` is one; a collection whose refs are not
|
|
36
|
+
`<doc>§<section>` (an id-keyed registry such as `usbids:1d6b:0002`) supplies its own object with these two
|
|
37
|
+
methods, raising `LibError` for text that is not a ref."""
|
|
38
|
+
|
|
39
|
+
def parse(self, text: str) -> Ref: ...
|
|
40
|
+
|
|
41
|
+
def format(self, ref: Ref) -> str: ...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RefGrammar:
|
|
45
|
+
"""Parses and formats refs for one library."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, doc_pattern: str, section_pattern: str = DOTTED_SECTION) -> None:
|
|
48
|
+
self._pattern = re.compile(
|
|
49
|
+
rf"\s*(?P<doc>{doc_pattern})?\s*"
|
|
50
|
+
rf"(?:(?:§|::)\s*(?P<section>{section_pattern}))?"
|
|
51
|
+
r"(?:#(?P<anchor>[^\s¶#]+))?"
|
|
52
|
+
r"(?:\s*¶\s*(?P<para>\d+))?"
|
|
53
|
+
r"(?::(?P<first>\d+)-(?P<last>\d+))?\s*"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def parse(self, text: str) -> Ref:
|
|
57
|
+
"""Parse `text`, or raise LibError. A string that names nothing is refused, never guessed at."""
|
|
58
|
+
match = self._pattern.fullmatch(text)
|
|
59
|
+
if match is None or not any(match.group(name) for name in ("doc", "section", "anchor", "first")):
|
|
60
|
+
raise LibError(f"not a ref: {text!r}", "expected <doc>§<section>[#anchor][¶n] or <doc>:<first>-<last>")
|
|
61
|
+
lines: tuple[int, int] | None = None
|
|
62
|
+
if match.group("first"):
|
|
63
|
+
if match.group("section") or match.group("para") or match.group("anchor"):
|
|
64
|
+
raise LibError(f"not a ref: {text!r}", "a line range cannot be combined with a section")
|
|
65
|
+
lines = (int(match.group("first")), int(match.group("last")))
|
|
66
|
+
if lines[0] < 1 or lines[1] < lines[0]:
|
|
67
|
+
raise LibError(f"not a line range: {text!r}", "lines are 1-based and first <= last")
|
|
68
|
+
para = int(match.group("para")) if match.group("para") else None
|
|
69
|
+
if para is not None and para < 1:
|
|
70
|
+
raise LibError(f"not a ref: {text!r}", "¶ is 1-based")
|
|
71
|
+
if para is not None and not (match.group("section") or match.group("anchor")):
|
|
72
|
+
raise LibError(f"not a ref: {text!r}", "¶ needs a section or an anchor to count within")
|
|
73
|
+
return Ref(match.group("doc"), match.group("section"), para, match.group("anchor"), lines)
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def format(ref: Ref) -> str:
|
|
77
|
+
"""The canonical spelling: no spaces, `§` for the section."""
|
|
78
|
+
out = ref.doc or ""
|
|
79
|
+
if ref.lines is not None:
|
|
80
|
+
return f"{out}:{ref.lines[0]}-{ref.lines[1]}"
|
|
81
|
+
if ref.section is not None:
|
|
82
|
+
out += f"§{ref.section}"
|
|
83
|
+
if ref.anchor is not None:
|
|
84
|
+
out += f"#{ref.anchor}"
|
|
85
|
+
if ref.para is not None:
|
|
86
|
+
out += f"¶{ref.para}"
|
|
87
|
+
return out
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""The collection registry: how the core finds a library without importing it.
|
|
2
|
+
|
|
3
|
+
A collection (`nist`, `itu`, `ieee`, ...) is one corpus with its own database, tool-name prefix, declared unit
|
|
4
|
+
and ref grammar (PLAN.md §5.8). The core never imports a collection by name. It reads two sources of
|
|
5
|
+
`name -> "module:attribute"` targets and imports a target only when that collection is asked for:
|
|
6
|
+
|
|
7
|
+
- `BUILTIN`: the collections that ship in this package as subpackages, each behind its own extra;
|
|
8
|
+
- the `primarysource.collections` entry-point group: a separately installed package that registers itself the
|
|
9
|
+
same way, for a collection with its own licence or release cadence.
|
|
10
|
+
|
|
11
|
+
Either way the target is a module attribute holding a `Collection`. A collection whose extra is not installed
|
|
12
|
+
fails with a `LibError` that names the install command, never with a bare `ImportError`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib
|
|
18
|
+
import re
|
|
19
|
+
from collections.abc import Mapping
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from importlib.metadata import entry_points
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from primarysource.errors import LibError
|
|
25
|
+
from primarysource.model import Unit
|
|
26
|
+
from primarysource.refs import RefParser
|
|
27
|
+
|
|
28
|
+
ENTRY_POINT_GROUP = "primarysource.collections"
|
|
29
|
+
|
|
30
|
+
# name -> "module:attribute" or "module:attribute [extra]", the entry-point spelling. A collection is added here
|
|
31
|
+
# when it moves in (PLAN.md §6). Without "[extra]" the extra is the collection's name; two collections that share
|
|
32
|
+
# one extra (posix and ieee802 in `[ieee]`) both name it.
|
|
33
|
+
BUILTIN: dict[str, str] = {
|
|
34
|
+
"rfc": "primarysource.rfc.collection:COLLECTION [rfc]",
|
|
35
|
+
"nist": "primarysource.nist.collection:COLLECTION",
|
|
36
|
+
"usb": "primarysource.usb.collection:COLLECTION",
|
|
37
|
+
"itu": "primarysource.itu.collection:COLLECTION",
|
|
38
|
+
"posix": "primarysource.ieee:POSIX [ieee]",
|
|
39
|
+
"ieee802": "primarysource.ieee:IEEE802 [ieee]",
|
|
40
|
+
"iana": "primarysource.iana:COLLECTION [iana]",
|
|
41
|
+
"cavp": "primarysource.cavp:COLLECTION",
|
|
42
|
+
"usbids": "primarysource.usbids:COLLECTION",
|
|
43
|
+
"draft": "primarysource.draft.collection:COLLECTION [draft]",
|
|
44
|
+
"c2sp": "primarysource.c2sp:C2SP",
|
|
45
|
+
"bbf": "primarysource.bbf:BBF [bbf]",
|
|
46
|
+
"ecma": "primarysource.ecma:ECMA [ecma]",
|
|
47
|
+
"gitdocs": "primarysource.gitdocs:COLLECTION",
|
|
48
|
+
"debpolicy": "primarysource.debpolicy:COLLECTION",
|
|
49
|
+
"rpmdocs": "primarysource.rpmdocs:COLLECTION",
|
|
50
|
+
"pms": "primarysource.pms:COLLECTION",
|
|
51
|
+
"pep": "primarysource.pep:COLLECTION",
|
|
52
|
+
"kconfig": "primarysource.kconfig:COLLECTION",
|
|
53
|
+
"iso": "primarysource.iso:ISO [iso]",
|
|
54
|
+
"zim": "primarysource.zim.collection:COLLECTION [zim]",
|
|
55
|
+
"cve": "primarysource.cve.collection:COLLECTION",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_NAME = re.compile(r"[a-z][a-z0-9]*")
|
|
59
|
+
_SPEC = re.compile(r"\s*(?P<target>[\w.]+:[\w.]+)\s*(?:\[\s*(?P<extra>[\w-]+)\s*\])?\s*")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _split(spec: str) -> tuple[str, str | None]:
|
|
63
|
+
"""`module:attribute [extra]` -> (`module:attribute`, `extra` or None)."""
|
|
64
|
+
match = _SPEC.fullmatch(spec)
|
|
65
|
+
if match is None:
|
|
66
|
+
raise LibError(f"malformed collection target {spec!r}", "expected 'module:attribute [extra]'")
|
|
67
|
+
return match["target"], match["extra"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class Collection:
|
|
72
|
+
"""What the core needs to know to serve a collection. Builders (index, readers, tools) are added as the core
|
|
73
|
+
grows; this is the part every collection has today.
|
|
74
|
+
|
|
75
|
+
`server` and `cli` are "module:attribute" targets, imported only when used, so a collection can be listed and
|
|
76
|
+
described without its extra installed: `server` names the `MCPServer` object, `cli` a `main(argv)` function
|
|
77
|
+
that runs the collection's own commands."""
|
|
78
|
+
|
|
79
|
+
name: str
|
|
80
|
+
title: str
|
|
81
|
+
prefix: str
|
|
82
|
+
unit: Unit
|
|
83
|
+
grammar: RefParser
|
|
84
|
+
server: str = ""
|
|
85
|
+
cli: str = ""
|
|
86
|
+
identify: str = ""
|
|
87
|
+
"""A "module:function" taking an `identify.Probe` and returning `identify.Candidate`s: how this collection
|
|
88
|
+
recognises its own documents in a supplied file. Empty when it has none."""
|
|
89
|
+
|
|
90
|
+
def __post_init__(self) -> None:
|
|
91
|
+
if not _NAME.fullmatch(self.name):
|
|
92
|
+
raise ValueError(f"collection name {self.name!r} must be lowercase letters and digits")
|
|
93
|
+
if self.prefix not in ("", f"{self.name}_"):
|
|
94
|
+
raise ValueError(f"tool prefix for {self.name!r} must be {self.name + '_'!r} or empty")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class _Target:
|
|
99
|
+
spec: str
|
|
100
|
+
install: str
|
|
101
|
+
# the hint when the collection's MCP server is what is missing: its own extra plus primarysource[mcp]
|
|
102
|
+
server_install: str = ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _targets() -> dict[str, _Target]:
|
|
106
|
+
"""Every registered collection, without importing any of them. A built-in wins over a plugin of the same
|
|
107
|
+
name, so an installed package cannot shadow one shipped here."""
|
|
108
|
+
found: dict[str, _Target] = {}
|
|
109
|
+
for ep in entry_points(group=ENTRY_POINT_GROUP):
|
|
110
|
+
dist = ep.dist.name if ep.dist is not None else ep.name
|
|
111
|
+
target, extra = _split(ep.value)
|
|
112
|
+
package = f"{dist}[{extra}]" if extra else dist
|
|
113
|
+
found[ep.name] = _Target(target, f'pip install "{package}"', f'pip install "{package}" "primarysource[mcp]"')
|
|
114
|
+
for name, spec in BUILTIN.items():
|
|
115
|
+
target, extra = _split(spec)
|
|
116
|
+
found[name] = _Target(
|
|
117
|
+
target,
|
|
118
|
+
f'pip install "primarysource[{extra or name}]"',
|
|
119
|
+
f'pip install "primarysource[{extra or name},mcp]"',
|
|
120
|
+
)
|
|
121
|
+
return found
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def available() -> tuple[str, ...]:
|
|
125
|
+
"""Names of every registered collection, sorted. Nothing is imported."""
|
|
126
|
+
return tuple(sorted(_targets()))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load(name: str, targets: Mapping[str, _Target] | None = None) -> Collection:
|
|
130
|
+
"""Import and return one collection, or raise a `LibError` that says what to do."""
|
|
131
|
+
known = _targets() if targets is None else targets
|
|
132
|
+
target = known.get(name)
|
|
133
|
+
if target is None:
|
|
134
|
+
names = ", ".join(sorted(known)) or "none"
|
|
135
|
+
raise LibError(f"no collection named {name!r} (registered: {names})", "primarysource collections")
|
|
136
|
+
module_name, _, attribute = target.spec.partition(":")
|
|
137
|
+
try:
|
|
138
|
+
module = importlib.import_module(module_name)
|
|
139
|
+
except ModuleNotFoundError as exc:
|
|
140
|
+
raise LibError(f"collection {name!r} needs {exc.name!r}, which is not installed", target.install) from exc
|
|
141
|
+
collection = getattr(module, attribute, None)
|
|
142
|
+
if not isinstance(collection, Collection) or collection.name != name:
|
|
143
|
+
raise LibError(f"{target.spec} does not hold the {name!r} collection", "")
|
|
144
|
+
return collection
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def attribute(collection: Collection, spec: str, what: str) -> Any:
|
|
148
|
+
"""Import a collection's `server` or `cli` target, mapping a missing dependency to the install hint."""
|
|
149
|
+
if not spec:
|
|
150
|
+
raise LibError(f"collection {collection.name!r} has no {what}", "")
|
|
151
|
+
module_name, _, name = spec.partition(":")
|
|
152
|
+
try:
|
|
153
|
+
module = importlib.import_module(module_name)
|
|
154
|
+
except ModuleNotFoundError as exc:
|
|
155
|
+
target = _targets().get(collection.name)
|
|
156
|
+
if target is None:
|
|
157
|
+
hint = f'pip install "primarysource[{collection.name}]"'
|
|
158
|
+
else:
|
|
159
|
+
hint = target.server_install if what == "server" and target.server_install else target.install
|
|
160
|
+
raise LibError(f"the {collection.name!r} {what} needs {exc.name!r}, which is not installed", hint) from exc
|
|
161
|
+
return getattr(module, name)
|
primarysource/render.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""The text shape of a tool result: body first, then footer lines in one fixed order.
|
|
2
|
+
|
|
3
|
+
Decision recorded in PLAN.md: results are text, because the passage dominates the tokens and a JSON string
|
|
4
|
+
escapes every newline in it. What a JSON result carried as fields is carried as `key: value` footer lines, in
|
|
5
|
+
the order below, so a reader (or a regex) finds them in the same place in every library.
|
|
6
|
+
|
|
7
|
+
The `next:` value is exactly what to pass back (PLAN.md §5.11): a ref string for a read, which `read` accepts as
|
|
8
|
+
its `ref`, and `start=<n>` for a list (search, outline, browse, grep), a 0-based offset for the same tool's
|
|
9
|
+
`start` argument. `next_start` and `parse_next_start` are the two halves of the list cursor.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
|
|
17
|
+
FOOTER_ORDER = ("cite", "unit", "span", "next", "relaxed", "confidence", "partial", "warning")
|
|
18
|
+
|
|
19
|
+
_NEXT_START = re.compile(r"start=(0|[1-9][0-9]*)")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render(body: str, footer: Mapping[str, str]) -> str:
|
|
23
|
+
"""Body, a blank line, then the known footer keys in FOOTER_ORDER. Unknown keys are a bug, not output."""
|
|
24
|
+
unknown = sorted(set(footer) - set(FOOTER_ORDER))
|
|
25
|
+
if unknown:
|
|
26
|
+
raise ValueError(f"unknown footer keys: {', '.join(unknown)}")
|
|
27
|
+
lines = [f"{key}: {footer[key]}" for key in FOOTER_ORDER if footer.get(key)]
|
|
28
|
+
for line in lines:
|
|
29
|
+
if "\n" in line:
|
|
30
|
+
raise ValueError("a footer value must be one line")
|
|
31
|
+
return body.rstrip("\n") + ("\n\n" + "\n".join(lines) if lines else "") + "\n"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def next_start(offset: int | None) -> str:
|
|
35
|
+
"""The `next:` value of a list page that continues at the 0-based `offset`: `start=<offset>`. None (the last
|
|
36
|
+
page) gives "", which `render` drops, so a footer with no `next:` line means there is nothing more."""
|
|
37
|
+
if offset is None:
|
|
38
|
+
return ""
|
|
39
|
+
if offset < 0:
|
|
40
|
+
raise ValueError("a list cursor is a 0-based offset")
|
|
41
|
+
return f"start={offset}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_next_start(value: str) -> int:
|
|
45
|
+
"""The offset in a list `next:` value, the inverse of `next_start`. Anything else is a bug in the caller."""
|
|
46
|
+
match = _NEXT_START.fullmatch(value.strip())
|
|
47
|
+
if match is None:
|
|
48
|
+
raise ValueError(f"not a list cursor: {value!r} (expected 'start=<n>')")
|
|
49
|
+
return int(match[1])
|
|
@@ -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
|
+
|