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
@@ -0,0 +1,211 @@
1
+ """``nistlib-fetch``: download the publications in data/sources.toml, extract their text and write the index.
2
+
3
+ This is the only module that touches the network, and only for URLs listed in sources.toml.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from primarysource.nist.paths import data_dir as default_data_dir, sources_file
9
+
10
+ import argparse
11
+ import os
12
+ import sys
13
+ import urllib.error
14
+ import urllib.request
15
+ from collections.abc import Sequence
16
+ from pathlib import Path
17
+
18
+ from primarysource.nist.ingest.extract import (
19
+ ExtractError,
20
+ extract_text,
21
+ find_pdftotext,
22
+ split_sections,
23
+ )
24
+ from primarysource.nist.ingest.controls import split_control_sections
25
+ from primarysource.nist.ingest.prose_layouts import detect_layout, split_for_layout
26
+ from primarysource.nist.ingest.index import read_index, sha256_bytes, sha256_file, write_index
27
+ from primarysource.nist.ingest.sources import Source, SourcesError, load_sources
28
+ from primarysource.nist.models import Document
29
+ from primarysource.nist.spec import SPEC
30
+ from primarysource.pdfcorpus.fts import build_after_fetch
31
+
32
+ USER_AGENT = "nistlib-fetch/0.1 (corpus fetcher; one request per listed URL)"
33
+ TIMEOUT_SECONDS = 120
34
+ MAX_BYTES = 200 * 1024 * 1024
35
+
36
+
37
+ class FetchError(RuntimeError):
38
+ """Raised when one document cannot be fetched or verified."""
39
+
40
+
41
+ def download(url: str, *, timeout: float = TIMEOUT_SECONDS) -> bytes:
42
+ """GET ``url`` and return the body, which must look like a PDF."""
43
+ request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) # noqa: S310
44
+ try:
45
+ with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310
46
+ body: bytes = response.read(MAX_BYTES + 1)
47
+ except (urllib.error.URLError, TimeoutError, OSError) as error:
48
+ raise FetchError(f"download failed for {url}: {error}") from error
49
+ if len(body) > MAX_BYTES:
50
+ raise FetchError(f"{url}: larger than {MAX_BYTES} bytes, refusing")
51
+ if not body.startswith(b"%PDF-"):
52
+ raise FetchError(f"{url}: response is not a PDF")
53
+ return body
54
+
55
+
56
+ def check_digest(doc_id: str, actual: str, expected: str) -> None:
57
+ """Raise FetchError when a recorded digest exists and differs from ``actual``."""
58
+ if expected and actual != expected:
59
+ raise FetchError(
60
+ f"{doc_id}: sha256 {actual} does not match the recorded {expected}"
61
+ )
62
+
63
+
64
+ def _store(pdf: Path, data: bytes) -> None:
65
+ temp = pdf.with_name(pdf.name + ".part")
66
+ temp.write_bytes(data)
67
+ os.replace(temp, pdf)
68
+
69
+
70
+ def obtain_pdf(
71
+ source: Source, corpus: Path, recorded: str, from_file: Path | None
72
+ ) -> str:
73
+ """Make ``corpus/<doc_id>.pdf`` exist with a matching digest and return that digest.
74
+
75
+ ``recorded`` is the digest from sources.toml, or failing that the one recorded by an earlier run.
76
+ A file already in place whose digest matches (or for which none is recorded) is kept. A mismatching
77
+ file is replaced by a fresh copy, which is itself verified before it is kept.
78
+ """
79
+ pdf = corpus / f"{source.doc_id}.pdf"
80
+ if from_file is not None:
81
+ data = from_file.read_bytes()
82
+ if not data.startswith(b"%PDF-"):
83
+ raise FetchError(f"{source.doc_id}: {from_file} is not a PDF")
84
+ digest = sha256_bytes(data)
85
+ check_digest(source.doc_id, digest, source.sha256)
86
+ if not (pdf.exists() and sha256_file(pdf) == digest):
87
+ _store(pdf, data)
88
+ return digest
89
+ if pdf.exists():
90
+ digest = sha256_file(pdf)
91
+ if not recorded or digest == recorded:
92
+ return digest
93
+ data = download(source.url)
94
+ digest = sha256_bytes(data)
95
+ check_digest(source.doc_id, digest, recorded)
96
+ _store(pdf, data)
97
+ return digest
98
+
99
+
100
+ def build_document(source: Source, corpus: Path, digest: str) -> Document:
101
+ """Extract text for ``source`` into the corpus and return its index entry."""
102
+ text = extract_text(corpus / f"{source.doc_id}.pdf")
103
+ text_name = f"{source.doc_id}.txt"
104
+ (corpus / text_name).write_text(text, encoding="utf-8", newline="")
105
+ if source.splitter == "controls":
106
+ sections = split_control_sections(text)
107
+ else:
108
+ sections = split_sections(text)
109
+ # Prose layouts the generic splitter cannot read (heading glued to a page header, "SECTION 1:",
110
+ # "CHAPTER ONE"). Used only when the generic splitter finds fewer than three sections.
111
+ if len(sections) < 3 and (layout := detect_layout(text)):
112
+ sections = split_for_layout(text, layout)
113
+ return Document(
114
+ doc_id=source.doc_id,
115
+ number=source.number,
116
+ title=source.title,
117
+ date=source.date,
118
+ status=source.status,
119
+ url=source.url,
120
+ sha256=digest,
121
+ text_file=text_name,
122
+ sections=sections,
123
+ )
124
+
125
+
126
+ def parse_from_files(values: Sequence[str]) -> dict[str, Path]:
127
+ """Parse ``DOC_ID=PATH`` arguments."""
128
+ result: dict[str, Path] = {}
129
+ for value in values:
130
+ doc_id, sep, path = value.partition("=")
131
+ if not sep or not doc_id or not path:
132
+ raise SourcesError(f"--from-file expects DOC_ID=PATH, got {value!r}")
133
+ result[doc_id] = Path(path)
134
+ return result
135
+
136
+
137
+ def run(data_dir: Path, from_files: dict[str, Path], only: Sequence[str]) -> int:
138
+ """Fetch, verify, extract and index. Returns the process exit code."""
139
+ sources = load_sources(sources_file(data_dir))
140
+ known = {s.doc_id for s in sources}
141
+ unknown = sorted((set(from_files) | set(only)) - known)
142
+ if unknown:
143
+ raise SourcesError(f"not in sources.toml: {', '.join(unknown)}")
144
+ find_pdftotext()
145
+ corpus = data_dir / "corpus"
146
+ corpus.mkdir(parents=True, exist_ok=True)
147
+ index_path = corpus / "index.json"
148
+ previous = {d.doc_id: d for d in read_index(index_path)}
149
+ wanted = set(only) | set(from_files) if (only or from_files) else known
150
+ documents: list[Document] = []
151
+ failures = 0
152
+ for source in sources:
153
+ old = previous.get(source.doc_id)
154
+ if source.doc_id not in wanted:
155
+ if old is not None and (corpus / old.text_file).exists():
156
+ documents.append(old)
157
+ continue
158
+ recorded = source.sha256 or (old.sha256 if old else "")
159
+ try:
160
+ digest = obtain_pdf(source, corpus, recorded, from_files.get(source.doc_id))
161
+ document = build_document(source, corpus, digest)
162
+ except (FetchError, ExtractError, OSError) as error:
163
+ print(f"{source.doc_id}: {error}", file=sys.stderr)
164
+ failures += 1
165
+ continue
166
+ note = "" if source.sha256 else " (digest recorded in index.json)"
167
+ print(
168
+ f"{source.doc_id}: {len(document.sections)} sections, sha256 {digest[:12]}{note}"
169
+ )
170
+ documents.append(document)
171
+ # Entries this run does not own (the -oscal catalogues from `nistlib.ingest.oscal`) are kept as they were.
172
+ for doc_id, old in previous.items():
173
+ if doc_id not in known and (corpus / old.text_file).exists():
174
+ documents.append(old)
175
+ write_index(index_path, documents)
176
+ print(f"wrote {index_path} with {len(documents)} document(s)")
177
+ if not build_after_fetch(SPEC, corpus):
178
+ failures += 1
179
+ return 1 if failures else 0
180
+
181
+
182
+ def main(argv: Sequence[str] | None = None) -> int:
183
+ parser = argparse.ArgumentParser(prog="nistlib-fetch", description=__doc__)
184
+ parser.add_argument(
185
+ "--data-dir",
186
+ type=Path,
187
+ default=default_data_dir(),
188
+ help="directory holding sources.toml and corpus/ (default: $NISTLIB_DATA, else the collection's home)",
189
+ )
190
+ parser.add_argument(
191
+ "--from-file",
192
+ action="append",
193
+ default=[],
194
+ metavar="DOC_ID=PATH",
195
+ help="use a PDF you already have for DOC_ID instead of downloading it (repeatable)",
196
+ )
197
+ parser.add_argument(
198
+ "doc_ids",
199
+ nargs="*",
200
+ help="only these documents (default: all, or those named by --from-file)",
201
+ )
202
+ args = parser.parse_args(argv)
203
+ try:
204
+ return run(args.data_dir, parse_from_files(args.from_file), args.doc_ids)
205
+ except (SourcesError, ExtractError) as error:
206
+ print(f"nistlib-fetch: {error}", file=sys.stderr)
207
+ return 2
208
+
209
+
210
+ if __name__ == "__main__":
211
+ raise SystemExit(main())
@@ -0,0 +1,14 @@
1
+ """The nist corpus index: the shared one (primarysource.pdfcorpus.index)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from primarysource.pdfcorpus.index import (
6
+ SCHEMA,
7
+ document_from_dict,
8
+ read_index,
9
+ sha256_bytes,
10
+ sha256_file,
11
+ write_index,
12
+ )
13
+
14
+ __all__ = ["SCHEMA", "document_from_dict", "read_index", "sha256_bytes", "sha256_file", "write_index"]