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,15 @@
|
|
|
1
|
+
"""`rpmdocs`: rpm's manual and manual pages, a pinned release, read as Markdown and scdoc. Served under `rpmdocs_`.
|
|
2
|
+
|
|
3
|
+
Stdlib only. `mcp` is imported by `server.py` when a server is built; `primarysource rpmdocs ...` runs `cli.py`.
|
|
4
|
+
`docs/rpmdocs.md` is the description; `catalogue.py` holds the pin, the licence and the build.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from primarysource.pkgdocs.runtime import make_collection
|
|
10
|
+
from primarysource.rpmdocs.catalogue import SPEC
|
|
11
|
+
|
|
12
|
+
COLLECTION = make_collection(SPEC, "primarysource.rpmdocs")
|
|
13
|
+
"""Refs: `rpmdocs:format_v6§Header`; see `pkgdocs.runtime`."""
|
|
14
|
+
|
|
15
|
+
__all__ = ["COLLECTION", "SPEC"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""The `rpmdocs` collection: rpm's manual (Markdown) and manual pages (scdoc) from rpm-software-management/rpm.
|
|
2
|
+
|
|
3
|
+
Source, placement and pin (read from the repository on 2026-11-13, see `docs/rpmdocs.md`):
|
|
4
|
+
|
|
5
|
+
- the tree is `docs/` at the commit of the tag `rpm-6.1.0-release`: `docs/manual/*.md` (the format documents
|
|
6
|
+
`format_v4`, `format_v6`, `tags`, `spec`, `macros`, `dependencies`, ...) and `docs/man/*.scd` (rpm(8),
|
|
7
|
+
rpm-macros(7), rpm-queryformat(7), ...), fetched as a GitHub tarball (1.2 MB);
|
|
8
|
+
- the licence is GPL-2.0-or-later: `COPYING` at the repository root says "In SPDX terms, the entire code base is
|
|
9
|
+
licensed GPL-2.0-or-later", with LGPL-2.0-or-later as an alternative for `lib/` and `rpmio/` only, which are
|
|
10
|
+
not the documentation. The files under `docs/` carry no licence of their own;
|
|
11
|
+
- placement is **layer**.
|
|
12
|
+
|
|
13
|
+
Ids: a manual page is `<name>.<section>` (`rpm-macros.7`, `rpm.8`; `rpm-macros` alone also works), a manual
|
|
14
|
+
document is its file name without `.md` (`format_v6`, `tags`).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from collections import Counter
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
|
|
23
|
+
from primarysource.errors import LibError
|
|
24
|
+
from primarysource.pkgdocs.fetch import Source
|
|
25
|
+
from primarysource.pkgdocs.model import Doc
|
|
26
|
+
from primarysource.pkgdocs.readers.markdown import read_markdown
|
|
27
|
+
from primarysource.pkgdocs.readers.scdoc import read_scdoc
|
|
28
|
+
from primarysource.pkgdocs.runtime import Build, Licence, Pin, Spec, TreeSpec, matching
|
|
29
|
+
|
|
30
|
+
COMMIT = "f832c7e69945248dd5bcfcacfd10c176ee300fa2"
|
|
31
|
+
|
|
32
|
+
PIN = Pin(
|
|
33
|
+
commit=COMMIT,
|
|
34
|
+
label="rpm-6.1.0-release",
|
|
35
|
+
date="2026-08-20",
|
|
36
|
+
files_sha256="610c31cafa342d40c7dd3e507749c1fc90fa32c3550d6b8a3e440f45b4a037b7",
|
|
37
|
+
sources=(
|
|
38
|
+
Source(f"https://codeload.github.com/rpm-software-management/rpm/tar.gz/{COMMIT}", strip=1, subdir="docs"),
|
|
39
|
+
Source(
|
|
40
|
+
f"https://github.com/rpm-software-management/rpm/archive/{COMMIT}.tar.gz",
|
|
41
|
+
strip=1,
|
|
42
|
+
hosts=("codeload.github.com",),
|
|
43
|
+
subdir="docs",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
LICENCE = Licence(
|
|
49
|
+
spdx="GPL-2.0-or-later",
|
|
50
|
+
redistribute="layer",
|
|
51
|
+
attribution="RPM Package Manager contributors",
|
|
52
|
+
evidence="COPYING at rpm-software-management/rpm " + COMMIT[:12] + ' ("In SPDX terms ... GPL-2.0-or-later")',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_MAN = re.compile(r"man/([a-z0-9._+-]+\.\d)\.scd")
|
|
56
|
+
_MANUAL = re.compile(r"manual/([A-Za-z0-9_.+-]+)\.md")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build(files: Mapping[str, bytes], pin: Pin) -> Build:
|
|
60
|
+
docs: list[Doc] = []
|
|
61
|
+
aliases: dict[str, str] = {}
|
|
62
|
+
skipped: Counter[str] = Counter()
|
|
63
|
+
for path in sorted(files):
|
|
64
|
+
man = _MAN.fullmatch(path)
|
|
65
|
+
manual = _MANUAL.fullmatch(path)
|
|
66
|
+
try:
|
|
67
|
+
if man:
|
|
68
|
+
page = man.group(1)
|
|
69
|
+
doc = read_scdoc(files[path], page)
|
|
70
|
+
aliases[page.rsplit(".", 1)[0]] = page
|
|
71
|
+
name, section = page.rsplit(".", 1)
|
|
72
|
+
body_title = _name_line(doc)
|
|
73
|
+
title = f"{name}({section})" + (f" - {body_title}" if body_title else "")
|
|
74
|
+
doc = Doc(doc.id, title, doc.unit, doc.sections, doc.meta, doc.warnings)
|
|
75
|
+
elif manual:
|
|
76
|
+
doc = read_markdown(files[path], manual.group(1))
|
|
77
|
+
else:
|
|
78
|
+
skipped["not a manual or man page source"] += 1
|
|
79
|
+
continue
|
|
80
|
+
except LibError:
|
|
81
|
+
skipped["not UTF-8 text"] += 1
|
|
82
|
+
continue
|
|
83
|
+
docs.append(doc.with_meta(path=f"docs/{path}", licence="GPL-2.0-or-later (COPYING, no file tag)"))
|
|
84
|
+
return Build(tuple(docs), aliases, skipped)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _name_line(doc: Doc) -> str:
|
|
88
|
+
"""The description in the NAME section's `*name* - description` line."""
|
|
89
|
+
for section in doc.sections:
|
|
90
|
+
if section.anchor == "name" and section.units:
|
|
91
|
+
_, sep, description = section.units[0].partition(" - ")
|
|
92
|
+
return " ".join(description.split()) if sep else ""
|
|
93
|
+
return ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
SPEC = Spec(
|
|
97
|
+
name="rpmdocs",
|
|
98
|
+
title="RPM documentation",
|
|
99
|
+
licence=LICENCE,
|
|
100
|
+
trees=(
|
|
101
|
+
TreeSpec(
|
|
102
|
+
name="docs",
|
|
103
|
+
title="docs/ of rpm-software-management/rpm (Markdown, scdoc)",
|
|
104
|
+
pins=(PIN,),
|
|
105
|
+
include=matching(r"manual/[A-Za-z0-9_.+-]+\.md|man/[a-z0-9._+-]+\.\d\.scd"),
|
|
106
|
+
build=build,
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
instructions=(
|
|
110
|
+
"rpm's manual (file formats format_v4/format_v6, tags, spec, macros, dependencies, ...) and manual pages "
|
|
111
|
+
"(rpm.8, rpm-macros.7, rpm-queryformat.7, ...) from the user's imported copy at a pinned release. Loop: "
|
|
112
|
+
"rpmdocs_search('<words>') -> rpmdocs_outline('format_v6') -> rpmdocs_read('<ref>'). A manual document's id "
|
|
113
|
+
"is its file name without .md; a man page's id is name.section ('rpm-macros.7', or just 'rpm-macros'). A "
|
|
114
|
+
"section is an id, an anchor, a heading or a unique part of one."
|
|
115
|
+
),
|
|
116
|
+
example_refs=("rpmdocs:format_v6§Header", "rpmdocs:tags§Tag types", "rpmdocs:rpm-macros.7§DESCRIPTION"),
|
|
117
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""`primarysource rpmdocs import|status|docs|outline|read|search|cite`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from primarysource.pkgdocs.serving import run
|
|
8
|
+
from primarysource.rpmdocs.catalogue import SPEC
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv: Sequence[str]) -> int:
|
|
12
|
+
return run(SPEC, argv)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""The MCP server for `rpmdocs` (`primarysource mcp --collection rpmdocs`). Imports `mcp` when this module loads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from primarysource.pkgdocs.serving import build_server
|
|
6
|
+
from primarysource.rpmdocs.catalogue import SPEC
|
|
7
|
+
|
|
8
|
+
server = build_server(SPEC)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""The MCP scaffold: how a collection's tools are registered, and how `primarysource mcp` serves collections.
|
|
2
|
+
|
|
3
|
+
Three rules from PLAN.md §5, enforced here so no collection has to remember them:
|
|
4
|
+
|
|
5
|
+
- **Errors** (§5.10). `guard` turns a `LibError` into the SDK's `ToolError`, which reaches the client as an
|
|
6
|
+
`isError` result. Its text is the `error:`, `hint:` and `action:` lines of `LibError.as_text`. The SDK puts
|
|
7
|
+
`Error executing tool <name>:` in front of every tool error's text, so the lines start after a newline: each
|
|
8
|
+
field is then at the start of its own line, where a reader or a regex finds it.
|
|
9
|
+
- **Text only** (§5.9). Every tool is registered with `structured_output=False`, so a result is never sent twice
|
|
10
|
+
(as text and again as `structuredContent`) and no tool advertises an output schema.
|
|
11
|
+
- **Prefixed names** (§5.12). `compose` builds the server `primarysource mcp` runs, from one or more collections'
|
|
12
|
+
own servers, naming every tool and prompt `<collection>_<name>` (a name that already carries the prefix keeps
|
|
13
|
+
it). `bare=True` keeps each collection's own names, for the old single-collection commands. A name, resource
|
|
14
|
+
URI or HTTP route that two collections both want is refused at startup, never silently shadowed.
|
|
15
|
+
|
|
16
|
+
This module imports only the standard library and the core at import time; `mcp` is imported when a server is
|
|
17
|
+
built, so the core and every collection import without the `[mcp]` extra (§5.13).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import functools
|
|
23
|
+
import inspect
|
|
24
|
+
from collections.abc import Awaitable, Callable, Iterable, Sequence
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Any, cast
|
|
27
|
+
|
|
28
|
+
from primarysource.errors import LibError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def tool_error(exc: LibError) -> Exception:
|
|
32
|
+
"""The SDK `ToolError` for `exc`: the fixed error lines, each at the start of a line of the result text."""
|
|
33
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
34
|
+
|
|
35
|
+
error: Exception = ToolError("\n" + exc.as_text())
|
|
36
|
+
return error
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def guard[F: Callable[..., Any]](fn: F) -> F:
|
|
40
|
+
"""Wrap a tool so a `LibError` becomes `tool_error(exc)`. Sync and async tools both keep their signature,
|
|
41
|
+
which the SDK reads to build the input schema."""
|
|
42
|
+
if getattr(fn, "__primarysource_guarded__", False):
|
|
43
|
+
return fn
|
|
44
|
+
if inspect.iscoroutinefunction(fn):
|
|
45
|
+
async_fn = cast(Callable[..., Awaitable[Any]], fn)
|
|
46
|
+
|
|
47
|
+
@functools.wraps(fn)
|
|
48
|
+
async def guarded_async(*args: Any, **kwargs: Any) -> Any:
|
|
49
|
+
try:
|
|
50
|
+
return await async_fn(*args, **kwargs)
|
|
51
|
+
except LibError as exc:
|
|
52
|
+
raise tool_error(exc) from None
|
|
53
|
+
|
|
54
|
+
wrapped: Any = guarded_async
|
|
55
|
+
else:
|
|
56
|
+
|
|
57
|
+
@functools.wraps(fn)
|
|
58
|
+
def guarded(*args: Any, **kwargs: Any) -> Any:
|
|
59
|
+
try:
|
|
60
|
+
return fn(*args, **kwargs)
|
|
61
|
+
except LibError as exc:
|
|
62
|
+
raise tool_error(exc) from None
|
|
63
|
+
|
|
64
|
+
wrapped = guarded
|
|
65
|
+
wrapped.__primarysource_guarded__ = True
|
|
66
|
+
return cast(F, wrapped)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def add_tools(server: Any, tools: Iterable[Callable[..., Any]], *, prefix: str = "", annotations: Any = None) -> None:
|
|
70
|
+
"""Register `tools` on an `MCPServer` the way every collection must: guarded, text only, and named
|
|
71
|
+
`prefix + fn.__name__` unless the function's name already starts with `prefix`."""
|
|
72
|
+
for fn in tools:
|
|
73
|
+
name = fn.__name__ if fn.__name__.startswith(prefix) else prefix + fn.__name__
|
|
74
|
+
server.add_tool(guard(fn), name=name, annotations=annotations, structured_output=False)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class Served:
|
|
79
|
+
"""One collection to serve: its registry name and title, and the `MCPServer` its own module built."""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
title: str
|
|
83
|
+
server: Any
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def public_name(collection: str, name: str, *, bare: bool = False) -> str:
|
|
87
|
+
"""The name `primarysource mcp` gives a collection's tool or prompt: `<collection>_<name>`, or `name` as it
|
|
88
|
+
is when it already starts with that prefix or when `bare` keeps the collection's own names."""
|
|
89
|
+
prefix = f"{collection}_"
|
|
90
|
+
return name if bare or name.startswith(prefix) else prefix + name
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _clash(kind: str, key: str, first: str, second: str, bare: bool) -> LibError:
|
|
94
|
+
if first == second:
|
|
95
|
+
return LibError(f"collection {first!r} defines the {kind} {key!r} twice", "")
|
|
96
|
+
if bare:
|
|
97
|
+
hint = "drop --bare-names, which suits one collection only, or serve them from separate processes"
|
|
98
|
+
else:
|
|
99
|
+
hint = "serve them from separate processes, or rename one of them in its collection"
|
|
100
|
+
return LibError(f"the {kind} {key!r} is wanted by both {first!r} and {second!r}", hint)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _instructions(served: Sequence[Served], bare: bool) -> str:
|
|
104
|
+
if bare and len(served) == 1:
|
|
105
|
+
return str(served[0].server.instructions or "")
|
|
106
|
+
parts = []
|
|
107
|
+
for item in served:
|
|
108
|
+
text = str(item.server.instructions or "").strip()
|
|
109
|
+
head = f"[{item.name}] {item.title}." + ("" if bare else f" Tools are named {item.name}_<tool>.")
|
|
110
|
+
parts.append(f"{head} {text}" if text else head)
|
|
111
|
+
return "\n\n".join(parts)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def compose(served: Sequence[Served], *, bare: bool = False) -> Any:
|
|
115
|
+
"""One `MCPServer` serving every tool, prompt, resource and HTTP route of `served`, renamed by `public_name`.
|
|
116
|
+
|
|
117
|
+
Each tool is rebuilt from its function with `structured_output=False` and behind `guard`, so the contract
|
|
118
|
+
holds whatever the collection registered. Each collection keeps its own state (database, home directory,
|
|
119
|
+
loaded corpus): its functions are reused, not copied. A collection's completion handler is carried over: a
|
|
120
|
+
request for a renamed prompt reaches it under the prompt's own name, and one for a resource template reaches
|
|
121
|
+
the collection that owns the template. A clash raises `LibError` before anything runs.
|
|
122
|
+
"""
|
|
123
|
+
if not served:
|
|
124
|
+
raise LibError("no collection to serve", "primarysource mcp --collection <name>")
|
|
125
|
+
from mcp.server.mcpserver import MCPServer
|
|
126
|
+
from mcp.server.mcpserver.tools import Tool
|
|
127
|
+
|
|
128
|
+
tools: dict[str, tuple[str, Any]] = {}
|
|
129
|
+
prompts: dict[str, tuple[str, Any]] = {}
|
|
130
|
+
prompt_names: dict[str, str] = {} # public prompt name -> the name its collection gave it
|
|
131
|
+
completers: dict[str, Any] = {} # collection -> its own `completion/complete` handler
|
|
132
|
+
resources: dict[str, tuple[str, Any]] = {}
|
|
133
|
+
templates: dict[str, tuple[str, Any]] = {}
|
|
134
|
+
routes: dict[str, tuple[str, Any]] = {}
|
|
135
|
+
|
|
136
|
+
def claim(table: dict[str, tuple[str, Any]], kind: str, key: str, owner: str, value: Any) -> None:
|
|
137
|
+
if key in table:
|
|
138
|
+
raise _clash(kind, key, table[key][0], owner, bare)
|
|
139
|
+
table[key] = (owner, value)
|
|
140
|
+
|
|
141
|
+
for item in served:
|
|
142
|
+
source = item.server
|
|
143
|
+
for tool in source._tool_manager.list_tools():
|
|
144
|
+
name = public_name(item.name, tool.name, bare=bare)
|
|
145
|
+
rebuilt = Tool.from_function(
|
|
146
|
+
guard(tool.fn),
|
|
147
|
+
name=name,
|
|
148
|
+
title=tool.title,
|
|
149
|
+
description=tool.description,
|
|
150
|
+
annotations=tool.annotations,
|
|
151
|
+
icons=tool.icons,
|
|
152
|
+
meta=tool.meta,
|
|
153
|
+
structured_output=False,
|
|
154
|
+
)
|
|
155
|
+
claim(tools, "tool", name, item.name, rebuilt)
|
|
156
|
+
for prompt in source._prompt_manager.list_prompts():
|
|
157
|
+
name = public_name(item.name, prompt.name, bare=bare)
|
|
158
|
+
claim(prompts, "prompt", name, item.name, prompt.model_copy(update={"name": name}))
|
|
159
|
+
prompt_names[name] = prompt.name
|
|
160
|
+
for resource in source._resource_manager.list_resources():
|
|
161
|
+
claim(resources, "resource", str(resource.uri), item.name, resource)
|
|
162
|
+
for template in source._resource_manager.list_templates():
|
|
163
|
+
claim(templates, "resource template", template.uri_template, item.name, template)
|
|
164
|
+
for route in source._custom_starlette_routes:
|
|
165
|
+
claim(routes, "HTTP route", f"{route.path} {sorted(route.methods or ())}", item.name, route)
|
|
166
|
+
entry = source._lowlevel_server.get_request_handler("completion/complete")
|
|
167
|
+
if entry is not None:
|
|
168
|
+
completers[item.name] = entry.handler
|
|
169
|
+
|
|
170
|
+
name = served[0].server.name if len(served) == 1 else "primarysource"
|
|
171
|
+
server = MCPServer(
|
|
172
|
+
name,
|
|
173
|
+
version=str(served[0].server.version or "") if len(served) == 1 else _version(),
|
|
174
|
+
instructions=_instructions(served, bare),
|
|
175
|
+
tools=[tool for _, tool in tools.values()],
|
|
176
|
+
)
|
|
177
|
+
for _, prompt in prompts.values():
|
|
178
|
+
server.add_prompt(prompt)
|
|
179
|
+
for _, resource in resources.values():
|
|
180
|
+
server.add_resource(resource)
|
|
181
|
+
for _, template in templates.values():
|
|
182
|
+
server._resource_manager._templates[template.uri_template] = template
|
|
183
|
+
server._custom_starlette_routes.extend(route for _, route in routes.values())
|
|
184
|
+
if completers:
|
|
185
|
+
_carry_completions(server, completers, prompts, prompt_names, templates)
|
|
186
|
+
return server
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _carry_completions(
|
|
190
|
+
server: Any,
|
|
191
|
+
completers: dict[str, Any],
|
|
192
|
+
prompts: dict[str, tuple[str, Any]],
|
|
193
|
+
prompt_names: dict[str, str],
|
|
194
|
+
templates: dict[str, tuple[str, Any]],
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Register one `completion/complete` handler on `server` that hands each request to the collection owning
|
|
197
|
+
the prompt or resource template it names, with the prompt's public name turned back into the collection's.
|
|
198
|
+
A request for something no collection completes gets no values, as the SDK answers when a handler returns
|
|
199
|
+
nothing."""
|
|
200
|
+
from mcp.types import CompleteRequestParams, CompleteResult, Completion, PromptReference
|
|
201
|
+
|
|
202
|
+
async def complete(ctx: Any, params: CompleteRequestParams) -> CompleteResult:
|
|
203
|
+
ref = params.ref
|
|
204
|
+
if isinstance(ref, PromptReference):
|
|
205
|
+
owner = prompts.get(ref.name, ("", None))[0]
|
|
206
|
+
if owner in completers:
|
|
207
|
+
original = ref.model_copy(update={"name": prompt_names[ref.name]})
|
|
208
|
+
params = params.model_copy(update={"ref": original})
|
|
209
|
+
else:
|
|
210
|
+
owner = templates.get(ref.uri, ("", None))[0]
|
|
211
|
+
handler = completers.get(owner)
|
|
212
|
+
if handler is None:
|
|
213
|
+
return CompleteResult(completion=Completion(values=[]))
|
|
214
|
+
result: CompleteResult = await handler(ctx, params)
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
server._lowlevel_server.add_request_handler("completion/complete", CompleteRequestParams, complete)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _version() -> str:
|
|
221
|
+
from primarysource import __version__
|
|
222
|
+
|
|
223
|
+
return __version__
|
primarysource/serve.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Run an MCP server on stdio or as one shared HTTP server: the transport switch every collection uses.
|
|
2
|
+
|
|
3
|
+
Lifted from rfclib's `rfclib mcp` (rfclib PR #1). stdio starts one server per client. http starts one long-lived
|
|
4
|
+
server that every client reaches by URL (MCP streamable HTTP at `/mcp`), stateless and with JSON responses, so a
|
|
5
|
+
restart loses no session. There is no authentication: the default bind is loopback, where the SDK checks the
|
|
6
|
+
Host header itself. A non-loopback bind gets the same check only when allowed hosts are named; without them it
|
|
7
|
+
warns, because anyone who can reach the port can use the server.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from primarysource.errors import LibError
|
|
18
|
+
|
|
19
|
+
MCP_HTTP_PORT = 8765
|
|
20
|
+
LOOPBACK = ("127.0.0.1", "localhost", "::1")
|
|
21
|
+
TRANSPORTS = {"stdio": "stdio", "http": "http", "streamable-http": "http", "streamable_http": "http"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalise_transport(transport: str) -> str:
|
|
25
|
+
"""`stdio` or `http`; the SDK's own spellings of streamable HTTP are accepted."""
|
|
26
|
+
found = TRANSPORTS.get(transport.strip().lower())
|
|
27
|
+
if found is None:
|
|
28
|
+
raise LibError(f"unknown transport {transport!r}", "use --transport stdio or --transport http")
|
|
29
|
+
return found
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def transport_security(host: str, allowed_hosts: Sequence[str]) -> Any:
|
|
33
|
+
"""The DNS-rebinding settings for a bind address: None on loopback (the SDK enables its own check there),
|
|
34
|
+
an allow-list when hosts are named, else None with a warning on stderr."""
|
|
35
|
+
if host in LOOPBACK:
|
|
36
|
+
return None
|
|
37
|
+
if not allowed_hosts:
|
|
38
|
+
print(
|
|
39
|
+
f"warning: listening on {host} with no authentication and no Host check; anyone who can reach the "
|
|
40
|
+
"port can use this server. Keep it behind a firewall or an authenticating reverse proxy.",
|
|
41
|
+
file=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
return None
|
|
44
|
+
from mcp.server.transport_security import TransportSecuritySettings
|
|
45
|
+
|
|
46
|
+
return TransportSecuritySettings(
|
|
47
|
+
enable_dns_rebinding_protection=True,
|
|
48
|
+
allowed_hosts=list(allowed_hosts),
|
|
49
|
+
allowed_origins=[f"{scheme}://{h}" for h in allowed_hosts for scheme in ("http", "https")],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run(
|
|
54
|
+
server: Any,
|
|
55
|
+
transport: str = "stdio",
|
|
56
|
+
host: str = "127.0.0.1",
|
|
57
|
+
port: int = MCP_HTTP_PORT,
|
|
58
|
+
allowed_hosts: Sequence[str] = (),
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Serve `server` (an `mcp.server.mcpserver.MCPServer`) until interrupted."""
|
|
61
|
+
kind = normalise_transport(transport)
|
|
62
|
+
# stdout is the JSON-RPC channel on stdio; per-request client logs would flood stderr either way
|
|
63
|
+
for name in ("httpx", "httpcore", "huggingface_hub"):
|
|
64
|
+
logging.getLogger(name).setLevel(logging.WARNING)
|
|
65
|
+
if kind == "stdio":
|
|
66
|
+
server.run(transport="stdio")
|
|
67
|
+
return
|
|
68
|
+
server.run(
|
|
69
|
+
transport="streamable-http",
|
|
70
|
+
host=host,
|
|
71
|
+
port=port,
|
|
72
|
+
stateless_http=True,
|
|
73
|
+
json_response=True,
|
|
74
|
+
transport_security=transport_security(host, allowed_hosts),
|
|
75
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Per-collection settings read from the environment: the network mode and the read-only switch.
|
|
2
|
+
|
|
3
|
+
Generalised from rfclib's `config.py` (`RFCLIB_NETWORK`, `RFCLIB_READONLY`, `require_network`). One process may
|
|
4
|
+
serve several collections (PLAN.md §5.8), so each setting is looked up per collection, most specific first, the
|
|
5
|
+
same way `home.collection_home` finds the home:
|
|
6
|
+
|
|
7
|
+
1. `<LEGACY_APP>_<KEY>`, e.g. `RFCLIB_NETWORK`: a setting from before the move keeps working;
|
|
8
|
+
2. `PRIMARYSOURCE_<COLLECTION>_<KEY>`, e.g. `PRIMARYSOURCE_NIST_NETWORK`: one collection of a shared process;
|
|
9
|
+
3. `PRIMARYSOURCE_<KEY>`: every collection.
|
|
10
|
+
|
|
11
|
+
The first variable that is set and not blank decides; a more specific `0` overrides a general `1`. A value that is
|
|
12
|
+
not understood is an error naming the variable, never a silent fallback: `RFCLIB_NETWORK=ofline` must not fetch.
|
|
13
|
+
|
|
14
|
+
Network modes:
|
|
15
|
+
|
|
16
|
+
- `auto` (the default): fetch what is asked for, and refresh catalogues when they are stale;
|
|
17
|
+
- `on-demand`: fetch only when a document is asked for;
|
|
18
|
+
- `offline`: never touch the network; answer from what is stored.
|
|
19
|
+
|
|
20
|
+
A read-only process (`<PREFIX>_READONLY=1`) must not write its library, so it is always offline: a fetch would
|
|
21
|
+
have to be stored.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
from collections.abc import Mapping
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from primarysource.errors import LibError
|
|
32
|
+
from primarysource.home import collection_home
|
|
33
|
+
|
|
34
|
+
NETWORK_MODES = ("auto", "on-demand", "offline")
|
|
35
|
+
DEFAULT_NETWORK = "auto"
|
|
36
|
+
_TRUE = ("1", "true", "yes", "on")
|
|
37
|
+
_FALSE = ("0", "false", "no", "off")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NetworkDisabled(LibError):
|
|
41
|
+
"""A fetch was needed and the settings forbid it (offline, or read-only)."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def prefixes(collection: str | None = None, legacy_app: str | None = None) -> tuple[str, ...]:
|
|
45
|
+
"""The variable prefixes consulted for `collection`, most specific first."""
|
|
46
|
+
found: list[str] = []
|
|
47
|
+
if legacy_app:
|
|
48
|
+
found.append(legacy_app.upper())
|
|
49
|
+
if collection:
|
|
50
|
+
found.append(f"PRIMARYSOURCE_{collection.upper()}")
|
|
51
|
+
found.append("PRIMARYSOURCE")
|
|
52
|
+
return tuple(found)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _lookup(key: str, names: tuple[str, ...], env: Mapping[str, str]) -> tuple[str, str] | None:
|
|
56
|
+
"""(variable, value) of the first `<prefix>_<key>` that is set and not blank, or None."""
|
|
57
|
+
for prefix in names:
|
|
58
|
+
variable = f"{prefix}_{key}"
|
|
59
|
+
value = (env.get(variable) or "").strip()
|
|
60
|
+
if value:
|
|
61
|
+
return variable, value
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Settings:
|
|
67
|
+
"""One collection's settings, read once from the environment by `settings()`."""
|
|
68
|
+
|
|
69
|
+
collection: str | None
|
|
70
|
+
home: Path | None
|
|
71
|
+
network: str
|
|
72
|
+
read_only: bool
|
|
73
|
+
network_variable: str
|
|
74
|
+
"""The variable that set `network` (or forced it, for a read-only process), for messages."""
|
|
75
|
+
read_only_variable: str
|
|
76
|
+
|
|
77
|
+
def require_network(self, what: str) -> None:
|
|
78
|
+
"""Return if `what` may be fetched now; raise `NetworkDisabled` naming the variable that forbids it."""
|
|
79
|
+
if self.read_only:
|
|
80
|
+
raise NetworkDisabled(
|
|
81
|
+
f"{what} is not stored here and this process is read-only ({self.read_only_variable})",
|
|
82
|
+
"ask the shared server that writes this library, or run the import without "
|
|
83
|
+
f"{self.read_only_variable}",
|
|
84
|
+
)
|
|
85
|
+
if self.network == "offline":
|
|
86
|
+
raise NetworkDisabled(
|
|
87
|
+
f"{what} is not stored here and {self.network_variable}=offline forbids fetching it",
|
|
88
|
+
f"run the import while online, or unset {self.network_variable}",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def settings(
|
|
93
|
+
collection: str | None = None,
|
|
94
|
+
legacy_app: str | None = None,
|
|
95
|
+
environ: Mapping[str, str] | None = None,
|
|
96
|
+
platform: str | None = None,
|
|
97
|
+
) -> Settings:
|
|
98
|
+
"""Read `collection`'s settings. With no collection only the `PRIMARYSOURCE_*` variables apply and `home` is
|
|
99
|
+
None. Raises `LibError` for a value it does not understand."""
|
|
100
|
+
env = os.environ if environ is None else environ
|
|
101
|
+
names = prefixes(collection, legacy_app)
|
|
102
|
+
|
|
103
|
+
read_only, read_only_variable = False, f"{names[0]}_READONLY"
|
|
104
|
+
found = _lookup("READONLY", names, env)
|
|
105
|
+
if found:
|
|
106
|
+
read_only_variable, value = found
|
|
107
|
+
if value.lower() in _TRUE:
|
|
108
|
+
read_only = True
|
|
109
|
+
elif value.lower() not in _FALSE:
|
|
110
|
+
raise LibError(
|
|
111
|
+
f"{read_only_variable}={value!r} is not a yes or a no", f"set {read_only_variable} to 1 or 0"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
network, network_variable = DEFAULT_NETWORK, f"{names[0]}_NETWORK"
|
|
115
|
+
found = _lookup("NETWORK", names, env)
|
|
116
|
+
if found:
|
|
117
|
+
network_variable, value = found
|
|
118
|
+
network = value.lower()
|
|
119
|
+
if network not in NETWORK_MODES:
|
|
120
|
+
raise LibError(
|
|
121
|
+
f"{network_variable}={value!r} is not a network mode",
|
|
122
|
+
f"set {network_variable} to one of {', '.join(NETWORK_MODES)}",
|
|
123
|
+
)
|
|
124
|
+
if read_only:
|
|
125
|
+
network, network_variable = "offline", read_only_variable
|
|
126
|
+
|
|
127
|
+
home = collection_home(collection, legacy_app, env, platform) if collection else None
|
|
128
|
+
return Settings(collection, home, network, read_only, network_variable, read_only_variable)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def network_mode(
|
|
132
|
+
collection: str | None = None, legacy_app: str | None = None, environ: Mapping[str, str] | None = None
|
|
133
|
+
) -> str:
|
|
134
|
+
"""`auto`, `on-demand` or `offline` for `collection`; always `offline` when it is read-only."""
|
|
135
|
+
return settings(collection, legacy_app, environ).network
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def read_only(
|
|
139
|
+
collection: str | None = None, legacy_app: str | None = None, environ: Mapping[str, str] | None = None
|
|
140
|
+
) -> bool:
|
|
141
|
+
"""True when `collection` must not write its library."""
|
|
142
|
+
return settings(collection, legacy_app, environ).read_only
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def require_network(
|
|
146
|
+
what: str,
|
|
147
|
+
collection: str | None = None,
|
|
148
|
+
legacy_app: str | None = None,
|
|
149
|
+
environ: Mapping[str, str] | None = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Raise `NetworkDisabled` unless `collection` may fetch `what` now. Read at each call, not cached."""
|
|
152
|
+
settings(collection, legacy_app, environ).require_network(what)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""USB-IF specifications as a primarysource collection. Importing this needs nothing beyond the core."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from primarysource.model import Unit
|
|
8
|
+
from primarysource.pdfcorpus.tools import GRAMMAR
|
|
9
|
+
from primarysource.registry import Collection
|
|
10
|
+
|
|
11
|
+
# The server registers the bare names (search, read, outline, info, cite); `primarysource mcp` serves them as
|
|
12
|
+
# usb_search, ... and `--bare-names` keeps usblib's names. Refs: `<doc_id>§<section>` and `<doc_id>:<first>-<last>`.
|
|
13
|
+
COLLECTION = Collection(
|
|
14
|
+
name="usb",
|
|
15
|
+
title="USB-IF specifications (CCID and others), fetched by the user",
|
|
16
|
+
prefix="usb_",
|
|
17
|
+
unit=Unit.BLOCK,
|
|
18
|
+
grammar=GRAMMAR,
|
|
19
|
+
server="primarysource.usb.server.mcp:mcp",
|
|
20
|
+
cli="primarysource.usb.collection:main",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv: Sequence[str]) -> int:
|
|
25
|
+
"""`primarysource usb fetch ...` (download, extract and index the specifications in sources.toml) and
|
|
26
|
+
`primarysource usb index` (rebuild the search index from the fetched corpus)."""
|
|
27
|
+
if argv and argv[0] == "index":
|
|
28
|
+
from primarysource.pdfcorpus import fts
|
|
29
|
+
from primarysource.usb.spec import SPEC
|
|
30
|
+
|
|
31
|
+
return fts.main(SPEC, argv[1:])
|
|
32
|
+
if not argv or argv[0] != "fetch":
|
|
33
|
+
print("usage: primarysource usb {fetch,index} [args]", flush=True)
|
|
34
|
+
return 2
|
|
35
|
+
from primarysource.usb.ingest import fetch
|
|
36
|
+
|
|
37
|
+
return int(fetch.main(list(argv[1:])))
|