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,233 @@
|
|
|
1
|
+
"""Read the imported POSIX HTML edition: page outlines, budgeted reads by ref, and a bounded grep.
|
|
2
|
+
|
|
3
|
+
A page (`functions/open`) is converted with the collection's private HTML reader (`_html`, profile `POSIX`) into a
|
|
4
|
+
`primarysource.model.Document` whose sections are the page's headings, keyed by their HTML anchors
|
|
5
|
+
(`tag_17_372_03`). Each section's units are the blocks under its heading, the heading excluded and subsections
|
|
6
|
+
included. The core has no index yet, so there is no search here: `grep` scans pages, under a time and hit budget.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import difflib
|
|
12
|
+
import html
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from primarysource import cursor
|
|
19
|
+
from primarysource.budget import page as budget_page
|
|
20
|
+
from primarysource.errors import LibError
|
|
21
|
+
from primarysource.ieee import POSIX, POSIX_PAGE, _html
|
|
22
|
+
from primarysource.ieee.catalogue import POSIX_2024, Edition
|
|
23
|
+
from primarysource.ieee.importer import INSTALL_RECORD, collection_home
|
|
24
|
+
from primarysource.model import Document, Section, span_hash
|
|
25
|
+
from primarysource.refs import Ref
|
|
26
|
+
from primarysource.render import next_start, render
|
|
27
|
+
|
|
28
|
+
VOLUMES = {"basedefs": "XBD", "functions": "XSH", "utilities": "XCU", "xrat": "XRAT"}
|
|
29
|
+
"""The four volumes' directories and their usual short names."""
|
|
30
|
+
|
|
31
|
+
MAX_NEEDLE = 200
|
|
32
|
+
MAX_HITS = 200
|
|
33
|
+
_PAGE = re.compile(POSIX_PAGE)
|
|
34
|
+
_TAGS = re.compile(r"<[^>]*>")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class Page:
|
|
39
|
+
"""One converted page: the model document plus the converter's output it was built from."""
|
|
40
|
+
|
|
41
|
+
page: str
|
|
42
|
+
document: Document
|
|
43
|
+
converted: _html.Document
|
|
44
|
+
|
|
45
|
+
def section_for(self, anchor: str) -> Section | None:
|
|
46
|
+
"""The section an anchor names: a heading's own anchor, or an id that sits just before a heading."""
|
|
47
|
+
found = self.document.section(anchor)
|
|
48
|
+
if found is None and anchor in self.converted.aliases:
|
|
49
|
+
head = self.converted.headings[self.converted.aliases[anchor]]
|
|
50
|
+
found = self.document.section(head.anchor)
|
|
51
|
+
return found
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_page(data: bytes, page: str, edition: Edition = POSIX_2024) -> Page:
|
|
55
|
+
"""Convert one page's bytes. All 1,616 pages of the 2024 archive are UTF-8 (measured); a stray byte is
|
|
56
|
+
replaced rather than refused, so one bad character cannot hide a page."""
|
|
57
|
+
converted = _html.convert(data.decode("utf-8", errors="replace"), page, _html.POSIX)
|
|
58
|
+
sections: list[Section] = []
|
|
59
|
+
first = converted.headings[0].block if converted.headings else len(converted.blocks)
|
|
60
|
+
if first:
|
|
61
|
+
sections.append(Section("", converted.title or page, tuple(converted.blocks[:first]), 0))
|
|
62
|
+
for position, head in enumerate(converted.headings):
|
|
63
|
+
start, end = converted.span(position)
|
|
64
|
+
anchor = head.anchor or f"_{position + 1}"
|
|
65
|
+
sections.append(Section(anchor, head.text, tuple(converted.blocks[start + 1 : end]), head.level))
|
|
66
|
+
title = converted.title or page
|
|
67
|
+
document = Document(f"{edition.id}§{page}", title, POSIX.unit, tuple(sections))
|
|
68
|
+
return Page(page, document, converted)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def installed(edition: Edition = POSIX_2024, home: Path | None = None) -> Path:
|
|
72
|
+
"""The directory of an imported edition, or a LibError that says how to import it."""
|
|
73
|
+
root = collection_home("posix", home) / edition.id
|
|
74
|
+
if not (root / INSTALL_RECORD).is_file():
|
|
75
|
+
raise LibError(
|
|
76
|
+
f"POSIX {edition.id} is not imported",
|
|
77
|
+
"",
|
|
78
|
+
f"download {edition.source_url} (personal use, see docs/ieee.md), then run "
|
|
79
|
+
f"primarysource posix import {edition.archive}",
|
|
80
|
+
)
|
|
81
|
+
return root
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def pages(volume: str | None = None, *, edition: Edition = POSIX_2024, home: Path | None = None) -> list[str]:
|
|
85
|
+
"""Page ids of the imported edition, sorted; only those under `volume` (`functions`) when given."""
|
|
86
|
+
root = installed(edition, home)
|
|
87
|
+
found = [p.relative_to(root).as_posix()[: -len(".html")] for p in root.rglob("*.html")]
|
|
88
|
+
return sorted(p for p in found if volume is None or p.startswith(volume + "/"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_page(page: str, *, edition: Edition = POSIX_2024, home: Path | None = None) -> Page:
|
|
92
|
+
"""Read and convert one page, refusing a page id outside the grammar and naming near misses."""
|
|
93
|
+
if not _PAGE.fullmatch(page):
|
|
94
|
+
raise LibError(f"not a POSIX page id: {page!r}", "a page is a path such as functions/open or utilities/pax")
|
|
95
|
+
root = installed(edition, home)
|
|
96
|
+
path = root.joinpath(*page.split("/")).with_name(page.split("/")[-1] + ".html")
|
|
97
|
+
if not path.is_file():
|
|
98
|
+
near = difflib.get_close_matches(page, pages(edition=edition, home=home), n=3)
|
|
99
|
+
raise LibError(f"no page {page!r} in {edition.id}", ("did you mean " + ", ".join(near)) if near else "")
|
|
100
|
+
return parse_page(path.read_bytes(), page, edition)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cite(page: Page, section: Section | None = None, para: int | str | None = None) -> str:
|
|
104
|
+
"""`IEEE Std 1003.1-2024 (The Open Group Base Specifications Issue 8), XSH open, DESCRIPTION ¶2`."""
|
|
105
|
+
volume = VOLUMES.get(page.page.split("/")[0], "")
|
|
106
|
+
where = f"{volume} {page.document.title}".strip()
|
|
107
|
+
out = f"{POSIX_2024.designations[0]} ({POSIX_2024.designations[1]}), {where}"
|
|
108
|
+
if section is not None and section.id:
|
|
109
|
+
out += f", {section.title}"
|
|
110
|
+
return out + (f" ¶{para}" if para is not None else "")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _ref_text(edition: str, page: str, anchor: str | None, para: int | None = None) -> str:
|
|
114
|
+
return POSIX.grammar.format(Ref(edition, page, para, anchor or None))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def outline(page: str, *, edition: Edition = POSIX_2024, home: Path | None = None) -> str:
|
|
118
|
+
"""The page's headings, indented by level, each with the ref that reads it."""
|
|
119
|
+
loaded = load_page(page, edition=edition, home=home)
|
|
120
|
+
heads = [s for s in loaded.document.sections if s.id]
|
|
121
|
+
top = min((s.depth for s in heads), default=1)
|
|
122
|
+
lines = [f"{' ' * (s.depth - top)}{s.title} {_ref_text(edition.id, page, s.id)}" for s in heads]
|
|
123
|
+
body = f"{loaded.document.title} {_ref_text(edition.id, page, None)}\n" + "\n".join(lines)
|
|
124
|
+
return render(body, {"cite": cite(loaded), "unit": POSIX.unit})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def read(ref: str, *, max_tokens: int = 2000, edition: Edition = POSIX_2024, home: Path | None = None) -> str:
|
|
128
|
+
"""A page or a section (`#anchor`), from block n with `¶n`, as text with the fixed footer. Long results are
|
|
129
|
+
paged: the `next:` footer is the ref that reads the next page."""
|
|
130
|
+
if "§" not in ref and "::" not in ref and _PAGE.match(ref.strip()):
|
|
131
|
+
ref = f"{edition.id}§{ref.strip()}" # a bare page (`functions/open#tag_17_372_03`) reads this edition
|
|
132
|
+
parsed = POSIX.grammar.parse(ref)
|
|
133
|
+
if parsed.lines is not None:
|
|
134
|
+
raise LibError(f"{ref!r}: POSIX pages have no line numbers", "use posix-2024§<page>#<anchor>¶<n>")
|
|
135
|
+
if parsed.doc is not None and parsed.doc != edition.id:
|
|
136
|
+
raise LibError(f"{parsed.doc!r} is not imported; this reads {edition.id}", "")
|
|
137
|
+
if parsed.section is None:
|
|
138
|
+
raise LibError(f"{ref!r} names no page", "for example posix-2024§functions/open")
|
|
139
|
+
loaded = load_page(parsed.section, edition=edition, home=home)
|
|
140
|
+
section: Section | None = None
|
|
141
|
+
units: tuple[str, ...] = tuple(loaded.converted.blocks)
|
|
142
|
+
if parsed.anchor is not None:
|
|
143
|
+
section = loaded.section_for(parsed.anchor)
|
|
144
|
+
if section is None:
|
|
145
|
+
anchors = [s.id for s in loaded.document.sections if s.id]
|
|
146
|
+
near = difflib.get_close_matches(parsed.anchor, anchors, n=3)
|
|
147
|
+
hint = ("did you mean " + ", ".join(near)) if near else f"posix_outline {parsed.section} lists them"
|
|
148
|
+
raise LibError(f"no anchor {parsed.anchor!r} on {parsed.section}", hint)
|
|
149
|
+
units = section.units
|
|
150
|
+
if parsed.para is not None and parsed.para > len(units):
|
|
151
|
+
raise LibError(f"{ref!r}: there are {len(units)} blocks here", f"use ¶1 to ¶{len(units)}" if units else "")
|
|
152
|
+
result = budget_page(units, parsed.para or 1, max_tokens)
|
|
153
|
+
body = "\n\n".join(result.units)
|
|
154
|
+
footer = {
|
|
155
|
+
"cite": cite(loaded, section, cursor.para_label(parsed.para, result)),
|
|
156
|
+
"unit": POSIX.unit,
|
|
157
|
+
"span": span_hash(body) if body else "",
|
|
158
|
+
"next": cursor.read_next(_ref_text(edition.id, parsed.section, parsed.anchor), result),
|
|
159
|
+
"warning": "one block is larger than max_tokens" if result.oversize else "",
|
|
160
|
+
}
|
|
161
|
+
return render(body or "(this section has no blocks of its own)", footer)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _plain(block: str) -> str:
|
|
165
|
+
return re.sub(r"\s+", " ", _html.to_plain(block).replace("*", "")).strip()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def grep(
|
|
169
|
+
needle: str,
|
|
170
|
+
*,
|
|
171
|
+
volume: str | None = None,
|
|
172
|
+
limit: int = 20,
|
|
173
|
+
start: int = 0,
|
|
174
|
+
seconds: float = 10.0,
|
|
175
|
+
edition: Edition = POSIX_2024,
|
|
176
|
+
home: Path | None = None,
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Pages whose converted text contains `needle`, case-insensitively, one line per matching block.
|
|
179
|
+
|
|
180
|
+
Bounded three ways: at most `limit` hits (or every hit of the first page, if it alone has more), at most
|
|
181
|
+
`seconds` of scanning, and a needle of at most 200 characters. A literal, not a pattern, so no input can make
|
|
182
|
+
the scan super-linear. Pages are taken whole, so no hit is skipped: when a bound stops the scan,
|
|
183
|
+
`next: start=<n>` is the 0-based index of the page to resume from, in the order `pages` lists them."""
|
|
184
|
+
wanted = needle.strip().casefold()
|
|
185
|
+
if not wanted or len(wanted) > MAX_NEEDLE:
|
|
186
|
+
raise LibError("grep needs between 1 and 200 characters of text", "")
|
|
187
|
+
limit = max(1, min(limit, MAX_HITS))
|
|
188
|
+
if start < 0:
|
|
189
|
+
raise LibError(f"start is a 0-based page offset, not {start}", "pass the start= value of the last next: line")
|
|
190
|
+
listing = pages(volume, edition=edition, home=home)
|
|
191
|
+
root = installed(edition, home)
|
|
192
|
+
deadline = time.monotonic() + seconds
|
|
193
|
+
hits: list[str] = []
|
|
194
|
+
resume: int | None = None
|
|
195
|
+
for index in range(start, len(listing)):
|
|
196
|
+
if len(hits) >= limit or time.monotonic() > deadline:
|
|
197
|
+
resume = index
|
|
198
|
+
break
|
|
199
|
+
page = listing[index]
|
|
200
|
+
raw = root.joinpath(*page.split("/")).with_name(page.split("/")[-1] + ".html").read_bytes()
|
|
201
|
+
rough = html.unescape(_TAGS.sub("", raw.decode("utf-8", errors="replace")))
|
|
202
|
+
if wanted not in re.sub(r"\s+", " ", rough).casefold():
|
|
203
|
+
continue
|
|
204
|
+
found = _page_hits(parse_page(raw, page, edition), wanted, edition.id)
|
|
205
|
+
if hits and len(hits) + len(found) > limit:
|
|
206
|
+
resume = index
|
|
207
|
+
break
|
|
208
|
+
hits.extend(found)
|
|
209
|
+
body = "\n".join(hits) if hits else f"no block contains {needle!r}"
|
|
210
|
+
footer = {
|
|
211
|
+
"unit": POSIX.unit,
|
|
212
|
+
"next": next_start(resume),
|
|
213
|
+
"warning": f"stopped before page {resume + 1} of {len(listing)}" if resume is not None else "",
|
|
214
|
+
}
|
|
215
|
+
return render(body, footer)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _page_hits(loaded: Page, wanted: str, edition_id: str) -> list[str]:
|
|
219
|
+
heads = [h for h in loaded.converted.headings if h.kind == "heading"]
|
|
220
|
+
found: list[str] = []
|
|
221
|
+
for index, block in enumerate(loaded.converted.blocks):
|
|
222
|
+
text = _plain(block)
|
|
223
|
+
if wanted not in text.casefold():
|
|
224
|
+
continue
|
|
225
|
+
owner = next((h for h in reversed(heads) if h.block < index), None)
|
|
226
|
+
if owner is None or not owner.anchor:
|
|
227
|
+
ref = _ref_text(edition_id, loaded.page, None, index + 1)
|
|
228
|
+
else:
|
|
229
|
+
ref = _ref_text(edition_id, loaded.page, owner.anchor, index - owner.block)
|
|
230
|
+
at = text.casefold().find(wanted)
|
|
231
|
+
snippet = text[max(0, at - 60) : at + len(wanted) + 60]
|
|
232
|
+
found.append(f"{ref}: {snippet}")
|
|
233
|
+
return found
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""MCP servers for the posix and ieee802 collections: `primarysource mcp --collection posix` (or `ieee802`).
|
|
2
|
+
|
|
3
|
+
Imported only when served, so `mcp` (the `[mcp]` extra) is needed only then. Tools carry the collection's prefix
|
|
4
|
+
(`posix_read`, `ieee802_outline`), answer in text with the fixed footer, and turn a `LibError` into an MCP tool
|
|
5
|
+
error (`isError`) whose text names the remedy. Every tool reads local files only; none writes or fetches.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from mcp.server.mcpserver import MCPServer
|
|
14
|
+
from mcp.types import ToolAnnotations
|
|
15
|
+
|
|
16
|
+
from primarysource import __version__
|
|
17
|
+
from primarysource.ieee import ieee802, posix
|
|
18
|
+
from primarysource.scaffold import add_tools
|
|
19
|
+
|
|
20
|
+
READ_ONLY = ToolAnnotations(read_only_hint=True, idempotent_hint=True, destructive_hint=False, open_world_hint=False)
|
|
21
|
+
|
|
22
|
+
POSIX_INSTRUCTIONS = (
|
|
23
|
+
"POSIX: IEEE Std 1003.1-2024, the same text as The Open Group Base Specifications Issue 8, from the user's "
|
|
24
|
+
"own imported copy of the HTML edition. Pages are addressed by path: functions/open (XSH), utilities/pax "
|
|
25
|
+
"(XCU), basedefs/stdio.h and basedefs/V1_chap03 (XBD), xrat/... (rationale). Loop: posix_grep('<words>') or "
|
|
26
|
+
"posix_outline('<page>') -> posix_read('<ref>'). Refs: 'posix-2024§functions/open' (page), "
|
|
27
|
+
"'posix-2024§functions/open#tag_17_372_03' (a heading's anchor), '...¶2' (the 2nd block under it). The "
|
|
28
|
+
"cite: footer line is the citation. If the edition is not imported, the error says how."
|
|
29
|
+
)
|
|
30
|
+
IEEE802_INSTRUCTIONS = (
|
|
31
|
+
"IEEE 802 standards from PDFs the user obtained through the IEEE GET Program and imported. "
|
|
32
|
+
"ieee802_outline('802.1Q-2022') lists clauses as refs ('802.1Q-2022§5.4') with their PDF pages. Reading "
|
|
33
|
+
"clause text is not built yet."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def posix_read(ref: str, max_tokens: int = 2000) -> str:
|
|
38
|
+
"""Read POSIX text by ref, cut at block boundaries within max_tokens. A 'next: <ref>' footer line is the ref
|
|
39
|
+
that reads the next page: pass it back as ref.
|
|
40
|
+
|
|
41
|
+
ref: 'posix-2024§functions/open' (whole page) | 'posix-2024§functions/open#tag_17_372_03' (one heading's
|
|
42
|
+
section, subsections included) | '...#tag_17_372_03¶2' (from its 2nd block). 'functions/open' alone also
|
|
43
|
+
works."""
|
|
44
|
+
return posix.read(ref, max_tokens=max_tokens)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def posix_outline(page: str) -> str:
|
|
48
|
+
"""List a POSIX page's headings, each with the ref that reads it. page: 'functions/open', 'utilities/pax',
|
|
49
|
+
'basedefs/V1_chap03'."""
|
|
50
|
+
return posix.outline(page)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def posix_grep(text: str, volume: str | None = None, limit: int = 20, start: int = 0) -> str:
|
|
54
|
+
"""Find blocks containing text (case-insensitive, literal, at most 200 characters). One line per hit:
|
|
55
|
+
'<ref>: <snippet>'. volume: functions | utilities | basedefs | xrat. A 'next: start=N' footer line means call
|
|
56
|
+
again with start=N (a 0-based page offset)."""
|
|
57
|
+
return posix.grep(text, volume=volume, limit=limit, start=start)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def posix_pages(volume: str | None = None) -> str:
|
|
61
|
+
"""List page ids, optionally of one volume: functions (XSH), utilities (XCU), basedefs (XBD), xrat."""
|
|
62
|
+
return "\n".join(posix.pages(volume)) or "no pages"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def ieee802_outline(designation: str) -> str:
|
|
66
|
+
"""List an imported 802 standard's clauses from its PDF bookmarks. designation: '802.1Q-2022'."""
|
|
67
|
+
return ieee802.outline(designation)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _server(name: str, instructions: str, tools: list[Callable[..., str]]) -> Any:
|
|
71
|
+
server = MCPServer(name, version=__version__, instructions=instructions)
|
|
72
|
+
add_tools(server, tools, annotations=READ_ONLY)
|
|
73
|
+
return server
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
posix_server = _server(
|
|
77
|
+
"primarysource-posix", POSIX_INSTRUCTIONS, [posix_read, posix_outline, posix_grep, posix_pages]
|
|
78
|
+
)
|
|
79
|
+
ieee802_server = _server("primarysource-ieee802", IEEE802_INSTRUCTIONS, [ieee802_outline])
|