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,146 @@
|
|
|
1
|
+
"""A draft's life cycle as the datatracker last reported it, and what that means for someone about to cite it.
|
|
2
|
+
|
|
3
|
+
`Status` is a snapshot with the time it was taken. It is stored beside the draft's revisions and printed by
|
|
4
|
+
`info`, and every read and cite result puts its verdict in the `warning:` line, so a caller never quotes a dead
|
|
5
|
+
draft without being told: RFC 2026 §2.2 says an Internet-Draft is removed after six months unchanged, has no
|
|
6
|
+
formal status and is subject to change or removal at any time.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import asdict, dataclass
|
|
13
|
+
from datetime import UTC, date, datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from primarysource.errors import LibError
|
|
17
|
+
|
|
18
|
+
STALE_DAYS = 14
|
|
19
|
+
|
|
20
|
+
ACTIVE = "active"
|
|
21
|
+
EXPIRED = "expired"
|
|
22
|
+
REPLACED = "replaced"
|
|
23
|
+
RFC = "rfc"
|
|
24
|
+
WITHDRAWN = "withdrawn"
|
|
25
|
+
UNKNOWN = "unknown"
|
|
26
|
+
|
|
27
|
+
_LEVELS = {
|
|
28
|
+
"active": ACTIVE,
|
|
29
|
+
"expired": EXPIRED,
|
|
30
|
+
"replaced": REPLACED,
|
|
31
|
+
"rfc": RFC,
|
|
32
|
+
"withdrawn by submitter": WITHDRAWN,
|
|
33
|
+
"withdrawn by ietf": WITHDRAWN,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class Status:
|
|
39
|
+
"""What the datatracker said about `name` at `checked`. `rev` is the latest revision it knew then;
|
|
40
|
+
`revisions` are (revision, publication date) pairs; `replaced_by` are the drafts that replace this one and
|
|
41
|
+
`became_rfc` the RFC it was published as (`RFC8210`), if any."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
title: str
|
|
45
|
+
state: str
|
|
46
|
+
rev: str
|
|
47
|
+
expires: str
|
|
48
|
+
replaced_by: tuple[str, ...]
|
|
49
|
+
became_rfc: str | None
|
|
50
|
+
revisions: tuple[tuple[str, str], ...]
|
|
51
|
+
authors: tuple[str, ...]
|
|
52
|
+
group: str
|
|
53
|
+
stream: str
|
|
54
|
+
pages: int | None
|
|
55
|
+
checked: str
|
|
56
|
+
|
|
57
|
+
def to_json(self) -> str:
|
|
58
|
+
return json.dumps(asdict(self), indent=1, ensure_ascii=False) + "\n"
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def from_json(text: str) -> Status:
|
|
62
|
+
"""Read a stored snapshot; a damaged one is a LibError that names the command that rewrites it."""
|
|
63
|
+
try:
|
|
64
|
+
raw: dict[str, Any] = json.loads(text)
|
|
65
|
+
return Status(
|
|
66
|
+
name=str(raw["name"]),
|
|
67
|
+
title=str(raw["title"]),
|
|
68
|
+
state=str(raw["state"]),
|
|
69
|
+
rev=str(raw["rev"]),
|
|
70
|
+
expires=str(raw["expires"]),
|
|
71
|
+
replaced_by=tuple(str(x) for x in raw["replaced_by"]),
|
|
72
|
+
became_rfc=None if raw["became_rfc"] is None else str(raw["became_rfc"]),
|
|
73
|
+
revisions=tuple((str(a), str(b)) for a, b in raw["revisions"]),
|
|
74
|
+
authors=tuple(str(x) for x in raw["authors"]),
|
|
75
|
+
group=str(raw["group"]),
|
|
76
|
+
stream=str(raw["stream"]),
|
|
77
|
+
pages=None if raw["pages"] is None else int(raw["pages"]),
|
|
78
|
+
checked=str(raw["checked"]),
|
|
79
|
+
)
|
|
80
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
81
|
+
raise LibError("a stored status snapshot is damaged", "", "primarysource draft refresh") from exc
|
|
82
|
+
|
|
83
|
+
def published(self, rev: str) -> str:
|
|
84
|
+
"""The date revision `rev` was posted (YYYY-MM-DD), or ''."""
|
|
85
|
+
for known, when in self.revisions:
|
|
86
|
+
if known == rev:
|
|
87
|
+
return when[:10]
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _day(text: str) -> date | None:
|
|
92
|
+
try:
|
|
93
|
+
return datetime.fromisoformat(text.replace("Z", "+00:00")).date() if text else None
|
|
94
|
+
except ValueError:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True, slots=True)
|
|
99
|
+
class Lifecycle:
|
|
100
|
+
"""`level` is one of the module constants; `text` is one sentence for a person; `alive` is True only for a
|
|
101
|
+
draft that is active and not past its expiry date."""
|
|
102
|
+
|
|
103
|
+
level: str
|
|
104
|
+
text: str
|
|
105
|
+
alive: bool
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def lifecycle(status: Status, today: date | None = None) -> Lifecycle:
|
|
109
|
+
"""The verdict on a snapshot. A draft whose datatracker state is still Active but whose expiry date has
|
|
110
|
+
passed is reported as expired by date, because the snapshot may simply be old."""
|
|
111
|
+
today = today or datetime.now(UTC).date()
|
|
112
|
+
level = _LEVELS.get(status.state.strip().lower(), UNKNOWN)
|
|
113
|
+
when = status.expires[:10]
|
|
114
|
+
if level == RFC or (status.became_rfc and level != REPLACED):
|
|
115
|
+
rfc = status.became_rfc or "an RFC"
|
|
116
|
+
return Lifecycle(RFC, f"published as {rfc}: cite the RFC, not this draft", False)
|
|
117
|
+
if level == REPLACED:
|
|
118
|
+
by = ", ".join(status.replaced_by) or "another draft"
|
|
119
|
+
return Lifecycle(REPLACED, f"replaced by {by}: this draft is dead, read and cite its successor", False)
|
|
120
|
+
if level == WITHDRAWN:
|
|
121
|
+
return Lifecycle(WITHDRAWN, f"{status.state.lower()}: do not rely on it", False)
|
|
122
|
+
if level == EXPIRED:
|
|
123
|
+
return Lifecycle(
|
|
124
|
+
EXPIRED,
|
|
125
|
+
f"expired on {when or 'an unknown date'}: not an active draft, and may have been abandoned",
|
|
126
|
+
False,
|
|
127
|
+
)
|
|
128
|
+
if level == ACTIVE:
|
|
129
|
+
end = _day(status.expires)
|
|
130
|
+
if end is not None and end < today:
|
|
131
|
+
return Lifecycle(
|
|
132
|
+
EXPIRED,
|
|
133
|
+
f"past its expiry date {when} though the datatracker listed it Active at the last check",
|
|
134
|
+
False,
|
|
135
|
+
)
|
|
136
|
+
return Lifecycle(
|
|
137
|
+
ACTIVE, f"active, expires {when}: work in progress" if when else "active: work in progress", True
|
|
138
|
+
)
|
|
139
|
+
return Lifecycle(UNKNOWN, f"the datatracker reported the state {status.state!r}: check it before citing", False)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def staleness(status: Status, today: date | None = None) -> int:
|
|
143
|
+
"""Days since the snapshot was taken (0 if its date cannot be read)."""
|
|
144
|
+
today = today or datetime.now(UTC).date()
|
|
145
|
+
taken = _day(status.checked)
|
|
146
|
+
return max(0, (today - taken).days) if taken else 0
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""The on-disk library: `$PRIMARYSOURCE_HOME/draft/<name>/<rev>/` per revision, `<name>/status.json` per draft.
|
|
2
|
+
|
|
3
|
+
draft-x-y/status.json the datatracker's last word (status.py), replaced on every refresh
|
|
4
|
+
draft-x-y/03/draft.txt the archive's text, byte for byte as fetched
|
|
5
|
+
draft-x-y/03/draft.xml the author's source, when the archive has one
|
|
6
|
+
draft-x-y/03/meta.json what was fetched, from where, its sha256, and its licence legend
|
|
7
|
+
draft-x-y/03/sections.json a cache of the segmentation; rebuilt from draft.txt when missing or stale
|
|
8
|
+
|
|
9
|
+
A revision directory appears whole or not at all (it is built beside the others and renamed into place), and
|
|
10
|
+
`meta.json` is what a reader trusts. A stored revision is never overwritten: the archive's revisions do not
|
|
11
|
+
change, so a different digest for one is an error to report, not an update to apply.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import secrets
|
|
21
|
+
import shutil
|
|
22
|
+
from dataclasses import asdict, dataclass, fields
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from primarysource.draft import names, segment
|
|
27
|
+
from primarysource.draft.status import Status
|
|
28
|
+
from primarysource.errors import LibError
|
|
29
|
+
from primarysource.home import atomic_write
|
|
30
|
+
from primarysource.home import collection_home as core_collection_home
|
|
31
|
+
|
|
32
|
+
COLLECTION = "draft"
|
|
33
|
+
READONLY_VARIABLE = "PRIMARYSOURCE_READONLY"
|
|
34
|
+
_REV_DIR = re.compile(r"[0-9]{2}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def readonly() -> bool:
|
|
38
|
+
"""True when `primarysource mcp --read-only` (or the variable) asked for no writes and no fetching."""
|
|
39
|
+
return os.environ.get(READONLY_VARIABLE, "").strip().lower() in ("1", "true", "yes", "on")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class RevMeta:
|
|
44
|
+
"""What one stored revision is. `xml_state` is `v2`, `v3`, `absent`, `refused`, `unparseable` or `skipped`."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
rev: str
|
|
48
|
+
title: str
|
|
49
|
+
published: str
|
|
50
|
+
authors: tuple[str, ...]
|
|
51
|
+
txt_sha256: str
|
|
52
|
+
txt_bytes: int
|
|
53
|
+
encoding: str
|
|
54
|
+
xml_state: str
|
|
55
|
+
xml_sha256: str
|
|
56
|
+
xml_bytes: int
|
|
57
|
+
restriction: str
|
|
58
|
+
copyright_year: int
|
|
59
|
+
ipr: str
|
|
60
|
+
imported: str
|
|
61
|
+
|
|
62
|
+
def to_json(self) -> str:
|
|
63
|
+
return json.dumps(asdict(self), indent=1, ensure_ascii=False) + "\n"
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def from_json(text: str) -> RevMeta:
|
|
67
|
+
try:
|
|
68
|
+
raw: dict[str, Any] = json.loads(text)
|
|
69
|
+
values = {f.name: raw[f.name] for f in fields(RevMeta)}
|
|
70
|
+
values["authors"] = tuple(str(a) for a in values["authors"])
|
|
71
|
+
for key in ("txt_bytes", "xml_bytes", "copyright_year"):
|
|
72
|
+
values[key] = int(values[key])
|
|
73
|
+
for key in set(values) - {"authors", "txt_bytes", "xml_bytes", "copyright_year"}:
|
|
74
|
+
values[key] = str(values[key])
|
|
75
|
+
return RevMeta(**values)
|
|
76
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
77
|
+
raise LibError(
|
|
78
|
+
"a stored revision's meta.json is damaged",
|
|
79
|
+
"",
|
|
80
|
+
"import that revision again: primarysource draft import",
|
|
81
|
+
) from exc
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Store:
|
|
85
|
+
"""One draft library. `root` is the collection's directory; by default `$PRIMARYSOURCE_HOME/draft`."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, root: Path | None = None) -> None:
|
|
88
|
+
self.root = root if root is not None else core_collection_home(COLLECTION)
|
|
89
|
+
|
|
90
|
+
def _dir(self, name: str, rev: str | None = None) -> Path:
|
|
91
|
+
base = self.root / names.parse_name(name)
|
|
92
|
+
return base if rev is None else base / names.valid_revision(rev)
|
|
93
|
+
|
|
94
|
+
def path(self, name: str, rev: str) -> Path:
|
|
95
|
+
"""Where one revision lives (it may not exist)."""
|
|
96
|
+
return self._dir(name, rev)
|
|
97
|
+
|
|
98
|
+
def names(self) -> list[str]:
|
|
99
|
+
"""Drafts with at least one stored revision, sorted."""
|
|
100
|
+
if not self.root.is_dir():
|
|
101
|
+
return []
|
|
102
|
+
return sorted(p.name for p in self.root.iterdir() if p.is_dir() and self.revisions(p.name))
|
|
103
|
+
|
|
104
|
+
def revisions(self, name: str) -> list[str]:
|
|
105
|
+
"""Stored revisions of `name`, oldest first. A directory without `meta.json` is a failed import: ignored."""
|
|
106
|
+
base = self._dir(name)
|
|
107
|
+
if not base.is_dir():
|
|
108
|
+
return []
|
|
109
|
+
return sorted(p.name for p in base.iterdir() if _REV_DIR.fullmatch(p.name) and (p / "meta.json").is_file())
|
|
110
|
+
|
|
111
|
+
def has(self, name: str, rev: str) -> bool:
|
|
112
|
+
return (self._dir(name, rev) / "meta.json").is_file()
|
|
113
|
+
|
|
114
|
+
def resolve(self, name: str, rev: str | None) -> tuple[str, bool]:
|
|
115
|
+
"""The revision to read and whether it was chosen (True) because none was named: the latest stored."""
|
|
116
|
+
stored = self.revisions(name)
|
|
117
|
+
if rev is not None:
|
|
118
|
+
if rev not in stored:
|
|
119
|
+
have = ", ".join(stored) or "none"
|
|
120
|
+
raise LibError(
|
|
121
|
+
f"revision {rev} of {name} is not stored (stored: {have})",
|
|
122
|
+
f"draft_info('{name}') says which revisions exist",
|
|
123
|
+
f"primarysource draft import {name}-{rev}",
|
|
124
|
+
)
|
|
125
|
+
return rev, False
|
|
126
|
+
if not stored:
|
|
127
|
+
raise LibError(
|
|
128
|
+
f"{name} is not stored", "draft_list lists what is stored", f"primarysource draft import {name}"
|
|
129
|
+
)
|
|
130
|
+
return stored[-1], True
|
|
131
|
+
|
|
132
|
+
def meta(self, name: str, rev: str) -> RevMeta:
|
|
133
|
+
path = self._dir(name, rev) / "meta.json"
|
|
134
|
+
try:
|
|
135
|
+
return RevMeta.from_json(path.read_text(encoding="utf-8"))
|
|
136
|
+
except OSError as exc:
|
|
137
|
+
raise LibError(
|
|
138
|
+
f"{name}-{rev} is not stored",
|
|
139
|
+
"draft_info says which revisions are",
|
|
140
|
+
f"primarysource draft import {name}-{rev}",
|
|
141
|
+
) from exc
|
|
142
|
+
|
|
143
|
+
def status(self, name: str) -> Status | None:
|
|
144
|
+
"""The datatracker snapshot for `name`, or None if it was never fetched."""
|
|
145
|
+
path = self._dir(name) / "status.json"
|
|
146
|
+
try:
|
|
147
|
+
return Status.from_json(path.read_text(encoding="utf-8"))
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def write_status(self, status: Status) -> None:
|
|
152
|
+
atomic_write(self._dir(status.name) / "status.json", status.to_json().encode("utf-8"))
|
|
153
|
+
|
|
154
|
+
def raw_text(self, name: str, rev: str) -> bytes:
|
|
155
|
+
try:
|
|
156
|
+
return (self._dir(name, rev) / "draft.txt").read_bytes()
|
|
157
|
+
except OSError as exc:
|
|
158
|
+
raise LibError(
|
|
159
|
+
f"{name}-{rev} has no stored text", "", f"primarysource draft import {name}-{rev}"
|
|
160
|
+
) from exc
|
|
161
|
+
|
|
162
|
+
def segmented(self, name: str, rev: str) -> segment.Segmented:
|
|
163
|
+
"""The sections of a stored revision, from the cache when it is current, else from the stored text (and the
|
|
164
|
+
cache is rewritten unless the library is read-only)."""
|
|
165
|
+
folder = self._dir(name, rev)
|
|
166
|
+
try:
|
|
167
|
+
cached = segment.Segmented.from_json((folder / "sections.json").read_text(encoding="utf-8"))
|
|
168
|
+
except OSError:
|
|
169
|
+
cached = None
|
|
170
|
+
if cached is not None:
|
|
171
|
+
return cached
|
|
172
|
+
text, _encoding = segment.decode(self.raw_text(name, rev))
|
|
173
|
+
found = segment.segment(text)
|
|
174
|
+
if not readonly():
|
|
175
|
+
with contextlib.suppress(OSError): # a read-only volume is a supported place to serve from
|
|
176
|
+
atomic_write(folder / "sections.json", found.to_json().encode("utf-8"))
|
|
177
|
+
return found
|
|
178
|
+
|
|
179
|
+
def install(self, meta: RevMeta, txt: bytes, xml: bytes | None, found: segment.Segmented) -> Path:
|
|
180
|
+
"""Write one revision whole, or not at all. Refuses to replace a stored revision."""
|
|
181
|
+
target = self._dir(meta.name, meta.rev)
|
|
182
|
+
if (target / "meta.json").is_file():
|
|
183
|
+
raise LibError(f"{meta.name}-{meta.rev} is already stored", "")
|
|
184
|
+
base = target.parent
|
|
185
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
staging = base / f".{meta.rev}.part-{secrets.token_hex(4)}"
|
|
187
|
+
staging.mkdir()
|
|
188
|
+
try:
|
|
189
|
+
atomic_write(staging / "draft.txt", txt)
|
|
190
|
+
if xml is not None:
|
|
191
|
+
atomic_write(staging / "draft.xml", xml)
|
|
192
|
+
atomic_write(staging / "sections.json", found.to_json().encode("utf-8"))
|
|
193
|
+
atomic_write(staging / "meta.json", meta.to_json().encode("utf-8"))
|
|
194
|
+
if target.exists(): # a failed earlier import left a directory without meta.json
|
|
195
|
+
shutil.rmtree(target)
|
|
196
|
+
os.replace(staging, target)
|
|
197
|
+
finally:
|
|
198
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
199
|
+
return target
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"terms": {
|
|
4
|
+
"licence": "IETF Trust Legal Provisions 5.0 §3.c, with BCP 78 (RFC 5378)",
|
|
5
|
+
"placement": "volume",
|
|
6
|
+
"terms_url": "https://trustee.ietf.org/documents/trust-legal-provisions/tlp-5/",
|
|
7
|
+
"terms_read": "2026-11-13",
|
|
8
|
+
"summary": "Authors keep the copyright in their Internet-Drafts (RFC 5378 §3.6); the IETF Trust may sublicense. TLP 5.0 §3.c allows copying and distributing a Contribution in full and without modification, or unmodified portions with attribution, but only for Contributions submitted after 2015-03-25 and not where a draft's own legend forbids it. Drafts are fetched by the reader onto a volume; none is shipped in an image."
|
|
9
|
+
},
|
|
10
|
+
"drafts": [
|
|
11
|
+
{
|
|
12
|
+
"name": "draft-ietf-sidrops-8210bis",
|
|
13
|
+
"why": "RPKI-to-Router protocol version 2 and the ASPA PDU: the specification rtrkit implements beyond RFC 8210.",
|
|
14
|
+
"consumers": [
|
|
15
|
+
"rtrkit"
|
|
16
|
+
],
|
|
17
|
+
"rev_read": "29",
|
|
18
|
+
"state": "Active",
|
|
19
|
+
"replaced_by": [],
|
|
20
|
+
"became_rfc": null,
|
|
21
|
+
"expires": "2027-05-16",
|
|
22
|
+
"checked": "2026-11-13",
|
|
23
|
+
"pins": {
|
|
24
|
+
"29": {
|
|
25
|
+
"txt": "9519b014550e43630ba484c2eda50c48a2a6bad4ec286690e695b9b6aa9681cf",
|
|
26
|
+
"xml": "1ce63b8f7338d9e2a073e47b8f82012f8c9f86ead6d0afe9d71b4986a0841292"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "draft-ietf-sidrops-aspa-profile",
|
|
32
|
+
"why": "The ASPA signed-object profile that draft-ietf-sidrops-8210bis section 1.2 cites for the ASPA PDU; no portfolio row names it yet.",
|
|
33
|
+
"consumers": [],
|
|
34
|
+
"rev_read": "31",
|
|
35
|
+
"state": "Active",
|
|
36
|
+
"replaced_by": [],
|
|
37
|
+
"became_rfc": null,
|
|
38
|
+
"expires": "2027-05-14",
|
|
39
|
+
"checked": "2026-11-13",
|
|
40
|
+
"pins": {
|
|
41
|
+
"31": {
|
|
42
|
+
"txt": "6f4b8381e1ef85164dd855a04c7692c476ae454b7e269d56d023df8b94db5b56",
|
|
43
|
+
"xml": "762ebb9587b186fe29b9a9e133f6572daaa4286fd0854dc5ba6c8278b3d14199"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "draft-ietf-sidrops-aspa-verification",
|
|
49
|
+
"why": "ASPA-based AS_PATH verification. RFC 9774 section 9.2 cites it as a Work in Progress.",
|
|
50
|
+
"consumers": [
|
|
51
|
+
"routevalid",
|
|
52
|
+
"asnkit"
|
|
53
|
+
],
|
|
54
|
+
"rev_read": "29",
|
|
55
|
+
"state": "Active",
|
|
56
|
+
"replaced_by": [],
|
|
57
|
+
"became_rfc": null,
|
|
58
|
+
"expires": "2027-04-19",
|
|
59
|
+
"checked": "2026-11-13",
|
|
60
|
+
"pins": {
|
|
61
|
+
"29": {
|
|
62
|
+
"txt": "3203e57006b2700b67570394951b94f68b98967afc14d75f4b52b2e90220b699",
|
|
63
|
+
"xml": "bdf3e0200c22eefab17264bf2abd89adcad73d88dc6397aadea84aaa4874fccb"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "draft-koch-librepgp",
|
|
69
|
+
"why": "LibrePGP message format, an extension of OpenPGP (RFC 4880, 5581, 6637, all since obsoleted by RFC 9580, which is a different line): what pgpenv needs for LibrePGP.",
|
|
70
|
+
"consumers": [
|
|
71
|
+
"pgpenv"
|
|
72
|
+
],
|
|
73
|
+
"rev_read": "06",
|
|
74
|
+
"state": "Active",
|
|
75
|
+
"replaced_by": [],
|
|
76
|
+
"became_rfc": null,
|
|
77
|
+
"expires": "2027-03-11",
|
|
78
|
+
"checked": "2026-11-13",
|
|
79
|
+
"pins": {
|
|
80
|
+
"06": {
|
|
81
|
+
"txt": "c82a7412d728408ed442b2c940e225a58f3db96d288008290e31f45741b2fdc1"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "draft-uberti-behave-turn-rest",
|
|
87
|
+
"why": "A REST API for obtaining ephemeral TURN credentials. The datatracker shows it expired in 2014, neither replaced nor published as an RFC.",
|
|
88
|
+
"consumers": [
|
|
89
|
+
"turnkit"
|
|
90
|
+
],
|
|
91
|
+
"rev_read": "00",
|
|
92
|
+
"state": "Expired",
|
|
93
|
+
"replaced_by": [],
|
|
94
|
+
"became_rfc": null,
|
|
95
|
+
"expires": "2014-01-16",
|
|
96
|
+
"checked": "2026-11-13",
|
|
97
|
+
"pins": {
|
|
98
|
+
"00": {
|
|
99
|
+
"txt": "ebcd0450f37481dfb2817482af1bc09532fdf7691016542498b6e4a64bbb7636"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|