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/home.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Where a library keeps its data, and writing there without leaving half a file.
|
|
2
|
+
|
|
3
|
+
One variable per library (`<APP>_HOME`), else a per-user data directory, never the working directory and never
|
|
4
|
+
a path beside the installed package: nistlib and usblib default to `parents[2]/data`, which points into
|
|
5
|
+
site-packages once installed from a wheel.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from primarysource.fsutil import atomic_write as atomic_write # moved to fsutil; kept here for importers
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_home(app: str, environ: Mapping[str, str] | None = None, platform: str | None = None) -> Path:
|
|
19
|
+
"""`$<APP>_HOME` if set, else the per-user data directory for `app`. Nothing is created."""
|
|
20
|
+
env = os.environ if environ is None else environ
|
|
21
|
+
system = sys.platform if platform is None else platform
|
|
22
|
+
explicit = env.get(f"{app.upper()}_HOME")
|
|
23
|
+
if explicit:
|
|
24
|
+
return Path(explicit)
|
|
25
|
+
if system == "win32" and env.get("LOCALAPPDATA"):
|
|
26
|
+
return Path(env["LOCALAPPDATA"]) / app
|
|
27
|
+
if system == "darwin":
|
|
28
|
+
return Path.home() / "Library" / "Application Support" / app
|
|
29
|
+
if env.get("XDG_DATA_HOME"):
|
|
30
|
+
return Path(env["XDG_DATA_HOME"]) / app
|
|
31
|
+
return Path.home() / ".local" / "share" / app
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def collection_home(
|
|
35
|
+
name: str,
|
|
36
|
+
legacy_app: str | None = None,
|
|
37
|
+
environ: Mapping[str, str] | None = None,
|
|
38
|
+
platform: str | None = None,
|
|
39
|
+
) -> Path:
|
|
40
|
+
"""Where one collection keeps its library (PLAN.md §5.8). The first that applies:
|
|
41
|
+
|
|
42
|
+
1. `$<LEGACY_APP>_HOME`, e.g. `RFCLIB_HOME`: an explicit setting from before the move keeps working;
|
|
43
|
+
2. `$PRIMARYSOURCE_<NAME>_HOME`, e.g. `PRIMARYSOURCE_ISO_HOME`: one collection somewhere of its own (another
|
|
44
|
+
drive, a mounted volume) while the others stay where they are;
|
|
45
|
+
3. `$PRIMARYSOURCE_HOME/<name>`;
|
|
46
|
+
4. the legacy app's per-user directory (`%LOCALAPPDATA%\\rfclib`), if it already exists, so an existing
|
|
47
|
+
library is found instead of an empty one being started beside it;
|
|
48
|
+
5. the per-user `primarysource/<name>`.
|
|
49
|
+
|
|
50
|
+
Nothing is created.
|
|
51
|
+
"""
|
|
52
|
+
env = dict(os.environ if environ is None else environ)
|
|
53
|
+
legacy_var = f"{legacy_app.upper()}_HOME" if legacy_app else ""
|
|
54
|
+
if legacy_app and env.get(legacy_var):
|
|
55
|
+
return Path(env[legacy_var])
|
|
56
|
+
own = env.get(f"PRIMARYSOURCE_{name.upper()}_HOME")
|
|
57
|
+
if own:
|
|
58
|
+
return Path(own)
|
|
59
|
+
if env.get("PRIMARYSOURCE_HOME"):
|
|
60
|
+
return Path(env["PRIMARYSOURCE_HOME"]) / name
|
|
61
|
+
if legacy_app:
|
|
62
|
+
old = resolve_home(legacy_app, env, platform)
|
|
63
|
+
if old.is_dir():
|
|
64
|
+
return old
|
|
65
|
+
return resolve_home("primarysource", env, platform) / name
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""IANA protocol registries as a primarysource collection: `iana`.
|
|
2
|
+
|
|
3
|
+
The registries are structured XML under `https://www.iana.org/assignments/`, dedicated to the public domain
|
|
4
|
+
(CC0 1.0) by IANA and the IETF for the Protocol Registries; `docs/iana.md` has the terms, the format and the ref
|
|
5
|
+
grammar. Nothing is shipped: `primarysource iana import <name>` (at most one request a second) fetches files into
|
|
6
|
+
`$PRIMARYSOURCE_HOME/iana/`.
|
|
7
|
+
|
|
8
|
+
Importing this package needs only the standard library; `mcp` is imported by `server.py` when the collection is
|
|
9
|
+
served (`primarysource mcp --collection iana`). `primarysource iana ...` runs `cli.py`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from primarysource.iana.reader import DOC, GRAMMAR
|
|
15
|
+
from primarysource.model import Unit
|
|
16
|
+
from primarysource.registry import Collection
|
|
17
|
+
|
|
18
|
+
COLLECTION = Collection(
|
|
19
|
+
name="iana",
|
|
20
|
+
title="IANA protocol registries (assignments)",
|
|
21
|
+
prefix="iana_",
|
|
22
|
+
unit=Unit.BLOCK,
|
|
23
|
+
grammar=GRAMMAR,
|
|
24
|
+
server="primarysource.iana.server:server",
|
|
25
|
+
cli="primarysource.iana.cli:main",
|
|
26
|
+
)
|
|
27
|
+
"""Refs: `dns-parameters§dns-parameters-4` (a registry, by its IANA id), `bgp-parameters§bgp-parameters-2#3` (the
|
|
28
|
+
record whose value is 3), `bgp-parameters§bgp-parameters-2¶4` (the 4th record in file order). `§` names the
|
|
29
|
+
registry id, `#` a value, `¶n` the nth record. `dns-parameters#dns-parameters-4`, IANA's own URL fragment, names
|
|
30
|
+
the registry too. The unit is the record: each is one line of the registry's columns and its RFC citations."""
|
|
31
|
+
|
|
32
|
+
__all__ = ["COLLECTION", "DOC"]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"note": "Catalogue metadata only. Registry content is fetched by the importer into $PRIMARYSOURCE_HOME/iana/ and never belongs in this repository.",
|
|
4
|
+
"terms": {
|
|
5
|
+
"licence": "CC0-1.0",
|
|
6
|
+
"placement": "layer",
|
|
7
|
+
"terms_url": "https://www.iana.org/help/licensing-terms",
|
|
8
|
+
"terms_read": "2026-11-13",
|
|
9
|
+
"summary": "Joint IANA/IETF statement (revised 2021-11-10): the Protocol Registries linked from iana.org/protocols may be freely used by any party for any purpose, under the CC0 1.0 dedication. It excludes the root zone, .INT and .ARPA, the IDN repository, the number registries and RIR allocation data (IP address and AS number registries) and the Time Zone Database. No warranty; third-party rights are not guaranteed absent. Nothing on iana.org or robots.txt states a request rate; the importer sends at most one request per second."
|
|
10
|
+
},
|
|
11
|
+
"registries": [
|
|
12
|
+
{
|
|
13
|
+
"name": "dns-parameters",
|
|
14
|
+
"title": "DNS parameters (classes, RR types, opcodes, rcodes, EDNS, EDE)",
|
|
15
|
+
"url": "https://www.iana.org/assignments/dns-parameters/dns-parameters.xml",
|
|
16
|
+
"likely_users": "namespy",
|
|
17
|
+
"read": "2026-11-13"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "dns-sec-alg-numbers",
|
|
21
|
+
"title": "DNS Security Algorithm Numbers",
|
|
22
|
+
"url": "https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xml",
|
|
23
|
+
"likely_users": "namespy",
|
|
24
|
+
"read": "2026-11-13"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "dnskey-flags",
|
|
28
|
+
"title": "DNSKEY RR Flags",
|
|
29
|
+
"url": "https://www.iana.org/assignments/dnskey-flags/dnskey-flags.xml",
|
|
30
|
+
"likely_users": "namespy",
|
|
31
|
+
"read": "2026-11-13"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "ds-rr-types",
|
|
35
|
+
"title": "Delegation Signer (DS) Resource Record Type Digest Algorithms",
|
|
36
|
+
"url": "https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xml",
|
|
37
|
+
"likely_users": "namespy",
|
|
38
|
+
"read": "2026-11-13"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "dnssec-nsec3-parameters",
|
|
42
|
+
"title": "DNSSEC NSEC3 Parameters",
|
|
43
|
+
"url": "https://www.iana.org/assignments/dnssec-nsec3-parameters/dnssec-nsec3-parameters.xml",
|
|
44
|
+
"likely_users": "namespy",
|
|
45
|
+
"read": "2026-11-13"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "dns-svcb",
|
|
49
|
+
"title": "DNS Service Bindings (SVCB)",
|
|
50
|
+
"url": "https://www.iana.org/assignments/dns-svcb/dns-svcb.xml",
|
|
51
|
+
"likely_users": "namespy",
|
|
52
|
+
"read": "2026-11-13"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "bootp-dhcp-parameters",
|
|
56
|
+
"title": "DHCP and BOOTP parameters (options, message types)",
|
|
57
|
+
"url": "https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xml",
|
|
58
|
+
"likely_users": "leasespy",
|
|
59
|
+
"read": "2026-11-13"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "dhcpv6-parameters",
|
|
63
|
+
"title": "DHCPv6 parameters",
|
|
64
|
+
"url": "https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xml",
|
|
65
|
+
"likely_users": "leasespy",
|
|
66
|
+
"read": "2026-11-13"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "bgp-parameters",
|
|
70
|
+
"title": "BGP parameters (message types, path attributes, capabilities, error codes)",
|
|
71
|
+
"url": "https://www.iana.org/assignments/bgp-parameters/bgp-parameters.xml",
|
|
72
|
+
"likely_users": "rtrkit, pathattr, bmpkit",
|
|
73
|
+
"read": "2026-11-13"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "bgp-extended-communities",
|
|
77
|
+
"title": "BGP Extended Communities",
|
|
78
|
+
"url": "https://www.iana.org/assignments/bgp-extended-communities/bgp-extended-communities.xml",
|
|
79
|
+
"likely_users": "pathattr, bmpkit",
|
|
80
|
+
"read": "2026-11-13"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "bgp-well-known-communities",
|
|
84
|
+
"title": "BGP Well-known Communities",
|
|
85
|
+
"url": "https://www.iana.org/assignments/bgp-well-known-communities/bgp-well-known-communities.xml",
|
|
86
|
+
"likely_users": "pathattr, bmpkit",
|
|
87
|
+
"read": "2026-11-13"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "bgp-tunnel-encapsulation",
|
|
91
|
+
"title": "BGP Tunnel Encapsulation",
|
|
92
|
+
"url": "https://www.iana.org/assignments/bgp-tunnel-encapsulation/bgp-tunnel-encapsulation.xml",
|
|
93
|
+
"likely_users": "pathattr",
|
|
94
|
+
"read": "2026-11-13"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "bgp-ls-parameters",
|
|
98
|
+
"title": "BGP-LS parameters",
|
|
99
|
+
"url": "https://www.iana.org/assignments/bgp-ls-parameters/bgp-ls-parameters.xml",
|
|
100
|
+
"likely_users": "pathattr, bmpkit",
|
|
101
|
+
"read": "2026-11-13"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "rpki",
|
|
105
|
+
"title": "Resource Public Key Infrastructure (RPKI) parameters",
|
|
106
|
+
"url": "https://www.iana.org/assignments/rpki/rpki.xml",
|
|
107
|
+
"likely_users": "rtrkit, rpkit",
|
|
108
|
+
"read": "2026-11-13"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "stun-parameters",
|
|
112
|
+
"title": "STUN parameters (methods, attributes, error codes)",
|
|
113
|
+
"url": "https://www.iana.org/assignments/stun-parameters/stun-parameters.xml",
|
|
114
|
+
"likely_users": "turnkit",
|
|
115
|
+
"read": "2026-11-13"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "tls-parameters",
|
|
119
|
+
"title": "TLS parameters (cipher suites, groups, signature schemes, alerts)",
|
|
120
|
+
"url": "https://www.iana.org/assignments/tls-parameters/tls-parameters.xml",
|
|
121
|
+
"likely_users": "certledger, ctspy",
|
|
122
|
+
"read": "2026-11-13"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "tls-extensiontype-values",
|
|
126
|
+
"title": "TLS extension types",
|
|
127
|
+
"url": "https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xml",
|
|
128
|
+
"likely_users": "certledger, ctspy",
|
|
129
|
+
"read": "2026-11-13"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "http-methods",
|
|
133
|
+
"title": "HTTP method registry",
|
|
134
|
+
"url": "https://www.iana.org/assignments/http-methods/http-methods.xml",
|
|
135
|
+
"likely_users": "h3tunnel, sfields",
|
|
136
|
+
"read": "2026-11-13"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "http-status-codes",
|
|
140
|
+
"title": "HTTP status codes",
|
|
141
|
+
"url": "https://www.iana.org/assignments/http-status-codes/http-status-codes.xml",
|
|
142
|
+
"likely_users": "h3tunnel, sfields",
|
|
143
|
+
"read": "2026-11-13"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "http-fields",
|
|
147
|
+
"title": "HTTP field name registry",
|
|
148
|
+
"url": "https://www.iana.org/assignments/http-fields/http-fields.xml",
|
|
149
|
+
"likely_users": "sfields",
|
|
150
|
+
"read": "2026-11-13"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "pkix-parameters",
|
|
154
|
+
"title": "PKIX parameters",
|
|
155
|
+
"url": "https://www.iana.org/assignments/pkix-parameters/pkix-parameters.xml",
|
|
156
|
+
"likely_users": "pkix, certledger",
|
|
157
|
+
"read": "2026-11-13"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "protocol-numbers",
|
|
161
|
+
"title": "IP protocol numbers",
|
|
162
|
+
"url": "https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xml",
|
|
163
|
+
"likely_users": "pathattr, syslogkit",
|
|
164
|
+
"read": "2026-11-13"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "address-family-numbers",
|
|
168
|
+
"title": "Address family numbers",
|
|
169
|
+
"url": "https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xml",
|
|
170
|
+
"likely_users": "bmpkit, pathattr",
|
|
171
|
+
"read": "2026-11-13"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "icmp-parameters",
|
|
175
|
+
"title": "ICMP parameters",
|
|
176
|
+
"url": "https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xml",
|
|
177
|
+
"likely_users": "namespy",
|
|
178
|
+
"read": "2026-11-13"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "icmpv6-parameters",
|
|
182
|
+
"title": "ICMPv6 parameters",
|
|
183
|
+
"url": "https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xml",
|
|
184
|
+
"likely_users": "namespy",
|
|
185
|
+
"read": "2026-11-13"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "ikev2-parameters",
|
|
189
|
+
"title": "IKEv2 parameters",
|
|
190
|
+
"url": "https://www.iana.org/assignments/ikev2-parameters/ikev2-parameters.xml",
|
|
191
|
+
"likely_users": "srtpkit",
|
|
192
|
+
"read": "2026-11-13"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "ipfix",
|
|
196
|
+
"title": "IP Flow Information Export (IPFIX) entities",
|
|
197
|
+
"url": "https://www.iana.org/assignments/ipfix/ipfix.xml",
|
|
198
|
+
"likely_users": "syslogkit",
|
|
199
|
+
"read": "2026-11-13"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "ppp-numbers",
|
|
203
|
+
"title": "Point-to-Point Protocol field assignments",
|
|
204
|
+
"url": "https://www.iana.org/assignments/ppp-numbers/ppp-numbers.xml",
|
|
205
|
+
"likely_users": "leasespy",
|
|
206
|
+
"read": "2026-11-13"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "radius-types",
|
|
210
|
+
"title": "RADIUS types",
|
|
211
|
+
"url": "https://www.iana.org/assignments/radius-types/radius-types.xml",
|
|
212
|
+
"likely_users": "tacacs",
|
|
213
|
+
"read": "2026-11-13"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "syslog-parameters",
|
|
217
|
+
"title": "Syslog parameters",
|
|
218
|
+
"url": "https://www.iana.org/assignments/syslog-parameters/syslog-parameters.xml",
|
|
219
|
+
"likely_users": "syslogkit",
|
|
220
|
+
"read": "2026-11-13"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "ethernet-numbers",
|
|
224
|
+
"title": "Ethernet numbers",
|
|
225
|
+
"url": "https://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xml",
|
|
226
|
+
"likely_users": "leasespy",
|
|
227
|
+
"read": "2026-11-13"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "ntp-parameters",
|
|
231
|
+
"title": "NTP parameters",
|
|
232
|
+
"url": "https://www.iana.org/assignments/ntp-parameters/ntp-parameters.xml",
|
|
233
|
+
"likely_users": "ntspy",
|
|
234
|
+
"read": "2026-11-13"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "tcp-parameters",
|
|
238
|
+
"title": "TCP parameters (options, flags)",
|
|
239
|
+
"url": "https://www.iana.org/assignments/tcp-parameters/tcp-parameters.xml",
|
|
240
|
+
"likely_users": "h3tunnel",
|
|
241
|
+
"read": "2026-11-13"
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""The catalogue of registries the portfolio needs, and IANA's terms for them. Metadata only, never content.
|
|
2
|
+
|
|
3
|
+
`catalogue.json` is package data. Each entry has the registry file's name, its URL and the date the URL was last
|
|
4
|
+
confirmed to serve a registry file (`read`). `likely_users` names the portfolio projects that want the data and is
|
|
5
|
+
indicative, not a dependency declaration. Any other registry under `https://www.iana.org/assignments/` can be
|
|
6
|
+
imported by name too (`primarysource iana import <name>`); the catalogue is the list the portfolio needs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from primarysource.errors import LibError
|
|
18
|
+
|
|
19
|
+
BASE = "https://www.iana.org/assignments/"
|
|
20
|
+
NAME = re.compile(r"[a-z0-9][a-z0-9._-]*")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class Terms:
|
|
25
|
+
licence: str
|
|
26
|
+
placement: str
|
|
27
|
+
terms_url: str
|
|
28
|
+
terms_read: str
|
|
29
|
+
summary: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class Entry:
|
|
34
|
+
name: str
|
|
35
|
+
title: str
|
|
36
|
+
url: str
|
|
37
|
+
likely_users: str
|
|
38
|
+
read: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def url_for(name: str) -> str:
|
|
42
|
+
"""`https://www.iana.org/assignments/<name>/<name>.xml`, the layout every registry file follows."""
|
|
43
|
+
if not NAME.fullmatch(name) or ".." in name:
|
|
44
|
+
raise LibError(
|
|
45
|
+
f"{name!r} is not a registry file name", "names are lowercase letters, digits, '-', '_' and '.'"
|
|
46
|
+
)
|
|
47
|
+
return f"{BASE}{name}/{name}.xml"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load() -> tuple[Terms, tuple[Entry, ...]]:
|
|
51
|
+
raw: dict[str, Any] = json.loads(
|
|
52
|
+
resources.files("primarysource.iana").joinpath("catalogue.json").read_text("utf-8")
|
|
53
|
+
)
|
|
54
|
+
t = raw["terms"]
|
|
55
|
+
terms = Terms(t["licence"], t["placement"], t["terms_url"], t["terms_read"], t["summary"])
|
|
56
|
+
entries = tuple(
|
|
57
|
+
Entry(e["name"], e["title"], e["url"], e.get("likely_users", ""), e["read"]) for e in raw["registries"]
|
|
58
|
+
)
|
|
59
|
+
for entry in entries:
|
|
60
|
+
if entry.url != url_for(entry.name):
|
|
61
|
+
raise ValueError(f"catalogue entry {entry.name!r} has an unexpected URL {entry.url!r}")
|
|
62
|
+
return terms, entries
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
TERMS, ENTRIES = _load()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def entry(name: str) -> Entry | None:
|
|
69
|
+
return next((e for e in ENTRIES if e.name == name), None)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""`primarysource iana ...`: the collection's own commands.
|
|
2
|
+
|
|
3
|
+
primarysource iana list
|
|
4
|
+
primarysource iana import <name>... | --all [--force] [--interval SECONDS]
|
|
5
|
+
primarysource iana read <ref> [--max-tokens N]
|
|
6
|
+
primarysource iana lookup <registry-ref> <value>
|
|
7
|
+
primarysource iana search <text> [--file NAME] [--limit N] [--start N]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
from primarysource.errors import LibError
|
|
17
|
+
from primarysource.iana import importer, reader
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parser() -> argparse.ArgumentParser:
|
|
21
|
+
parser = argparse.ArgumentParser(prog="primarysource iana", description="IANA protocol registries")
|
|
22
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
23
|
+
commands.add_parser("list", help="the catalogue, marked imported or not")
|
|
24
|
+
imp = commands.add_parser("import", help="fetch registry XML from iana.org (at most 1 request per second)")
|
|
25
|
+
imp.add_argument("names", nargs="*", help="registry file names, e.g. dns-parameters")
|
|
26
|
+
imp.add_argument("--all", action="store_true", help="every registry in the catalogue")
|
|
27
|
+
imp.add_argument("--force", action="store_true", help="fetch even if the server says nothing changed")
|
|
28
|
+
imp.add_argument("--interval", type=float, default=importer.MIN_INTERVAL, help="seconds between requests (>= 1)")
|
|
29
|
+
read = commands.add_parser("read", help="read by ref")
|
|
30
|
+
read.add_argument("ref")
|
|
31
|
+
read.add_argument("--max-tokens", type=int, default=2000)
|
|
32
|
+
look = commands.add_parser("lookup", help="the record(s) with a value")
|
|
33
|
+
look.add_argument("registry")
|
|
34
|
+
look.add_argument("value")
|
|
35
|
+
find = commands.add_parser("search", help="find records containing every word")
|
|
36
|
+
find.add_argument("text")
|
|
37
|
+
find.add_argument("--file")
|
|
38
|
+
find.add_argument("--limit", type=int, default=20)
|
|
39
|
+
find.add_argument("--start", type=int, default=0)
|
|
40
|
+
return parser
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _out(text: str) -> int:
|
|
44
|
+
# UTF-8 bytes: registry text has names a Windows console code page cannot encode
|
|
45
|
+
sys.stdout.flush()
|
|
46
|
+
sys.stdout.buffer.write((text if text.endswith("\n") else text + "\n").encode("utf-8"))
|
|
47
|
+
sys.stdout.buffer.flush()
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main(argv: Sequence[str]) -> int:
|
|
52
|
+
args = _parser().parse_args(list(argv))
|
|
53
|
+
try:
|
|
54
|
+
if args.command == "list":
|
|
55
|
+
return _out(reader.listing())
|
|
56
|
+
if args.command == "import":
|
|
57
|
+
names = importer.catalogue_names() if args.all else args.names
|
|
58
|
+
report = importer.import_registries(names, force=args.force, interval=args.interval)
|
|
59
|
+
for o in report.outcomes:
|
|
60
|
+
print(f"{o.name}: {o.status}, updated {o.updated or '-'}, {o.size:,} bytes, sha256 {o.sha256[:16]}")
|
|
61
|
+
return 0
|
|
62
|
+
if args.command == "read":
|
|
63
|
+
return _out(reader.read(args.ref, max_tokens=args.max_tokens))
|
|
64
|
+
if args.command == "lookup":
|
|
65
|
+
return _out(reader.lookup(args.registry, args.value))
|
|
66
|
+
return _out(reader.search(args.text, file=args.file, limit=args.limit, start=args.start))
|
|
67
|
+
except LibError as exc:
|
|
68
|
+
print(exc.as_text(), file=sys.stderr)
|
|
69
|
+
return 1
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Fetch registry XML from iana.org into `$PRIMARYSOURCE_HOME/iana/`.
|
|
2
|
+
|
|
3
|
+
Polite by construction: at most one request per `interval` seconds (never under 1.0), an identifying User-Agent,
|
|
4
|
+
conditional requests (`If-Modified-Since`) so an unchanged file costs a 304, no redirects followed, only
|
|
5
|
+
`https://www.iana.org/assignments/<name>/<name>.xml`, and a response size ceiling. Each file is parsed with the
|
|
6
|
+
hardened reader and its root id checked against the name before anything is written; the bytes are stored
|
|
7
|
+
exactly as served, atomically, and `manifest.json` records sha256, size and the registry's `updated` date.
|
|
8
|
+
|
|
9
|
+
The transport is a parameter, so tests inject one; the default uses `urllib`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from primarysource.errors import LibError
|
|
26
|
+
from primarysource.home import atomic_write
|
|
27
|
+
from primarysource.iana import xmlsafe
|
|
28
|
+
from primarysource.iana.catalogue import BASE, ENTRIES, url_for
|
|
29
|
+
from primarysource.iana.model import from_tree
|
|
30
|
+
from primarysource.iana.store import MANIFEST, MAX_BYTES, home, read_manifest
|
|
31
|
+
|
|
32
|
+
USER_AGENT = "primarysource-iana/0.1 (local registry mirror; at most 1 request per second)"
|
|
33
|
+
MIN_INTERVAL = 1.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class Response:
|
|
38
|
+
status: int
|
|
39
|
+
headers: Mapping[str, str]
|
|
40
|
+
body: bytes
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Transport = Callable[[str, Mapping[str, str]], Response]
|
|
44
|
+
"""`transport(url, request_headers) -> Response`. It must not follow redirects and must bound the body."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
48
|
+
def redirect_request(self, *_args: object, **_kwargs: object) -> None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def urllib_transport(url: str, headers: Mapping[str, str]) -> Response:
|
|
53
|
+
"""The default transport: one GET, https to www.iana.org only, no redirects, body capped at MAX_BYTES."""
|
|
54
|
+
if not url.startswith(BASE):
|
|
55
|
+
raise LibError(f"refusing to fetch {url!r}", f"only {BASE}<name>/<name>.xml is fetched")
|
|
56
|
+
opener = urllib.request.build_opener(_NoRedirect)
|
|
57
|
+
request = urllib.request.Request(url, headers=dict(headers)) # noqa: S310 - the https prefix is checked above
|
|
58
|
+
try:
|
|
59
|
+
with opener.open(request, timeout=60) as reply:
|
|
60
|
+
body = reply.read(MAX_BYTES + 1)
|
|
61
|
+
return Response(reply.status, {k.lower(): v for k, v in reply.headers.items()}, body)
|
|
62
|
+
except urllib.error.HTTPError as exc:
|
|
63
|
+
return Response(exc.code, {k.lower(): v for k, v in exc.headers.items()}, b"")
|
|
64
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
65
|
+
raise LibError(f"could not reach {url}: {exc}", "check the network and try again") from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class Outcome:
|
|
70
|
+
name: str
|
|
71
|
+
status: str
|
|
72
|
+
"""`fetched`, `unchanged` (HTTP 304) or `identical` (HTTP 200 with the sha256 already held)."""
|
|
73
|
+
sha256: str
|
|
74
|
+
size: int
|
|
75
|
+
updated: str
|
|
76
|
+
path: Path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(slots=True)
|
|
80
|
+
class Report:
|
|
81
|
+
outcomes: list[Outcome] = field(default_factory=list)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def catalogue_names() -> list[str]:
|
|
85
|
+
return [e.name for e in ENTRIES]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def import_registries(
|
|
89
|
+
names: Sequence[str],
|
|
90
|
+
*,
|
|
91
|
+
base: Path | None = None,
|
|
92
|
+
transport: Transport | None = None,
|
|
93
|
+
interval: float = MIN_INTERVAL,
|
|
94
|
+
force: bool = False,
|
|
95
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
96
|
+
clock: Callable[[], float] = time.monotonic,
|
|
97
|
+
user_agent: str = USER_AGENT,
|
|
98
|
+
now: Callable[[], datetime] = lambda: datetime.now(UTC),
|
|
99
|
+
) -> Report:
|
|
100
|
+
"""Fetch each named registry file. The first failure stops the run and leaves earlier files in place."""
|
|
101
|
+
if os.environ.get("PRIMARYSOURCE_READONLY"):
|
|
102
|
+
raise LibError("the library is read-only (PRIMARYSOURCE_READONLY is set), so nothing is fetched", "unset it")
|
|
103
|
+
if interval < MIN_INTERVAL:
|
|
104
|
+
raise LibError(f"an interval under {MIN_INTERVAL:g} second is refused", "registries are fetched politely")
|
|
105
|
+
wanted = list(dict.fromkeys(names))
|
|
106
|
+
if not wanted:
|
|
107
|
+
raise LibError("no registry named", "name one, e.g. dns-parameters, or use --all for the catalogue")
|
|
108
|
+
urls = {name: url_for(name) for name in wanted} # validates every spelling before the first request
|
|
109
|
+
send: Transport = transport or urllib_transport
|
|
110
|
+
root = base or home()
|
|
111
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
files = read_manifest(root)
|
|
113
|
+
report = Report()
|
|
114
|
+
last: float | None = None
|
|
115
|
+
for name in wanted:
|
|
116
|
+
if last is not None:
|
|
117
|
+
wait = interval - (clock() - last)
|
|
118
|
+
if wait > 0:
|
|
119
|
+
sleep(wait)
|
|
120
|
+
url = urls[name]
|
|
121
|
+
headers = {"User-Agent": user_agent, "Accept": "application/xml"}
|
|
122
|
+
previous = files.get(name, {})
|
|
123
|
+
target = root / f"{name}.xml"
|
|
124
|
+
if not force and target.is_file() and previous.get("last_modified"):
|
|
125
|
+
headers["If-Modified-Since"] = str(previous["last_modified"])
|
|
126
|
+
last = clock()
|
|
127
|
+
reply = send(url, headers)
|
|
128
|
+
if reply.status == 304 and target.is_file() and previous:
|
|
129
|
+
size = int(previous.get("size", 0))
|
|
130
|
+
report.outcomes.append(
|
|
131
|
+
Outcome(
|
|
132
|
+
name, "unchanged", str(previous.get("sha256", "")), size, str(previous.get("updated", "")), target
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
continue
|
|
136
|
+
if reply.status != 200:
|
|
137
|
+
raise LibError(
|
|
138
|
+
f"{url} answered HTTP {reply.status}", "check the name against https://www.iana.org/protocols"
|
|
139
|
+
)
|
|
140
|
+
if len(reply.body) > MAX_BYTES:
|
|
141
|
+
raise LibError(f"{url} is over {MAX_BYTES:,} bytes", "this is not the expected registry file")
|
|
142
|
+
parsed = from_tree(xmlsafe.parse(reply.body, MAX_BYTES), name)
|
|
143
|
+
digest = hashlib.sha256(reply.body).hexdigest()
|
|
144
|
+
status = "identical" if target.is_file() and previous.get("sha256") == digest else "fetched"
|
|
145
|
+
atomic_write(target, reply.body)
|
|
146
|
+
files[name] = {
|
|
147
|
+
"url": url,
|
|
148
|
+
"sha256": digest,
|
|
149
|
+
"size": len(reply.body),
|
|
150
|
+
"updated": parsed.updated,
|
|
151
|
+
"fetched": now().isoformat(timespec="seconds"),
|
|
152
|
+
"last_modified": {k.lower(): v for k, v in reply.headers.items()}.get("last-modified", ""),
|
|
153
|
+
}
|
|
154
|
+
manifest = json.dumps({"schema": 1, "files": files}, indent=2, sort_keys=True) + "\n"
|
|
155
|
+
atomic_write(root / MANIFEST, manifest.encode("utf-8"))
|
|
156
|
+
report.outcomes.append(Outcome(name, status, digest, len(reply.body), parsed.updated, target))
|
|
157
|
+
return report
|