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.
Files changed (303) hide show
  1. primarysource/__init__.py +5 -0
  2. primarysource/bbf/__init__.py +56 -0
  3. primarysource/bbf/_xml.py +63 -0
  4. primarysource/bbf/catalogue.json +777 -0
  5. primarysource/bbf/catalogue.py +137 -0
  6. primarysource/bbf/cli.py +79 -0
  7. primarysource/bbf/datamodel.py +281 -0
  8. primarysource/bbf/importer.py +151 -0
  9. primarysource/bbf/library.py +380 -0
  10. primarysource/bbf/server.py +72 -0
  11. primarysource/budget.py +63 -0
  12. primarysource/c2sp/__init__.py +53 -0
  13. primarysource/c2sp/_fetch.py +5 -0
  14. primarysource/c2sp/catalogue.json +231 -0
  15. primarysource/c2sp/catalogue.py +95 -0
  16. primarysource/c2sp/cli.py +62 -0
  17. primarysource/c2sp/importer.py +90 -0
  18. primarysource/c2sp/library.py +309 -0
  19. primarysource/c2sp/markdown.py +31 -0
  20. primarysource/c2sp/server.py +68 -0
  21. primarysource/cavp/__init__.py +42 -0
  22. primarysource/cavp/cli.py +84 -0
  23. primarysource/cavp/data/vectors.toml +47 -0
  24. primarysource/cavp/importer.py +156 -0
  25. primarysource/cavp/model.py +279 -0
  26. primarysource/cavp/parse.py +269 -0
  27. primarysource/cavp/paths.py +17 -0
  28. primarysource/cavp/server.py +67 -0
  29. primarysource/cavp/tools.py +169 -0
  30. primarysource/cli.py +107 -0
  31. primarysource/cursor.py +82 -0
  32. primarysource/cve/__init__.py +5 -0
  33. primarysource/cve/__main__.py +6 -0
  34. primarysource/cve/cli.py +144 -0
  35. primarysource/cve/collection.py +20 -0
  36. primarysource/cve/config.py +58 -0
  37. primarysource/cve/errors.py +16 -0
  38. primarysource/cve/kernel.py +232 -0
  39. primarysource/cve/mcp.py +139 -0
  40. primarysource/cve/parse.py +318 -0
  41. primarysource/cve/py.typed +0 -0
  42. primarysource/cve/service.py +760 -0
  43. primarysource/cve/store.py +214 -0
  44. primarysource/cve/sync.py +438 -0
  45. primarysource/cve/views.py +304 -0
  46. primarysource/debpolicy/__init__.py +16 -0
  47. primarysource/debpolicy/catalogue.py +263 -0
  48. primarysource/debpolicy/cli.py +12 -0
  49. primarysource/debpolicy/server.py +11 -0
  50. primarysource/draft/__init__.py +11 -0
  51. primarysource/draft/catalogue.py +117 -0
  52. primarysource/draft/cli.py +149 -0
  53. primarysource/draft/collection.py +21 -0
  54. primarysource/draft/datatracker.py +158 -0
  55. primarysource/draft/importer.py +163 -0
  56. primarysource/draft/licence.py +89 -0
  57. primarysource/draft/names.py +64 -0
  58. primarysource/draft/net.py +133 -0
  59. primarysource/draft/segment.py +192 -0
  60. primarysource/draft/server.py +91 -0
  61. primarysource/draft/status.py +146 -0
  62. primarysource/draft/store.py +199 -0
  63. primarysource/draft/tracked.json +104 -0
  64. primarysource/draft/views.py +430 -0
  65. primarysource/draft/xmlmeta.py +74 -0
  66. primarysource/ecma/__init__.py +52 -0
  67. primarysource/ecma/_pdf.py +143 -0
  68. primarysource/ecma/catalogue.json +22 -0
  69. primarysource/ecma/catalogue.py +100 -0
  70. primarysource/ecma/cli.py +67 -0
  71. primarysource/ecma/identify.py +25 -0
  72. primarysource/ecma/importer.py +108 -0
  73. primarysource/ecma/library.py +240 -0
  74. primarysource/ecma/server.py +67 -0
  75. primarysource/errors.py +29 -0
  76. primarysource/fetch.py +360 -0
  77. primarysource/fsutil.py +150 -0
  78. primarysource/gitdocs/__init__.py +15 -0
  79. primarysource/gitdocs/catalogue.py +116 -0
  80. primarysource/gitdocs/cli.py +12 -0
  81. primarysource/gitdocs/server.py +8 -0
  82. primarysource/home.py +65 -0
  83. primarysource/iana/__init__.py +32 -0
  84. primarysource/iana/catalogue.json +244 -0
  85. primarysource/iana/catalogue.py +69 -0
  86. primarysource/iana/cli.py +69 -0
  87. primarysource/iana/importer.py +157 -0
  88. primarysource/iana/model.py +325 -0
  89. primarysource/iana/reader.py +315 -0
  90. primarysource/iana/server.py +67 -0
  91. primarysource/iana/store.py +83 -0
  92. primarysource/iana/xmlsafe.py +47 -0
  93. primarysource/identify.py +186 -0
  94. primarysource/ieee/__init__.py +58 -0
  95. primarysource/ieee/_html.py +49 -0
  96. primarysource/ieee/catalogue.json +74 -0
  97. primarysource/ieee/catalogue.py +123 -0
  98. primarysource/ieee/cli.py +79 -0
  99. primarysource/ieee/identify.py +24 -0
  100. primarysource/ieee/ieee802.py +47 -0
  101. primarysource/ieee/importer.py +305 -0
  102. primarysource/ieee/posix.py +233 -0
  103. primarysource/ieee/server.py +79 -0
  104. primarysource/index.py +446 -0
  105. primarysource/iso/__init__.py +51 -0
  106. primarysource/iso/cli.py +98 -0
  107. primarysource/iso/designation.py +95 -0
  108. primarysource/iso/identify.py +100 -0
  109. primarysource/iso/importer.py +224 -0
  110. primarysource/iso/library.py +249 -0
  111. primarysource/iso/server.py +69 -0
  112. primarysource/itu/__init__.py +41 -0
  113. primarysource/itu/__main__.py +8 -0
  114. primarysource/itu/asn1mod.py +478 -0
  115. primarysource/itu/catalog.py +472 -0
  116. primarysource/itu/cli.py +445 -0
  117. primarysource/itu/collection.py +20 -0
  118. primarysource/itu/corpus.py +118 -0
  119. primarysource/itu/errata.py +16 -0
  120. primarysource/itu/formal.py +565 -0
  121. primarysource/itu/identify.py +26 -0
  122. primarysource/itu/index.py +490 -0
  123. primarysource/itu/mcp.py +474 -0
  124. primarysource/itu/py.typed +0 -0
  125. primarysource/itu/sources.py +600 -0
  126. primarysource/itu/text.py +609 -0
  127. primarysource/kconfig/__init__.py +16 -0
  128. primarysource/kconfig/catalogue.py +98 -0
  129. primarysource/kconfig/cli.py +12 -0
  130. primarysource/kconfig/server.py +8 -0
  131. primarysource/model.py +63 -0
  132. primarysource/nist/__init__.py +5 -0
  133. primarysource/nist/collection.py +46 -0
  134. primarysource/nist/data/acquired-nist.json +1657 -0
  135. primarysource/nist/data/nist-catalog.json +12090 -0
  136. primarysource/nist/data/sources.toml +1996 -0
  137. primarysource/nist/identify.py +41 -0
  138. primarysource/nist/ingest/__init__.py +1 -0
  139. primarysource/nist/ingest/acquire_nist.py +128 -0
  140. primarysource/nist/ingest/controls.py +82 -0
  141. primarysource/nist/ingest/discover_nist.py +218 -0
  142. primarysource/nist/ingest/extract.py +48 -0
  143. primarysource/nist/ingest/fetch.py +211 -0
  144. primarysource/nist/ingest/index.py +14 -0
  145. primarysource/nist/ingest/oscal.py +480 -0
  146. primarysource/nist/ingest/prose_layouts.py +236 -0
  147. primarysource/nist/ingest/sources.py +94 -0
  148. primarysource/nist/ingest/status.py +255 -0
  149. primarysource/nist/models.py +7 -0
  150. primarysource/nist/paths.py +33 -0
  151. primarysource/nist/server/__init__.py +3 -0
  152. primarysource/nist/server/corpus.py +38 -0
  153. primarysource/nist/server/mcp.py +19 -0
  154. primarysource/nist/server/tools.py +35 -0
  155. primarysource/nist/spec.py +38 -0
  156. primarysource/pdfcorpus/__init__.py +16 -0
  157. primarysource/pdfcorpus/corpus.py +159 -0
  158. primarysource/pdfcorpus/fts.py +175 -0
  159. primarysource/pdfcorpus/index.py +70 -0
  160. primarysource/pdfcorpus/models.py +30 -0
  161. primarysource/pdfcorpus/server.py +96 -0
  162. primarysource/pdfcorpus/spec.py +37 -0
  163. primarysource/pdfcorpus/split.py +391 -0
  164. primarysource/pdfcorpus/tools.py +244 -0
  165. primarysource/pep/__init__.py +15 -0
  166. primarysource/pep/catalogue.py +191 -0
  167. primarysource/pep/cli.py +12 -0
  168. primarysource/pep/server.py +8 -0
  169. primarysource/pkgdocs/__init__.py +19 -0
  170. primarysource/pkgdocs/fetch.py +356 -0
  171. primarysource/pkgdocs/index.py +89 -0
  172. primarysource/pkgdocs/model.py +76 -0
  173. primarysource/pkgdocs/readers/__init__.py +1 -0
  174. primarysource/pkgdocs/readers/_common.py +134 -0
  175. primarysource/pkgdocs/readers/asciidoc.py +160 -0
  176. primarysource/pkgdocs/readers/latex.py +315 -0
  177. primarysource/pkgdocs/readers/markdown.py +173 -0
  178. primarysource/pkgdocs/readers/pod.py +132 -0
  179. primarysource/pkgdocs/readers/rst.py +239 -0
  180. primarysource/pkgdocs/readers/scdoc.py +108 -0
  181. primarysource/pkgdocs/runtime.py +552 -0
  182. primarysource/pkgdocs/serving.py +158 -0
  183. primarysource/pkgdocs/store.py +167 -0
  184. primarysource/pms/__init__.py +16 -0
  185. primarysource/pms/catalogue.py +88 -0
  186. primarysource/pms/cli.py +12 -0
  187. primarysource/pms/server.py +8 -0
  188. primarysource/py.typed +0 -0
  189. primarysource/readers/__init__.py +10 -0
  190. primarysource/readers/bookmarks.py +192 -0
  191. primarysource/readers/html.py +1312 -0
  192. primarysource/readers/markdown.py +189 -0
  193. primarysource/readers/pdf.py +187 -0
  194. primarysource/readers/xml.py +230 -0
  195. primarysource/refs.py +87 -0
  196. primarysource/registry.py +161 -0
  197. primarysource/render.py +49 -0
  198. primarysource/rfc/__init__.py +3 -0
  199. primarysource/rfc/__main__.py +6 -0
  200. primarysource/rfc/cite.py +204 -0
  201. primarysource/rfc/cli.py +903 -0
  202. primarysource/rfc/collection.py +19 -0
  203. primarysource/rfc/config.py +138 -0
  204. primarysource/rfc/errata.py +203 -0
  205. primarysource/rfc/fsutil.py +82 -0
  206. primarysource/rfc/graph.py +213 -0
  207. primarysource/rfc/index/__init__.py +1 -0
  208. primarysource/rfc/index/chunker.py +281 -0
  209. primarysource/rfc/index/embed.py +166 -0
  210. primarysource/rfc/index/hybrid.py +305 -0
  211. primarysource/rfc/index/lexical.py +223 -0
  212. primarysource/rfc/index/rfcxml.py +673 -0
  213. primarysource/rfc/index/search.py +508 -0
  214. primarysource/rfc/index/txtseg.py +585 -0
  215. primarysource/rfc/library.py +294 -0
  216. primarysource/rfc/mcp/__init__.py +1 -0
  217. primarysource/rfc/mcp/server.py +381 -0
  218. primarysource/rfc/packs/__init__.py +1 -0
  219. primarysource/rfc/packs/builder.py +228 -0
  220. primarysource/rfc/packs/collections/all-current-txt.json +10 -0
  221. primarysource/rfc/packs/collections/essentials.json +19 -0
  222. primarysource/rfc/packs/collections/fyi-current.json +10 -0
  223. primarysource/rfc/packs/collections/standards-current.json +10 -0
  224. primarysource/rfc/packs/collections/year-2025.json +10 -0
  225. primarysource/rfc/packs/collections/year-2026.json +10 -0
  226. primarysource/rfc/packs/collections.py +163 -0
  227. primarysource/rfc/packs/docsetexport.py +163 -0
  228. primarysource/rfc/packs/epub.py +159 -0
  229. primarysource/rfc/packs/export.py +306 -0
  230. primarysource/rfc/packs/htmlexport.py +82 -0
  231. primarysource/rfc/packs/serve.py +143 -0
  232. primarysource/rfc/packs/zimexport.py +171 -0
  233. primarysource/rfc/query.py +466 -0
  234. primarysource/rfc/subjects.py +457 -0
  235. primarysource/rfc/sync/__init__.py +1 -0
  236. primarysource/rfc/sync/downloader.py +132 -0
  237. primarysource/rfc/sync/manifest.py +162 -0
  238. primarysource/rfc/sync/scanner.py +49 -0
  239. primarysource/rfc/sync/state.py +464 -0
  240. primarysource/rfc/views.py +1067 -0
  241. primarysource/rpmdocs/__init__.py +15 -0
  242. primarysource/rpmdocs/catalogue.py +117 -0
  243. primarysource/rpmdocs/cli.py +12 -0
  244. primarysource/rpmdocs/server.py +8 -0
  245. primarysource/scaffold.py +223 -0
  246. primarysource/serve.py +75 -0
  247. primarysource/settings.py +152 -0
  248. primarysource/usb/__init__.py +5 -0
  249. primarysource/usb/collection.py +37 -0
  250. primarysource/usb/data/acquired.json +2025 -0
  251. primarysource/usb/data/sources-usb.toml +852 -0
  252. primarysource/usb/data/sources.toml +10 -0
  253. primarysource/usb/data/usb-catalog.json +1663 -0
  254. primarysource/usb/data/usb-classification.json +1986 -0
  255. primarysource/usb/data/usb-corpus-build.json +608 -0
  256. primarysource/usb/data/usb-pdf-choice.json +839 -0
  257. primarysource/usb/data/usb-split-measure.json +1803 -0
  258. primarysource/usb/ingest/__init__.py +1 -0
  259. primarysource/usb/ingest/acquire.py +130 -0
  260. primarysource/usb/ingest/discover_usb.py +88 -0
  261. primarysource/usb/ingest/extract.py +48 -0
  262. primarysource/usb/ingest/fetch.py +197 -0
  263. primarysource/usb/ingest/index.py +14 -0
  264. primarysource/usb/ingest/sources.py +85 -0
  265. primarysource/usb/models.py +7 -0
  266. primarysource/usb/paths.py +33 -0
  267. primarysource/usb/server/__init__.py +3 -0
  268. primarysource/usb/server/corpus.py +38 -0
  269. primarysource/usb/server/mcp.py +19 -0
  270. primarysource/usb/server/tools.py +35 -0
  271. primarysource/usb/spec.py +20 -0
  272. primarysource/usbids/__init__.py +56 -0
  273. primarysource/usbids/cli.py +65 -0
  274. primarysource/usbids/importer.py +272 -0
  275. primarysource/usbids/parse.py +283 -0
  276. primarysource/usbids/server.py +55 -0
  277. primarysource/usbids/store.py +80 -0
  278. primarysource/usbids/tools.py +174 -0
  279. primarysource/zim/__init__.py +5 -0
  280. primarysource/zim/__main__.py +8 -0
  281. primarysource/zim/_libzim.py +37 -0
  282. primarysource/zim/catalog.py +594 -0
  283. primarysource/zim/cli.py +555 -0
  284. primarysource/zim/collection.py +41 -0
  285. primarysource/zim/errors.py +100 -0
  286. primarysource/zim/groups.py +30 -0
  287. primarysource/zim/library.py +668 -0
  288. primarysource/zim/manage.py +636 -0
  289. primarysource/zim/markdown.py +1247 -0
  290. primarysource/zim/mcp.py +201 -0
  291. primarysource/zim/purl.py +130 -0
  292. primarysource/zim/py.typed +0 -0
  293. primarysource/zim/refs.py +90 -0
  294. primarysource/zim/service.py +1182 -0
  295. primarysource/zim/snapshot.py +197 -0
  296. primarysource/zim/sync.py +269 -0
  297. primarysource/zim/views.py +230 -0
  298. primarysource-0.1.0.dist-info/METADATA +118 -0
  299. primarysource-0.1.0.dist-info/RECORD +303 -0
  300. primarysource-0.1.0.dist-info/WHEEL +4 -0
  301. primarysource-0.1.0.dist-info/entry_points.txt +2 -0
  302. primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
  303. primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
