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,123 @@
|
|
|
1
|
+
"""The catalogue of IEEE editions: designations, titles, sources and pins. Metadata only, never text.
|
|
2
|
+
|
|
3
|
+
`catalogue.json` is package data. The pins it holds are also exposed as module constants (`POSIX_2024`,
|
|
4
|
+
`POSIX_TERMS`, `IEEE802_TERMS`) so that the core's collection manifest (`cl.collection_manifest`, owned by the
|
|
5
|
+
core) can lift them when it lands. Until then this module is the only reader of the file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from importlib import resources
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from primarysource.errors import LibError
|
|
16
|
+
|
|
17
|
+
PLACEMENTS = ("layer", "volume", "none")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Terms:
|
|
22
|
+
"""What a collection's publisher allows, and therefore where its text may be placed."""
|
|
23
|
+
|
|
24
|
+
collection: str
|
|
25
|
+
licence: str
|
|
26
|
+
placement: str
|
|
27
|
+
terms_url: str
|
|
28
|
+
terms_read: str
|
|
29
|
+
summary: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class Edition:
|
|
34
|
+
"""One edition of one document. `sha256` is None until a pin exists (an 802 PDF the user has not supplied)."""
|
|
35
|
+
|
|
36
|
+
collection: str
|
|
37
|
+
id: str
|
|
38
|
+
designations: tuple[str, ...]
|
|
39
|
+
title: str
|
|
40
|
+
publisher: str
|
|
41
|
+
edition_date: str
|
|
42
|
+
page_url: str
|
|
43
|
+
source_url: str | None
|
|
44
|
+
archive: str | None
|
|
45
|
+
sha256: str | None
|
|
46
|
+
size: int | None
|
|
47
|
+
root: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _text(entry: dict[str, Any], key: str) -> str:
|
|
51
|
+
value = entry.get(key)
|
|
52
|
+
if not isinstance(value, str) or not value:
|
|
53
|
+
raise ValueError(f"catalogue entry {entry.get('id')!r}: {key} must be a non-empty string")
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _optional(entry: dict[str, Any], key: str) -> str | None:
|
|
58
|
+
value = entry.get(key)
|
|
59
|
+
if value is not None and not isinstance(value, str):
|
|
60
|
+
raise ValueError(f"catalogue entry {entry.get('id')!r}: {key} must be a string or null")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse(raw: str) -> tuple[dict[str, Terms], dict[str, Edition]]:
|
|
65
|
+
"""Validate the catalogue text. A malformed catalogue is a packaging bug, so it raises ValueError."""
|
|
66
|
+
data = json.loads(raw)
|
|
67
|
+
if data.get("schema") != 1:
|
|
68
|
+
raise ValueError("catalogue schema must be 1")
|
|
69
|
+
terms: dict[str, Terms] = {}
|
|
70
|
+
editions: dict[str, Edition] = {}
|
|
71
|
+
for collection, body in data["collections"].items():
|
|
72
|
+
placement = _text(body, "placement")
|
|
73
|
+
if placement not in PLACEMENTS:
|
|
74
|
+
raise ValueError(f"{collection}: placement {placement!r} is not one of {PLACEMENTS}")
|
|
75
|
+
terms[collection] = Terms(
|
|
76
|
+
collection,
|
|
77
|
+
_text(body, "licence"),
|
|
78
|
+
placement,
|
|
79
|
+
_text(body, "terms_url"),
|
|
80
|
+
_text(body, "terms_read"),
|
|
81
|
+
_text(body, "terms_summary"),
|
|
82
|
+
)
|
|
83
|
+
for entry in body["editions"]:
|
|
84
|
+
digest = _optional(entry, "sha256")
|
|
85
|
+
if digest is not None and (len(digest) != 64 or digest.strip("0123456789abcdef")):
|
|
86
|
+
raise ValueError(f"{entry.get('id')!r}: sha256 must be 64 lowercase hex digits")
|
|
87
|
+
size = entry.get("size")
|
|
88
|
+
if size is not None and (not isinstance(size, int) or size <= 0):
|
|
89
|
+
raise ValueError(f"{entry.get('id')!r}: size must be a positive integer or null")
|
|
90
|
+
edition = Edition(
|
|
91
|
+
collection,
|
|
92
|
+
_text(entry, "id"),
|
|
93
|
+
tuple(entry["designations"]),
|
|
94
|
+
_text(entry, "title"),
|
|
95
|
+
_text(entry, "publisher"),
|
|
96
|
+
_text(entry, "edition_date"),
|
|
97
|
+
_text(entry, "page_url"),
|
|
98
|
+
_optional(entry, "source_url"),
|
|
99
|
+
_optional(entry, "archive"),
|
|
100
|
+
digest,
|
|
101
|
+
size,
|
|
102
|
+
_optional(entry, "root"),
|
|
103
|
+
)
|
|
104
|
+
if edition.id in editions:
|
|
105
|
+
raise ValueError(f"duplicate edition id {edition.id!r}")
|
|
106
|
+
editions[edition.id] = edition
|
|
107
|
+
return terms, editions
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
TERMS, EDITIONS = parse(resources.files("primarysource.ieee").joinpath("catalogue.json").read_text(encoding="utf-8"))
|
|
111
|
+
|
|
112
|
+
POSIX_TERMS = TERMS["posix"]
|
|
113
|
+
IEEE802_TERMS = TERMS["ieee802"]
|
|
114
|
+
POSIX_2024 = EDITIONS["posix-2024"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def edition(collection: str, edition_id: str) -> Edition:
|
|
118
|
+
"""The catalogued edition, or a LibError that lists the ones this collection knows."""
|
|
119
|
+
found = EDITIONS.get(edition_id)
|
|
120
|
+
if found is None or found.collection != collection:
|
|
121
|
+
known = ", ".join(sorted(e.id for e in EDITIONS.values() if e.collection == collection)) or "none"
|
|
122
|
+
raise LibError(f"{edition_id!r} is not in the {collection} catalogue (known: {known})", "")
|
|
123
|
+
return found
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""`primarysource posix ...` and `primarysource ieee802 ...`: the collections' own commands.
|
|
2
|
+
|
|
3
|
+
primarysource posix import <susv5.tgz>
|
|
4
|
+
primarysource posix pages [volume]
|
|
5
|
+
primarysource posix outline <page>
|
|
6
|
+
primarysource posix read <ref> [--max-tokens N]
|
|
7
|
+
primarysource posix grep <text> [--volume V] [--limit N] [--start N]
|
|
8
|
+
primarysource ieee802 import <file.pdf> <designation>
|
|
9
|
+
primarysource ieee802 outline <designation>
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
|
|
18
|
+
from primarysource.errors import LibError
|
|
19
|
+
from primarysource.ieee import ieee802, importer, posix
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _posix_parser() -> argparse.ArgumentParser:
|
|
23
|
+
parser = argparse.ArgumentParser(prog="primarysource posix", description="IEEE Std 1003.1-2024 (POSIX)")
|
|
24
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
25
|
+
commands.add_parser("import", help="verify and import the user's susv5.tgz").add_argument("archive")
|
|
26
|
+
commands.add_parser("pages", help="list page ids").add_argument("volume", nargs="?")
|
|
27
|
+
commands.add_parser("outline", help="a page's headings with refs").add_argument("page")
|
|
28
|
+
read = commands.add_parser("read", help="read by ref")
|
|
29
|
+
read.add_argument("ref")
|
|
30
|
+
read.add_argument("--max-tokens", type=int, default=2000)
|
|
31
|
+
grep = commands.add_parser("grep", help="find blocks containing text")
|
|
32
|
+
grep.add_argument("text")
|
|
33
|
+
grep.add_argument("--volume")
|
|
34
|
+
grep.add_argument("--limit", type=int, default=20)
|
|
35
|
+
grep.add_argument("--start", type=int, default=0)
|
|
36
|
+
return parser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ieee802_parser() -> argparse.ArgumentParser:
|
|
40
|
+
parser = argparse.ArgumentParser(prog="primarysource ieee802", description="IEEE 802 standards (GET Program)")
|
|
41
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
42
|
+
imp = commands.add_parser("import", help="import a PDF you obtained yourself")
|
|
43
|
+
imp.add_argument("pdf")
|
|
44
|
+
imp.add_argument("designation")
|
|
45
|
+
commands.add_parser("outline", help="clauses from the PDF's bookmarks").add_argument("designation")
|
|
46
|
+
return parser
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run(text: str) -> int:
|
|
50
|
+
sys.stdout.write(text if text.endswith("\n") else text + "\n")
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def posix_main(argv: Sequence[str]) -> int:
|
|
55
|
+
args = _posix_parser().parse_args(list(argv))
|
|
56
|
+
try:
|
|
57
|
+
if args.command == "import":
|
|
58
|
+
return importer.main(["posix", args.archive])
|
|
59
|
+
if args.command == "pages":
|
|
60
|
+
return _run("\n".join(posix.pages(args.volume)))
|
|
61
|
+
if args.command == "outline":
|
|
62
|
+
return _run(posix.outline(args.page))
|
|
63
|
+
if args.command == "read":
|
|
64
|
+
return _run(posix.read(args.ref, max_tokens=args.max_tokens))
|
|
65
|
+
return _run(posix.grep(args.text, volume=args.volume, limit=args.limit, start=args.start))
|
|
66
|
+
except LibError as exc:
|
|
67
|
+
print(exc.as_text(), file=sys.stderr)
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ieee802_main(argv: Sequence[str]) -> int:
|
|
72
|
+
args = _ieee802_parser().parse_args(list(argv))
|
|
73
|
+
try:
|
|
74
|
+
if args.command == "import":
|
|
75
|
+
return importer.main(["ieee802", args.pdf, args.designation])
|
|
76
|
+
return _run(ieee802.outline(args.designation))
|
|
77
|
+
except LibError as exc:
|
|
78
|
+
print(exc.as_text(), file=sys.stderr)
|
|
79
|
+
return 1
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""How the ieee802 collection recognises an IEEE 802 standard in a supplied PDF (`registry.Collection.identify`).
|
|
2
|
+
|
|
3
|
+
Covers and running headers read `IEEE Std 802.1Q-2022`. The importer takes the user's own PDF and the designation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from primarysource.identify import Candidate, Probe, candidates
|
|
11
|
+
|
|
12
|
+
_STD = re.compile(r"\bIEEE\s+Std\.?\s+(?P<designation>802(?:\.\d{1,2}[A-Za-z]{0,4})*-\d{4})\b")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _label(match: re.Match[str]) -> str:
|
|
16
|
+
return f"IEEE Std {match['designation']}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def identify(probe: Probe) -> list[Candidate]:
|
|
20
|
+
def command(label: str) -> str:
|
|
21
|
+
where = probe.path if probe.path is not None else probe.name
|
|
22
|
+
return f'primarysource ieee802 import "{where}" {label.removeprefix("IEEE Std ")}'
|
|
23
|
+
|
|
24
|
+
return candidates(probe, "ieee802", _STD, _label, command)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Read what `importer.import_pdf` stored for an 802 standard: its clause outline from the PDF's bookmarks.
|
|
2
|
+
|
|
3
|
+
Reading a clause's sentences (`802.1Q-2022§5.4¶2`) needs a clause segmenter over the extracted page text; that is
|
|
4
|
+
the core's planned `cl.segment_bookmarks` (itulib's walker), so it is a `[ ]` row (`ieee.ieee802_read`), not code
|
|
5
|
+
written twice here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from primarysource.errors import LibError
|
|
14
|
+
from primarysource.ieee import IEEE802
|
|
15
|
+
from primarysource.ieee.catalogue import edition
|
|
16
|
+
from primarysource.ieee.importer import INSTALL_RECORD, collection_home, normalise_designation
|
|
17
|
+
from primarysource.refs import Ref
|
|
18
|
+
from primarysource.render import render
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def installed(designation: str, home: Path | None = None) -> Path:
|
|
22
|
+
"""The directory of an imported 802 standard, or a LibError that says how to import it."""
|
|
23
|
+
pinned = edition("ieee802", normalise_designation(designation))
|
|
24
|
+
root = collection_home("ieee802", home) / pinned.id
|
|
25
|
+
if not (root / INSTALL_RECORD).is_file():
|
|
26
|
+
raise LibError(
|
|
27
|
+
f"{pinned.id} is not imported",
|
|
28
|
+
"",
|
|
29
|
+
f"obtain the PDF yourself from {pinned.page_url} (IEEE GET Program terms, see docs/ieee.md), then run "
|
|
30
|
+
f"primarysource ieee802 import <file.pdf> {pinned.id}",
|
|
31
|
+
)
|
|
32
|
+
return root
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def outline(designation: str, *, home: Path | None = None) -> str:
|
|
36
|
+
"""The PDF's bookmarks, indented by depth, with a ref for each numbered clause and its PDF page."""
|
|
37
|
+
root = installed(designation, home)
|
|
38
|
+
pinned = edition("ieee802", normalise_designation(designation))
|
|
39
|
+
entries = json.loads((root / "outline.json").read_text(encoding="utf-8"))
|
|
40
|
+
lines = []
|
|
41
|
+
for entry in entries:
|
|
42
|
+
ref = IEEE802.grammar.format(Ref(pinned.id, entry["clause"])) if entry["clause"] else ""
|
|
43
|
+
where = f"p. {entry['page']}" if entry["page"] else "no page"
|
|
44
|
+
parts = [entry["title"], ref, f"({where})"]
|
|
45
|
+
lines.append(" " * (entry["depth"] - 1) + " ".join(part for part in parts if part))
|
|
46
|
+
body = "\n".join(lines) if lines else f"{pinned.id} has no bookmarks"
|
|
47
|
+
return render(body, {"cite": f"{pinned.designations[0]}, {pinned.title}", "unit": IEEE802.unit})
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Import a user-supplied IEEE document into `$PRIMARYSOURCE_HOME/<collection>/<edition>/`.
|
|
2
|
+
|
|
3
|
+
Nothing here fetches. The user downloads the POSIX archive or an 802 PDF themselves, under the publisher's terms
|
|
4
|
+
(`docs/ieee.md`), and hands the file over; a name, an email address, a login or a terms checkbox is a human act
|
|
5
|
+
this code never performs.
|
|
6
|
+
|
|
7
|
+
`import_posix` checks the archive against the pinned sha256 before reading a single member, then streams the
|
|
8
|
+
tarball (`tarfile` mode `r|gz`, no seeking) and writes only the HTML pages, under bounds, into a staging directory
|
|
9
|
+
that replaces the installed edition in one rename. This is an installation step: the readers in `posix.py` then
|
|
10
|
+
read one page file at a time.
|
|
11
|
+
|
|
12
|
+
`import_pdf` reads an 802 PDF with `pypdf` (the `[ieee]` extra) and stores its bookmarks and page text.
|
|
13
|
+
|
|
14
|
+
Run as `python -m primarysource.ieee.importer posix <susv5.tgz>` or `... ieee802 <file.pdf> <designation>`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import secrets
|
|
24
|
+
import shutil
|
|
25
|
+
import sys
|
|
26
|
+
import tarfile
|
|
27
|
+
from collections import Counter
|
|
28
|
+
from collections.abc import Sequence
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import UTC, datetime
|
|
31
|
+
from pathlib import Path, PurePosixPath
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from primarysource.errors import LibError
|
|
35
|
+
from primarysource.home import atomic_write
|
|
36
|
+
from primarysource.home import collection_home as core_collection_home
|
|
37
|
+
from primarysource.ieee import POSIX_PAGE
|
|
38
|
+
from primarysource.ieee.catalogue import POSIX_2024, Edition, edition
|
|
39
|
+
|
|
40
|
+
INSTALL_RECORD = "install.json"
|
|
41
|
+
INSTALL_HINT = 'pip install "primarysource[ieee]"'
|
|
42
|
+
_PAGE = re.compile(POSIX_PAGE)
|
|
43
|
+
_CHUNK = 1 << 20
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class Limits:
|
|
48
|
+
"""Ceilings on what an import will accept. The real POSIX 2024 archive measures 4,962,222 bytes, 1,715
|
|
49
|
+
members, 1,616 HTML pages, 24,248,288 bytes of HTML, largest page 459,326 bytes."""
|
|
50
|
+
|
|
51
|
+
archive_bytes: int = 64_000_000
|
|
52
|
+
members: int = 10_000
|
|
53
|
+
member_bytes: int = 4_000_000
|
|
54
|
+
total_bytes: int = 200_000_000
|
|
55
|
+
pdf_bytes: int = 300_000_000
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class Report:
|
|
60
|
+
"""What an import wrote and what it skipped, by reason."""
|
|
61
|
+
|
|
62
|
+
edition: str
|
|
63
|
+
path: Path
|
|
64
|
+
pages: int
|
|
65
|
+
bytes: int
|
|
66
|
+
skipped: dict[str, int] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def collection_home(collection: str, home: Path | None = None) -> Path:
|
|
70
|
+
"""The core's `home.collection_home` (`$PRIMARYSOURCE_HOME/<collection>`, PLAN.md §5.8), or
|
|
71
|
+
`<home>/<collection>` when a home is given (tests, the measurement script)."""
|
|
72
|
+
return home / collection if home is not None else core_collection_home(collection)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def sha256_file(path: Path, limit: int) -> tuple[str, int]:
|
|
76
|
+
"""Digest and size of a file, refusing one larger than `limit` without reading past it."""
|
|
77
|
+
digest = hashlib.sha256()
|
|
78
|
+
size = 0
|
|
79
|
+
with path.open("rb") as stream:
|
|
80
|
+
while chunk := stream.read(_CHUNK):
|
|
81
|
+
size += len(chunk)
|
|
82
|
+
if size > limit:
|
|
83
|
+
raise LibError(f"{path.name} is over {limit:,} bytes; this is not the expected file", "")
|
|
84
|
+
digest.update(chunk)
|
|
85
|
+
return digest.hexdigest(), size
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _verify(path: Path, pinned: Edition, limit: int) -> tuple[str, int]:
|
|
89
|
+
if not path.is_file():
|
|
90
|
+
raise LibError(f"no file at {path}", f"download {pinned.source_url or pinned.page_url} first")
|
|
91
|
+
got, size = sha256_file(path, limit)
|
|
92
|
+
if pinned.sha256 is not None and got != pinned.sha256:
|
|
93
|
+
raise LibError(
|
|
94
|
+
f"{path.name} has sha256 {got}, not the {pinned.sha256} pinned for {pinned.id}: the file is damaged, "
|
|
95
|
+
"truncated, or a different edition or archive format",
|
|
96
|
+
f"download {pinned.source_url or pinned.page_url} again (docs/ieee.md says how)",
|
|
97
|
+
)
|
|
98
|
+
return got, size
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _member_path(name: str) -> PurePosixPath:
|
|
102
|
+
"""The member's path, or a LibError for one that could escape the install directory."""
|
|
103
|
+
if "\x00" in name or "\\" in name or name.startswith("/") or re.match(r"[A-Za-z]:", name):
|
|
104
|
+
raise LibError(f"refusing archive member {name!r}: absolute or unsafe path", "")
|
|
105
|
+
path = PurePosixPath(name)
|
|
106
|
+
if ".." in path.parts:
|
|
107
|
+
raise LibError(f"refusing archive member {name!r}: path traversal", "")
|
|
108
|
+
return path
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _install(staging: Path, target: Path) -> None:
|
|
112
|
+
"""Replace `target` with `staging` by renames, so a reader sees the old edition or the new one."""
|
|
113
|
+
old = target.with_name(f".{target.name}.old-{secrets.token_hex(4)}")
|
|
114
|
+
if target.exists():
|
|
115
|
+
os.replace(target, old)
|
|
116
|
+
try:
|
|
117
|
+
os.replace(staging, target)
|
|
118
|
+
except BaseException:
|
|
119
|
+
if old.exists():
|
|
120
|
+
os.replace(old, target)
|
|
121
|
+
raise
|
|
122
|
+
shutil.rmtree(old, ignore_errors=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def import_posix(
|
|
126
|
+
archive: Path | str,
|
|
127
|
+
*,
|
|
128
|
+
home: Path | None = None,
|
|
129
|
+
pinned: Edition = POSIX_2024,
|
|
130
|
+
limits: Limits | None = None,
|
|
131
|
+
) -> Report:
|
|
132
|
+
"""Verify and install the POSIX HTML archive. Refuses, naming the cause, on a digest mismatch, an unsafe or
|
|
133
|
+
oversize member, or more members or bytes than `limits` allow; nothing is installed unless all of it passed."""
|
|
134
|
+
limits = limits or Limits()
|
|
135
|
+
path = Path(archive)
|
|
136
|
+
if pinned.sha256 is None or pinned.root is None:
|
|
137
|
+
raise LibError(f"{pinned.id} has no pinned archive", "")
|
|
138
|
+
digest, size = _verify(path, pinned, limits.archive_bytes)
|
|
139
|
+
base = collection_home(pinned.collection, home)
|
|
140
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
staging = base / f".{pinned.id}.part-{secrets.token_hex(4)}"
|
|
142
|
+
staging.mkdir()
|
|
143
|
+
skipped: Counter[str] = Counter()
|
|
144
|
+
pages = 0
|
|
145
|
+
written = 0
|
|
146
|
+
try:
|
|
147
|
+
with path.open("rb") as raw, tarfile.open(fileobj=raw, mode="r|gz") as tar:
|
|
148
|
+
for count, member in enumerate(tar, start=1):
|
|
149
|
+
if count > limits.members:
|
|
150
|
+
raise LibError(f"{path.name} has more than {limits.members:,} members", "")
|
|
151
|
+
name = _member_path(member.name)
|
|
152
|
+
if not member.isfile():
|
|
153
|
+
skipped["not a regular file"] += 1
|
|
154
|
+
continue
|
|
155
|
+
relative = str(name)
|
|
156
|
+
if not relative.startswith(pinned.root):
|
|
157
|
+
skipped["outside " + pinned.root] += 1
|
|
158
|
+
continue
|
|
159
|
+
relative = relative[len(pinned.root) :]
|
|
160
|
+
if not relative.endswith(".html"):
|
|
161
|
+
skipped["not html"] += 1
|
|
162
|
+
continue
|
|
163
|
+
if not _PAGE.fullmatch(relative[: -len(".html")]):
|
|
164
|
+
skipped["no page id"] += 1
|
|
165
|
+
continue
|
|
166
|
+
if member.size > limits.member_bytes:
|
|
167
|
+
raise LibError(f"refusing {member.name!r}: {member.size:,} bytes is over the per-page limit", "")
|
|
168
|
+
stream = tar.extractfile(member)
|
|
169
|
+
data = stream.read(limits.member_bytes + 1) if stream is not None else b""
|
|
170
|
+
if len(data) > limits.member_bytes:
|
|
171
|
+
raise LibError(f"refusing {member.name!r}: over the per-page limit", "")
|
|
172
|
+
written += len(data)
|
|
173
|
+
if written > limits.total_bytes:
|
|
174
|
+
raise LibError(f"{path.name} unpacks to more than {limits.total_bytes:,} bytes of pages", "")
|
|
175
|
+
destination = staging.joinpath(*PurePosixPath(relative).parts)
|
|
176
|
+
if staging.resolve() not in destination.resolve().parents:
|
|
177
|
+
raise LibError(f"refusing archive member {member.name!r}: resolves outside the install", "")
|
|
178
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
destination.write_bytes(data)
|
|
180
|
+
pages += 1
|
|
181
|
+
record = {
|
|
182
|
+
"edition": pinned.id,
|
|
183
|
+
"archive": path.name,
|
|
184
|
+
"sha256": digest,
|
|
185
|
+
"size": size,
|
|
186
|
+
"pages": pages,
|
|
187
|
+
"bytes": written,
|
|
188
|
+
"skipped": dict(sorted(skipped.items())),
|
|
189
|
+
"imported": datetime.now(UTC).isoformat(timespec="seconds"),
|
|
190
|
+
}
|
|
191
|
+
(staging / INSTALL_RECORD).write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
|
|
192
|
+
target = base / pinned.id
|
|
193
|
+
_install(staging, target)
|
|
194
|
+
except tarfile.TarError as exc:
|
|
195
|
+
raise LibError(f"{path.name} is not a readable gzip tar archive ({exc})", "") from exc
|
|
196
|
+
finally:
|
|
197
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
198
|
+
return Report(pinned.id, target, pages, written, dict(skipped))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_CLAUSE = re.compile(r"\s*(?:Annex\s+)?(?P<id>[0-9]+(?:\.[0-9]+)*|[A-Z](?:\.[0-9]+)*)\b[\s.:)-]*(?P<title>.*)")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def clause_of(title: str) -> str | None:
|
|
205
|
+
"""The clause or annex number a bookmark title starts with (`5.4 Bridge`, `Annex A`), or None."""
|
|
206
|
+
match = _CLAUSE.match(title)
|
|
207
|
+
if match is None:
|
|
208
|
+
return None
|
|
209
|
+
ident = match["id"]
|
|
210
|
+
if len(ident) == 1 and ident.isalpha() and not title.lstrip().startswith(("Annex", ident + ".", ident + " ")):
|
|
211
|
+
return None
|
|
212
|
+
return ident
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def normalise_designation(text: str) -> str:
|
|
216
|
+
"""`IEEE Std 802.1Q-2022` and `IEEE 802.1Q-2022` -> `802.1Q-2022`."""
|
|
217
|
+
return re.sub(r"^\s*IEEE\s+(?:Std\.?\s+)?", "", text).strip()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def import_pdf(
|
|
221
|
+
pdf: Path | str,
|
|
222
|
+
designation: str,
|
|
223
|
+
*,
|
|
224
|
+
home: Path | None = None,
|
|
225
|
+
limits: Limits | None = None,
|
|
226
|
+
) -> Report:
|
|
227
|
+
"""Store the bookmarks and page text of a user-supplied 802 PDF. Refuses a designation the catalogue does not
|
|
228
|
+
list, a file that is not a PDF, a digest that differs from a pin, and an encrypted PDF (never decrypted)."""
|
|
229
|
+
limits = limits or Limits()
|
|
230
|
+
pinned = edition("ieee802", normalise_designation(designation))
|
|
231
|
+
path = Path(pdf)
|
|
232
|
+
if not path.is_file():
|
|
233
|
+
raise LibError(f"no file at {path}", f"download {pinned.id} from {pinned.page_url} yourself first")
|
|
234
|
+
with path.open("rb") as stream:
|
|
235
|
+
if stream.read(5) != b"%PDF-":
|
|
236
|
+
raise LibError(f"{path.name} is not a PDF", "")
|
|
237
|
+
digest, size = _verify(path, pinned, limits.pdf_bytes)
|
|
238
|
+
try:
|
|
239
|
+
import pypdf
|
|
240
|
+
except ModuleNotFoundError as exc:
|
|
241
|
+
raise LibError(f"importing an 802 PDF needs {exc.name!r}, which is not installed", INSTALL_HINT) from exc
|
|
242
|
+
try:
|
|
243
|
+
reader = pypdf.PdfReader(path)
|
|
244
|
+
if reader.is_encrypted:
|
|
245
|
+
raise LibError(f"{path.name} is encrypted; primarysource never decrypts a document", "")
|
|
246
|
+
outline = _outline(reader, reader.outline, 1)
|
|
247
|
+
pages = [page.extract_text() or "" for page in reader.pages]
|
|
248
|
+
except pypdf.errors.PyPdfError as exc:
|
|
249
|
+
raise LibError(f"{path.name} could not be read as a PDF ({exc})", "") from exc
|
|
250
|
+
target = collection_home("ieee802", home) / pinned.id
|
|
251
|
+
record: dict[str, Any] = {
|
|
252
|
+
"edition": pinned.id,
|
|
253
|
+
"file": path.name,
|
|
254
|
+
"sha256": digest,
|
|
255
|
+
"size": size,
|
|
256
|
+
"pinned": pinned.sha256 is not None,
|
|
257
|
+
"backend": f"pypdf {pypdf.__version__}",
|
|
258
|
+
"pages": len(pages),
|
|
259
|
+
"imported": datetime.now(UTC).isoformat(timespec="seconds"),
|
|
260
|
+
}
|
|
261
|
+
atomic_write(target / "outline.json", (json.dumps(outline, indent=1) + "\n").encode("utf-8"))
|
|
262
|
+
atomic_write(target / "pages.json", json.dumps(pages).encode("utf-8"))
|
|
263
|
+
atomic_write(target / INSTALL_RECORD, (json.dumps(record, indent=2) + "\n").encode("utf-8"))
|
|
264
|
+
return Report(pinned.id, target, len(pages), sum(len(p) for p in pages), {})
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _outline(reader: Any, items: Sequence[Any], depth: int) -> list[dict[str, Any]]:
|
|
268
|
+
"""Bookmarks in reading order: depth, title, clause number (or None) and 1-based page."""
|
|
269
|
+
found: list[dict[str, Any]] = []
|
|
270
|
+
if depth > 12:
|
|
271
|
+
return found
|
|
272
|
+
for item in items:
|
|
273
|
+
if isinstance(item, list):
|
|
274
|
+
found.extend(_outline(reader, item, depth + 1))
|
|
275
|
+
continue
|
|
276
|
+
title = str(getattr(item, "title", "") or "").strip()
|
|
277
|
+
try:
|
|
278
|
+
page = reader.get_destination_page_number(item) + 1
|
|
279
|
+
except Exception: # a broken destination loses its page, not the import
|
|
280
|
+
page = None
|
|
281
|
+
found.append({"depth": depth, "title": title, "clause": clause_of(title), "page": page})
|
|
282
|
+
return found
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
286
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
287
|
+
try:
|
|
288
|
+
if len(args) == 2 and args[0] == "posix":
|
|
289
|
+
report = import_posix(args[1])
|
|
290
|
+
elif len(args) == 3 and args[0] == "ieee802":
|
|
291
|
+
report = import_pdf(args[1], args[2])
|
|
292
|
+
else:
|
|
293
|
+
print("usage: python -m primarysource.ieee.importer posix <susv5.tgz>", file=sys.stderr)
|
|
294
|
+
print(" python -m primarysource.ieee.importer ieee802 <file.pdf> <designation>", file=sys.stderr)
|
|
295
|
+
return 2
|
|
296
|
+
except LibError as exc:
|
|
297
|
+
print(exc.as_text(), file=sys.stderr)
|
|
298
|
+
return 1
|
|
299
|
+
skipped = ", ".join(f"{reason}: {n}" for reason, n in sorted(report.skipped.items())) or "none"
|
|
300
|
+
print(f"{report.edition}: {report.pages} pages, {report.bytes:,} bytes -> {report.path} (skipped: {skipped})")
|
|
301
|
+
return 0
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
raise SystemExit(main())
|