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.
Files changed (303) hide show
  1. primarysource/__init__.py +5 -0
  2. primarysource/bbf/__init__.py +56 -0
  3. primarysource/bbf/_xml.py +63 -0
  4. primarysource/bbf/catalogue.json +777 -0
  5. primarysource/bbf/catalogue.py +137 -0
  6. primarysource/bbf/cli.py +79 -0
  7. primarysource/bbf/datamodel.py +281 -0
  8. primarysource/bbf/importer.py +151 -0
  9. primarysource/bbf/library.py +380 -0
  10. primarysource/bbf/server.py +72 -0
  11. primarysource/budget.py +63 -0
  12. primarysource/c2sp/__init__.py +53 -0
  13. primarysource/c2sp/_fetch.py +5 -0
  14. primarysource/c2sp/catalogue.json +231 -0
  15. primarysource/c2sp/catalogue.py +95 -0
  16. primarysource/c2sp/cli.py +62 -0
  17. primarysource/c2sp/importer.py +90 -0
  18. primarysource/c2sp/library.py +309 -0
  19. primarysource/c2sp/markdown.py +31 -0
  20. primarysource/c2sp/server.py +68 -0
  21. primarysource/cavp/__init__.py +42 -0
  22. primarysource/cavp/cli.py +84 -0
  23. primarysource/cavp/data/vectors.toml +47 -0
  24. primarysource/cavp/importer.py +156 -0
  25. primarysource/cavp/model.py +279 -0
  26. primarysource/cavp/parse.py +269 -0
  27. primarysource/cavp/paths.py +17 -0
  28. primarysource/cavp/server.py +67 -0
  29. primarysource/cavp/tools.py +169 -0
  30. primarysource/cli.py +107 -0
  31. primarysource/cursor.py +82 -0
  32. primarysource/cve/__init__.py +5 -0
  33. primarysource/cve/__main__.py +6 -0
  34. primarysource/cve/cli.py +144 -0
  35. primarysource/cve/collection.py +20 -0
  36. primarysource/cve/config.py +58 -0
  37. primarysource/cve/errors.py +16 -0
  38. primarysource/cve/kernel.py +232 -0
  39. primarysource/cve/mcp.py +139 -0
  40. primarysource/cve/parse.py +318 -0
  41. primarysource/cve/py.typed +0 -0
  42. primarysource/cve/service.py +760 -0
  43. primarysource/cve/store.py +214 -0
  44. primarysource/cve/sync.py +438 -0
  45. primarysource/cve/views.py +304 -0
  46. primarysource/debpolicy/__init__.py +16 -0
  47. primarysource/debpolicy/catalogue.py +263 -0
  48. primarysource/debpolicy/cli.py +12 -0
  49. primarysource/debpolicy/server.py +11 -0
  50. primarysource/draft/__init__.py +11 -0
  51. primarysource/draft/catalogue.py +117 -0
  52. primarysource/draft/cli.py +149 -0
  53. primarysource/draft/collection.py +21 -0
  54. primarysource/draft/datatracker.py +158 -0
  55. primarysource/draft/importer.py +163 -0
  56. primarysource/draft/licence.py +89 -0
  57. primarysource/draft/names.py +64 -0
  58. primarysource/draft/net.py +133 -0
  59. primarysource/draft/segment.py +192 -0
  60. primarysource/draft/server.py +91 -0
  61. primarysource/draft/status.py +146 -0
  62. primarysource/draft/store.py +199 -0
  63. primarysource/draft/tracked.json +104 -0
  64. primarysource/draft/views.py +430 -0
  65. primarysource/draft/xmlmeta.py +74 -0
  66. primarysource/ecma/__init__.py +52 -0
  67. primarysource/ecma/_pdf.py +143 -0
  68. primarysource/ecma/catalogue.json +22 -0
  69. primarysource/ecma/catalogue.py +100 -0
  70. primarysource/ecma/cli.py +67 -0
  71. primarysource/ecma/identify.py +25 -0
  72. primarysource/ecma/importer.py +108 -0
  73. primarysource/ecma/library.py +240 -0
  74. primarysource/ecma/server.py +67 -0
  75. primarysource/errors.py +29 -0
  76. primarysource/fetch.py +360 -0
  77. primarysource/fsutil.py +150 -0
  78. primarysource/gitdocs/__init__.py +15 -0
  79. primarysource/gitdocs/catalogue.py +116 -0
  80. primarysource/gitdocs/cli.py +12 -0
  81. primarysource/gitdocs/server.py +8 -0
  82. primarysource/home.py +65 -0
  83. primarysource/iana/__init__.py +32 -0
  84. primarysource/iana/catalogue.json +244 -0
  85. primarysource/iana/catalogue.py +69 -0
  86. primarysource/iana/cli.py +69 -0
  87. primarysource/iana/importer.py +157 -0
  88. primarysource/iana/model.py +325 -0
  89. primarysource/iana/reader.py +315 -0
  90. primarysource/iana/server.py +67 -0
  91. primarysource/iana/store.py +83 -0
  92. primarysource/iana/xmlsafe.py +47 -0
  93. primarysource/identify.py +186 -0
  94. primarysource/ieee/__init__.py +58 -0
  95. primarysource/ieee/_html.py +49 -0
  96. primarysource/ieee/catalogue.json +74 -0
  97. primarysource/ieee/catalogue.py +123 -0
  98. primarysource/ieee/cli.py +79 -0
  99. primarysource/ieee/identify.py +24 -0
  100. primarysource/ieee/ieee802.py +47 -0
  101. primarysource/ieee/importer.py +305 -0
  102. primarysource/ieee/posix.py +233 -0
  103. primarysource/ieee/server.py +79 -0
  104. primarysource/index.py +446 -0
  105. primarysource/iso/__init__.py +51 -0
  106. primarysource/iso/cli.py +98 -0
  107. primarysource/iso/designation.py +95 -0
  108. primarysource/iso/identify.py +100 -0
  109. primarysource/iso/importer.py +224 -0
  110. primarysource/iso/library.py +249 -0
  111. primarysource/iso/server.py +69 -0
  112. primarysource/itu/__init__.py +41 -0
  113. primarysource/itu/__main__.py +8 -0
  114. primarysource/itu/asn1mod.py +478 -0
  115. primarysource/itu/catalog.py +472 -0
  116. primarysource/itu/cli.py +445 -0
  117. primarysource/itu/collection.py +20 -0
  118. primarysource/itu/corpus.py +118 -0
  119. primarysource/itu/errata.py +16 -0
  120. primarysource/itu/formal.py +565 -0
  121. primarysource/itu/identify.py +26 -0
  122. primarysource/itu/index.py +490 -0
  123. primarysource/itu/mcp.py +474 -0
  124. primarysource/itu/py.typed +0 -0
  125. primarysource/itu/sources.py +600 -0
  126. primarysource/itu/text.py +609 -0
  127. primarysource/kconfig/__init__.py +16 -0
  128. primarysource/kconfig/catalogue.py +98 -0
  129. primarysource/kconfig/cli.py +12 -0
  130. primarysource/kconfig/server.py +8 -0
  131. primarysource/model.py +63 -0
  132. primarysource/nist/__init__.py +5 -0
  133. primarysource/nist/collection.py +46 -0
  134. primarysource/nist/data/acquired-nist.json +1657 -0
  135. primarysource/nist/data/nist-catalog.json +12090 -0
  136. primarysource/nist/data/sources.toml +1996 -0
  137. primarysource/nist/identify.py +41 -0
  138. primarysource/nist/ingest/__init__.py +1 -0
  139. primarysource/nist/ingest/acquire_nist.py +128 -0
  140. primarysource/nist/ingest/controls.py +82 -0
  141. primarysource/nist/ingest/discover_nist.py +218 -0
  142. primarysource/nist/ingest/extract.py +48 -0
  143. primarysource/nist/ingest/fetch.py +211 -0
  144. primarysource/nist/ingest/index.py +14 -0
  145. primarysource/nist/ingest/oscal.py +480 -0
  146. primarysource/nist/ingest/prose_layouts.py +236 -0
  147. primarysource/nist/ingest/sources.py +94 -0
  148. primarysource/nist/ingest/status.py +255 -0
  149. primarysource/nist/models.py +7 -0
  150. primarysource/nist/paths.py +33 -0
  151. primarysource/nist/server/__init__.py +3 -0
  152. primarysource/nist/server/corpus.py +38 -0
  153. primarysource/nist/server/mcp.py +19 -0
  154. primarysource/nist/server/tools.py +35 -0
  155. primarysource/nist/spec.py +38 -0
  156. primarysource/pdfcorpus/__init__.py +16 -0
  157. primarysource/pdfcorpus/corpus.py +159 -0
  158. primarysource/pdfcorpus/fts.py +175 -0
  159. primarysource/pdfcorpus/index.py +70 -0
  160. primarysource/pdfcorpus/models.py +30 -0
  161. primarysource/pdfcorpus/server.py +96 -0
  162. primarysource/pdfcorpus/spec.py +37 -0
  163. primarysource/pdfcorpus/split.py +391 -0
  164. primarysource/pdfcorpus/tools.py +244 -0
  165. primarysource/pep/__init__.py +15 -0
  166. primarysource/pep/catalogue.py +191 -0
  167. primarysource/pep/cli.py +12 -0
  168. primarysource/pep/server.py +8 -0
  169. primarysource/pkgdocs/__init__.py +19 -0
  170. primarysource/pkgdocs/fetch.py +356 -0
  171. primarysource/pkgdocs/index.py +89 -0
  172. primarysource/pkgdocs/model.py +76 -0
  173. primarysource/pkgdocs/readers/__init__.py +1 -0
  174. primarysource/pkgdocs/readers/_common.py +134 -0
  175. primarysource/pkgdocs/readers/asciidoc.py +160 -0
  176. primarysource/pkgdocs/readers/latex.py +315 -0
  177. primarysource/pkgdocs/readers/markdown.py +173 -0
  178. primarysource/pkgdocs/readers/pod.py +132 -0
  179. primarysource/pkgdocs/readers/rst.py +239 -0
  180. primarysource/pkgdocs/readers/scdoc.py +108 -0
  181. primarysource/pkgdocs/runtime.py +552 -0
  182. primarysource/pkgdocs/serving.py +158 -0
  183. primarysource/pkgdocs/store.py +167 -0
  184. primarysource/pms/__init__.py +16 -0
  185. primarysource/pms/catalogue.py +88 -0
  186. primarysource/pms/cli.py +12 -0
  187. primarysource/pms/server.py +8 -0
  188. primarysource/py.typed +0 -0
  189. primarysource/readers/__init__.py +10 -0
  190. primarysource/readers/bookmarks.py +192 -0
  191. primarysource/readers/html.py +1312 -0
  192. primarysource/readers/markdown.py +189 -0
  193. primarysource/readers/pdf.py +187 -0
  194. primarysource/readers/xml.py +230 -0
  195. primarysource/refs.py +87 -0
  196. primarysource/registry.py +161 -0
  197. primarysource/render.py +49 -0
  198. primarysource/rfc/__init__.py +3 -0
  199. primarysource/rfc/__main__.py +6 -0
  200. primarysource/rfc/cite.py +204 -0
  201. primarysource/rfc/cli.py +903 -0
  202. primarysource/rfc/collection.py +19 -0
  203. primarysource/rfc/config.py +138 -0
  204. primarysource/rfc/errata.py +203 -0
  205. primarysource/rfc/fsutil.py +82 -0
  206. primarysource/rfc/graph.py +213 -0
  207. primarysource/rfc/index/__init__.py +1 -0
  208. primarysource/rfc/index/chunker.py +281 -0
  209. primarysource/rfc/index/embed.py +166 -0
  210. primarysource/rfc/index/hybrid.py +305 -0
  211. primarysource/rfc/index/lexical.py +223 -0
  212. primarysource/rfc/index/rfcxml.py +673 -0
  213. primarysource/rfc/index/search.py +508 -0
  214. primarysource/rfc/index/txtseg.py +585 -0
  215. primarysource/rfc/library.py +294 -0
  216. primarysource/rfc/mcp/__init__.py +1 -0
  217. primarysource/rfc/mcp/server.py +381 -0
  218. primarysource/rfc/packs/__init__.py +1 -0
  219. primarysource/rfc/packs/builder.py +228 -0
  220. primarysource/rfc/packs/collections/all-current-txt.json +10 -0
  221. primarysource/rfc/packs/collections/essentials.json +19 -0
  222. primarysource/rfc/packs/collections/fyi-current.json +10 -0
  223. primarysource/rfc/packs/collections/standards-current.json +10 -0
  224. primarysource/rfc/packs/collections/year-2025.json +10 -0
  225. primarysource/rfc/packs/collections/year-2026.json +10 -0
  226. primarysource/rfc/packs/collections.py +163 -0
  227. primarysource/rfc/packs/docsetexport.py +163 -0
  228. primarysource/rfc/packs/epub.py +159 -0
  229. primarysource/rfc/packs/export.py +306 -0
  230. primarysource/rfc/packs/htmlexport.py +82 -0
  231. primarysource/rfc/packs/serve.py +143 -0
  232. primarysource/rfc/packs/zimexport.py +171 -0
  233. primarysource/rfc/query.py +466 -0
  234. primarysource/rfc/subjects.py +457 -0
  235. primarysource/rfc/sync/__init__.py +1 -0
  236. primarysource/rfc/sync/downloader.py +132 -0
  237. primarysource/rfc/sync/manifest.py +162 -0
  238. primarysource/rfc/sync/scanner.py +49 -0
  239. primarysource/rfc/sync/state.py +464 -0
  240. primarysource/rfc/views.py +1067 -0
  241. primarysource/rpmdocs/__init__.py +15 -0
  242. primarysource/rpmdocs/catalogue.py +117 -0
  243. primarysource/rpmdocs/cli.py +12 -0
  244. primarysource/rpmdocs/server.py +8 -0
  245. primarysource/scaffold.py +223 -0
  246. primarysource/serve.py +75 -0
  247. primarysource/settings.py +152 -0
  248. primarysource/usb/__init__.py +5 -0
  249. primarysource/usb/collection.py +37 -0
  250. primarysource/usb/data/acquired.json +2025 -0
  251. primarysource/usb/data/sources-usb.toml +852 -0
  252. primarysource/usb/data/sources.toml +10 -0
  253. primarysource/usb/data/usb-catalog.json +1663 -0
  254. primarysource/usb/data/usb-classification.json +1986 -0
  255. primarysource/usb/data/usb-corpus-build.json +608 -0
  256. primarysource/usb/data/usb-pdf-choice.json +839 -0
  257. primarysource/usb/data/usb-split-measure.json +1803 -0
  258. primarysource/usb/ingest/__init__.py +1 -0
  259. primarysource/usb/ingest/acquire.py +130 -0
  260. primarysource/usb/ingest/discover_usb.py +88 -0
  261. primarysource/usb/ingest/extract.py +48 -0
  262. primarysource/usb/ingest/fetch.py +197 -0
  263. primarysource/usb/ingest/index.py +14 -0
  264. primarysource/usb/ingest/sources.py +85 -0
  265. primarysource/usb/models.py +7 -0
  266. primarysource/usb/paths.py +33 -0
  267. primarysource/usb/server/__init__.py +3 -0
  268. primarysource/usb/server/corpus.py +38 -0
  269. primarysource/usb/server/mcp.py +19 -0
  270. primarysource/usb/server/tools.py +35 -0
  271. primarysource/usb/spec.py +20 -0
  272. primarysource/usbids/__init__.py +56 -0
  273. primarysource/usbids/cli.py +65 -0
  274. primarysource/usbids/importer.py +272 -0
  275. primarysource/usbids/parse.py +283 -0
  276. primarysource/usbids/server.py +55 -0
  277. primarysource/usbids/store.py +80 -0
  278. primarysource/usbids/tools.py +174 -0
  279. primarysource/zim/__init__.py +5 -0
  280. primarysource/zim/__main__.py +8 -0
  281. primarysource/zim/_libzim.py +37 -0
  282. primarysource/zim/catalog.py +594 -0
  283. primarysource/zim/cli.py +555 -0
  284. primarysource/zim/collection.py +41 -0
  285. primarysource/zim/errors.py +100 -0
  286. primarysource/zim/groups.py +30 -0
  287. primarysource/zim/library.py +668 -0
  288. primarysource/zim/manage.py +636 -0
  289. primarysource/zim/markdown.py +1247 -0
  290. primarysource/zim/mcp.py +201 -0
  291. primarysource/zim/purl.py +130 -0
  292. primarysource/zim/py.typed +0 -0
  293. primarysource/zim/refs.py +90 -0
  294. primarysource/zim/service.py +1182 -0
  295. primarysource/zim/snapshot.py +197 -0
  296. primarysource/zim/sync.py +269 -0
  297. primarysource/zim/views.py +230 -0
  298. primarysource-0.1.0.dist-info/METADATA +118 -0
  299. primarysource-0.1.0.dist-info/RECORD +303 -0
  300. primarysource-0.1.0.dist-info/WHEEL +4 -0
  301. primarysource-0.1.0.dist-info/entry_points.txt +2 -0
  302. primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
  303. 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