@@ -0,0 +1,100 @@
1
+ """How the iso collection recognises an ISO publication in a supplied PDF (`registry.Collection.identify`).
2
+
3
+ An ISO cover prints `Reference number` and then the designation (`ISO/IEC 7816-3:2006(E)`), and every later page
4
+ repeats the designation as a running header, so the cover and the header outweigh whatever the file is called.
5
+ The PDF's title metadata is often useless (`Microsoft Word - C038770e.doc`) and is only one piece of evidence.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ from primarysource.identify import Candidate, Probe, candidates, points
13
+ from primarysource.iso.designation import _PATTERN, Designation, _from_match
14
+
15
+ _REFERENCE = re.compile(r"Reference\s+number\s+(?P<designation>ISO\S*(?:\s+\S+){0,3})", re.IGNORECASE)
16
+ REFERENCE_POINTS = 3
17
+
18
+
19
+ def _label(match: re.Match[str]) -> str | None:
20
+ return _from_match(match).label
21
+
22
+
23
+ def designation_of(label: str) -> Designation:
24
+ match = _PATTERN.fullmatch(label)
25
+ if match is None: # every label this module produces parses; a foreign one is a bug
26
+ raise ValueError(label)
27
+ return _from_match(match)
28
+
29
+
30
+ _EDITION = re.compile(r"\bedition\b", re.IGNORECASE)
31
+ _DATE = re.compile(r"\d{4}-\d{2}-\d{2}")
32
+ _STOP = re.compile(r"Reference\s+number|\bPartie\b|[àâçéèêëîïôûùüÿœ]")
33
+ _JUNK_TITLE = re.compile(r"^\s*$|^(?:Microsoft Word|untitled)|\.(?:docx?|pdf|indd|fm)\b", re.IGNORECASE)
34
+
35
+
36
+ def cover_title(probe: Probe) -> str:
37
+ """The English title printed on an ISO cover: the lines after the edition line, up to the French title or
38
+ the `Reference number` line. Empty when the cover does not have that shape."""
39
+ if not probe.pages:
40
+ return ""
41
+ lines = [" ".join(line.split()) for line in probe.pages[0].splitlines() if line.strip()]
42
+ start = next((i + 1 for i, line in enumerate(lines) if _EDITION.search(line)), None)
43
+ if start is None:
44
+ return ""
45
+ title: list[str] = []
46
+ for line in lines[start:]:
47
+ if _STOP.search(line):
48
+ break
49
+ if not _DATE.fullmatch(line):
50
+ title.append(line)
51
+ return " ".join(title)
52
+
53
+
54
+ def useful_title(title: str, designation: str) -> bool:
55
+ """False for title metadata that names a file or a word processor (`Microsoft Word - C038770e.doc`) or that is
56
+ only the designation again."""
57
+ return not _JUNK_TITLE.search(title) and title.strip() != designation
58
+
59
+
60
+ def _merge(found: list[Candidate]) -> list[Candidate]:
61
+ """One candidate per document: `ISO 7816-3:2006` from a file name and `ISO/IEC 7816-3:2006` from the cover
62
+ are the same document (`7816-3@2006`), spelled by the surest evidence, with the evidence pooled."""
63
+ by_doc: dict[str, list[Candidate]] = {}
64
+ for candidate in found:
65
+ by_doc.setdefault(designation_of(candidate.designation).doc, []).append(candidate)
66
+ merged: list[Candidate] = []
67
+ for group in by_doc.values():
68
+ best = max(group, key=lambda c: c.score)
69
+ evidence = tuple(dict.fromkeys(e for c in group for e in c.evidence))
70
+ merged.append(Candidate(best.collection, best.designation, points(evidence), evidence, best.command))
71
+ return merged
72
+
73
+
74
+ def identify(probe: Probe) -> list[Candidate]:
75
+ """ISO designations in the probe, the cover's `Reference number` line counting 3 more."""
76
+
77
+ def command(label: str) -> str:
78
+ where = f'"{probe.path}"' if probe.path is not None else f'"{probe.name}"'
79
+ return f'primarysource iso import {where} --designation "{label}"'
80
+
81
+ found = _merge(candidates(probe, "iso", _PATTERN, _label, command))
82
+ if not probe.pages:
83
+ return found
84
+ referenced = set()
85
+ for match in _REFERENCE.finditer(probe.pages[0]):
86
+ inner = _PATTERN.search(match["designation"])
87
+ if inner is not None:
88
+ referenced.add(_from_match(inner).label)
89
+ out: list[Candidate] = []
90
+ for candidate in found:
91
+ if candidate.designation in referenced:
92
+ candidate = Candidate(
93
+ candidate.collection,
94
+ candidate.designation,
95
+ candidate.score + REFERENCE_POINTS,
96
+ ("cover reference number", *candidate.evidence),
97
+ candidate.command,
98
+ )
99
+ out.append(candidate)
100
+ return out
@@ -0,0 +1,224 @@
1
+ """Import the owner's own purchased ISO PDFs into `$PRIMARYSOURCE_HOME/iso/`.
2
+
3
+ `import_folder` reads every new or changed PDF in the home; `import_file` takes one file and, optionally, the
4
+ designation printed on its cover. Which standard a PDF is comes from its content first (`iso.identify`: the cover's
5
+ `Reference number` line and the running header), then its file name, so a renamed download still imports and a
6
+ file named for the wrong standard is caught. A PDF another collection knows (an ITU-T Recommendation, a NIST
7
+ publication) is refused with that collection's command. Each PDF is read once with `readers.pdf.extract` (cut at
8
+ its bookmarks, or at its numbered headings when it has none, as older ISO editions do) into
9
+ `extracted/<key>.json`, which is what serving reads, so the PDF is never copied and serving never needs `pypdf`.
10
+ `install.json` records the designation, the sha256, size and mtime of the source, the extractor and the
11
+ segmenter. When the PDF's title metadata is useless (`Microsoft Word - C038770e.doc`), the English title on the
12
+ cover is used. After an import the core FTS5 index (`index.sqlite`) is rebuilt atomically. Nothing here fetches,
13
+ and nothing records the buyer's watermark or a distributor's tracking string.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from collections.abc import Iterator
20
+ from dataclasses import dataclass
21
+ from datetime import UTC, datetime
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from primarysource import index
26
+ from primarysource.errors import LibError
27
+ from primarysource.fsutil import atomic_write, sha256
28
+ from primarysource.iso.designation import Designation, from_filename, parse
29
+ from primarysource.iso.identify import cover_title, useful_title
30
+ from primarysource.model import Document, Section, Unit
31
+
32
+ INSTALL_RECORD = "install.json"
33
+ INDEX_FILE = "index.sqlite"
34
+ EXTRACTED = "extracted"
35
+ REBUILD_COMMAND = "primarysource iso index"
36
+ IMPORT_COMMAND = "primarysource iso import"
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class ImportResult:
41
+ doc: str
42
+ designation: str
43
+ sections: int
44
+ headings_found: int
45
+ segmenter: str = ""
46
+ note: str = ""
47
+
48
+
49
+ def read_record(home: Path) -> dict[str, Any]:
50
+ try:
51
+ loaded = json.loads((home / INSTALL_RECORD).read_text(encoding="utf-8"))
52
+ except (OSError, ValueError):
53
+ return {"documents": {}}
54
+ if isinstance(loaded, dict) and isinstance(loaded.get("documents"), dict):
55
+ return loaded
56
+ return {"documents": {}}
57
+
58
+
59
+ def _write_record(home: Path, record: dict[str, Any]) -> None:
60
+ record["collection"] = "iso"
61
+ atomic_write(home / INSTALL_RECORD, (json.dumps(record, indent=2, sort_keys=True) + "\n").encode("utf-8"))
62
+
63
+
64
+ def _is_pdf(path: Path) -> bool:
65
+ return path.is_file() and path.suffix.lower() == ".pdf"
66
+
67
+
68
+ def designate(path: Path, explicit: str | None = None) -> tuple[Designation, str]:
69
+ """Which ISO standard `path` is, and a note when the file name disagrees with the cover. An explicit
70
+ designation wins; then the PDF's own content (a score of 3 or more: the cover, or a running header and the
71
+ file name); then the file name. A PDF another collection recognises is refused with that collection's
72
+ command, and one nobody recognises asks for `--designation`."""
73
+ if explicit:
74
+ return parse(explicit), ""
75
+ from primarysource import identify
76
+ from primarysource.iso.identify import designation_of
77
+ from primarysource.iso.identify import identify as iso_identify
78
+
79
+ probe = identify.probe_pdf(path)
80
+ named = from_filename(path.name)
81
+ ours = sorted(iso_identify(probe), key=lambda c: -c.score)
82
+ if ours and ours[0].score >= 3:
83
+ found = designation_of(ours[0].designation)
84
+ note = ""
85
+ if named is not None and named.doc != found.doc:
86
+ note = f"the file name says {named.label}, the PDF says {found.label}; using the PDF"
87
+ return found, note
88
+ if named is not None:
89
+ return named, ""
90
+ others = [c for c in identify.identify(probe, identify.registered_hooks()) if c.collection != "iso"]
91
+ if others:
92
+ best = others[0]
93
+ raise LibError(
94
+ f"{path.name} is {best.designation}, a {best.collection} document, not an ISO standard", "", best.command
95
+ )
96
+ raise LibError(
97
+ f"{path.name}: no ISO designation found on its cover, in its running headers or in its file name",
98
+ "",
99
+ f'{IMPORT_COMMAND} "{path}" --designation "ISO/IEC 9899:2024" (as printed on the cover)',
100
+ )
101
+
102
+
103
+ def import_file(home: Path, path: Path, designation: str | None = None, *, reindex: bool = True) -> ImportResult:
104
+ """Identify, extract and record one PDF. Nothing is recorded unless the PDF reads and can be cut into
105
+ sections (at its bookmarks, or at three or more numbered headings)."""
106
+ from primarysource.readers import pdf
107
+
108
+ if not path.is_file():
109
+ raise LibError(f"{path} is not a file", "", f"check the path, then {IMPORT_COMMAND}")
110
+ try:
111
+ data = path.read_bytes()
112
+ stat = path.stat()
113
+ except OSError as exc:
114
+ raise LibError(f"cannot read {path}: {exc}", "", f"check the file, then {IMPORT_COMMAND}") from exc
115
+ if len(data) > pdf.MAX_BYTES:
116
+ raise LibError(f"{path.name} is {len(data)} bytes, over the {pdf.MAX_BYTES} byte bound", "")
117
+ if not data.startswith(b"%PDF-"):
118
+ raise LibError(f"{path.name} is not a PDF", "", "use the PDF as delivered by the ISO store")
119
+ found, note = designate(path, designation)
120
+ extraction = pdf.extract(data, path, numbered=True)
121
+ digest = sha256(data)
122
+ extraction.update({"designation": found.label, "doc": found.doc, "sha256": digest})
123
+ if not useful_title(str(extraction["title"]), found.label):
124
+ from primarysource import identify
125
+
126
+ extraction["title"] = cover_title(identify.probe_pdf(path))
127
+ atomic_write(
128
+ home / EXTRACTED / f"{found.key}.json",
129
+ (json.dumps(extraction, ensure_ascii=False) + "\n").encode("utf-8"),
130
+ )
131
+ record = read_record(home)
132
+ record["documents"][found.doc] = {
133
+ "designation": found.label,
134
+ "key": found.key,
135
+ "title": extraction["title"],
136
+ "source": str(path),
137
+ "sha256": digest,
138
+ "size": stat.st_size,
139
+ "mtime_ns": stat.st_mtime_ns,
140
+ "extractor": extraction["extractor"],
141
+ "segmenter": extraction["segmenter"],
142
+ "imported": datetime.now(UTC).isoformat(timespec="seconds"),
143
+ }
144
+ _write_record(home, record)
145
+ if reindex:
146
+ rebuild_index(home)
147
+ return ImportResult(
148
+ found.doc,
149
+ found.label,
150
+ len(extraction["sections"]),
151
+ int(extraction["headings_found"]),
152
+ str(extraction["segmenter"]),
153
+ note,
154
+ )
155
+
156
+
157
+ def _unchanged(entry: dict[str, Any] | None, path: Path) -> bool:
158
+ if not entry or entry.get("source") != str(path):
159
+ return False
160
+ try:
161
+ stat = path.stat()
162
+ except OSError:
163
+ return False
164
+ return entry.get("size") == stat.st_size and entry.get("mtime_ns") == stat.st_mtime_ns
165
+
166
+
167
+ def import_folder(home: Path, folder: Path | None = None) -> tuple[list[ImportResult], list[tuple[str, str]]]:
168
+ """Import every PDF directly in `folder` (default: the home itself) that is new or changed since its last
169
+ import, identifying each from its content. Returns what was imported and what was skipped, with the reason.
170
+ The index is rebuilt once at the end when anything was imported."""
171
+ folder = folder or home
172
+ if not folder.is_dir():
173
+ raise LibError(
174
+ f"{folder} does not exist",
175
+ "",
176
+ f"create it (or set PRIMARYSOURCE_ISO_HOME to where your ISO PDFs are), put the PDFs in it, then "
177
+ f"{IMPORT_COMMAND}",
178
+ )
179
+ record = read_record(home)
180
+ by_source = {entry.get("source"): entry for entry in record["documents"].values()}
181
+ done: list[ImportResult] = []
182
+ skipped: list[tuple[str, str]] = []
183
+ for path in sorted(p for p in folder.iterdir() if _is_pdf(p)):
184
+ if _unchanged(by_source.get(str(path)), path):
185
+ skipped.append((path.name, "already imported, unchanged"))
186
+ continue
187
+ try:
188
+ done.append(import_file(home, path, reindex=False))
189
+ except LibError as exc:
190
+ skipped.append((path.name, exc.message + (f"; {exc.action}" if exc.action else "")))
191
+ if done:
192
+ rebuild_index(home)
193
+ return done, skipped
194
+
195
+
196
+ def documents(home: Path) -> Iterator[Document]:
197
+ """Every imported standard as a core `Document`, one section per bookmarked clause, sentences as units."""
198
+ record = read_record(home)
199
+ for doc, entry in sorted(record["documents"].items()):
200
+ stored = home / EXTRACTED / f"{entry['key']}.json"
201
+ try:
202
+ raw = json.loads(stored.read_text(encoding="utf-8"))
203
+ sections = tuple(
204
+ Section(
205
+ str(s["id"]),
206
+ str(s["title"]),
207
+ tuple(line for line in str(s["text"]).split("\n") if line),
208
+ int(s["depth"]) + 1,
209
+ )
210
+ for s in raw["sections"]
211
+ )
212
+ except (OSError, ValueError, KeyError, TypeError) as exc:
213
+ raise LibError(
214
+ f"the extraction of {entry.get('designation', doc)} is unreadable: {exc}",
215
+ "",
216
+ f"re-import it: {IMPORT_COMMAND}",
217
+ ) from exc
218
+ title = f"{entry['designation']} {entry.get('title') or ''}".strip()
219
+ yield Document(doc, title, Unit.SENTENCE, sections)
220
+
221
+
222
+ def rebuild_index(home: Path) -> int:
223
+ """Rebuild the search index from every stored extraction; returns the number of sections indexed."""
224
+ return index.rebuild(home / INDEX_FILE, documents(home), rebuild_command=REBUILD_COMMAND)
@@ -0,0 +1,249 @@
1
+ """Read, outline, search and cite the owner's imported ISO standards. Local files only; nothing here fetches.
2
+
3
+ A ref is `iso:9899@2024§6.5.2¶3`: the standard's number, `@` and its year (the newest imported year when omitted),
4
+ `§` and a clause as the PDF's bookmarks number it (`6.5.2`, `A` for Annex A, `A.1`) or the slug of an unnumbered
5
+ bookmark (`bibliography`), and `¶n`, the nth sentence of that clause's span. The span is the clause's own text,
6
+ then each subsection as a `§6.5.2.1 Title` line followed by its text. A printed designation works too:
7
+ `ISO/IEC 9899:2024§6.5.2`. A ref with no section is the whole standard.
8
+
9
+ Search goes through the core FTS5 index (`index.sqlite`, BM25, heading matches weighted 4); read, outline and cite
10
+ read the stored extraction.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from primarysource import budget, cursor, index
22
+ from primarysource.ecma._pdf import PdfDocument, PdfSection
23
+ from primarysource.errors import LibError
24
+ from primarysource.home import collection_home
25
+ from primarysource.iso.designation import parse, split_doc
26
+ from primarysource.iso.importer import (
27
+ EXTRACTED,
28
+ IMPORT_COMMAND,
29
+ INDEX_FILE,
30
+ REBUILD_COMMAND,
31
+ read_record,
32
+ )
33
+ from primarysource.model import span_hash
34
+ from primarysource.refs import Ref, RefGrammar
35
+ from primarysource.render import render
36
+
37
+ PREFIX = re.compile(r"^iso:", re.IGNORECASE)
38
+ _PRINTED = re.compile(r"^(?P<head>ISO[^§¶]*?)\s*(?P<rest>(?:§|¶).*)?$", re.IGNORECASE)
39
+ RANKING = index.Ranking(doc_title=1.0, title=4.0, body=1.0)
40
+ SEARCH_WINDOW = 500
41
+
42
+
43
+ def grammar() -> RefGrammar:
44
+ from primarysource.iso import ISO_DOC, ISO_SECTION
45
+
46
+ return RefGrammar(ISO_DOC, ISO_SECTION)
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class Loaded:
51
+ doc: str
52
+ designation: str
53
+ title: str
54
+ document: PdfDocument
55
+
56
+
57
+ def default_home() -> Path:
58
+ return collection_home("iso")
59
+
60
+
61
+ def _not_imported(what: str, home: Path) -> LibError:
62
+ have = ", ".join(sorted(read_record(home)["documents"])) or "nothing"
63
+ return LibError(
64
+ f"{what} is not imported (have: {have})",
65
+ "iso_docs lists what is imported",
66
+ f"put the PDF in {home}, then run: {IMPORT_COMMAND}",
67
+ )
68
+
69
+
70
+ def parse_ref(text: str) -> Ref:
71
+ """Parse an iso ref, accepting the `iso:` prefix and a printed designation for the document."""
72
+ body = PREFIX.sub("", text.strip(), count=1)
73
+ printed = _PRINTED.match(body)
74
+ if printed:
75
+ body = parse(printed["head"]).doc + (printed["rest"] or "")
76
+ body, doc_para = cursor.split_doc_para(body)
77
+ ref = grammar().parse(body)
78
+ if doc_para is not None:
79
+ ref = Ref(ref.doc, None, doc_para, None, None)
80
+ if ref.lines is not None or ref.anchor is not None:
81
+ raise LibError(f"not an iso ref: {text!r}", "iso refs are <number>[@year][§clause][¶n], e.g. 9899@2024§6.5.2")
82
+ if ref.doc is None:
83
+ raise LibError(f"not an iso ref: {text!r}", "start with the standard's number, e.g. 9899@2024")
84
+ return ref
85
+
86
+
87
+ def resolve(token: str, home: Path) -> str:
88
+ """The imported document a token names: exact when it carries `@year`, else the newest imported year of that
89
+ number (the standard itself before an amendment)."""
90
+ number, rest = split_doc(token)
91
+ documents = read_record(home)["documents"]
92
+ if rest is not None:
93
+ doc = f"{number}@{rest}"
94
+ if doc not in documents:
95
+ raise _not_imported(f"ISO {doc}", home)
96
+ return doc
97
+ candidates: list[str] = [str(d) for d in documents if str(d).split("@", 1)[0] == number]
98
+ if not candidates:
99
+ raise _not_imported(f"ISO {number}", home)
100
+ return max(candidates, key=lambda d: ("+" not in d, d.split("@", 1)[1]))
101
+
102
+
103
+ def load(token: str, home: Path | None = None) -> Loaded:
104
+ home = home or default_home()
105
+ doc = resolve(token, home)
106
+ entry = read_record(home)["documents"][doc]
107
+ try:
108
+ raw: Any = json.loads((home / EXTRACTED / f"{entry['key']}.json").read_text(encoding="utf-8"))
109
+ except (OSError, ValueError, KeyError) as exc:
110
+ raise LibError(
111
+ f"cannot read the import of {entry.get('designation', doc)}: {exc}", "", f"re-import it: {IMPORT_COMMAND}"
112
+ ) from exc
113
+ document = PdfDocument.from_json(raw)
114
+ return Loaded(doc, str(entry["designation"]), document.title or str(entry["designation"]), document)
115
+
116
+
117
+ def _title(doc: Loaded) -> str:
118
+ """The PDF title without the designation it often starts with."""
119
+ title = doc.title
120
+ if title.upper().startswith(doc.designation.upper()):
121
+ title = title[len(doc.designation) :].lstrip(" -—:")
122
+ return title
123
+
124
+
125
+ def _cite_text(doc: Loaded, section: PdfSection | None, para: int | str | None) -> str:
126
+ where = f", §{section.id} {section.title}" if section else ""
127
+ if para:
128
+ where += f" ¶{para}"
129
+ title = _title(doc)
130
+ return f"{doc.designation}{', ' + title if title else ''}{where}"
131
+
132
+
133
+ def _doc(ref: Ref) -> str:
134
+ if ref.doc is None:
135
+ raise LibError("the ref names no document", "start with the standard's number")
136
+ return ref.doc
137
+
138
+
139
+ def _select(ref: Ref, home: Path | None) -> tuple[Loaded, PdfSection | None, tuple[str, ...], str]:
140
+ doc = load(_doc(ref), home)
141
+ if ref.section is None:
142
+ units: list[str] = []
143
+ for section in doc.document.sections:
144
+ units.append(f"§{section.id} {section.title}")
145
+ units.extend(section.sentences)
146
+ return doc, None, tuple(units), ""
147
+ position, warning = doc.document.find(ref.section)
148
+ return doc, doc.document.sections[position], doc.document.span(position), warning
149
+
150
+
151
+ def read(ref_text: str, max_tokens: int = 2000, home: Path | None = None) -> str:
152
+ """The text of a ref, cut at sentence boundaries within `max_tokens`. A `¶n` ref starts at sentence n and runs to
153
+ the end of its scope; the `next:` footer is the ref that reads the next page."""
154
+ ref = parse_ref(ref_text)
155
+ doc, section, units, warning = _select(ref, home)
156
+ if not units:
157
+ raise LibError(f"{ref_text}: that clause has no text of its own", "read its subsections, see iso_outline")
158
+ first = cursor.first_unit(ref.para, len(units), "sentences", ref_text)
159
+ chosen = budget.page(units, start=first, max_tokens=max_tokens)
160
+ body = "\n".join(chosen.units)
161
+ scope = f"iso:{doc.doc}" + (f"§{section.id}" if section else "")
162
+ footer = {
163
+ "cite": _cite_text(doc, section, cursor.para_label(ref.para, chosen)),
164
+ "unit": "sentence",
165
+ "span": span_hash(body),
166
+ "next": cursor.read_next(scope, chosen),
167
+ "warning": warning,
168
+ }
169
+ return render(body, footer)
170
+
171
+
172
+ def outline(token: str, start: int = 0, limit: int = 200, home: Path | None = None) -> str:
173
+ """A standard's bookmarked clauses, indented by depth, each with the ref that reads it and its PDF page."""
174
+ doc = load(token, home)
175
+ rows = [
176
+ f"{' ' * section.depth}{section.id} {section.title} iso:{doc.doc}§{section.id} (p. {section.page})"
177
+ for section in doc.document.sections
178
+ ]
179
+ shown, nxt = cursor.window(rows, start, limit)
180
+ head = f"{doc.designation} {_title(doc)} [iso:{doc.doc}, {doc.document.pages} pages]"
181
+ return render("\n".join([head, *shown]), {"next": nxt})
182
+
183
+
184
+ def search(query: str, doc: str | None = None, limit: int = 10, start: int = 0, home: Path | None = None) -> str:
185
+ """Clauses containing every query word (else any, flagged `relaxed`), best first by BM25: one line per hit,
186
+ `<ref> <clause title>: <snippet>`."""
187
+ home = home or default_home()
188
+ if not read_record(home)["documents"]:
189
+ raise LibError(
190
+ "no ISO standards are imported",
191
+ "iso_docs lists what is imported",
192
+ f"put your PDFs in {home}, then run: {IMPORT_COMMAND}",
193
+ )
194
+ wanted = resolve(doc, home) if doc else None
195
+ try:
196
+ conn = index.open_read(home / INDEX_FILE, rebuild_command=REBUILD_COMMAND)
197
+ except LibError as exc:
198
+ raise LibError(exc.message, "", REBUILD_COMMAND) from exc
199
+ try:
200
+ found = index.search(conn, query, ranking=RANKING, limit=SEARCH_WINDOW)
201
+ finally:
202
+ conn.close()
203
+ hits = [hit for hit in found.hits if wanted is None or hit.doc_id == wanted]
204
+ if not hits:
205
+ return render("no matches", {})
206
+ shown, nxt = cursor.window(hits, start, limit)
207
+ lines = [f"iso:{hit.doc_id}§{hit.section_id} {hit.title}: {' '.join(hit.snippet.split())}" for hit in shown]
208
+ footer = {"next": nxt, "relaxed": "only some query words matched" if found.relaxed else ""}
209
+ return render("\n".join(lines), footer)
210
+
211
+
212
+ def cite(ref_text: str, home: Path | None = None) -> str:
213
+ """The citation string for a ref, with the span hash of the exact text cited."""
214
+ ref = parse_ref(ref_text)
215
+ doc, section, units, warning = _select(ref, home)
216
+ if ref.para is not None:
217
+ if ref.para > len(units):
218
+ raise LibError(f"{ref_text}: the span has {len(units)} sentences", f"use ¶1 to ¶{len(units)}")
219
+ units = (units[ref.para - 1],)
220
+ footer = {
221
+ "cite": _cite_text(doc, section, ref.para),
222
+ "unit": "sentence",
223
+ "span": span_hash("\n".join(units)),
224
+ "warning": warning,
225
+ }
226
+ label = f"iso:{doc.doc}" + (f"§{section.id}" if section else "") + (f"¶{ref.para}" if ref.para else "")
227
+ return render(label, footer)
228
+
229
+
230
+ def docs(home: Path | None = None) -> str:
231
+ """What is imported, with each source file's state: present, changed since import, or missing."""
232
+ home = home or default_home()
233
+ record = read_record(home)["documents"]
234
+ if not record:
235
+ return render(f"nothing imported; put your purchased ISO PDFs in {home} and run: {IMPORT_COMMAND}", {})
236
+ lines = []
237
+ for doc, entry in sorted(record.items()):
238
+ source = Path(str(entry.get("source", "")))
239
+ try:
240
+ stat = source.stat()
241
+ state = (
242
+ "source unchanged"
243
+ if stat.st_size == entry.get("size") and stat.st_mtime_ns == entry.get("mtime_ns")
244
+ else "source changed since import"
245
+ )
246
+ except OSError:
247
+ state = "source file missing"
248
+ lines.append(f"iso:{doc} {entry['designation']} {entry.get('title', '')} ({state})")
249
+ return render("\n".join(lines), {})
@@ -0,0 +1,69 @@
1
+ """MCP server for the iso collection: `primarysource mcp --collection iso`.
2
+
3
+ Imported only when served, so `mcp` (the `[mcp]` extra) is needed only then. Tools carry the `iso_` prefix, answer
4
+ in text with the fixed footer, and turn a `LibError` into an MCP tool error with `error:`/`hint:`/`action:` lines.
5
+ Every tool reads local files only; none writes or fetches. Serve it on the buyer's own machine (stdio, or a
6
+ container with the iso home mounted read-only): the PDFs behind it are single-user copies.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
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.iso import library
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
+ INSTRUCTIONS = (
23
+ "ISO standards from the user's own purchased PDFs. Loop: iso_search('<words>') -> iso_read('<ref>'). "
24
+ "Refs: 'iso:9899@2024§6.5.2' (a clause as numbered in the PDF), '...¶3' (from its 3rd sentence), '9899' means "
25
+ "the newest imported year, 'A' is Annex A; 'ISO/IEC 9899:2024§6.5.2' works too. iso_outline('9899') lists "
26
+ "clauses as refs; iso_cite('<ref>') gives the citation and the hash of the cited text. iso_docs lists what is "
27
+ "imported. If a standard is not imported, the error says what the user must do."
28
+ )
29
+
30
+
31
+ def iso_read(ref: str, max_tokens: int = 2000) -> str:
32
+ """Read ISO text by ref, cut at sentence boundaries within max_tokens. A 'next: <ref>' footer line is the ref
33
+ that reads the next page: pass it back as ref.
34
+
35
+ ref: 'iso:9899@2024' (whole standard) | 'iso:9899@2024§6.5.2' (a clause and its subsections) |
36
+ 'iso:9899@2024§6.5.2¶3' (from its 3rd sentence) | 'ISO/IEC 9899:2024§A'. iso_cite gives exactly one sentence."""
37
+ return library.read(ref, max_tokens=max_tokens)
38
+
39
+
40
+ def iso_search(query: str, doc: str | None = None, limit: int = 10, start: int = 0) -> str:
41
+ """Find clauses containing the query words (all of them, else any, flagged 'relaxed'), best first. One line per
42
+ hit: '<ref> <clause title>: <snippet>'. doc restricts to one standard, e.g. '9899' or '9899@2024'. A
43
+ 'next: start=N' footer line means call again with start=N (a 0-based offset)."""
44
+ return library.search(query, doc=doc, limit=limit, start=start)
45
+
46
+
47
+ def iso_outline(doc: str, start: int = 0, limit: int = 200) -> str:
48
+ """List a standard's clauses, indented by depth, each with the ref that reads it and its PDF page. A
49
+ 'next: start=N' footer line means call again with start=N."""
50
+ return library.outline(doc, start=start, limit=limit)
51
+
52
+
53
+ def iso_cite(ref: str) -> str:
54
+ """The citation string for a ref, with the sha256 prefix of the exact text cited (cite: and span: lines)."""
55
+ return library.cite(ref)
56
+
57
+
58
+ def iso_docs() -> str:
59
+ """List the imported ISO standards and whether each source PDF is unchanged, changed or missing."""
60
+ return library.docs()
61
+
62
+
63
+ def _build() -> Any:
64
+ built = MCPServer("primarysource-iso", version=__version__, instructions=INSTRUCTIONS)
65
+ add_tools(built, (iso_read, iso_search, iso_outline, iso_cite, iso_docs), annotations=READ_ONLY)
66
+ return built
67
+
68
+
69
+ server = _build()
@@ -0,0 +1,41 @@
1
+ """itulib: ITU-T recommendations as a local, citation-precise library and MCP server.
2
+
3
+ X.680 and X.690 are the normative source for BER and DER and are not in the RFC series, so `rfclib` cannot
4
+ answer a question about tag-number minimality or DER canonicalisation. This is the same shape of tool pointed
5
+ at the other corpus: clause-precise search, paging, citation, and an errata surface.
6
+
7
+ The corpus is not in this package and cannot be. ITU's own notice, page iii of X.690 (02/2021), forbids
8
+ reproduction without written permission, so `fetch` downloads it on the user's machine into `ITULIB_HOME`.
9
+ Every entry point raises `CorpusMissing` naming that command rather than returning an empty answer, because a
10
+ missing file and a document with nothing to say must never look the same.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ from .corpus import CorpusMissing, fetch, has, home, require
18
+ from .sources import AMENDMENTS, BY_REC, IN_FORCE, SUPERSEDED, Amendment, Edition, current, resolve
19
+ from .text import Clause, clauses, find, pages, tidy
20
+
21
+ __all__ = [
22
+ "__version__",
23
+ "AMENDMENTS",
24
+ "BY_REC",
25
+ "IN_FORCE",
26
+ "SUPERSEDED",
27
+ "Amendment",
28
+ "Clause",
29
+ "CorpusMissing",
30
+ "Edition",
31
+ "clauses",
32
+ "current",
33
+ "fetch",
34
+ "find",
35
+ "has",
36
+ "home",
37
+ "pages",
38
+ "require",
39
+ "resolve",
40
+ "tidy",
41
+ ]