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,8 @@
1
+ """`python -m primarysource.itu` is the same as `primarysource itu`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -0,0 +1,478 @@
1
+ """Reading an ASN.1 module as a document: header, IMPORTS, EXPORTS, and one section per assignment.
2
+
3
+ This is a structural reader, not a validator. It finds the boundaries the module itself draws - the module
4
+ reference and its object identifier, the IMPORTS clause, the EXPORTS clause, and where each `Name ::= ...`
5
+ assignment begins and ends - and hands back the original text of each piece. It never interprets types, so a
6
+ construct it does not know about costs nothing worse than a coarser section. `asn1core` decides what is valid.
7
+
8
+ The input is hostile by assumption: it came over the network from a third party, or from a file a user pointed
9
+ at. So every loop is bounded by the input length, nesting is a counter rather than recursion, and the limits in
10
+ `Limits` turn pathological input into `ModuleError`, never into a hang or an unbounded allocation.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import html
16
+ import re
17
+ from dataclasses import dataclass, field
18
+
19
+ ROOT_ARCS = {
20
+ "itu-t": 0,
21
+ "ccitt": 0,
22
+ "iso": 1,
23
+ "joint-iso-itu-t": 2,
24
+ "joint-iso-ccitt": 2,
25
+ }
26
+
27
+
28
+ class ModuleError(ValueError):
29
+ """The text is not an ASN.1 module this reader can take apart, or it exceeds a limit."""
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Limits:
34
+ max_octets: int = 2 * 1024 * 1024
35
+ max_tokens: int = 400_000
36
+ max_depth: int = 64
37
+ max_assignments: int = 20_000
38
+ max_modules: int = 64
39
+
40
+
41
+ DEFAULT_LIMITS = Limits()
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class Token:
46
+ text: str
47
+ start: int
48
+ end: int
49
+ line: int
50
+ first_on_line: bool
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class Import:
55
+ symbols: tuple[str, ...]
56
+ module: str
57
+ oid_text: str | None
58
+ with_successors: bool = False
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class Assignment:
63
+ name: str
64
+ kind: str # type | class | value | set
65
+ parameterised: bool
66
+ start: int
67
+ end: int
68
+ line: int
69
+ text: str
70
+
71
+
72
+ @dataclass
73
+ class Module:
74
+ name: str
75
+ oid_text: str | None
76
+ oid: str | None
77
+ tagging: str | None
78
+ extensibility_implied: bool
79
+ exports: str | tuple[str, ...] | None # None: no EXPORTS clause (everything is exported)
80
+ imports: tuple[Import, ...]
81
+ assignments: tuple[Assignment, ...]
82
+ iri: str | None = None
83
+ edition_note: str | None = None
84
+ source: str = ""
85
+ warnings: list[str] = field(default_factory=list)
86
+
87
+ def assignment(self, name: str) -> Assignment | None:
88
+ for item in self.assignments:
89
+ if item.name == name:
90
+ return item
91
+ return None
92
+
93
+
94
+ # ---------------------------------------------------------------------------------------------------- unwrap
95
+
96
+ _TAG = re.compile(r"<[^>]{0,2000}>")
97
+ _BREAK = re.compile(r"<br\s*/?>", re.IGNORECASE)
98
+ _ACTIVE = re.compile(r"<(script|style)\b.{0,200000}?</\1\s*>", re.IGNORECASE | re.DOTALL)
99
+ _HEADER_EDITION = re.compile(r"ITU-T\s+([A-Z]\.\d+(?:\.\d+)?)\s*\((\d{2})/(\d{4})\)")
100
+
101
+
102
+ def unwrap_html(raw: bytes, limits: Limits = DEFAULT_LIMITS) -> tuple[str, str | None]:
103
+ """Source text of a module published inside an HTML wrapper, and the edition its header names.
104
+
105
+ ITU serves each module as `<html><body><pre>...</pre></body></html>` with one comment line linking to
106
+ the Recommendation. Tags are dropped without being interpreted, entities are decoded, and nothing in the
107
+ wrapper is ever executed or followed. Plain text with no wrapper passes through unchanged.
108
+ """
109
+ if len(raw) > limits.max_octets:
110
+ raise ModuleError(f"{len(raw)} octets exceeds the {limits.max_octets} limit")
111
+ if b"\x00" in raw:
112
+ raise ModuleError("NUL octet in module text")
113
+ text = raw.decode("utf-8", errors="replace")
114
+ stripped = text.lstrip()
115
+ if stripped[:5].lower() in {"<html", "<!doc"} or stripped.lower().startswith("<pre"):
116
+ start = stripped.lower().find("<pre")
117
+ body = stripped[start:] if start >= 0 else stripped
118
+ end = body.lower().rfind("</pre>")
119
+ if end >= 0:
120
+ body = body[:end]
121
+ text = html.unescape(_TAG.sub("", _BREAK.sub("\n", _ACTIVE.sub("", body))))
122
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
123
+ found = _HEADER_EDITION.search(text[:4000])
124
+ note = f"{found.group(1)} ({found.group(2)}/{found.group(3)})" if found else None
125
+ return text, note
126
+
127
+
128
+ # ------------------------------------------------------------------------------------------------- tokenizer
129
+
130
+ _IDENT = re.compile(r"#?[A-Za-z](?:[A-Za-z0-9]|-(?!-))*")
131
+ _NUMBER = re.compile(r"\d+")
132
+ _QUOTED = re.compile(r"'[0-9A-Fa-f01\s]*'[BbHh]")
133
+
134
+
135
+ def tokenize(text: str, limits: Limits = DEFAULT_LIMITS) -> list[Token]:
136
+ """Tokens with comments removed. Comments and strings are skipped, never recursed into."""
137
+ tokens: list[Token] = []
138
+ pos, line, n = 0, 1, len(text)
139
+ first = True
140
+ while pos < n:
141
+ ch = text[pos]
142
+ if ch == "\n":
143
+ line += 1
144
+ pos += 1
145
+ first = True
146
+ continue
147
+ if ch in " \t\f\v":
148
+ pos += 1
149
+ continue
150
+ if text.startswith("--", pos):
151
+ pos += 2
152
+ while pos < n and text[pos] != "\n" and not text.startswith("--", pos):
153
+ pos += 1
154
+ if text.startswith("--", pos):
155
+ pos += 2
156
+ continue
157
+ if text.startswith("/*", pos):
158
+ depth = 1
159
+ pos += 2
160
+ while pos < n and depth:
161
+ if text.startswith("/*", pos):
162
+ depth += 1
163
+ pos += 2
164
+ elif text.startswith("*/", pos):
165
+ depth -= 1
166
+ pos += 2
167
+ else:
168
+ if text[pos] == "\n":
169
+ line += 1
170
+ first = True
171
+ pos += 1
172
+ if depth:
173
+ raise ModuleError(f"unterminated /* comment (line {line})")
174
+ continue
175
+ start = pos
176
+ if ch == '"':
177
+ pos += 1
178
+ while True:
179
+ end = text.find('"', pos)
180
+ if end < 0:
181
+ raise ModuleError(f"unterminated string (line {line})")
182
+ pos = end + 1
183
+ if text.startswith('"', pos):
184
+ pos += 1
185
+ continue
186
+ break
187
+ line += text.count("\n", start, pos)
188
+ elif text.startswith("::=", pos) or text.startswith("...", pos):
189
+ pos += 3
190
+ elif text.startswith("..", pos):
191
+ pos += 2
192
+ elif (
193
+ (ch == "'" and (m := _QUOTED.match(text, pos)))
194
+ or (m := _IDENT.match(text, pos))
195
+ or (m := _NUMBER.match(text, pos))
196
+ ):
197
+ pos = m.end()
198
+ else:
199
+ pos += 1
200
+ tokens.append(Token(text[start:pos], start, pos, line, first))
201
+ first = False
202
+ if len(tokens) > limits.max_tokens:
203
+ raise ModuleError(f"more than {limits.max_tokens} tokens")
204
+ return tokens
205
+
206
+
207
+ _OPEN = {"{": "}", "(": ")", "[": "]"}
208
+ _CLOSE = {"}", ")", "]"}
209
+
210
+
211
+ # ----------------------------------------------------------------------------------------------------- parse
212
+
213
+
214
+ def _oid_numeric(arcs_text: str) -> str | None:
215
+ inner = arcs_text.strip()[1:-1]
216
+ parts = re.findall(r"[A-Za-z][A-Za-z0-9-]*\s*\(\s*\d+\s*\)|[A-Za-z][A-Za-z0-9-]*|\d+", inner)
217
+ numbers: list[str] = []
218
+ for index, part in enumerate(parts):
219
+ named = re.fullmatch(r"([A-Za-z][A-Za-z0-9-]*)\s*\(\s*(\d+)\s*\)", part)
220
+ if named:
221
+ numbers.append(named.group(2))
222
+ elif part.isdigit():
223
+ numbers.append(part)
224
+ elif index == 0 and part in ROOT_ARCS:
225
+ numbers.append(str(ROOT_ARCS[part]))
226
+ else:
227
+ return None
228
+ return ".".join(numbers) if numbers else None
229
+
230
+
231
+ def _classify(left: list[Token], right: list[Token]) -> tuple[str, bool]:
232
+ name = left[0].text
233
+ # `Name {A, B} ::=` is a parameterised definition; `Name CLASS-NAME ::=` is an object set.
234
+ parameterised = len(left) > 1 and left[1].text == "{"
235
+ if name[0].islower():
236
+ return "value", False
237
+ if len(left) == 1:
238
+ return (
239
+ "class" if right and right[0].text in {"CLASS", "TYPE-IDENTIFIER", "ABSTRACT-SYNTAX"} else "type"
240
+ ), False
241
+ if parameterised:
242
+ closer = _match(left, 1)
243
+ if closer == len(left) - 1:
244
+ return ("class" if right and right[0].text == "CLASS" else "type"), True
245
+ return "set", False
246
+
247
+
248
+ def _match(tokens: list[Token], index: int) -> int:
249
+ depth = 0
250
+ for i in range(index, len(tokens)):
251
+ if tokens[i].text in _OPEN:
252
+ depth += 1
253
+ elif tokens[i].text in _CLOSE:
254
+ depth -= 1
255
+ if depth == 0:
256
+ return i
257
+ return len(tokens) - 1
258
+
259
+
260
+ def _braced(tokens: list[Token], index: int, text: str, limits: Limits) -> tuple[str, int]:
261
+ """Text of the bracketed group starting at tokens[index], and the index after it."""
262
+ depth = 0
263
+ for i in range(index, len(tokens)):
264
+ t = tokens[i].text
265
+ if t in _OPEN:
266
+ depth += 1
267
+ if depth > limits.max_depth:
268
+ raise ModuleError(f"nesting deeper than {limits.max_depth} (line {tokens[i].line})")
269
+ elif t in _CLOSE:
270
+ depth -= 1
271
+ if depth == 0:
272
+ return text[tokens[index].start : tokens[i].end], i + 1
273
+ raise ModuleError(f"unbalanced bracket (line {tokens[index].line})")
274
+
275
+
276
+ def _parse_imports(tokens: list[Token], i: int, text: str, limits: Limits) -> tuple[list[Import], int]:
277
+ """tokens[i] is the first token after IMPORTS. Returns the clauses and the index after the `;`."""
278
+ result: list[Import] = []
279
+ symbols: list[str] = []
280
+ n = len(tokens)
281
+ while i < n:
282
+ t = tokens[i].text
283
+ if t == ";":
284
+ return result, i + 1
285
+ if t == ",":
286
+ i += 1
287
+ elif t == "FROM":
288
+ i += 1
289
+ if i >= n or not tokens[i].text.lstrip("#")[:1].isalpha():
290
+ raise ModuleError(f"IMPORTS: FROM without a module (line {tokens[i - 1].line})")
291
+ module = tokens[i].text
292
+ i += 1
293
+ oid = None
294
+ if i < n and tokens[i].text == "{":
295
+ oid, i = _braced(tokens, i, text, limits)
296
+ elif (
297
+ i + 1 < n
298
+ and tokens[i].text[:1].islower()
299
+ and tokens[i + 1].text not in {",", "FROM"}
300
+ and tokens[i + 1].text[:1] != ","
301
+ ):
302
+ oid = tokens[i].text
303
+ i += 1
304
+ successors = False
305
+ if i + 1 < n and tokens[i].text == "WITH" and tokens[i + 1].text in {"SUCCESSORS", "DESCENDANTS"}:
306
+ successors = True
307
+ i += 2
308
+ result.append(Import(tuple(symbols), module, oid, successors))
309
+ symbols = []
310
+ elif t == "{":
311
+ # a parameterised import `Name{}`: keep the braces on the symbol
312
+ group, i = _braced(tokens, i, text, limits)
313
+ if symbols:
314
+ symbols[-1] += group
315
+ elif t[:1].isalpha() or t[:1] == "#":
316
+ symbols.append(t)
317
+ i += 1
318
+ else:
319
+ raise ModuleError(f"IMPORTS: unexpected {t!r} (line {tokens[i].line})")
320
+ if len(result) > limits.max_assignments:
321
+ raise ModuleError("IMPORTS: too many clauses")
322
+ raise ModuleError("IMPORTS clause has no terminating ';'")
323
+
324
+
325
+ def parse_modules(text: str, *, limits: Limits = DEFAULT_LIMITS) -> list[Module]:
326
+ """Every module in `text`, in order. At least one is required."""
327
+ tokens = tokenize(text, limits)
328
+ modules: list[Module] = []
329
+ i, n = 0, len(tokens)
330
+ while i < n:
331
+ if len(modules) >= limits.max_modules:
332
+ raise ModuleError(f"more than {limits.max_modules} modules in one file")
333
+ if not tokens[i].text[:1].isupper():
334
+ i += 1
335
+ continue
336
+ # header: Name [{oid}] DEFINITIONS [tagging] [EXTENSIBILITY IMPLIED] ::= BEGIN
337
+ name = tokens[i].text
338
+ j = i + 1
339
+ oid_text = None
340
+ if j < n and tokens[j].text == "{":
341
+ oid_text, j = _braced(tokens, j, text, limits)
342
+ iri = None
343
+ if j < n and tokens[j].text.startswith('"'): # an IRI value after the OID (X.680 module header)
344
+ iri = tokens[j].text
345
+ j += 1
346
+ if not (j < n and tokens[j].text in {"DEFINITIONS", "ENCODING-DEFINITIONS"}):
347
+ i += 1
348
+ continue
349
+ j += 1
350
+ tagging = None
351
+ implied = False
352
+ while j < n and tokens[j].text != "::=":
353
+ word = tokens[j].text
354
+ if word in {"EXPLICIT", "IMPLICIT", "AUTOMATIC"}:
355
+ tagging = word
356
+ elif word == "EXTENSIBILITY":
357
+ implied = True
358
+ elif word not in {"TAGS", "IMPLIED"}:
359
+ raise ModuleError(f"module header: unexpected {word!r} (line {tokens[j].line})")
360
+ j += 1
361
+ if not (j + 1 < n and tokens[j].text == "::=" and tokens[j + 1].text == "BEGIN"):
362
+ raise ModuleError(f"module {name}: DEFINITIONS without ::= BEGIN")
363
+ j += 2
364
+ body_start = j
365
+ # body: everything to the END that closes it, tracking bracket depth
366
+ depth = 0
367
+ k = j
368
+ end_index = -1
369
+ unbalanced = False
370
+ while k < n:
371
+ t = tokens[k].text
372
+ if t in _OPEN:
373
+ depth += 1
374
+ if depth > limits.max_depth:
375
+ raise ModuleError(f"nesting deeper than {limits.max_depth} (line {tokens[k].line})")
376
+ elif t in _CLOSE:
377
+ depth = max(0, depth - 1)
378
+ elif t == "END" and (depth == 0 or tokens[k].first_on_line):
379
+ # END is reserved, so one starting a line closes the module even inside an unclosed bracket:
380
+ # ITU publishes at least one module with a missing brace (X.509 (2000) AttributeCertificate
381
+ # Definitions), and refusing the whole module for it would hide every other assignment.
382
+ if depth:
383
+ unbalanced = True
384
+ end_index = k
385
+ break
386
+ k += 1
387
+ if end_index < 0:
388
+ raise ModuleError(f"module {name}: no END")
389
+ module = _parse_body(name, oid_text, tagging, implied, tokens[body_start:end_index], text, limits)
390
+ module.iri = iri
391
+ if unbalanced:
392
+ module.warnings.append(
393
+ "unbalanced brackets in the published text; later assignments may be merged into one section"
394
+ )
395
+ modules.append(module)
396
+ i = end_index + 1
397
+ if not modules:
398
+ raise ModuleError("no module found: expected `Name {oid} DEFINITIONS ::= BEGIN ... END`")
399
+ return modules
400
+
401
+
402
+ def _parse_body(
403
+ name: str,
404
+ oid_text: str | None,
405
+ tagging: str | None,
406
+ implied: bool,
407
+ body: list[Token],
408
+ text: str,
409
+ limits: Limits,
410
+ ) -> Module:
411
+ exports: str | tuple[str, ...] | None = None
412
+ imports: list[Import] = []
413
+ i, n = 0, len(body)
414
+ if i < n and body[i].text == "EXPORTS":
415
+ i += 1
416
+ names: list[str] = []
417
+ while i < n and body[i].text != ";":
418
+ if body[i].text != ",":
419
+ names.append(body[i].text)
420
+ i += 1
421
+ i += 1
422
+ exports = "all" if names == ["All"] else tuple(names)
423
+ if i < n and body[i].text == "IMPORTS":
424
+ imports, i = _parse_imports(body, i + 1, text, limits)
425
+ # assignments: depth-0 `::=` tokens; each assignment begins at the first token of its line
426
+ marks: list[int] = []
427
+ depth = 0
428
+ for k in range(i, n):
429
+ t = body[k].text
430
+ if t in _OPEN:
431
+ depth += 1
432
+ elif t in _CLOSE:
433
+ depth = max(0, depth - 1)
434
+ elif t == "::=" and depth == 0:
435
+ marks.append(k)
436
+ if len(marks) > limits.max_assignments:
437
+ raise ModuleError(f"more than {limits.max_assignments} assignments")
438
+ starts: list[int] = []
439
+ floor = i
440
+ for mark in marks:
441
+ s = mark - 1
442
+ while s > floor and not body[s].first_on_line:
443
+ s -= 1
444
+ # a left side that wraps: `Name\n {params} ::=` or `name\n Type ::=`
445
+ while s > floor and body[s].text in {"{", "::="}:
446
+ s -= 1
447
+ starts.append(max(s, floor))
448
+ floor = mark + 1
449
+ assignments: list[Assignment] = []
450
+ for index, mark in enumerate(marks):
451
+ s = starts[index]
452
+ e = starts[index + 1] if index + 1 < len(starts) else n
453
+ left, right = body[s:mark], body[mark + 1 : e]
454
+ if not left or not left[0].text[:1].isalpha():
455
+ continue
456
+ kind, parameterised = _classify(left, right)
457
+ end_char = body[e - 1].end if e > mark + 1 else body[mark].end
458
+ assignments.append(
459
+ Assignment(
460
+ name=left[0].text,
461
+ kind=kind,
462
+ parameterised=parameterised,
463
+ start=left[0].start,
464
+ end=end_char,
465
+ line=left[0].line,
466
+ text=text[left[0].start : end_char],
467
+ )
468
+ )
469
+ return Module(
470
+ name=name,
471
+ oid_text=oid_text,
472
+ oid=_oid_numeric(oid_text) if oid_text else None,
473
+ tagging=tagging,
474
+ extensibility_implied=implied,
475
+ exports=exports,
476
+ imports=tuple(imports),
477
+ assignments=tuple(assignments),
478
+ )