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
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Read, outline, search and cite imported Broadband Forum documents. Local files only; nothing here fetches.
|
|
2
|
+
|
|
3
|
+
Two kinds of document share one ref shape, `bbf:<doc>§<section>¶n` (the `bbf:` prefix is optional):
|
|
4
|
+
|
|
5
|
+
- a data-model file, `bbf:tr-181-2-22-0-ip`: `§` is the full path of an object, parameter, command, event,
|
|
6
|
+
profile or data type, `¶n` the nth paragraph of its description (see `datamodel.py`);
|
|
7
|
+
- a Technical Report, `bbf:tr-069-1-6-1`: `§` is a clause number as the PDF's bookmarks give it (`3.5`,
|
|
8
|
+
`A.3.2.3`), `A` for `Annex A.`, `Appendix-I` for `Appendix I.`, or the slug of an unnumbered bookmark
|
|
9
|
+
(`normative-references`); `¶n` is the nth sentence of the clause's span (its own text, then each subsection
|
|
10
|
+
as a `§3.5.1 Title` line and its text).
|
|
11
|
+
|
|
12
|
+
Every result's `unit:` footer says which unit `¶n` counts.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import functools
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from primarysource import budget, cursor
|
|
24
|
+
from primarysource.bbf import datamodel
|
|
25
|
+
from primarysource.bbf.catalogue import DOC_ID
|
|
26
|
+
from primarysource.bbf.catalogue import catalogue as packaged_catalogue
|
|
27
|
+
from primarysource.bbf.importer import read_record
|
|
28
|
+
from primarysource.c2sp._fetch import sha256
|
|
29
|
+
from primarysource.ecma import _pdf
|
|
30
|
+
from primarysource.errors import LibError
|
|
31
|
+
from primarysource.home import collection_home
|
|
32
|
+
from primarysource.model import span_hash
|
|
33
|
+
from primarysource.refs import Ref, RefGrammar
|
|
34
|
+
from primarysource.render import render
|
|
35
|
+
|
|
36
|
+
PREFIX = re.compile(r"^bbf:", re.IGNORECASE)
|
|
37
|
+
MAX_LISTING = 400
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def grammar() -> RefGrammar:
|
|
41
|
+
from primarysource.bbf import BBF_DOC, BBF_SECTION
|
|
42
|
+
|
|
43
|
+
return RefGrammar(BBF_DOC, BBF_SECTION)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class LoadedModel:
|
|
48
|
+
id: str
|
|
49
|
+
model: datamodel.DataModel
|
|
50
|
+
commit: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class LoadedReport:
|
|
55
|
+
id: str
|
|
56
|
+
designation: str
|
|
57
|
+
title: str
|
|
58
|
+
edition: str
|
|
59
|
+
document: _pdf.PdfDocument
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def default_home() -> Path:
|
|
63
|
+
return collection_home("bbf")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@functools.lru_cache(maxsize=8)
|
|
67
|
+
def _parse_model(digest: str, path: str) -> datamodel.DataModel:
|
|
68
|
+
"""Parse an installed file, cached by its verified digest so a search over many files parses each once."""
|
|
69
|
+
return datamodel.parse(Path(path).read_bytes())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def parse_ref(text: str) -> Ref:
|
|
73
|
+
"""Parse a bbf ref, accepting the optional `bbf:` prefix and `<doc>¶n` (unit n of a whole-report read)."""
|
|
74
|
+
body, doc_para = cursor.split_doc_para(PREFIX.sub("", text.strip(), count=1))
|
|
75
|
+
ref = grammar().parse(body)
|
|
76
|
+
if doc_para is not None:
|
|
77
|
+
ref = Ref(ref.doc, None, doc_para, None, None)
|
|
78
|
+
if ref.lines is not None or ref.anchor is not None:
|
|
79
|
+
raise LibError(f"not a bbf ref: {text!r}", "bbf refs are <doc>[§<section>][¶n]")
|
|
80
|
+
if ref.doc is None:
|
|
81
|
+
raise LibError(f"not a bbf ref: {text!r}", "start with the document, e.g. tr-069-1-6-1 or tr-181-2-22-0-ip")
|
|
82
|
+
return ref
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _doc(ref: Ref) -> str:
|
|
86
|
+
"""The document of a parsed ref; `parse_ref` has already refused a ref without one."""
|
|
87
|
+
if ref.doc is None:
|
|
88
|
+
raise LibError("the ref names no document", "start with the document id")
|
|
89
|
+
return ref.doc
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load(token: str, home: Path | None = None) -> LoadedModel | LoadedReport:
|
|
93
|
+
"""Load one installed document by id, verifying the stored file's sha256."""
|
|
94
|
+
home = home or default_home()
|
|
95
|
+
if not DOC_ID.fullmatch(token):
|
|
96
|
+
raise LibError(f"not a Broadband Forum document id: {token!r}", "bbf_docs lists what is catalogued")
|
|
97
|
+
record = read_record(home)
|
|
98
|
+
if token in record["models"]:
|
|
99
|
+
path = home / "models" / f"{token}.xml"
|
|
100
|
+
try:
|
|
101
|
+
digest = sha256(path.read_bytes())
|
|
102
|
+
except OSError as exc:
|
|
103
|
+
raise LibError(
|
|
104
|
+
f"cannot read {path.name}: {exc}", "", f"re-import it: primarysource bbf import-models {token}"
|
|
105
|
+
) from exc
|
|
106
|
+
if digest != record["models"][token]:
|
|
107
|
+
raise LibError(
|
|
108
|
+
f"{token}.xml does not match its recorded sha256",
|
|
109
|
+
"",
|
|
110
|
+
f"re-import it: primarysource bbf import-models {token}",
|
|
111
|
+
)
|
|
112
|
+
return LoadedModel(token, _parse_model(digest, str(path)), str(record.get("commit", "")))
|
|
113
|
+
if token in record["reports"]:
|
|
114
|
+
pdf = home / "reports" / f"{token}.pdf"
|
|
115
|
+
stored = home / "reports" / f"{token}.json"
|
|
116
|
+
pin = record["reports"][token].get("sha256")
|
|
117
|
+
try:
|
|
118
|
+
if not isinstance(pin, str) or sha256(pdf.read_bytes()) != pin:
|
|
119
|
+
raise LibError(
|
|
120
|
+
f"{token}.pdf does not match its recorded sha256",
|
|
121
|
+
"",
|
|
122
|
+
f"re-import it: primarysource bbf import-report {token}",
|
|
123
|
+
)
|
|
124
|
+
raw = json.loads(stored.read_text(encoding="utf-8"))
|
|
125
|
+
except (OSError, ValueError) as exc:
|
|
126
|
+
raise LibError(
|
|
127
|
+
f"cannot read the import of {token}: {exc}",
|
|
128
|
+
"",
|
|
129
|
+
f"re-import it: primarysource bbf import-report {token}",
|
|
130
|
+
) from exc
|
|
131
|
+
document = _pdf.PdfDocument.from_json(raw)
|
|
132
|
+
return LoadedReport(
|
|
133
|
+
token,
|
|
134
|
+
str(raw.get("designation", token.upper())),
|
|
135
|
+
str(raw.get("title", "")),
|
|
136
|
+
str(raw.get("edition", "")),
|
|
137
|
+
document,
|
|
138
|
+
)
|
|
139
|
+
have = ", ".join(sorted({*record["models"], *record["reports"]})[:12]) or "nothing"
|
|
140
|
+
raise LibError(
|
|
141
|
+
f"{token} is not imported (have: {have})",
|
|
142
|
+
"bbf_docs lists what is catalogued and imported",
|
|
143
|
+
f"primarysource bbf import-models {token} (a data model) or primarysource bbf import-report {token}",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _blob_url(model: LoadedModel) -> str:
|
|
148
|
+
return f"https://github.com/BroadbandForum/cwmp-data-models/blob/{model.commit}/{model.id}.xml"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _cite_model(doc: LoadedModel, node: datamodel.Node | None, para: int | str | None) -> str:
|
|
152
|
+
where = f", {node.kind} {node.id}" if node else ""
|
|
153
|
+
if para:
|
|
154
|
+
where += f" ¶{para}"
|
|
155
|
+
return (
|
|
156
|
+
f"Broadband Forum, {doc.id}.xml ({doc.model.title}), cwmp-data-models commit {doc.commit[:7]}{where}. "
|
|
157
|
+
f"BSD-3-Clause. {_blob_url(doc)}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _cite_report(doc: LoadedReport, section: _pdf.PdfSection | None, para: int | str | None) -> str:
|
|
162
|
+
where = f", §{section.id} {section.title}" if section else ""
|
|
163
|
+
if para:
|
|
164
|
+
where += f" ¶{para}"
|
|
165
|
+
url = next(
|
|
166
|
+
(r.page_url for r in packaged_catalogue().reports if r.id == doc.id), "https://www.broadband-forum.org/"
|
|
167
|
+
)
|
|
168
|
+
return f"Broadband Forum, {doc.designation} {doc.title}, {doc.edition}{where}. {url}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _report_units(doc: LoadedReport, ref: Ref) -> tuple[_pdf.PdfSection | None, tuple[str, ...], str]:
|
|
172
|
+
if ref.section is None:
|
|
173
|
+
units: list[str] = []
|
|
174
|
+
for section in doc.document.sections:
|
|
175
|
+
units.append(f"§{section.id} {section.title}")
|
|
176
|
+
units.extend(section.sentences)
|
|
177
|
+
return None, tuple(units), ""
|
|
178
|
+
index, warning = doc.document.find(ref.section)
|
|
179
|
+
return doc.document.sections[index], doc.document.span(index), warning
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _overview(doc: LoadedModel) -> str:
|
|
183
|
+
counts: dict[str, int] = {}
|
|
184
|
+
for node in doc.model.nodes:
|
|
185
|
+
counts[node.kind] = counts.get(node.kind, 0) + 1
|
|
186
|
+
summary = ", ".join(f"{n} {k}" for k, n in sorted(counts.items()))
|
|
187
|
+
lines = [f"{doc.model.title}", f"spec {doc.model.spec}", f"file {doc.model.file}", f"nodes: {summary or 'none'}"]
|
|
188
|
+
if doc.model.imports:
|
|
189
|
+
lines.append("imports: " + ", ".join(doc.model.imports))
|
|
190
|
+
lines.append(f"bbf_outline('{doc.id}') lists the nodes; read one as '{doc.id}§<path>'")
|
|
191
|
+
return "\n".join(lines)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def read(ref_text: str, max_tokens: int = 2000, home: Path | None = None) -> str:
|
|
195
|
+
"""The text of a ref, cut at unit boundaries within `max_tokens`. A `¶n` ref starts at unit n and runs to the end
|
|
196
|
+
of its scope; the `next:` footer is the ref that reads the next page."""
|
|
197
|
+
ref = parse_ref(ref_text)
|
|
198
|
+
doc = load(_doc(ref), home)
|
|
199
|
+
if isinstance(doc, LoadedModel):
|
|
200
|
+
if ref.section is None:
|
|
201
|
+
if ref.para is not None:
|
|
202
|
+
raise LibError(f"{ref_text}: ¶ needs a node", "name one after §, e.g. §Device.IP.Interface.{i}")
|
|
203
|
+
return render(_overview(doc), {"cite": _cite_model(doc, None, None), "unit": "paragraph"})
|
|
204
|
+
node, warning = doc.model.find(ref.section)
|
|
205
|
+
paragraphs = node.paragraphs
|
|
206
|
+
first = cursor.first_unit(ref.para, len(paragraphs), "paragraphs", ref_text)
|
|
207
|
+
chosen = budget.page(paragraphs, start=first, max_tokens=max_tokens) if paragraphs else None
|
|
208
|
+
parts = [] if ref.para is not None else [node.header()]
|
|
209
|
+
contents = doc.model.children(node)
|
|
210
|
+
if contents and ref.para is None:
|
|
211
|
+
names = [c.id.rsplit(".", 1)[-1] for c in contents[:80]]
|
|
212
|
+
more = f" (+{len(contents) - 80} more)" if len(contents) > 80 else ""
|
|
213
|
+
parts.append("contains: " + ", ".join(names) + more)
|
|
214
|
+
if chosen is not None:
|
|
215
|
+
parts.extend(chosen.units)
|
|
216
|
+
body = "\n\n".join(parts)
|
|
217
|
+
footer = {
|
|
218
|
+
"cite": _cite_model(doc, node, cursor.para_label(ref.para, chosen) if chosen else None),
|
|
219
|
+
"unit": "paragraph",
|
|
220
|
+
"span": span_hash(body),
|
|
221
|
+
"next": cursor.read_next(f"bbf:{doc.id}§{node.id}", chosen) if chosen else "",
|
|
222
|
+
"warning": warning,
|
|
223
|
+
}
|
|
224
|
+
return render(body, footer)
|
|
225
|
+
section, units, warning = _report_units(doc, ref)
|
|
226
|
+
if not units:
|
|
227
|
+
raise LibError(f"{ref_text}: that clause has no text of its own", "read its subsections, see bbf_outline")
|
|
228
|
+
first = cursor.first_unit(ref.para, len(units), "sentences", ref_text)
|
|
229
|
+
chosen = budget.page(units, start=first, max_tokens=max_tokens)
|
|
230
|
+
body = "\n".join(chosen.units)
|
|
231
|
+
scope = f"bbf:{doc.id}" + (f"§{section.id}" if section else "")
|
|
232
|
+
footer = {
|
|
233
|
+
"cite": _cite_report(doc, section, cursor.para_label(ref.para, chosen)),
|
|
234
|
+
"unit": "sentence",
|
|
235
|
+
"span": span_hash(body),
|
|
236
|
+
"next": cursor.read_next(scope, chosen),
|
|
237
|
+
"warning": warning,
|
|
238
|
+
}
|
|
239
|
+
return render(body, footer)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def outline(token: str, under: str = "", limit: int = 200, start: int = 0, home: Path | None = None) -> str:
|
|
243
|
+
"""A document's structure. A report: its bookmarked clauses with refs and pages. A data model: its objects,
|
|
244
|
+
commands, events, profiles and data types, indented by path depth; `under` (a node path) lists everything
|
|
245
|
+
below that node, parameters included. Both are paged by `limit` and the 0-based `start`."""
|
|
246
|
+
doc = load(token, home)
|
|
247
|
+
if isinstance(doc, LoadedReport):
|
|
248
|
+
head = f"{doc.designation} {doc.title}, {doc.edition} [{doc.id}, {doc.document.pages} pages]"
|
|
249
|
+
clauses = [
|
|
250
|
+
f"{' ' * section.depth}{section.id} {section.title} bbf:{doc.id}§{section.id} (p. {section.page})"
|
|
251
|
+
for section in doc.document.sections
|
|
252
|
+
]
|
|
253
|
+
shown, nxt = cursor.window(clauses, start, limit)
|
|
254
|
+
return render("\n".join([head, *shown]), {"next": nxt})
|
|
255
|
+
prefix = under.strip().rstrip(".")
|
|
256
|
+
rows = []
|
|
257
|
+
for node in doc.model.nodes:
|
|
258
|
+
if prefix:
|
|
259
|
+
if not (node.id == prefix or node.id.startswith((prefix + ".", prefix + "()"))):
|
|
260
|
+
continue
|
|
261
|
+
elif node.kind == "parameter":
|
|
262
|
+
continue
|
|
263
|
+
depth = len([part for part in node.id.split(".") if part and part != "{i}"]) - 1
|
|
264
|
+
rows.append(f"{' ' * max(0, min(depth, 12))}{node.kind} {node.id}")
|
|
265
|
+
if not rows:
|
|
266
|
+
raise LibError(f"nothing under {under!r} in {doc.id}", "omit under to list the objects")
|
|
267
|
+
shown, nxt = cursor.window(rows, start, limit)
|
|
268
|
+
header = f"{doc.model.title} [{doc.id}] refs: bbf:{doc.id}§<path>"
|
|
269
|
+
return render("\n".join([header, *shown]), {"next": nxt})
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _documents(doc: str | None, home: Path) -> list[str]:
|
|
273
|
+
record = read_record(home)
|
|
274
|
+
if doc:
|
|
275
|
+
return [doc]
|
|
276
|
+
ids = [*sorted(record["reports"]), *sorted(record["models"])]
|
|
277
|
+
if not ids:
|
|
278
|
+
raise LibError(
|
|
279
|
+
"no Broadband Forum documents are imported",
|
|
280
|
+
"bbf_docs lists the catalogue",
|
|
281
|
+
"primarysource bbf import-models",
|
|
282
|
+
)
|
|
283
|
+
return ids
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def search(query: str, doc: str | None = None, limit: int = 10, start: int = 0, home: Path | None = None) -> str:
|
|
287
|
+
"""Units containing every query word (else at least half, flagged `relaxed`), best first: one line per hit,
|
|
288
|
+
`<ref>: <snippet>`. A data-model node whose path matches is a hit with no ¶."""
|
|
289
|
+
terms = _pdf.query_terms(query)
|
|
290
|
+
home = home or default_home()
|
|
291
|
+
tokens = _documents(doc, home)
|
|
292
|
+
loaded = [load(token, home) for token in tokens]
|
|
293
|
+
scored: list[tuple[float, int, str, str]] = []
|
|
294
|
+
relaxed = False
|
|
295
|
+
for need_all in (True, False):
|
|
296
|
+
for order, item in enumerate(loaded):
|
|
297
|
+
if isinstance(item, LoadedReport):
|
|
298
|
+
for section in item.document.sections:
|
|
299
|
+
for score, number in _pdf.score_units(section.sentences, terms, section.title, need_all):
|
|
300
|
+
ref = f"bbf:{item.id}§{section.id}¶{number}"
|
|
301
|
+
scored.append((-score, order, ref, _pdf.snippet(section.sentences[number - 1], terms)))
|
|
302
|
+
continue
|
|
303
|
+
for node in item.model.nodes:
|
|
304
|
+
units = (node.header(), *node.paragraphs)
|
|
305
|
+
for score, number in _pdf.score_units(units, terms, node.id, need_all):
|
|
306
|
+
ref = f"bbf:{item.id}§{node.id}" + (f"¶{number - 1}" if number > 1 else "")
|
|
307
|
+
scored.append((-score, order, ref, _pdf.snippet(units[number - 1], terms)))
|
|
308
|
+
if scored:
|
|
309
|
+
relaxed = not need_all
|
|
310
|
+
break
|
|
311
|
+
if not scored:
|
|
312
|
+
return render("no matches", {})
|
|
313
|
+
scored.sort()
|
|
314
|
+
shown, nxt = cursor.window(scored, start, limit)
|
|
315
|
+
footer = {"next": nxt, "relaxed": "only some query words matched" if relaxed else ""}
|
|
316
|
+
return render("\n".join(f"{ref}: {snip}" for _, _, ref, snip in shown), footer)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def cite(ref_text: str, home: Path | None = None) -> str:
|
|
320
|
+
"""The citation string for a ref, with the span hash of the exact text cited."""
|
|
321
|
+
ref = parse_ref(ref_text)
|
|
322
|
+
doc = load(_doc(ref), home)
|
|
323
|
+
if isinstance(doc, LoadedModel):
|
|
324
|
+
if ref.section is None:
|
|
325
|
+
return render(
|
|
326
|
+
doc.id, {"cite": _cite_model(doc, None, None), "unit": "paragraph", "span": span_hash(_overview(doc))}
|
|
327
|
+
)
|
|
328
|
+
node, warning = doc.model.find(ref.section)
|
|
329
|
+
text = "\n\n".join(node.paragraphs)
|
|
330
|
+
if ref.para is not None:
|
|
331
|
+
if ref.para > len(node.paragraphs):
|
|
332
|
+
raise LibError(
|
|
333
|
+
f"{ref_text}: the description has {len(node.paragraphs)} paragraphs", "use a ¶ within range"
|
|
334
|
+
)
|
|
335
|
+
text = node.paragraphs[ref.para - 1]
|
|
336
|
+
footer = {
|
|
337
|
+
"cite": _cite_model(doc, node, ref.para),
|
|
338
|
+
"unit": "paragraph",
|
|
339
|
+
"span": span_hash(text),
|
|
340
|
+
"warning": warning,
|
|
341
|
+
}
|
|
342
|
+
return render(f"{doc.id}§{node.id}", footer)
|
|
343
|
+
section, units, warning = _report_units(doc, ref)
|
|
344
|
+
if ref.para is not None:
|
|
345
|
+
if ref.para > len(units):
|
|
346
|
+
raise LibError(f"{ref_text}: the span has {len(units)} sentences", f"use ¶1 to ¶{len(units)}")
|
|
347
|
+
units = (units[ref.para - 1],)
|
|
348
|
+
footer = {
|
|
349
|
+
"cite": _cite_report(doc, section, ref.para),
|
|
350
|
+
"unit": "sentence",
|
|
351
|
+
"span": span_hash("\n".join(units)),
|
|
352
|
+
"warning": warning,
|
|
353
|
+
}
|
|
354
|
+
return render(f"{doc.id}" + (f"§{section.id}" if section else ""), footer)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def docs(filter_text: str = "", home: Path | None = None) -> str:
|
|
358
|
+
"""The catalogue and what is imported: reports first, then data-model files (at most `MAX_LISTING` lines)."""
|
|
359
|
+
home = home or default_home()
|
|
360
|
+
cat = packaged_catalogue()
|
|
361
|
+
record = read_record(home)
|
|
362
|
+
needle = filter_text.casefold()
|
|
363
|
+
lines = [
|
|
364
|
+
f"reports: {cat.report_terms.licence}, placement {cat.report_terms.placement}",
|
|
365
|
+
]
|
|
366
|
+
for report in cat.reports:
|
|
367
|
+
if needle in report.id or needle in report.title.casefold():
|
|
368
|
+
state = "imported" if report.id in record["reports"] else "not imported"
|
|
369
|
+
lines.append(f"{state:<13} {report.id} {report.designation} {report.title}, {report.issue}")
|
|
370
|
+
lines.append(f"models: {cat.model_terms.licence}, placement {cat.model_terms.placement}, commit {cat.commit[:7]}")
|
|
371
|
+
shown = 0
|
|
372
|
+
for model in cat.models:
|
|
373
|
+
if needle in model.id or needle in model.title.casefold():
|
|
374
|
+
if shown >= MAX_LISTING:
|
|
375
|
+
lines.append("... (narrow with a filter)")
|
|
376
|
+
break
|
|
377
|
+
state = "imported" if model.id in record["models"] else "not imported"
|
|
378
|
+
lines.append(f"{state:<13} {model.id} {model.title}")
|
|
379
|
+
shown += 1
|
|
380
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""MCP server for the bbf collection: `primarysource mcp --collection bbf`.
|
|
2
|
+
|
|
3
|
+
Imported only when served, so `mcp` (the `[mcp]` extra) is needed only then. Tools carry the `bbf_` prefix,
|
|
4
|
+
answer in text with the fixed footer, and turn a `LibError` into an MCP tool error naming the remedy. Every tool
|
|
5
|
+
reads local files only; none writes or fetches.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from mcp.server.mcpserver import MCPServer
|
|
13
|
+
from mcp.types import ToolAnnotations
|
|
14
|
+
|
|
15
|
+
from primarysource import __version__
|
|
16
|
+
from primarysource.bbf import library
|
|
17
|
+
from primarysource.scaffold import add_tools
|
|
18
|
+
|
|
19
|
+
READ_ONLY = ToolAnnotations(read_only_hint=True, idempotent_hint=True, destructive_hint=False, open_world_hint=False)
|
|
20
|
+
|
|
21
|
+
INSTRUCTIONS = (
|
|
22
|
+
"Broadband Forum documents from the user's own imports: Technical Reports (TR-069 Amendment 6 Corrigendum 1, "
|
|
23
|
+
"CWMP 1.4) and the TR-181 data-model XML. Loop: bbf_search('<words>') -> bbf_read('<ref>'). Refs: "
|
|
24
|
+
"'bbf:tr-069-1-6-1§3.5' (a report clause as numbered in the PDF; '¶2' is its 2nd sentence) and "
|
|
25
|
+
"'bbf:tr-181-2-22-0-ip§Device.IP.Interface.{i}.Enable' (a data-model node by full path; '¶1' is the 1st "
|
|
26
|
+
"paragraph of its description, BBF {{macros}} left unexpanded). bbf_outline('<doc>') lists clauses or nodes; "
|
|
27
|
+
"bbf_cite('<ref>') gives the citation and the hash of the cited text. If nothing is imported, the error says how."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bbf_read(ref: str, max_tokens: int = 2000) -> str:
|
|
32
|
+
"""Read Broadband Forum text by ref, cut at unit boundaries within max_tokens. A 'next: <ref>' footer line is
|
|
33
|
+
the ref that reads the next page: pass it back as ref.
|
|
34
|
+
|
|
35
|
+
ref: 'tr-069-1-6-1§3.5' (report clause, subsections included) | 'tr-069-1-6-1§3.5¶2' (from its 2nd sentence) |
|
|
36
|
+
'tr-181-2-22-0-ip§Device.IP.Interface.{i}' (a data-model object: header, contained names, description) |
|
|
37
|
+
'...{i}.Enable¶1' (from its 1st description paragraph). 'tr-181-2-22-0-ip' alone gives the file's overview.
|
|
38
|
+
bbf_cite gives exactly one unit."""
|
|
39
|
+
return library.read(ref, max_tokens=max_tokens)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def bbf_search(query: str, doc: str | None = None, limit: int = 10, start: int = 0) -> str:
|
|
43
|
+
"""Find units containing the query words (all of them, else at least half, flagged 'relaxed'). One line per
|
|
44
|
+
hit: '<ref>: <snippet>'. doc restricts to one document id. A 'next: start=N' footer line means call again with
|
|
45
|
+
start=N (a 0-based offset)."""
|
|
46
|
+
return library.search(query, doc=doc, limit=limit, start=start)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def bbf_outline(doc: str, under: str = "", limit: int = 200, start: int = 0) -> str:
|
|
50
|
+
"""A report's clauses with refs and pages, or a data model's objects (indented by depth). under: a node path
|
|
51
|
+
to list everything below it, parameters included, e.g. 'Device.IP.Interface.{i}'. A 'next: start=N' footer
|
|
52
|
+
line means call again with start=N."""
|
|
53
|
+
return library.outline(doc, under=under, limit=limit, start=start)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def bbf_cite(ref: str) -> str:
|
|
57
|
+
"""The citation string for a ref, with the sha256 prefix of the exact text cited (cite: and span: lines)."""
|
|
58
|
+
return library.cite(ref)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def bbf_docs(filter: str = "") -> str: # noqa: A002 - the tool's argument name
|
|
62
|
+
"""List the catalogued reports and data-model files and whether each is imported; filter narrows by id."""
|
|
63
|
+
return library.docs(filter)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build() -> Any:
|
|
67
|
+
built = MCPServer("primarysource-bbf", version=__version__, instructions=INSTRUCTIONS)
|
|
68
|
+
add_tools(built, (bbf_read, bbf_search, bbf_outline, bbf_cite, bbf_docs), annotations=READ_ONLY)
|
|
69
|
+
return built
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
server = _build()
|
primarysource/budget.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Token-budgeted paging over units.
|
|
2
|
+
|
|
3
|
+
rfclib, itulib and zimlore each define `CHARS_PER_TOKEN = 4` and each wrote this loop. The rules they agree on:
|
|
4
|
+
a unit is never split, a page always makes progress even when one unit exceeds the budget, and the result says
|
|
5
|
+
where to continue.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
CHARS_PER_TOKEN = 4
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def tokens(text: str) -> int:
|
|
17
|
+
"""The estimate all three libraries use: characters divided by four, rounded up."""
|
|
18
|
+
return -(-len(text) // CHARS_PER_TOKEN)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class Page:
|
|
23
|
+
"""Units `first..last` (1-based, inclusive) of `total`. `next_start` is None on the last page."""
|
|
24
|
+
|
|
25
|
+
units: tuple[str, ...]
|
|
26
|
+
first: int
|
|
27
|
+
last: int
|
|
28
|
+
total: int
|
|
29
|
+
next_start: int | None
|
|
30
|
+
oversize: bool
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def page(units: Sequence[str], start: int = 1, max_tokens: int = 2000) -> Page:
|
|
34
|
+
"""Take whole units from `start` (1-based) until the next one would exceed `max_tokens`.
|
|
35
|
+
|
|
36
|
+
The first unit is always taken, so a caller following `next_start` terminates; `oversize` reports that the
|
|
37
|
+
page went over budget for that reason. A `start` past the end gives an empty page, not an error: the caller
|
|
38
|
+
asked for a position, and "nothing there" is the true answer.
|
|
39
|
+
"""
|
|
40
|
+
if start < 1:
|
|
41
|
+
raise ValueError("start is 1-based")
|
|
42
|
+
if max_tokens < 1:
|
|
43
|
+
raise ValueError("max_tokens must be positive")
|
|
44
|
+
total = len(units)
|
|
45
|
+
taken: list[str] = []
|
|
46
|
+
used = 0
|
|
47
|
+
index = start - 1
|
|
48
|
+
while index < total:
|
|
49
|
+
cost = tokens(units[index])
|
|
50
|
+
if taken and used + cost > max_tokens:
|
|
51
|
+
break
|
|
52
|
+
taken.append(units[index])
|
|
53
|
+
used += cost
|
|
54
|
+
index += 1
|
|
55
|
+
last = start + len(taken) - 1
|
|
56
|
+
return Page(
|
|
57
|
+
units=tuple(taken),
|
|
58
|
+
first=start if taken else 0,
|
|
59
|
+
last=last if taken else 0,
|
|
60
|
+
total=total,
|
|
61
|
+
next_start=index + 1 if index < total else None,
|
|
62
|
+
oversize=used > max_tokens,
|
|
63
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""C2SP: the Community Cryptography Specification Project's specifications (CC BY 4.0), as one collection.
|
|
2
|
+
|
|
3
|
+
The source is Markdown in https://github.com/C2SP/C2SP, pinned to one commit (`catalogue.json`) and imported with
|
|
4
|
+
`primarysource c2sp import`. Because the text is CC BY 4.0 it may also be placed in an image layer; the importer
|
|
5
|
+
itself only writes `$PRIMARYSOURCE_HOME/c2sp/<commit>/`. `docs/c2sp.md` is the user-facing description.
|
|
6
|
+
|
|
7
|
+
Importing this package needs only the standard library; `mcp` (the `[mcp]` extra) only when a collection is
|
|
8
|
+
served (`primarysource mcp --collection c2sp`). `primarysource c2sp ...` runs `cli.py`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from primarysource.model import Unit
|
|
14
|
+
from primarysource.refs import Ref, RefGrammar
|
|
15
|
+
from primarysource.registry import Collection
|
|
16
|
+
|
|
17
|
+
C2SP_DOC = r"[A-Za-z0-9][A-Za-z0-9-]*(?:@[0-9a-f]{7,40})?"
|
|
18
|
+
"""A specification by file name, case as in the repository, optionally pinned to a commit prefix:
|
|
19
|
+
`static-ct-api`, `tlog-tiles@09f3e51`, `BLAKE3`."""
|
|
20
|
+
|
|
21
|
+
C2SP_SECTION = r"[^#¶:§\s][^#¶:§]*"
|
|
22
|
+
"""A heading's text (`Log Entries`) or its slug (`log-entries`). May contain spaces, never `:`, `#`, `¶` or `§`."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Refs:
|
|
26
|
+
"""The collection's ref parser: `library.parse_ref` (which also takes the `c2sp:` prefix and refuses what the
|
|
27
|
+
collection has no meaning for), imported on first use so the collection loads without its reader."""
|
|
28
|
+
|
|
29
|
+
_grammar = RefGrammar(C2SP_DOC, C2SP_SECTION)
|
|
30
|
+
|
|
31
|
+
def parse(self, text: str) -> Ref:
|
|
32
|
+
from primarysource.c2sp.library import parse_ref
|
|
33
|
+
|
|
34
|
+
return parse_ref(text)
|
|
35
|
+
|
|
36
|
+
def format(self, ref: Ref) -> str:
|
|
37
|
+
return self._grammar.format(ref)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
C2SP = Collection(
|
|
41
|
+
name="c2sp",
|
|
42
|
+
title="C2SP specifications (Community Cryptography Specification Project)",
|
|
43
|
+
prefix="c2sp_",
|
|
44
|
+
unit=Unit.BLOCK,
|
|
45
|
+
grammar=_Refs(),
|
|
46
|
+
server="primarysource.c2sp.server:server",
|
|
47
|
+
cli="primarysource.c2sp.cli:main",
|
|
48
|
+
)
|
|
49
|
+
"""Refs: `c2sp:static-ct-api@09f3e51§Log Entries¶2`. `§` names a heading by its text or its slug; `¶n` counts
|
|
50
|
+
blank-line-separated blocks (a paragraph, a list, a fenced code block) in that heading's span, subsections included,
|
|
51
|
+
its own heading excluded. The `c2sp:` prefix and the `@commit` are optional."""
|
|
52
|
+
|
|
53
|
+
__all__ = ["C2SP", "C2SP_DOC", "C2SP_SECTION"]
|