primarysource 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- primarysource/__init__.py +5 -0
- primarysource/bbf/__init__.py +56 -0
- primarysource/bbf/_xml.py +63 -0
- primarysource/bbf/catalogue.json +777 -0
- primarysource/bbf/catalogue.py +137 -0
- primarysource/bbf/cli.py +79 -0
- primarysource/bbf/datamodel.py +281 -0
- primarysource/bbf/importer.py +151 -0
- primarysource/bbf/library.py +380 -0
- primarysource/bbf/server.py +72 -0
- primarysource/budget.py +63 -0
- primarysource/c2sp/__init__.py +53 -0
- primarysource/c2sp/_fetch.py +5 -0
- primarysource/c2sp/catalogue.json +231 -0
- primarysource/c2sp/catalogue.py +95 -0
- primarysource/c2sp/cli.py +62 -0
- primarysource/c2sp/importer.py +90 -0
- primarysource/c2sp/library.py +309 -0
- primarysource/c2sp/markdown.py +31 -0
- primarysource/c2sp/server.py +68 -0
- primarysource/cavp/__init__.py +42 -0
- primarysource/cavp/cli.py +84 -0
- primarysource/cavp/data/vectors.toml +47 -0
- primarysource/cavp/importer.py +156 -0
- primarysource/cavp/model.py +279 -0
- primarysource/cavp/parse.py +269 -0
- primarysource/cavp/paths.py +17 -0
- primarysource/cavp/server.py +67 -0
- primarysource/cavp/tools.py +169 -0
- primarysource/cli.py +107 -0
- primarysource/cursor.py +82 -0
- primarysource/cve/__init__.py +5 -0
- primarysource/cve/__main__.py +6 -0
- primarysource/cve/cli.py +144 -0
- primarysource/cve/collection.py +20 -0
- primarysource/cve/config.py +58 -0
- primarysource/cve/errors.py +16 -0
- primarysource/cve/kernel.py +232 -0
- primarysource/cve/mcp.py +139 -0
- primarysource/cve/parse.py +318 -0
- primarysource/cve/py.typed +0 -0
- primarysource/cve/service.py +760 -0
- primarysource/cve/store.py +214 -0
- primarysource/cve/sync.py +438 -0
- primarysource/cve/views.py +304 -0
- primarysource/debpolicy/__init__.py +16 -0
- primarysource/debpolicy/catalogue.py +263 -0
- primarysource/debpolicy/cli.py +12 -0
- primarysource/debpolicy/server.py +11 -0
- primarysource/draft/__init__.py +11 -0
- primarysource/draft/catalogue.py +117 -0
- primarysource/draft/cli.py +149 -0
- primarysource/draft/collection.py +21 -0
- primarysource/draft/datatracker.py +158 -0
- primarysource/draft/importer.py +163 -0
- primarysource/draft/licence.py +89 -0
- primarysource/draft/names.py +64 -0
- primarysource/draft/net.py +133 -0
- primarysource/draft/segment.py +192 -0
- primarysource/draft/server.py +91 -0
- primarysource/draft/status.py +146 -0
- primarysource/draft/store.py +199 -0
- primarysource/draft/tracked.json +104 -0
- primarysource/draft/views.py +430 -0
- primarysource/draft/xmlmeta.py +74 -0
- primarysource/ecma/__init__.py +52 -0
- primarysource/ecma/_pdf.py +143 -0
- primarysource/ecma/catalogue.json +22 -0
- primarysource/ecma/catalogue.py +100 -0
- primarysource/ecma/cli.py +67 -0
- primarysource/ecma/identify.py +25 -0
- primarysource/ecma/importer.py +108 -0
- primarysource/ecma/library.py +240 -0
- primarysource/ecma/server.py +67 -0
- primarysource/errors.py +29 -0
- primarysource/fetch.py +360 -0
- primarysource/fsutil.py +150 -0
- primarysource/gitdocs/__init__.py +15 -0
- primarysource/gitdocs/catalogue.py +116 -0
- primarysource/gitdocs/cli.py +12 -0
- primarysource/gitdocs/server.py +8 -0
- primarysource/home.py +65 -0
- primarysource/iana/__init__.py +32 -0
- primarysource/iana/catalogue.json +244 -0
- primarysource/iana/catalogue.py +69 -0
- primarysource/iana/cli.py +69 -0
- primarysource/iana/importer.py +157 -0
- primarysource/iana/model.py +325 -0
- primarysource/iana/reader.py +315 -0
- primarysource/iana/server.py +67 -0
- primarysource/iana/store.py +83 -0
- primarysource/iana/xmlsafe.py +47 -0
- primarysource/identify.py +186 -0
- primarysource/ieee/__init__.py +58 -0
- primarysource/ieee/_html.py +49 -0
- primarysource/ieee/catalogue.json +74 -0
- primarysource/ieee/catalogue.py +123 -0
- primarysource/ieee/cli.py +79 -0
- primarysource/ieee/identify.py +24 -0
- primarysource/ieee/ieee802.py +47 -0
- primarysource/ieee/importer.py +305 -0
- primarysource/ieee/posix.py +233 -0
- primarysource/ieee/server.py +79 -0
- primarysource/index.py +446 -0
- primarysource/iso/__init__.py +51 -0
- primarysource/iso/cli.py +98 -0
- primarysource/iso/designation.py +95 -0
- primarysource/iso/identify.py +100 -0
- primarysource/iso/importer.py +224 -0
- primarysource/iso/library.py +249 -0
- primarysource/iso/server.py +69 -0
- primarysource/itu/__init__.py +41 -0
- primarysource/itu/__main__.py +8 -0
- primarysource/itu/asn1mod.py +478 -0
- primarysource/itu/catalog.py +472 -0
- primarysource/itu/cli.py +445 -0
- primarysource/itu/collection.py +20 -0
- primarysource/itu/corpus.py +118 -0
- primarysource/itu/errata.py +16 -0
- primarysource/itu/formal.py +565 -0
- primarysource/itu/identify.py +26 -0
- primarysource/itu/index.py +490 -0
- primarysource/itu/mcp.py +474 -0
- primarysource/itu/py.typed +0 -0
- primarysource/itu/sources.py +600 -0
- primarysource/itu/text.py +609 -0
- primarysource/kconfig/__init__.py +16 -0
- primarysource/kconfig/catalogue.py +98 -0
- primarysource/kconfig/cli.py +12 -0
- primarysource/kconfig/server.py +8 -0
- primarysource/model.py +63 -0
- primarysource/nist/__init__.py +5 -0
- primarysource/nist/collection.py +46 -0
- primarysource/nist/data/acquired-nist.json +1657 -0
- primarysource/nist/data/nist-catalog.json +12090 -0
- primarysource/nist/data/sources.toml +1996 -0
- primarysource/nist/identify.py +41 -0
- primarysource/nist/ingest/__init__.py +1 -0
- primarysource/nist/ingest/acquire_nist.py +128 -0
- primarysource/nist/ingest/controls.py +82 -0
- primarysource/nist/ingest/discover_nist.py +218 -0
- primarysource/nist/ingest/extract.py +48 -0
- primarysource/nist/ingest/fetch.py +211 -0
- primarysource/nist/ingest/index.py +14 -0
- primarysource/nist/ingest/oscal.py +480 -0
- primarysource/nist/ingest/prose_layouts.py +236 -0
- primarysource/nist/ingest/sources.py +94 -0
- primarysource/nist/ingest/status.py +255 -0
- primarysource/nist/models.py +7 -0
- primarysource/nist/paths.py +33 -0
- primarysource/nist/server/__init__.py +3 -0
- primarysource/nist/server/corpus.py +38 -0
- primarysource/nist/server/mcp.py +19 -0
- primarysource/nist/server/tools.py +35 -0
- primarysource/nist/spec.py +38 -0
- primarysource/pdfcorpus/__init__.py +16 -0
- primarysource/pdfcorpus/corpus.py +159 -0
- primarysource/pdfcorpus/fts.py +175 -0
- primarysource/pdfcorpus/index.py +70 -0
- primarysource/pdfcorpus/models.py +30 -0
- primarysource/pdfcorpus/server.py +96 -0
- primarysource/pdfcorpus/spec.py +37 -0
- primarysource/pdfcorpus/split.py +391 -0
- primarysource/pdfcorpus/tools.py +244 -0
- primarysource/pep/__init__.py +15 -0
- primarysource/pep/catalogue.py +191 -0
- primarysource/pep/cli.py +12 -0
- primarysource/pep/server.py +8 -0
- primarysource/pkgdocs/__init__.py +19 -0
- primarysource/pkgdocs/fetch.py +356 -0
- primarysource/pkgdocs/index.py +89 -0
- primarysource/pkgdocs/model.py +76 -0
- primarysource/pkgdocs/readers/__init__.py +1 -0
- primarysource/pkgdocs/readers/_common.py +134 -0
- primarysource/pkgdocs/readers/asciidoc.py +160 -0
- primarysource/pkgdocs/readers/latex.py +315 -0
- primarysource/pkgdocs/readers/markdown.py +173 -0
- primarysource/pkgdocs/readers/pod.py +132 -0
- primarysource/pkgdocs/readers/rst.py +239 -0
- primarysource/pkgdocs/readers/scdoc.py +108 -0
- primarysource/pkgdocs/runtime.py +552 -0
- primarysource/pkgdocs/serving.py +158 -0
- primarysource/pkgdocs/store.py +167 -0
- primarysource/pms/__init__.py +16 -0
- primarysource/pms/catalogue.py +88 -0
- primarysource/pms/cli.py +12 -0
- primarysource/pms/server.py +8 -0
- primarysource/py.typed +0 -0
- primarysource/readers/__init__.py +10 -0
- primarysource/readers/bookmarks.py +192 -0
- primarysource/readers/html.py +1312 -0
- primarysource/readers/markdown.py +189 -0
- primarysource/readers/pdf.py +187 -0
- primarysource/readers/xml.py +230 -0
- primarysource/refs.py +87 -0
- primarysource/registry.py +161 -0
- primarysource/render.py +49 -0
- primarysource/rfc/__init__.py +3 -0
- primarysource/rfc/__main__.py +6 -0
- primarysource/rfc/cite.py +204 -0
- primarysource/rfc/cli.py +903 -0
- primarysource/rfc/collection.py +19 -0
- primarysource/rfc/config.py +138 -0
- primarysource/rfc/errata.py +203 -0
- primarysource/rfc/fsutil.py +82 -0
- primarysource/rfc/graph.py +213 -0
- primarysource/rfc/index/__init__.py +1 -0
- primarysource/rfc/index/chunker.py +281 -0
- primarysource/rfc/index/embed.py +166 -0
- primarysource/rfc/index/hybrid.py +305 -0
- primarysource/rfc/index/lexical.py +223 -0
- primarysource/rfc/index/rfcxml.py +673 -0
- primarysource/rfc/index/search.py +508 -0
- primarysource/rfc/index/txtseg.py +585 -0
- primarysource/rfc/library.py +294 -0
- primarysource/rfc/mcp/__init__.py +1 -0
- primarysource/rfc/mcp/server.py +381 -0
- primarysource/rfc/packs/__init__.py +1 -0
- primarysource/rfc/packs/builder.py +228 -0
- primarysource/rfc/packs/collections/all-current-txt.json +10 -0
- primarysource/rfc/packs/collections/essentials.json +19 -0
- primarysource/rfc/packs/collections/fyi-current.json +10 -0
- primarysource/rfc/packs/collections/standards-current.json +10 -0
- primarysource/rfc/packs/collections/year-2025.json +10 -0
- primarysource/rfc/packs/collections/year-2026.json +10 -0
- primarysource/rfc/packs/collections.py +163 -0
- primarysource/rfc/packs/docsetexport.py +163 -0
- primarysource/rfc/packs/epub.py +159 -0
- primarysource/rfc/packs/export.py +306 -0
- primarysource/rfc/packs/htmlexport.py +82 -0
- primarysource/rfc/packs/serve.py +143 -0
- primarysource/rfc/packs/zimexport.py +171 -0
- primarysource/rfc/query.py +466 -0
- primarysource/rfc/subjects.py +457 -0
- primarysource/rfc/sync/__init__.py +1 -0
- primarysource/rfc/sync/downloader.py +132 -0
- primarysource/rfc/sync/manifest.py +162 -0
- primarysource/rfc/sync/scanner.py +49 -0
- primarysource/rfc/sync/state.py +464 -0
- primarysource/rfc/views.py +1067 -0
- primarysource/rpmdocs/__init__.py +15 -0
- primarysource/rpmdocs/catalogue.py +117 -0
- primarysource/rpmdocs/cli.py +12 -0
- primarysource/rpmdocs/server.py +8 -0
- primarysource/scaffold.py +223 -0
- primarysource/serve.py +75 -0
- primarysource/settings.py +152 -0
- primarysource/usb/__init__.py +5 -0
- primarysource/usb/collection.py +37 -0
- primarysource/usb/data/acquired.json +2025 -0
- primarysource/usb/data/sources-usb.toml +852 -0
- primarysource/usb/data/sources.toml +10 -0
- primarysource/usb/data/usb-catalog.json +1663 -0
- primarysource/usb/data/usb-classification.json +1986 -0
- primarysource/usb/data/usb-corpus-build.json +608 -0
- primarysource/usb/data/usb-pdf-choice.json +839 -0
- primarysource/usb/data/usb-split-measure.json +1803 -0
- primarysource/usb/ingest/__init__.py +1 -0
- primarysource/usb/ingest/acquire.py +130 -0
- primarysource/usb/ingest/discover_usb.py +88 -0
- primarysource/usb/ingest/extract.py +48 -0
- primarysource/usb/ingest/fetch.py +197 -0
- primarysource/usb/ingest/index.py +14 -0
- primarysource/usb/ingest/sources.py +85 -0
- primarysource/usb/models.py +7 -0
- primarysource/usb/paths.py +33 -0
- primarysource/usb/server/__init__.py +3 -0
- primarysource/usb/server/corpus.py +38 -0
- primarysource/usb/server/mcp.py +19 -0
- primarysource/usb/server/tools.py +35 -0
- primarysource/usb/spec.py +20 -0
- primarysource/usbids/__init__.py +56 -0
- primarysource/usbids/cli.py +65 -0
- primarysource/usbids/importer.py +272 -0
- primarysource/usbids/parse.py +283 -0
- primarysource/usbids/server.py +55 -0
- primarysource/usbids/store.py +80 -0
- primarysource/usbids/tools.py +174 -0
- primarysource/zim/__init__.py +5 -0
- primarysource/zim/__main__.py +8 -0
- primarysource/zim/_libzim.py +37 -0
- primarysource/zim/catalog.py +594 -0
- primarysource/zim/cli.py +555 -0
- primarysource/zim/collection.py +41 -0
- primarysource/zim/errors.py +100 -0
- primarysource/zim/groups.py +30 -0
- primarysource/zim/library.py +668 -0
- primarysource/zim/manage.py +636 -0
- primarysource/zim/markdown.py +1247 -0
- primarysource/zim/mcp.py +201 -0
- primarysource/zim/purl.py +130 -0
- primarysource/zim/py.typed +0 -0
- primarysource/zim/refs.py +90 -0
- primarysource/zim/service.py +1182 -0
- primarysource/zim/snapshot.py +197 -0
- primarysource/zim/sync.py +269 -0
- primarysource/zim/views.py +230 -0
- primarysource-0.1.0.dist-info/METADATA +118 -0
- primarysource-0.1.0.dist-info/RECORD +303 -0
- primarysource-0.1.0.dist-info/WHEEL +4 -0
- primarysource-0.1.0.dist-info/entry_points.txt +2 -0
- primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
- primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
|
@@ -0,0 +1,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())
|