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,35 @@
1
+ """The five usb tools: the shared ones (primarysource.pdfcorpus.tools)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from primarysource.pdfcorpus.tools import (
6
+ GRAMMAR,
7
+ OUTLINE_ROWS,
8
+ READ_TOKENS,
9
+ ToolError,
10
+ cite,
11
+ info,
12
+ outline,
13
+ parse_lines,
14
+ read,
15
+ resolve,
16
+ resolve_doc,
17
+ resolve_section,
18
+ search,
19
+ )
20
+
21
+ __all__ = [
22
+ "GRAMMAR",
23
+ "OUTLINE_ROWS",
24
+ "READ_TOKENS",
25
+ "ToolError",
26
+ "cite",
27
+ "info",
28
+ "outline",
29
+ "parse_lines",
30
+ "read",
31
+ "resolve",
32
+ "resolve_doc",
33
+ "resolve_section",
34
+ "search",
35
+ ]
@@ -0,0 +1,20 @@
1
+ """The usb collection's settings for the shared skeleton (primarysource.pdfcorpus)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from primarysource.usb.paths import corpus_dir
6
+ from primarysource.pdfcorpus.spec import CorpusSpec
7
+
8
+
9
+ SPEC = CorpusSpec(
10
+ name="usb",
11
+ server_name="usblib",
12
+ env_var="USBLIB_CORPUS",
13
+ fetch_command="primarysource usb fetch",
14
+ index_command="primarysource usb index",
15
+ default_dir=corpus_dir,
16
+ instructions=(
17
+ "USB-IF specifications from a locally fetched corpus. search('<words>') -> read('<doc_id>§<section>') -> cite('<doc_id>§<section>'). Search hits start with that ref; a read's 'next:' line is a ref to pass back to read. Documents are addressed by doc_id or by number. Check info(doc) for status before quoting. A missing corpus or search index is an error whose 'action:' line is the command to run (`primarysource usb fetch`, `primarysource usb index`)."
18
+ ),
19
+ cite_example='USB-IF CCID Rev 1.1 §6.1.1',
20
+ )
@@ -0,0 +1,56 @@
1
+ """The USB ID database: vendors, devices, classes and the other tables of the `usb.ids` file.
2
+
3
+ One collection, `usbids`. The file is published by the linux-usb.org USB ID Repository, which says its contents
4
+ "can be distributed under the terms of either the GNU General Public License (version 2 or later) or of the
5
+ 3-clause BSD License" (docs/usbids.md has the URL and date). It is not USB-IF's text, and has nothing to do with
6
+ the `usb` collection's specifications except that the two answer questions about the same bus.
7
+
8
+ Standard library only. `primarysource usbids import` fetches the file (`importer.py`); `server.py` needs `mcp`
9
+ and is imported only when served (`primarysource mcp --collection usbids`); `primarysource usbids ...` runs
10
+ `cli.py`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from primarysource.errors import LibError
16
+ from primarysource.model import Unit
17
+ from primarysource.refs import Ref
18
+ from primarysource.registry import Collection
19
+ from primarysource.usbids.parse import format_ref, parse_ref
20
+
21
+
22
+ class UsbidsRefs:
23
+ """The collection's `RefParser`. A ref names one entry, so the core's `Ref` carries the table in `doc` and the
24
+ entry's ids in `section`: `usbids:1d6b:0002` is doc `usbids`, section `1d6b:0002`; `usbids:class/09/00` is
25
+ doc `usbids`, section `class/09/00`."""
26
+
27
+ def parse(self, text: str) -> Ref:
28
+ section, key = parse_ref(text)
29
+ return Ref("usbids", ":".join(key) if section == "vendor" else f"{section}/" + "/".join(key))
30
+
31
+ def format(self, ref: Ref) -> str:
32
+ if ref.doc not in (None, "usbids") or ref.section is None or ref.para or ref.anchor or ref.lines:
33
+ raise LibError(f"not a usbids ref: {ref!r}", "a usbids ref is a doc `usbids` and a section only")
34
+ section, key = parse_ref(ref.section)
35
+ return format_ref(section, key)
36
+
37
+
38
+ COLLECTION = Collection(
39
+ name="usbids",
40
+ title="USB ID Database (usb.ids)",
41
+ prefix="usbids_",
42
+ unit=Unit.BLOCK,
43
+ grammar=UsbidsRefs(),
44
+ server="primarysource.usbids.server:server",
45
+ cli="primarysource.usbids.cli:main",
46
+ )
47
+ """Refs name one entry, not a section of a document, so they do not fit `RefGrammar` (`<doc>§<section>`);
48
+ `UsbidsRefs` parses the spelling with `parse.parse_ref`:
49
+
50
+ usbids:1d6b a vendor usbids:1d6b:0002 a device (vendor id : product id, hex)
51
+ usbids:class/09 a class usbids:class/09/00/02 class / subclass / protocol
52
+ usbids:hut/01/002 a HID usage page and usage; also at/, hid/, item/, bias/, phy/, lang/, hcc/, vt/
53
+
54
+ `¶n` is not used: every entry is one line, and a result's `unit` is `line`."""
55
+
56
+ __all__ = ["COLLECTION"]
@@ -0,0 +1,65 @@
1
+ """`primarysource usbids ...`: the collection's own commands.
2
+
3
+ primarysource usbids import [--latest] [--sha256 H] [--force]
4
+ primarysource usbids sections
5
+ primarysource usbids lookup <ref> [--max-tokens N]
6
+ primarysource usbids search <text> [--section S] [--limit N] [--start N]
7
+ primarysource usbids classify <class> [subclass [protocol]]
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.usbids import importer, tools
18
+
19
+
20
+ def _parser() -> argparse.ArgumentParser:
21
+ parser = argparse.ArgumentParser(prog="primarysource usbids", description="the USB ID database (usb.ids)")
22
+ commands = parser.add_subparsers(dest="command", required=True)
23
+ imp = commands.add_parser("import", help="fetch usb.ids (one request)")
24
+ imp.add_argument("--latest", action="store_true")
25
+ imp.add_argument("--sha256")
26
+ imp.add_argument("--force", action="store_true")
27
+ commands.add_parser("sections", help="the imported snapshot and its counts")
28
+ lookup = commands.add_parser("lookup", help="one entry by ref")
29
+ lookup.add_argument("ref")
30
+ lookup.add_argument("--max-tokens", type=int, default=2000)
31
+ search = commands.add_parser("search", help="entries by name")
32
+ search.add_argument("text")
33
+ search.add_argument("--section")
34
+ search.add_argument("--limit", type=int, default=20)
35
+ search.add_argument("--start", type=int, default=0)
36
+ classify = commands.add_parser("classify", help="name a class code triple")
37
+ classify.add_argument("base_class")
38
+ classify.add_argument("subclass", nargs="?")
39
+ classify.add_argument("protocol", nargs="?")
40
+ return parser
41
+
42
+
43
+ def _run(text: str) -> int:
44
+ sys.stdout.write(text if text.endswith("\n") else text + "\n")
45
+ return 0
46
+
47
+
48
+ def main(argv: Sequence[str]) -> int:
49
+ args = _parser().parse_args(list(argv))
50
+ try:
51
+ if args.command == "import":
52
+ forwarded = [flag for flag, on in (("--latest", args.latest), ("--force", args.force)) if on]
53
+ if args.sha256:
54
+ forwarded += ["--sha256", args.sha256]
55
+ return importer.main(forwarded)
56
+ if args.command == "sections":
57
+ return _run(tools.sections())
58
+ if args.command == "lookup":
59
+ return _run(tools.lookup(args.ref, max_tokens=args.max_tokens))
60
+ if args.command == "search":
61
+ return _run(tools.search(args.text, section=args.section, limit=args.limit, start=args.start))
62
+ return _run(tools.classify(args.base_class, args.subclass, args.protocol))
63
+ except LibError as exc:
64
+ print(exc.as_text(), file=sys.stderr)
65
+ return 1
@@ -0,0 +1,272 @@
1
+ """Fetch the USB ID database (`usb.ids`) into `$PRIMARYSOURCE_HOME/usbids/`.
2
+
3
+ Polite by construction: one file, at most one request per `interval` seconds (never under 1.0) across the source
4
+ list, an identifying User-Agent, conditional requests (`If-None-Match`, `If-Modified-Since`) so an unchanged file
5
+ costs a 304, no redirects followed, https only and only the hosts in `SOURCES`, and a body ceiling.
6
+
7
+ The file changes daily, so a pin cannot be a promise about the future. `PIN` is the snapshot this code was
8
+ developed against. A body whose sha256 is the pinned one installs. A body that is a different snapshot installs
9
+ only on `--latest` (or `--sha256 <digest>` naming it), and a body that claims the pinned version with other bytes
10
+ is refused as damaged. The parse is checked before anything is written: no entries, or any unplaceable line,
11
+ refuses the import. The bytes are stored exactly as served, atomically; `manifest.json` records the digest.
12
+
13
+ The transport is a parameter, so tests inject one; the default uses `urllib`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import hashlib
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+ import urllib.error
25
+ import urllib.request
26
+ from collections.abc import Callable, Mapping, Sequence
27
+ from dataclasses import dataclass
28
+ from datetime import UTC, datetime
29
+ from pathlib import Path
30
+
31
+ from primarysource.errors import LibError
32
+ from primarysource.home import atomic_write
33
+ from primarysource.usbids import store
34
+ from primarysource.usbids.parse import Limits, parse
35
+
36
+ USER_AGENT = "primarysource-usbids/0.1 (local usb.ids mirror; at most 1 request per second)"
37
+ MIN_INTERVAL = 1.0
38
+ MAX_BYTES = 8_000_000
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class Pin:
43
+ """One snapshot of `usb.ids`: its `# Version:`, `# Date:`, size and sha256."""
44
+
45
+ version: str
46
+ date: str
47
+ size: int
48
+ sha256: str
49
+
50
+
51
+ PIN = Pin(
52
+ version="2026.06.26",
53
+ date="2026-06-26 20:34:02",
54
+ size=730_605,
55
+ sha256="f5a48b0cc8dae1607c2f0bae6b8dc13f2ecef69dbeaeaf34b4be8e280d34dba4",
56
+ )
57
+ """Measured 2026-11-13 from both sources below; their bytes had the same length and Last-Modified."""
58
+
59
+ SOURCES = (
60
+ "https://usb-ids.gowdy.us/usb.ids",
61
+ "https://raw.githubusercontent.com/usbids/usbids/master/usb.ids",
62
+ )
63
+ """The maintainer's host (the one linux-usb.org's page points users at), then the GitHub mirror. `www.linux-usb.org`
64
+ is not used: its https certificate is for sf.net (hostname mismatch) and its plain http would leave the sha256 as
65
+ the only integrity check."""
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class Response:
70
+ status: int
71
+ headers: Mapping[str, str]
72
+ body: bytes
73
+
74
+
75
+ Transport = Callable[[str, Mapping[str, str]], Response]
76
+ """`transport(url, request_headers) -> Response`. It must not follow redirects and must bound the body."""
77
+
78
+
79
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
80
+ def redirect_request(self, *_args: object, **_kwargs: object) -> None:
81
+ return None
82
+
83
+
84
+ def urllib_transport(url: str, headers: Mapping[str, str]) -> Response:
85
+ """The default transport: one GET to a listed https source, no redirects, body capped at MAX_BYTES."""
86
+ if url not in SOURCES:
87
+ raise LibError(f"refusing to fetch {url!r}", "only the sources in primarysource.usbids.importer.SOURCES")
88
+ opener = urllib.request.build_opener(_NoRedirect)
89
+ request = urllib.request.Request(url, headers=dict(headers)) # noqa: S310 - url is one of SOURCES (https)
90
+ try:
91
+ with opener.open(request, timeout=60) as reply:
92
+ body = reply.read(MAX_BYTES + 1)
93
+ return Response(reply.status, {k.lower(): v for k, v in reply.headers.items()}, body)
94
+ except urllib.error.HTTPError as exc:
95
+ return Response(exc.code, {k.lower(): v for k, v in exc.headers.items()}, b"")
96
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
97
+ raise LibError(f"could not reach {url}: {exc}", "check the network and try again") from exc
98
+
99
+
100
+ @dataclass(frozen=True, slots=True)
101
+ class Outcome:
102
+ status: str
103
+ """`fetched`, `unchanged` (HTTP 304), `identical` (HTTP 200, the sha256 already held)."""
104
+ url: str
105
+ sha256: str
106
+ size: int
107
+ version: str
108
+ date: str
109
+ pinned: bool
110
+ path: Path
111
+ counts: dict[str, int]
112
+ requests: int
113
+
114
+
115
+ def import_usbids(
116
+ *,
117
+ base: Path | None = None,
118
+ transport: Transport = urllib_transport,
119
+ sources: Sequence[str] = SOURCES,
120
+ pin: Pin = PIN,
121
+ latest: bool = False,
122
+ sha256: str | None = None,
123
+ force: bool = False,
124
+ interval: float = MIN_INTERVAL,
125
+ sleep: Callable[[float], None] = time.sleep,
126
+ clock: Callable[[], float] = time.monotonic,
127
+ now: Callable[[], datetime] = lambda: datetime.now(UTC),
128
+ ) -> Outcome:
129
+ """Fetch `usb.ids` from the first source that answers 200 (or 304), check it, install it.
130
+
131
+ `latest` accepts a snapshot other than the pinned one; `sha256` accepts exactly the named one; `force` skips
132
+ the conditional request and replaces a file whose digest disagrees with its manifest.
133
+ """
134
+ if os.environ.get("PRIMARYSOURCE_READONLY"):
135
+ raise LibError("the library is read-only (PRIMARYSOURCE_READONLY is set), so nothing is fetched", "unset it")
136
+ if interval < MIN_INTERVAL:
137
+ raise LibError(f"an interval under {MIN_INTERVAL:g} second is refused", "the sources are fetched politely")
138
+ expected = sha256.lower() if sha256 else None
139
+ if expected is not None and len(expected) != 64:
140
+ raise LibError(f"{sha256!r} is not a sha256 hex digest", "")
141
+ for url in sources:
142
+ if not url.startswith("https://"):
143
+ raise LibError(f"refusing source {url!r}: https only", "")
144
+ root = base or store.home()
145
+ target = root / store.FILE
146
+ previous = store.read_manifest(root)
147
+ requests = 0
148
+ last: float | None = None
149
+ failures: list[str] = []
150
+ for url in sources:
151
+ if last is not None:
152
+ wait = interval - (clock() - last)
153
+ if wait > 0:
154
+ sleep(wait)
155
+ headers = {"User-Agent": USER_AGENT, "Accept": "text/plain"}
156
+ if not force and target.is_file() and previous.get("url") == url:
157
+ if previous.get("etag"):
158
+ headers["If-None-Match"] = str(previous["etag"])
159
+ if previous.get("last_modified"):
160
+ headers["If-Modified-Since"] = str(previous["last_modified"])
161
+ last = clock()
162
+ requests += 1
163
+ try:
164
+ reply = transport(url, headers)
165
+ except LibError as exc:
166
+ failures.append(exc.message)
167
+ continue
168
+ if reply.status == 304 and target.is_file() and previous:
169
+ held = store.load(root)
170
+ size = int(previous.get("size") or 0)
171
+ return Outcome(
172
+ "unchanged",
173
+ url,
174
+ held.sha256,
175
+ size,
176
+ held.ids.version,
177
+ held.ids.date,
178
+ bool(previous.get("pinned")),
179
+ held.path,
180
+ held.ids.counts(),
181
+ requests,
182
+ )
183
+ if reply.status != 200:
184
+ failures.append(f"{url} answered HTTP {reply.status}")
185
+ continue
186
+ if len(reply.body) > MAX_BYTES:
187
+ failures.append(f"{url} is over {MAX_BYTES:,} bytes")
188
+ continue
189
+ return _install(reply, url, root, previous, pin, latest, expected, requests, now)
190
+ raise LibError("no source gave usb.ids: " + "; ".join(failures), "check the network and try again")
191
+
192
+
193
+ def _install(
194
+ reply: Response,
195
+ url: str,
196
+ root: Path,
197
+ previous: Mapping[str, object],
198
+ pin: Pin,
199
+ latest: bool,
200
+ expected: str | None,
201
+ requests: int,
202
+ now: Callable[[], datetime],
203
+ ) -> Outcome:
204
+ digest = hashlib.sha256(reply.body).hexdigest()
205
+ ids = parse(reply.body, Limits(data_bytes=MAX_BYTES))
206
+ if not ids.entries:
207
+ raise LibError(f"{url} did not give a usb.ids file (no entries parsed)", "this is not the expected file")
208
+ if ids.problem_count:
209
+ first = ids.problems[0]
210
+ raise LibError(
211
+ f"{url}: {ids.problem_count} line(s) are not usb.ids syntax (first: line {first.line}, {first.reason})",
212
+ "nothing was written; report the line or parse a copy with primarysource.usbids.parse",
213
+ )
214
+ pinned = digest == pin.sha256
215
+ if expected is not None:
216
+ if digest != expected:
217
+ raise LibError(f"{url} has sha256 {digest}, not the {expected} you named", "nothing was written")
218
+ elif not pinned:
219
+ if ids.version == pin.version:
220
+ raise LibError(
221
+ f"{url} claims version {pin.version} but has sha256 {digest}, not the pinned {pin.sha256}: "
222
+ "the file is damaged or altered",
223
+ "nothing was written",
224
+ )
225
+ if not latest:
226
+ raise LibError(
227
+ f"{url} serves snapshot {ids.version or '(no version)'} with sha256 {digest}; the pin is "
228
+ f"{pin.version} ({pin.sha256})",
229
+ "check the digest, then re-run with --latest (or --sha256 <digest>) to install this snapshot",
230
+ )
231
+ target = root / store.FILE
232
+ status = "identical" if target.is_file() and previous.get("sha256") == digest else "fetched"
233
+ atomic_write(target, reply.body)
234
+ record = {
235
+ "url": url,
236
+ "sha256": digest,
237
+ "size": len(reply.body),
238
+ "version": ids.version,
239
+ "date": ids.date,
240
+ "fetched": now().isoformat(timespec="seconds"),
241
+ "etag": reply.headers.get("etag", ""),
242
+ "last_modified": reply.headers.get("last-modified", ""),
243
+ "pinned": pinned,
244
+ "licence": store.LICENCE,
245
+ "counts": ids.counts(),
246
+ }
247
+ atomic_write(root / store.MANIFEST, (json.dumps({"schema": 1, "file": record}, indent=2) + "\n").encode("utf-8"))
248
+ return Outcome(
249
+ status, url, digest, len(reply.body), ids.version, ids.date, pinned, target, ids.counts(), requests
250
+ )
251
+
252
+
253
+ def main(argv: Sequence[str] | None = None) -> int:
254
+ parser = argparse.ArgumentParser(prog="primarysource usbids import", description="fetch usb.ids (one request)")
255
+ parser.add_argument("--latest", action="store_true", help="accept a snapshot other than the pinned one")
256
+ parser.add_argument("--sha256", help="accept exactly the snapshot with this digest")
257
+ parser.add_argument("--force", action="store_true", help="unconditional fetch; replace a file that changed")
258
+ args = parser.parse_args(list(sys.argv[1:] if argv is None else argv))
259
+ try:
260
+ out = import_usbids(latest=args.latest, sha256=args.sha256, force=args.force)
261
+ except LibError as exc:
262
+ print(exc.as_text(), file=sys.stderr)
263
+ return 1
264
+ print(f"{out.status}: {out.path}")
265
+ print(f"version {out.version}, date {out.date}, {out.size:,} bytes, sha256 {out.sha256}")
266
+ print(f"source {out.url}; pinned snapshot: {'yes' if out.pinned else 'no'}")
267
+ print(", ".join(f"{n:,} {k}" for k, n in out.counts.items()))
268
+ return 0
269
+
270
+
271
+ if __name__ == "__main__":
272
+ raise SystemExit(main())