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,325 @@
|
|
|
1
|
+
"""IANA registry XML as rows with their citations.
|
|
2
|
+
|
|
3
|
+
The format (checked 2026-11-13 against the files themselves and their RELAX NG, `_support/iana-registry.rng`):
|
|
4
|
+
a root `<registry id="<file name>">` in the namespace `http://www.iana.org/assignments` holds a `title`, an
|
|
5
|
+
`updated` date, sub-`<registry id=...>` elements (which may nest) and a `<people>` table. A registry holds
|
|
6
|
+
`registration_rule`, `expert`, `note`, `range` and `xref` metadata and `<record>` rows. A record's columns are
|
|
7
|
+
ordinary child elements whose names differ per registry (`value`, `name`, `description`, `type`, `code`,
|
|
8
|
+
`length`, ...), plus `xref` citations; `date` and `updated` attributes mark when it was added or changed.
|
|
9
|
+
|
|
10
|
+
This module takes a parsed tree and makes it into frozen dataclasses. It never reads a file and never fetches.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import xml.etree.ElementTree as ET # noqa: S405 - elements come from xmlsafe.parse; nothing is parsed here
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
|
|
19
|
+
from primarysource.errors import LibError
|
|
20
|
+
|
|
21
|
+
NAMESPACE = "http://www.iana.org/assignments"
|
|
22
|
+
_NS = "{" + NAMESPACE + "}"
|
|
23
|
+
_ID = re.compile(r"[A-Za-z_][A-Za-z0-9_.\-]*")
|
|
24
|
+
_RFC = re.compile(r"(rfc|bcp|std)(\d+)")
|
|
25
|
+
_NUMBER = r"(?:0[xX][0-9A-Fa-f]+|\d+)"
|
|
26
|
+
_RANGE = re.compile(rf"\s*({_NUMBER})(?:\s*-\s*({_NUMBER}))?\s*")
|
|
27
|
+
_SPACE = re.compile(r"\s+")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _local(element: ET.Element) -> str:
|
|
31
|
+
tag = element.tag
|
|
32
|
+
return tag[len(_NS) :] if tag.startswith(_NS) else tag
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _squash(text: str) -> str:
|
|
36
|
+
return _SPACE.sub(" ", text).strip()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class Xref:
|
|
41
|
+
"""One `<xref>`: what a registry row or note cites. `kind` is the schema's `type`: rfc, rfc-errata, draft,
|
|
42
|
+
uri, registry, person, note or text. `fragment` is the part cited (`Section 3`, `Appendix A`), or empty;
|
|
43
|
+
`section` is the section number alone, for a ref the `rfc` collection can read."""
|
|
44
|
+
|
|
45
|
+
kind: str
|
|
46
|
+
data: str
|
|
47
|
+
fragment: str = ""
|
|
48
|
+
section: str = ""
|
|
49
|
+
text: str = ""
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def rfc(self) -> str:
|
|
53
|
+
"""`RFC4271`, `BCP14` or `STD5` for an rfc xref, else empty."""
|
|
54
|
+
match = _RFC.fullmatch(self.data) if self.kind == "rfc" else None
|
|
55
|
+
return f"{match[1].upper()}{match[2]}" if match else ""
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def ref(self) -> str:
|
|
59
|
+
"""A ref the `rfc` collection reads (`RFC4271§3.1`), or `RFC4271` without a section; empty if not an RFC."""
|
|
60
|
+
if not self.rfc:
|
|
61
|
+
return ""
|
|
62
|
+
return f"{self.rfc}§{self.section}" if self.section else self.rfc
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def label(self) -> str:
|
|
66
|
+
"""How the citation reads in running text: `RFC4271, Section 3`, `draft-ietf-x-00`, `RFC errata 123`."""
|
|
67
|
+
if self.kind == "rfc" and self.rfc:
|
|
68
|
+
base = self.rfc
|
|
69
|
+
elif self.kind == "rfc-errata":
|
|
70
|
+
base = f"RFC errata {self.data}"
|
|
71
|
+
elif self.kind == "note":
|
|
72
|
+
base = f"note {self.data}"
|
|
73
|
+
elif self.kind == "registry":
|
|
74
|
+
base = f"registry {self.data}"
|
|
75
|
+
elif self.kind in ("person", "text"):
|
|
76
|
+
base = self.text or self.data
|
|
77
|
+
else:
|
|
78
|
+
base = self.data
|
|
79
|
+
return f"{base}, {self.fragment}" if self.fragment else base
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class Record:
|
|
84
|
+
"""A row. `key` is the `<value>` column, else the first column's text. `fields` are the columns in file
|
|
85
|
+
order as (element name, text) with empty ones dropped; `xrefs` every citation under the row."""
|
|
86
|
+
|
|
87
|
+
ordinal: int
|
|
88
|
+
key: str
|
|
89
|
+
fields: tuple[tuple[str, str], ...]
|
|
90
|
+
xrefs: tuple[Xref, ...] = ()
|
|
91
|
+
date: str = ""
|
|
92
|
+
updated: str = ""
|
|
93
|
+
id: str = ""
|
|
94
|
+
|
|
95
|
+
def field(self, name: str) -> str:
|
|
96
|
+
return next((text for key, text in self.fields if key == name), "")
|
|
97
|
+
|
|
98
|
+
def text(self) -> str:
|
|
99
|
+
"""One line: `value: 1 | name: OPEN | refs: RFC4271`."""
|
|
100
|
+
parts = [f"{name}: {text}" for name, text in self.fields]
|
|
101
|
+
if self.xrefs:
|
|
102
|
+
parts.append("refs: " + ", ".join(dict.fromkeys(x.label for x in self.xrefs)))
|
|
103
|
+
return " | ".join(parts) if parts else self.key
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True)
|
|
107
|
+
class Range:
|
|
108
|
+
"""A `<range>`: which part of the number space follows which registration rule."""
|
|
109
|
+
|
|
110
|
+
value: str
|
|
111
|
+
rule: str
|
|
112
|
+
hex: str = ""
|
|
113
|
+
note: str = ""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class Registry:
|
|
118
|
+
"""One `<registry>`: the root's file-level registry and every sub-registry, flattened in file order. `parent`
|
|
119
|
+
is the enclosing registry's id, or empty for the root."""
|
|
120
|
+
|
|
121
|
+
id: str
|
|
122
|
+
title: str
|
|
123
|
+
parent: str = ""
|
|
124
|
+
created: str = ""
|
|
125
|
+
updated: str = ""
|
|
126
|
+
rules: tuple[str, ...] = ()
|
|
127
|
+
experts: tuple[str, ...] = ()
|
|
128
|
+
notes: tuple[str, ...] = ()
|
|
129
|
+
ranges: tuple[Range, ...] = ()
|
|
130
|
+
xrefs: tuple[Xref, ...] = ()
|
|
131
|
+
records: tuple[Record, ...] = ()
|
|
132
|
+
footnotes: tuple[tuple[str, str], ...] = ()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True, slots=True)
|
|
136
|
+
class RegistryFile:
|
|
137
|
+
"""One registry file (`dns-parameters.xml`): its registries in file order and the people it names."""
|
|
138
|
+
|
|
139
|
+
name: str
|
|
140
|
+
title: str
|
|
141
|
+
updated: str
|
|
142
|
+
registries: tuple[Registry, ...]
|
|
143
|
+
people: dict[str, str] = field(default_factory=dict)
|
|
144
|
+
|
|
145
|
+
def registry(self, registry_id: str) -> Registry | None:
|
|
146
|
+
return next((r for r in self.registries if r.id == registry_id), None)
|
|
147
|
+
|
|
148
|
+
def children(self, registry_id: str) -> tuple[Registry, ...]:
|
|
149
|
+
return tuple(r for r in self.registries if r.parent == registry_id)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _xref(element: ET.Element, people: dict[str, str]) -> Xref:
|
|
153
|
+
kind = element.get("type", "")
|
|
154
|
+
data = element.get("data", "")
|
|
155
|
+
fragment = ""
|
|
156
|
+
section = ""
|
|
157
|
+
for attribute in ("section", "appendix", "page", "table", "figure", "anchor"):
|
|
158
|
+
value = element.get(attribute)
|
|
159
|
+
if value:
|
|
160
|
+
fragment = f"anchor {value}" if attribute == "anchor" else f"{attribute.capitalize()} {value}"
|
|
161
|
+
if attribute == "section":
|
|
162
|
+
section = value
|
|
163
|
+
break
|
|
164
|
+
text = _squash("".join(element.itertext()))
|
|
165
|
+
if kind == "person":
|
|
166
|
+
text = people.get(data, "") or data.replace("_", " ")
|
|
167
|
+
return Xref(kind, data, fragment, section, text)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _inline(element: ET.Element, people: dict[str, str]) -> str:
|
|
171
|
+
"""Text with inline xrefs spelled out (`[RFC6762]`), whitespace collapsed."""
|
|
172
|
+
parts = [element.text or ""]
|
|
173
|
+
for child in element:
|
|
174
|
+
if _local(child) == "xref":
|
|
175
|
+
parts.append(f"[{_xref(child, people).label}]")
|
|
176
|
+
else:
|
|
177
|
+
parts.append(_inline(child, people))
|
|
178
|
+
parts.append(child.tail or "")
|
|
179
|
+
return _squash("".join(parts))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _all_xrefs(element: ET.Element, people: dict[str, str]) -> tuple[Xref, ...]:
|
|
183
|
+
found = [_xref(x, people) for x in element.iter() if _local(x) == "xref"]
|
|
184
|
+
return tuple(dict.fromkeys(found))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _record(element: ET.Element, ordinal: int, people: dict[str, str]) -> Record:
|
|
188
|
+
columns: list[tuple[str, str]] = []
|
|
189
|
+
for child in element:
|
|
190
|
+
name = _local(child)
|
|
191
|
+
if name == "xref":
|
|
192
|
+
continue
|
|
193
|
+
text = _inline(child, people)
|
|
194
|
+
if text:
|
|
195
|
+
columns.append((name, text))
|
|
196
|
+
key = next((text for name, text in columns if name == "value"), columns[0][1] if columns else "")
|
|
197
|
+
return Record(
|
|
198
|
+
ordinal=ordinal,
|
|
199
|
+
key=key,
|
|
200
|
+
fields=tuple(columns),
|
|
201
|
+
xrefs=_all_xrefs(element, people),
|
|
202
|
+
date=element.get("date", ""),
|
|
203
|
+
updated=element.get("updated", ""),
|
|
204
|
+
id=element.get("id", ""),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _range(element: ET.Element, people: dict[str, str]) -> Range:
|
|
209
|
+
def column(name: str) -> str:
|
|
210
|
+
child = element.find(_NS + name)
|
|
211
|
+
return _inline(child, people) if child is not None else ""
|
|
212
|
+
|
|
213
|
+
return Range(column("value"), column("registration_rule"), column("hex"), column("note"))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _registry(element: ET.Element, parent: str, people: dict[str, str], out: list[Registry]) -> None:
|
|
217
|
+
registry_id = element.get("id", "")
|
|
218
|
+
if not _ID.fullmatch(registry_id):
|
|
219
|
+
raise LibError(f"a registry has the id {registry_id!r}, which is not an IANA registry id", "")
|
|
220
|
+
slot = len(out)
|
|
221
|
+
out.append(Registry(registry_id, "")) # keeps file order; replaced below once the children are done
|
|
222
|
+
title = created = updated = ""
|
|
223
|
+
rules: list[str] = []
|
|
224
|
+
experts: list[str] = []
|
|
225
|
+
notes: list[str] = []
|
|
226
|
+
ranges: list[Range] = []
|
|
227
|
+
xrefs: list[Xref] = []
|
|
228
|
+
records: list[Record] = []
|
|
229
|
+
footnotes: list[tuple[str, str]] = []
|
|
230
|
+
for child in element:
|
|
231
|
+
name = _local(child)
|
|
232
|
+
if name == "title":
|
|
233
|
+
title = _inline(child, people)
|
|
234
|
+
elif name == "created":
|
|
235
|
+
created = _squash(child.text or "")
|
|
236
|
+
elif name == "updated":
|
|
237
|
+
updated = _squash(child.text or "")
|
|
238
|
+
elif name == "registration_rule":
|
|
239
|
+
rules.append(_inline(child, people))
|
|
240
|
+
elif name == "expert":
|
|
241
|
+
experts.append(_inline(child, people))
|
|
242
|
+
elif name == "note":
|
|
243
|
+
notes.append(_inline(child, people))
|
|
244
|
+
elif name == "range":
|
|
245
|
+
ranges.append(_range(child, people))
|
|
246
|
+
elif name == "xref":
|
|
247
|
+
xrefs.append(_xref(child, people))
|
|
248
|
+
elif name == "record":
|
|
249
|
+
records.append(_record(child, len(records) + 1, people))
|
|
250
|
+
elif name == "footnote":
|
|
251
|
+
footnotes.append((child.get("anchor", ""), _inline(child, people)))
|
|
252
|
+
elif name == "registry":
|
|
253
|
+
_registry(child, registry_id, people, out)
|
|
254
|
+
out[slot] = Registry(
|
|
255
|
+
registry_id,
|
|
256
|
+
title,
|
|
257
|
+
parent,
|
|
258
|
+
created,
|
|
259
|
+
updated,
|
|
260
|
+
tuple(rules),
|
|
261
|
+
tuple(experts),
|
|
262
|
+
tuple(notes),
|
|
263
|
+
tuple(ranges),
|
|
264
|
+
tuple(dict.fromkeys(xrefs)),
|
|
265
|
+
tuple(records),
|
|
266
|
+
tuple(footnotes),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def from_tree(root: ET.Element, name: str | None = None) -> RegistryFile:
|
|
271
|
+
"""Build a `RegistryFile` from a tree parsed by `xmlsafe.parse`. A tree that is not an IANA registry is
|
|
272
|
+
refused; so is a root whose `id` is not `name`, when `name` is given."""
|
|
273
|
+
if root.tag != _NS + "registry":
|
|
274
|
+
raise LibError(
|
|
275
|
+
f"the root element is {root.tag!r}, not an IANA <registry> in {NAMESPACE}",
|
|
276
|
+
"this is not a registry file; check the URL",
|
|
277
|
+
)
|
|
278
|
+
root_id = root.get("id", "")
|
|
279
|
+
if not _ID.fullmatch(root_id):
|
|
280
|
+
raise LibError(f"the root registry has the id {root_id!r}", "this is not a registry file")
|
|
281
|
+
if name is not None and root_id != name:
|
|
282
|
+
raise LibError(f"the file is registry {root_id!r}, not {name!r}", "check the URL against the catalogue")
|
|
283
|
+
people: dict[str, str] = {}
|
|
284
|
+
for person in root.iter(_NS + "person"):
|
|
285
|
+
label = person.find(_NS + "name")
|
|
286
|
+
if person.get("id") and label is not None:
|
|
287
|
+
people[person.get("id", "")] = _squash(label.text or "")
|
|
288
|
+
registries: list[Registry] = []
|
|
289
|
+
_registry(root, "", people, registries)
|
|
290
|
+
top = registries[0]
|
|
291
|
+
return RegistryFile(root_id, top.title, top.updated, tuple(registries), people)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _int(text: str) -> int:
|
|
295
|
+
"""Decimal, or hex with a 0x prefix. Leading zeros are decimal (`010` is ten), unlike `int(text, 0)`."""
|
|
296
|
+
return int(text, 16) if text[:2].lower() == "0x" else int(text, 10)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def numeric_range(text: str) -> tuple[int, int] | None:
|
|
300
|
+
"""`6-255` -> (6, 255); `0x0010` -> (16, 16); anything else (a name, `*`, `1.2`) -> None."""
|
|
301
|
+
match = _RANGE.fullmatch(text)
|
|
302
|
+
if match is None:
|
|
303
|
+
return None
|
|
304
|
+
low = _int(match[1])
|
|
305
|
+
high = _int(match[2] or match[1])
|
|
306
|
+
return (low, high) if low <= high else None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def number_of(text: str) -> int | None:
|
|
310
|
+
"""A single number (decimal or 0x hex) as an int, else None."""
|
|
311
|
+
span = numeric_range(text)
|
|
312
|
+
return span[0] if span is not None and span[0] == span[1] else None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def matches(record: Record, query: str) -> bool:
|
|
316
|
+
"""Does `query` name this row by value? A single number matches a row whose `value` (or `hex`) is that
|
|
317
|
+
number or a range containing it; anything else matches a row whose `value` equals it, ignoring case."""
|
|
318
|
+
wanted = number_of(query)
|
|
319
|
+
if wanted is None:
|
|
320
|
+
return query.strip().casefold() == record.key.casefold()
|
|
321
|
+
for candidate in (record.key, record.field("hex")):
|
|
322
|
+
span = numeric_range(candidate)
|
|
323
|
+
if span is not None and span[0] <= wanted <= span[1]:
|
|
324
|
+
return True
|
|
325
|
+
return False
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Read, look up and search imported IANA registries. Every function returns text with the fixed footer and raises
|
|
2
|
+
a `LibError` that names its remedy; nothing here writes or fetches.
|
|
3
|
+
|
|
4
|
+
Refs (grammar in `docs/iana.md`):
|
|
5
|
+
|
|
6
|
+
dns-parameters the file: its registries, each with the ref that reads it
|
|
7
|
+
dns-parameters§dns-parameters-4 one registry: header, then its records
|
|
8
|
+
dns-parameters#dns-parameters-4 the same, in IANA's own URL-fragment spelling
|
|
9
|
+
bgp-parameters§bgp-parameters-2#3 the record(s) whose value is 3 (a range containing 3 counts)
|
|
10
|
+
bgp-parameters§bgp-parameters-2¶4 from the 4th record of the registry, in file order
|
|
11
|
+
bgp-parameters§bgp-parameters-2#3¶2 from the 2nd record whose value is 3
|
|
12
|
+
dns-parameters¶5 the file's registry listing, from its 5th registry
|
|
13
|
+
|
|
14
|
+
A read's `next:` footer is the ref that reads the next page; a `¶n` read starts at record n and runs on.
|
|
15
|
+
iana:bgp-parameters§bgp-parameters-2¶4 an `iana:` prefix is accepted and dropped
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from primarysource import cursor
|
|
24
|
+
from primarysource.budget import page as budget_page
|
|
25
|
+
from primarysource.errors import LibError
|
|
26
|
+
from primarysource.iana import catalogue, store
|
|
27
|
+
from primarysource.iana.catalogue import BASE, NAME
|
|
28
|
+
from primarysource.iana.model import Record, Registry, RegistryFile, matches
|
|
29
|
+
from primarysource.model import span_hash
|
|
30
|
+
from primarysource.refs import Ref, RefGrammar
|
|
31
|
+
from primarysource.render import render
|
|
32
|
+
|
|
33
|
+
DOC = r"[a-z0-9][a-z0-9._-]*"
|
|
34
|
+
REGISTRY = r"[A-Za-z_][A-Za-z0-9_.\-]*"
|
|
35
|
+
_GRAMMAR = RefGrammar(DOC, REGISTRY)
|
|
36
|
+
UNIT = "record (¶n is the nth record of the registry, in file order)"
|
|
37
|
+
MAX_QUERY = 200
|
|
38
|
+
_SNIPPET = 200
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class Target:
|
|
43
|
+
"""A parsed ref: the file, the registry (None for the file itself), a value to look up and a record ordinal."""
|
|
44
|
+
|
|
45
|
+
file: str
|
|
46
|
+
registry: str | None = None
|
|
47
|
+
value: str | None = None
|
|
48
|
+
para: int | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_ref(text: str) -> Target:
|
|
52
|
+
"""Parse a ref or raise LibError. With a `§`, the anchor is a value; with no `§`, it names a registry."""
|
|
53
|
+
body = text.strip()
|
|
54
|
+
if body[:5].lower() == "iana:":
|
|
55
|
+
body = body[5:]
|
|
56
|
+
body, listing_para = cursor.split_doc_para(body)
|
|
57
|
+
parsed = _GRAMMAR.parse(body)
|
|
58
|
+
if parsed.lines is not None:
|
|
59
|
+
raise LibError(f"not an iana ref: {text!r}", "registries have no line ranges; use ¶n for the nth record")
|
|
60
|
+
if parsed.doc is None:
|
|
61
|
+
raise LibError(
|
|
62
|
+
f"not an iana ref: {text!r}", "start with the registry file's name, e.g. dns-parameters§dns-parameters-4"
|
|
63
|
+
)
|
|
64
|
+
if parsed.section is not None:
|
|
65
|
+
return Target(parsed.doc, parsed.section, parsed.anchor, parsed.para)
|
|
66
|
+
if parsed.anchor is not None:
|
|
67
|
+
return Target(parsed.doc, parsed.anchor, None, parsed.para)
|
|
68
|
+
if parsed.para is not None:
|
|
69
|
+
raise LibError(f"not an iana ref: {text!r}", "¶n needs a registry: dns-parameters§dns-parameters-4¶2")
|
|
70
|
+
return Target(parsed.doc, None, None, listing_para)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_ref(file: str, registry: str | None = None, value: str | None = None, para: int | None = None) -> str:
|
|
74
|
+
out = file
|
|
75
|
+
if registry is not None:
|
|
76
|
+
out += f"§{registry}"
|
|
77
|
+
if value is not None:
|
|
78
|
+
out += f"#{value}"
|
|
79
|
+
if para is not None:
|
|
80
|
+
out += f"¶{para}"
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class IanaRefs:
|
|
85
|
+
"""The collection's `RefParser`: `parse_ref` and `format_ref`, with the core's `Ref` carrying the file as `doc`,
|
|
86
|
+
the registry as `section`, a value as `anchor` and the record ordinal as `para`."""
|
|
87
|
+
|
|
88
|
+
def parse(self, text: str) -> Ref:
|
|
89
|
+
target = parse_ref(text)
|
|
90
|
+
return Ref(target.file, target.registry, target.para, target.value)
|
|
91
|
+
|
|
92
|
+
def format(self, ref: Ref) -> str:
|
|
93
|
+
if ref.doc is None or ref.lines is not None:
|
|
94
|
+
raise LibError(f"not an iana ref: {ref!r}", "an iana ref starts with the registry file's name")
|
|
95
|
+
return format_ref(ref.doc, ref.section, ref.anchor, ref.para)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
GRAMMAR = IanaRefs()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _url(file: str, registry: str | None = None) -> str:
|
|
102
|
+
return f"{BASE}{file}" + (f"#{registry}" if registry and registry != file else "")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _registry(loaded: store.Loaded, target: Target) -> Registry:
|
|
106
|
+
found = loaded.file.registry(target.registry or "")
|
|
107
|
+
if found is None:
|
|
108
|
+
ids = ", ".join(r.id for r in loaded.file.registries[:6])
|
|
109
|
+
raise LibError(
|
|
110
|
+
f"{target.file} has no registry {target.registry!r} (its registries start: {ids}, ...)",
|
|
111
|
+
f"read {target.file} to list them with their refs",
|
|
112
|
+
)
|
|
113
|
+
return found
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _warning(loaded: store.Loaded) -> str:
|
|
117
|
+
if loaded.digest_ok is False:
|
|
118
|
+
return "the file differs from the sha256 recorded when it was imported; import it again to trust it"
|
|
119
|
+
if loaded.digest_ok is None:
|
|
120
|
+
return "the file has no manifest entry, so its provenance is not recorded"
|
|
121
|
+
return ""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _header(file: RegistryFile, registry: Registry) -> str:
|
|
125
|
+
lines = [f"{registry.title} [{registry.id}] (file updated {file.updated or 'unknown'}"]
|
|
126
|
+
if registry.updated and registry.updated != file.updated:
|
|
127
|
+
lines[0] += f", registry updated {registry.updated}"
|
|
128
|
+
lines[0] += ")"
|
|
129
|
+
lines.append(f"source: {_url(file.name, registry.id)}")
|
|
130
|
+
for rule in registry.rules:
|
|
131
|
+
lines.append(f"registration procedure: {rule}")
|
|
132
|
+
for span in registry.ranges:
|
|
133
|
+
extra = f" (hex {span.hex})" if span.hex else ""
|
|
134
|
+
note = f"; {span.note}" if span.note else ""
|
|
135
|
+
lines.append(f"range {span.value}{extra}: {span.rule}{note}")
|
|
136
|
+
if registry.experts:
|
|
137
|
+
lines.append("expert: " + "; ".join(registry.experts))
|
|
138
|
+
for note in registry.notes:
|
|
139
|
+
lines.append(f"note: {note}")
|
|
140
|
+
for anchor, text in registry.footnotes:
|
|
141
|
+
lines.append(f"footnote {anchor}: {text}")
|
|
142
|
+
if registry.xrefs:
|
|
143
|
+
lines.append("references: " + ", ".join(dict.fromkeys(x.label for x in registry.xrefs)))
|
|
144
|
+
children = [r for r in file.registries if r.parent == registry.id]
|
|
145
|
+
for child in children:
|
|
146
|
+
lines.append(f"sub-registry: {format_ref(file.name, child.id)} {child.title}")
|
|
147
|
+
return "\n".join(lines)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _row(record: Record) -> str:
|
|
151
|
+
return f"¶{record.ordinal} {record.text()}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _cite(file: RegistryFile, registry: Registry | None = None, record: Record | None = None) -> str:
|
|
155
|
+
title = registry.title if registry else file.title
|
|
156
|
+
out = f'IANA, "{title}"'
|
|
157
|
+
if record is not None:
|
|
158
|
+
out += f", value {record.key}"
|
|
159
|
+
out += f", {_url(file.name, registry.id if registry else None)}, file updated {file.updated or 'unknown'}"
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def read(ref: str, max_tokens: int = 2000, base: Path | None = None) -> str:
|
|
164
|
+
"""Read by ref (see the module docstring). Pages are whole records; the `next:` footer is the ref that reads the
|
|
165
|
+
next page."""
|
|
166
|
+
target = parse_ref(ref)
|
|
167
|
+
loaded = store.load(target.file, base)
|
|
168
|
+
file = loaded.file
|
|
169
|
+
if target.registry is None:
|
|
170
|
+
units = tuple(
|
|
171
|
+
f"{' ' * _depth(file, r)}{format_ref(file.name, r.id)} {r.title} ({len(r.records)} records)"
|
|
172
|
+
for r in file.registries
|
|
173
|
+
)
|
|
174
|
+
result = budget_page(units, cursor.first_unit(target.para, len(units), "registries", ref), max_tokens)
|
|
175
|
+
body = f"{file.title} (file updated {file.updated or 'unknown'})\nsource: {_url(file.name)}\n\n"
|
|
176
|
+
body += "\n".join(result.units)
|
|
177
|
+
footer = {
|
|
178
|
+
"cite": _cite(file),
|
|
179
|
+
"unit": "registry (the listing is in file order)",
|
|
180
|
+
"next": cursor.read_next(file.name, result),
|
|
181
|
+
"warning": _warning(loaded),
|
|
182
|
+
}
|
|
183
|
+
return render(body, footer)
|
|
184
|
+
registry = _registry(loaded, target)
|
|
185
|
+
records: tuple[Record, ...] = registry.records
|
|
186
|
+
if target.value is not None:
|
|
187
|
+
records = tuple(r for r in records if matches(r, target.value or ""))
|
|
188
|
+
if not records:
|
|
189
|
+
return render(
|
|
190
|
+
f"no record in {registry.id} has value {target.value!r}",
|
|
191
|
+
{"cite": _cite(file, registry), "unit": UNIT, "warning": _warning(loaded)},
|
|
192
|
+
)
|
|
193
|
+
if target.para is not None and target.para > len(records):
|
|
194
|
+
where = f"{len(records)} records" + (f" with value {target.value!r}" if target.value is not None else "")
|
|
195
|
+
raise LibError(f"{ref!r}: {registry.id} has {where}", f"read {format_ref(file.name, registry.id)}")
|
|
196
|
+
units = tuple(_row(r) for r in records)
|
|
197
|
+
result = budget_page(units, target.para or 1, max_tokens)
|
|
198
|
+
body = "\n".join(result.units)
|
|
199
|
+
if target.para is None and target.value is None:
|
|
200
|
+
body = _header(file, registry) + "\n\n" + body
|
|
201
|
+
elif target.value is not None:
|
|
202
|
+
body = f"{registry.title} [{registry.id}]\n\n" + body
|
|
203
|
+
one = records[result.first - 1] if len(result.units) == 1 and (target.para or target.value) else None
|
|
204
|
+
footer = {
|
|
205
|
+
"cite": _cite(file, registry, one),
|
|
206
|
+
"unit": UNIT,
|
|
207
|
+
"span": span_hash(body),
|
|
208
|
+
"next": cursor.read_next(format_ref(file.name, registry.id, target.value), result),
|
|
209
|
+
"warning": _warning(loaded),
|
|
210
|
+
}
|
|
211
|
+
return render(body, footer)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _depth(file: RegistryFile, registry: Registry) -> int:
|
|
215
|
+
depth = 0
|
|
216
|
+
parent = registry.parent
|
|
217
|
+
while parent:
|
|
218
|
+
depth += 1
|
|
219
|
+
found = file.registry(parent)
|
|
220
|
+
parent = found.parent if found else ""
|
|
221
|
+
return depth
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def lookup(where: str, value: str, base: Path | None = None) -> str:
|
|
225
|
+
"""The record(s) with this value in a registry, or in every registry of a file if `where` is a file name.
|
|
226
|
+
|
|
227
|
+
A single number matches a record whose value is that number or a range containing it (decimal or 0x hex). Any
|
|
228
|
+
other text matches a value equal to it, ignoring case; failing that, a column equal to it (`A` is an RR type)."""
|
|
229
|
+
if not value.strip() or len(value) > MAX_QUERY:
|
|
230
|
+
raise LibError(f"not a value to look up: {value!r}", f"give 1 to {MAX_QUERY} characters")
|
|
231
|
+
target = parse_ref(where)
|
|
232
|
+
loaded = store.load(target.file, base)
|
|
233
|
+
file = loaded.file
|
|
234
|
+
scope = [_registry(loaded, target)] if target.registry else list(file.registries)
|
|
235
|
+
lines: list[str] = []
|
|
236
|
+
pool: list[tuple[Registry, Record]] = []
|
|
237
|
+
for registry in scope:
|
|
238
|
+
hits = [r for r in registry.records if matches(r, value)]
|
|
239
|
+
if not hits:
|
|
240
|
+
wanted = value.strip().casefold()
|
|
241
|
+
hits = [r for r in registry.records if any(text.casefold() == wanted for _, text in r.fields)]
|
|
242
|
+
for record in hits:
|
|
243
|
+
pool.append((registry, record))
|
|
244
|
+
for registry, record in pool:
|
|
245
|
+
lines.append(f"{format_ref(file.name, registry.id, para=record.ordinal)} {registry.title}: {record.text()}")
|
|
246
|
+
if not lines:
|
|
247
|
+
return render(
|
|
248
|
+
f"no record has value {value!r} in {where}",
|
|
249
|
+
{"cite": _cite(file, scope[0] if len(scope) == 1 else None), "unit": UNIT, "warning": _warning(loaded)},
|
|
250
|
+
)
|
|
251
|
+
only = pool[0][0] if len({r.id for r, _ in pool}) == 1 else None
|
|
252
|
+
footer = {
|
|
253
|
+
"cite": _cite(file, only, pool[0][1] if len(pool) == 1 else None),
|
|
254
|
+
"unit": UNIT,
|
|
255
|
+
"span": span_hash("\n".join(lines)),
|
|
256
|
+
"warning": _warning(loaded),
|
|
257
|
+
}
|
|
258
|
+
return render("\n".join(lines), footer)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def search(text: str, file: str | None = None, limit: int = 20, start: int = 0, base: Path | None = None) -> str:
|
|
262
|
+
"""Records whose registry title and columns contain every word of `text` (case-insensitive, literal, at most
|
|
263
|
+
200 characters), across every imported file or one. One line per hit: `<ref> <registry>: <record>`."""
|
|
264
|
+
words = text.casefold().split()
|
|
265
|
+
if not words or len(text) > MAX_QUERY:
|
|
266
|
+
raise LibError(f"not a search: {text!r}", f"give 1 to {MAX_QUERY} characters")
|
|
267
|
+
names = (file,) if file else store.imported(base)
|
|
268
|
+
if not names:
|
|
269
|
+
raise LibError(
|
|
270
|
+
"no registry is imported",
|
|
271
|
+
"iana_list shows the catalogue",
|
|
272
|
+
"primarysource iana import --all (or name one, e.g. dns-parameters)",
|
|
273
|
+
)
|
|
274
|
+
hits: list[str] = []
|
|
275
|
+
warnings: list[str] = []
|
|
276
|
+
for name in names:
|
|
277
|
+
if not NAME.fullmatch(name):
|
|
278
|
+
raise LibError(f"{name!r} is not a registry file name", "")
|
|
279
|
+
loaded = store.load(name, base)
|
|
280
|
+
if loaded.digest_ok is False:
|
|
281
|
+
warnings.append(name)
|
|
282
|
+
for registry in loaded.file.registries:
|
|
283
|
+
title = registry.title.casefold()
|
|
284
|
+
for record in registry.records:
|
|
285
|
+
row = record.text()
|
|
286
|
+
if all(w in title or w in row.casefold() for w in words):
|
|
287
|
+
snippet = row if len(row) <= _SNIPPET else row[: _SNIPPET - 1] + "…"
|
|
288
|
+
hits.append(f"{format_ref(name, registry.id, para=record.ordinal)} {registry.title}: {snippet}")
|
|
289
|
+
shown, nxt = cursor.window(hits, start, limit)
|
|
290
|
+
body = "\n".join(shown) if shown else f"no record matches {text!r}"
|
|
291
|
+
footer = {
|
|
292
|
+
"unit": UNIT,
|
|
293
|
+
"next": nxt,
|
|
294
|
+
"warning": ("files differ from their recorded sha256: " + ", ".join(warnings)) if warnings else "",
|
|
295
|
+
}
|
|
296
|
+
return render(body, footer)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def listing(base: Path | None = None) -> str:
|
|
300
|
+
"""The catalogue, each registry marked with whether it is imported, its `updated` date and sha256 prefix."""
|
|
301
|
+
root = base or store.home()
|
|
302
|
+
manifest = store.read_manifest(root)
|
|
303
|
+
present = set(store.imported(root))
|
|
304
|
+
lines = []
|
|
305
|
+
for entry in catalogue.ENTRIES:
|
|
306
|
+
recorded = manifest.get(entry.name, {})
|
|
307
|
+
state = (
|
|
308
|
+
f"imported, updated {recorded.get('updated') or '?'}, sha256 {str(recorded.get('sha256', ''))[:12]}"
|
|
309
|
+
if entry.name in present
|
|
310
|
+
else "not imported"
|
|
311
|
+
)
|
|
312
|
+
lines.append(f"{entry.name} {entry.title} [{state}]")
|
|
313
|
+
for name in sorted(present - {e.name for e in catalogue.ENTRIES}):
|
|
314
|
+
lines.append(f"{name} (not in the catalogue) [imported]")
|
|
315
|
+
return render("\n".join(lines), {})
|