texsmith 0.0.2.dev0__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 (252) hide show
  1. texsmith/__init__.py +107 -0
  2. texsmith/_alias.py +59 -0
  3. texsmith/adapters/__init__.py +6 -0
  4. texsmith/adapters/docker.py +258 -0
  5. texsmith/adapters/handlers/__init__.py +3 -0
  6. texsmith/adapters/handlers/_assets.py +363 -0
  7. texsmith/adapters/handlers/_helpers.py +62 -0
  8. texsmith/adapters/handlers/_mermaid.py +122 -0
  9. texsmith/adapters/handlers/admonitions.py +267 -0
  10. texsmith/adapters/handlers/basic.py +228 -0
  11. texsmith/adapters/handlers/blocks.py +851 -0
  12. texsmith/adapters/handlers/code.py +309 -0
  13. texsmith/adapters/handlers/inline.py +937 -0
  14. texsmith/adapters/handlers/links.py +239 -0
  15. texsmith/adapters/handlers/media.py +380 -0
  16. texsmith/adapters/latex/__init__.py +10 -0
  17. texsmith/adapters/latex/engines/__init__.py +726 -0
  18. texsmith/adapters/latex/engines/latex/__init__.py +26 -0
  19. texsmith/adapters/latex/engines/latex/log.py +720 -0
  20. texsmith/adapters/latex/engines/latex/runner.py +28 -0
  21. texsmith/adapters/latex/engines/tectonic/__init__.py +38 -0
  22. texsmith/adapters/latex/formatter.py +297 -0
  23. texsmith/adapters/latex/latexmk.py +148 -0
  24. texsmith/adapters/latex/partials/acronym.tex +1 -0
  25. texsmith/adapters/latex/partials/add.tex +1 -0
  26. texsmith/adapters/latex/partials/addition.tex +1 -0
  27. texsmith/adapters/latex/partials/blockquote.tex +3 -0
  28. texsmith/adapters/latex/partials/callout.tex +3 -0
  29. texsmith/adapters/latex/partials/choices.tex +5 -0
  30. texsmith/adapters/latex/partials/citation.tex +1 -0
  31. texsmith/adapters/latex/partials/codeblock.tex +7 -0
  32. texsmith/adapters/latex/partials/codeblock_listings.tex +5 -0
  33. texsmith/adapters/latex/partials/codeblock_pygments.tex +5 -0
  34. texsmith/adapters/latex/partials/codeblock_verbatim.tex +12 -0
  35. texsmith/adapters/latex/partials/codeinline.tex +1 -0
  36. texsmith/adapters/latex/partials/codeinlinett.tex +1 -0
  37. texsmith/adapters/latex/partials/comment.tex +1 -0
  38. texsmith/adapters/latex/partials/del.tex +1 -0
  39. texsmith/adapters/latex/partials/deletion.tex +1 -0
  40. texsmith/adapters/latex/partials/description_list.tex +5 -0
  41. texsmith/adapters/latex/partials/enquote.tex +1 -0
  42. texsmith/adapters/latex/partials/epigraph.tex +1 -0
  43. texsmith/adapters/latex/partials/exercises_solutions.tex +7 -0
  44. texsmith/adapters/latex/partials/figure.tex +33 -0
  45. texsmith/adapters/latex/partials/figure_tcolorbox.tex +15 -0
  46. texsmith/adapters/latex/partials/footnote.tex +3 -0
  47. texsmith/adapters/latex/partials/glossary.tex +1 -0
  48. texsmith/adapters/latex/partials/heading.tex +14 -0
  49. texsmith/adapters/latex/partials/highlight.tex +25 -0
  50. texsmith/adapters/latex/partials/horizontal_rule.tex +1 -0
  51. texsmith/adapters/latex/partials/href.tex +1 -0
  52. texsmith/adapters/latex/partials/icon.tex +1 -0
  53. texsmith/adapters/latex/partials/include.tex +1 -0
  54. texsmith/adapters/latex/partials/index.tex +1 -0
  55. texsmith/adapters/latex/partials/italic.tex +1 -0
  56. texsmith/adapters/latex/partials/keystroke.tex +46 -0
  57. texsmith/adapters/latex/partials/label.tex +1 -0
  58. texsmith/adapters/latex/partials/list_acronyms.tex +5 -0
  59. texsmith/adapters/latex/partials/list_glossary.tex +8 -0
  60. texsmith/adapters/latex/partials/multicolumn.tex +3 -0
  61. texsmith/adapters/latex/partials/ordered_list.tex +5 -0
  62. texsmith/adapters/latex/partials/pagestyle.tex +1 -0
  63. texsmith/adapters/latex/partials/ref.tex +1 -0
  64. texsmith/adapters/latex/partials/regex.tex +1 -0
  65. texsmith/adapters/latex/partials/smallcaps.tex +1 -0
  66. texsmith/adapters/latex/partials/strikethrough.tex +1 -0
  67. texsmith/adapters/latex/partials/strong.tex +1 -0
  68. texsmith/adapters/latex/partials/subscript.tex +1 -0
  69. texsmith/adapters/latex/partials/substitution.tex +1 -0
  70. texsmith/adapters/latex/partials/superscript.tex +1 -0
  71. texsmith/adapters/latex/partials/tabbed.tex +3 -0
  72. texsmith/adapters/latex/partials/table.tex +48 -0
  73. texsmith/adapters/latex/partials/underline.tex +1 -0
  74. texsmith/adapters/latex/partials/unordered_list.tex +5 -0
  75. texsmith/adapters/latex/partials/url.tex +1 -0
  76. texsmith/adapters/latex/pygments.py +98 -0
  77. texsmith/adapters/latex/pyxindy.py +64 -0
  78. texsmith/adapters/latex/renderer.py +220 -0
  79. texsmith/adapters/latex/tectonic.py +366 -0
  80. texsmith/adapters/latex/utils.py +86 -0
  81. texsmith/adapters/markdown/__init__.py +342 -0
  82. texsmith/adapters/plugins/__init__.py +8 -0
  83. texsmith/adapters/plugins/material.py +174 -0
  84. texsmith/adapters/plugins/snippet.py +1734 -0
  85. texsmith/adapters/transformers/__init__.py +122 -0
  86. texsmith/adapters/transformers/base.py +140 -0
  87. texsmith/adapters/transformers/strategies.py +1456 -0
  88. texsmith/adapters/transformers/utils.py +59 -0
  89. texsmith/api/__init__.py +80 -0
  90. texsmith/api/_utils.py +56 -0
  91. texsmith/api/document.py +645 -0
  92. texsmith/api/pipeline.py +229 -0
  93. texsmith/api/service.py +648 -0
  94. texsmith/api/templates.py +287 -0
  95. texsmith/core/__init__.py +6 -0
  96. texsmith/core/bibliography/__init__.py +57 -0
  97. texsmith/core/bibliography/collection.py +354 -0
  98. texsmith/core/bibliography/doi.py +194 -0
  99. texsmith/core/bibliography/issues.py +15 -0
  100. texsmith/core/bibliography/parsing.py +45 -0
  101. texsmith/core/callouts.py +99 -0
  102. texsmith/core/config.py +191 -0
  103. texsmith/core/context.py +242 -0
  104. texsmith/core/conversion/__init__.py +103 -0
  105. texsmith/core/conversion/core.py +780 -0
  106. texsmith/core/conversion/debug.py +87 -0
  107. texsmith/core/conversion/inputs.py +474 -0
  108. texsmith/core/conversion/renderer.py +578 -0
  109. texsmith/core/conversion/templates.py +646 -0
  110. texsmith/core/conversion_contexts.py +95 -0
  111. texsmith/core/diagnostics.py +118 -0
  112. texsmith/core/exceptions.py +41 -0
  113. texsmith/core/fonts/__init__.py +3 -0
  114. texsmith/core/fragments/__init__.py +716 -0
  115. texsmith/core/fragments/base.py +117 -0
  116. texsmith/core/metadata.py +280 -0
  117. texsmith/core/mustache.py +86 -0
  118. texsmith/core/partials.py +20 -0
  119. texsmith/core/rules.py +394 -0
  120. texsmith/core/templates/__init__.py +58 -0
  121. texsmith/core/templates/base.py +343 -0
  122. texsmith/core/templates/builtins.py +68 -0
  123. texsmith/core/templates/context_usage.py +137 -0
  124. texsmith/core/templates/loader.py +303 -0
  125. texsmith/core/templates/manifest.py +861 -0
  126. texsmith/core/templates/runtime.py +347 -0
  127. texsmith/core/templates/text.py +14 -0
  128. texsmith/core/templates/wrapper.py +340 -0
  129. texsmith/core/user_dir.py +179 -0
  130. texsmith/devtools.py +28 -0
  131. texsmith/extensions/__init__.py +162 -0
  132. texsmith/extensions/index/__init__.py +21 -0
  133. texsmith/extensions/index/markdown.py +94 -0
  134. texsmith/extensions/index/mkdocs_plugin.py +136 -0
  135. texsmith/extensions/index/registry.py +57 -0
  136. texsmith/extensions/index/renderer.py +183 -0
  137. texsmith/extensions/index/templates/index.tex +1 -0
  138. texsmith/extensions/latex_raw.py +115 -0
  139. texsmith/extensions/latex_text.py +117 -0
  140. texsmith/extensions/mermaid.py +267 -0
  141. texsmith/extensions/missing_footnotes.py +125 -0
  142. texsmith/extensions/multi_citations.py +54 -0
  143. texsmith/extensions/progressbar/__init__.py +9 -0
  144. texsmith/extensions/progressbar/markdown.py +212 -0
  145. texsmith/extensions/progressbar/renderer.py +117 -0
  146. texsmith/extensions/smallcaps.py +48 -0
  147. texsmith/extensions/texlogos/__init__.py +10 -0
  148. texsmith/extensions/texlogos/markdown.py +236 -0
  149. texsmith/extensions/texlogos/renderer.py +81 -0
  150. texsmith/extensions/texlogos/specs.py +66 -0
  151. texsmith/fonts/__init__.py +57 -0
  152. texsmith/fonts/cache.py +46 -0
  153. texsmith/fonts/constants.py +60 -0
  154. texsmith/fonts/coverage.py +275 -0
  155. texsmith/fonts/downloader.py +103 -0
  156. texsmith/fonts/fallback.py +438 -0
  157. texsmith/fonts/html_scripts.py +168 -0
  158. texsmith/fonts/logging.py +127 -0
  159. texsmith/fonts/pipeline.py +338 -0
  160. texsmith/fonts/scripts.py +493 -0
  161. texsmith/fonts/ucharclasses.py +164 -0
  162. texsmith/fragments/__init__.py +11 -0
  163. texsmith/fragments/bibliography/__init__.py +65 -0
  164. texsmith/fragments/bibliography/fragment.toml +3 -0
  165. texsmith/fragments/bibliography/ts-bibliography-backmatter.jinja.tex +35 -0
  166. texsmith/fragments/bibliography/ts-bibliography.jinja.tex +10 -0
  167. texsmith/fragments/callouts/__init__.py +88 -0
  168. texsmith/fragments/callouts/fragment.toml +3 -0
  169. texsmith/fragments/callouts/ts-callouts.jinja.sty +129 -0
  170. texsmith/fragments/code/__init__.py +82 -0
  171. texsmith/fragments/code/fragment.toml +3 -0
  172. texsmith/fragments/code/ts-code.jinja.sty +140 -0
  173. texsmith/fragments/extra/__init__.py +180 -0
  174. texsmith/fragments/extra/fragment.toml +3 -0
  175. texsmith/fragments/extra/ts-extra.jinja.tex +13 -0
  176. texsmith/fragments/fonts/__init__.py +880 -0
  177. texsmith/fragments/fonts/fragment.toml +3 -0
  178. texsmith/fragments/fonts/ts-fonts.jinja.sty +292 -0
  179. texsmith/fragments/frame/__init__.py +170 -0
  180. texsmith/fragments/frame/fragment.toml +3 -0
  181. texsmith/fragments/frame/ts-frame.tex.jinja +49 -0
  182. texsmith/fragments/geometry/__init__.py +169 -0
  183. texsmith/fragments/geometry/fragment.toml +3 -0
  184. texsmith/fragments/geometry/paper.py +526 -0
  185. texsmith/fragments/geometry/ts_geometry.tex.jinja +53 -0
  186. texsmith/fragments/glossary/__init__.py +67 -0
  187. texsmith/fragments/glossary/fragment.toml +3 -0
  188. texsmith/fragments/glossary/ts-glossary-backmatter.jinja.tex +5 -0
  189. texsmith/fragments/glossary/ts-glossary.jinja.sty +28 -0
  190. texsmith/fragments/index/__init__.py +66 -0
  191. texsmith/fragments/index/fragment.toml +3 -0
  192. texsmith/fragments/index/ts-index-backmatter.jinja.tex +3 -0
  193. texsmith/fragments/index/ts-index.jinja.sty +12 -0
  194. texsmith/fragments/keystrokes/__init__.py +69 -0
  195. texsmith/fragments/keystrokes/fragment.toml +3 -0
  196. texsmith/fragments/keystrokes/ts-keystrokes.jinja.sty +20 -0
  197. texsmith/fragments/todolist/__init__.py +71 -0
  198. texsmith/fragments/todolist/fragment.toml +3 -0
  199. texsmith/fragments/todolist/ts-todolist.jinja.sty +21 -0
  200. texsmith/fragments/typesetting/__init__.py +214 -0
  201. texsmith/fragments/typesetting/fragment.toml +3 -0
  202. texsmith/fragments/typesetting/ts-typesetting.tex.jinja +81 -0
  203. texsmith/index.py +26 -0
  204. texsmith/plugins/__init__.py +14 -0
  205. texsmith/progressbar.py +8 -0
  206. texsmith/quotes.py +40 -0
  207. texsmith/smart_dashes.py +66 -0
  208. texsmith/templates/__init__.py +3 -0
  209. texsmith/templates/article/README.md +33 -0
  210. texsmith/templates/article/__init__.py +281 -0
  211. texsmith/templates/article/template/manifest.toml +125 -0
  212. texsmith/templates/article/template/mermaid-config.json +18 -0
  213. texsmith/templates/article/template/template.tex +74 -0
  214. texsmith/templates/book/README.md +26 -0
  215. texsmith/templates/book/__init__.py +75 -0
  216. texsmith/templates/book/overrides/codeblock.tex +7 -0
  217. texsmith/templates/book/overrides/codeinline.tex +1 -0
  218. texsmith/templates/book/template/fixtoc.sty +81 -0
  219. texsmith/templates/book/template/manifest.toml +198 -0
  220. texsmith/templates/book/template/template.tex +314 -0
  221. texsmith/templates/common/__init__.py +1 -0
  222. texsmith/templates/common/latexmkrc +46 -0
  223. texsmith/templates/letter/README.md +59 -0
  224. texsmith/templates/letter/__init__.py +495 -0
  225. texsmith/templates/letter/demo.md +31 -0
  226. texsmith/templates/letter/fonts/modernline bold.otf +0 -0
  227. texsmith/templates/letter/fonts/modernline.otf +0 -0
  228. texsmith/templates/letter/manifest.toml +197 -0
  229. texsmith/templates/letter/template/callouts.jinja.sty +290 -0
  230. texsmith/templates/letter/template/template.tex +123 -0
  231. texsmith/templates/snippet/README.md +21 -0
  232. texsmith/templates/snippet/__init__.py +80 -0
  233. texsmith/templates/snippet/template/manifest.toml +66 -0
  234. texsmith/templates/snippet/template/template.tex +47 -0
  235. texsmith/texlogos.py +15 -0
  236. texsmith/ui/__init__.py +6 -0
  237. texsmith/ui/cli/__init__.py +22 -0
  238. texsmith/ui/cli/_options.py +338 -0
  239. texsmith/ui/cli/app.py +65 -0
  240. texsmith/ui/cli/bibliography.py +300 -0
  241. texsmith/ui/cli/commands/__init__.py +14 -0
  242. texsmith/ui/cli/commands/render.py +1128 -0
  243. texsmith/ui/cli/commands/templates.py +397 -0
  244. texsmith/ui/cli/diagnostics.py +36 -0
  245. texsmith/ui/cli/presenter.py +663 -0
  246. texsmith/ui/cli/state.py +263 -0
  247. texsmith/ui/cli/utils.py +235 -0
  248. texsmith-0.0.2.dev0.dist-info/METADATA +187 -0
  249. texsmith-0.0.2.dev0.dist-info/RECORD +252 -0
  250. texsmith-0.0.2.dev0.dist-info/WHEEL +4 -0
  251. texsmith-0.0.2.dev0.dist-info/entry_points.txt +22 -0
  252. texsmith-0.0.2.dev0.dist-info/licenses/LICENSE.md +21 -0
texsmith/__init__.py ADDED
@@ -0,0 +1,107 @@
1
+ """Primary public API for TeXSmith."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
6
+
7
+ from texsmith import _alias as _legacy_aliases
8
+ from texsmith.api import (
9
+ ConversionBundle,
10
+ ConversionRequest,
11
+ ConversionResponse,
12
+ ConversionService,
13
+ Document,
14
+ DocumentRenderOptions,
15
+ LaTeXFragment,
16
+ RenderSettings,
17
+ SlotAssignment,
18
+ TemplateOptions,
19
+ TemplateRenderResult,
20
+ TemplateSession,
21
+ TitleStrategy,
22
+ classify_input_source,
23
+ convert_documents,
24
+ get_template,
25
+ )
26
+ from texsmith.core.bibliography import (
27
+ BibliographyCollection,
28
+ BibliographyIssue,
29
+ DoiBibliographyFetcher,
30
+ DoiLookupError,
31
+ bibliography_data_from_string,
32
+ )
33
+ from texsmith.core.config import BookConfig, LaTeXConfig
34
+ from texsmith.core.context import AssetRegistry, DocumentState, RenderContext
35
+ from texsmith.core.rules import RenderPhase, renders
36
+ from texsmith.core.templates import (
37
+ DEFAULT_TEMPLATE_LANGUAGE,
38
+ TemplateBinding,
39
+ TemplateError,
40
+ TemplateRuntime,
41
+ TemplateSlot,
42
+ WrappableTemplate,
43
+ build_template_overrides,
44
+ copy_template_assets,
45
+ load_template,
46
+ load_template_runtime,
47
+ resolve_template_language,
48
+ )
49
+ from texsmith.core.user_dir import (
50
+ TexsmithUserDir,
51
+ configure_user_dir,
52
+ get_user_dir,
53
+ user_dir_context,
54
+ )
55
+
56
+
57
+ try:
58
+ __version__ = _pkg_version("texsmith")
59
+ except PackageNotFoundError:
60
+ __version__ = "0.0.0"
61
+
62
+ __all__ = [
63
+ "DEFAULT_TEMPLATE_LANGUAGE",
64
+ "AssetRegistry",
65
+ "BibliographyCollection",
66
+ "BibliographyIssue",
67
+ "BookConfig",
68
+ "ConversionBundle",
69
+ "ConversionRequest",
70
+ "ConversionResponse",
71
+ "ConversionService",
72
+ "Document",
73
+ "DocumentRenderOptions",
74
+ "DocumentState",
75
+ "DoiBibliographyFetcher",
76
+ "DoiLookupError",
77
+ "LaTeXConfig",
78
+ "LaTeXFragment",
79
+ "RenderContext",
80
+ "RenderPhase",
81
+ "RenderSettings",
82
+ "SlotAssignment",
83
+ "TemplateBinding",
84
+ "TemplateError",
85
+ "TemplateOptions",
86
+ "TemplateRenderResult",
87
+ "TemplateRuntime",
88
+ "TemplateSession",
89
+ "TemplateSlot",
90
+ "TexsmithUserDir",
91
+ "TitleStrategy",
92
+ "WrappableTemplate",
93
+ "__version__",
94
+ "bibliography_data_from_string",
95
+ "build_template_overrides",
96
+ "classify_input_source",
97
+ "configure_user_dir",
98
+ "convert_documents",
99
+ "copy_template_assets",
100
+ "get_template",
101
+ "get_user_dir",
102
+ "load_template",
103
+ "load_template_runtime",
104
+ "renders",
105
+ "resolve_template_language",
106
+ "user_dir_context",
107
+ ]
texsmith/_alias.py ADDED
@@ -0,0 +1,59 @@
1
+ """Register legacy extension module aliases for backwards compatibility."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+ import types
8
+
9
+
10
+ _EXTENSION_ALIAS_MODULES: dict[str, tuple[str, dict[str, str] | None]] = {
11
+ "smallcaps": ("texsmith.extensions.smallcaps", None),
12
+ "mermaid": ("texsmith.extensions.mermaid", None),
13
+ "multi_citations": ("texsmith.extensions.multi_citations", None),
14
+ "latex_raw": ("texsmith.extensions.latex_raw", None),
15
+ "latex_text": ("texsmith.extensions.latex_text", None),
16
+ "missing_footnotes": ("texsmith.extensions.missing_footnotes", None),
17
+ "rawlatex": ("texsmith.extensions.latex_raw", {"RawLatexExtension": "LatexRawExtension"}),
18
+ }
19
+
20
+
21
+ class _AliasModule(types.ModuleType):
22
+ """Lazy module wrapper that proxies attribute access to the canonical module."""
23
+
24
+ __slots__ = ("_alias_map", "_target")
25
+
26
+ def __init__(self, name: str, target: str, alias_map: dict[str, str] | None) -> None:
27
+ super().__init__(name)
28
+ self._target = target
29
+ self._alias_map = alias_map or {}
30
+ self.__package__ = name.rpartition(".")[0]
31
+
32
+ def _load(self) -> types.ModuleType:
33
+ module = importlib.import_module(self._target)
34
+ if self._alias_map:
35
+ for alias_attr, target_attr in self._alias_map.items():
36
+ setattr(module, alias_attr, getattr(module, target_attr))
37
+ exported = set(getattr(module, "__all__", [])) | set(self._alias_map.keys())
38
+ if exported:
39
+ module.__all__ = sorted(exported)
40
+ sys.modules[self.__name__] = module
41
+ return module
42
+
43
+ def __getattr__(self, item: str) -> object:
44
+ module = self._load()
45
+ return getattr(module, item)
46
+
47
+
48
+ def register_aliases() -> None:
49
+ package = sys.modules.get("texsmith")
50
+ if package is None: # pragma: no cover - defensive
51
+ return
52
+ for alias, (target, attr_map) in _EXTENSION_ALIAS_MODULES.items():
53
+ module_name = f"texsmith.{alias}"
54
+ placeholder = _AliasModule(module_name, target, attr_map)
55
+ sys.modules.setdefault(module_name, placeholder)
56
+ setattr(package, alias, placeholder)
57
+
58
+
59
+ register_aliases()
@@ -0,0 +1,6 @@
1
+ """Integration layer for TexSmith adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ __all__ = []
@@ -0,0 +1,258 @@
1
+ """Abstractions for invoking Docker containers safely."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from dataclasses import dataclass, field
7
+ import os
8
+ from pathlib import Path
9
+ import shutil
10
+ import subprocess
11
+
12
+ from texsmith.core.exceptions import TransformerExecutionError
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class VolumeMount:
17
+ """Bind mount configuration."""
18
+
19
+ source: Path | str
20
+ target: str
21
+ read_only: bool = False
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class DockerLimits:
26
+ """Runtime constraints for Docker containers."""
27
+
28
+ cpus: float | int | None = None
29
+ memory: str | None = None
30
+ pids_limit: int | None = None
31
+
32
+
33
+ @dataclass(slots=True)
34
+ class DockerRunRequest:
35
+ """Full request payload for a Docker execution."""
36
+
37
+ image: str
38
+ args: Sequence[str] = field(default_factory=tuple)
39
+ mounts: Sequence[VolumeMount] = field(default_factory=tuple)
40
+ environment: Mapping[str, str] = field(default_factory=dict)
41
+ workdir: str | None = None
42
+ user: str | None = None
43
+ use_host_user: bool = True
44
+ remove: bool = True
45
+ limits: DockerLimits | None = None
46
+ network: str | None = None
47
+ extra_args: Sequence[str] = field(default_factory=tuple)
48
+
49
+
50
+ class DockerRunner:
51
+ """Utility class encapsulating Docker invocations."""
52
+
53
+ def __init__(self, executable: str | None = None) -> None:
54
+ self._explicit_executable = executable
55
+ self._cached_executable: str | None = None
56
+
57
+ def is_available(self) -> bool:
58
+ """Return True when Docker can be located."""
59
+ try:
60
+ return self._resolve_executable(optional=True) is not None
61
+ except TransformerExecutionError:
62
+ return False
63
+
64
+ def reset(self) -> None:
65
+ """Clear cached executable lookup results."""
66
+ self._cached_executable = None
67
+
68
+ def run(
69
+ self,
70
+ request: DockerRunRequest,
71
+ *,
72
+ capture_output: bool = True,
73
+ text: bool = True,
74
+ ) -> subprocess.CompletedProcess[str]:
75
+ """Execute Docker with the supplied request."""
76
+ command = self._build_run_command(request)
77
+ try:
78
+ result = subprocess.run(
79
+ command,
80
+ check=False,
81
+ capture_output=capture_output,
82
+ text=text,
83
+ )
84
+ except FileNotFoundError as exc:
85
+ self._cached_executable = None
86
+ raise TransformerExecutionError("Docker executable could not be located.") from exc
87
+ except OSError as exc:
88
+ raise TransformerExecutionError(f"Failed to invoke Docker: {exc}") from exc
89
+
90
+ if result.returncode != 0:
91
+ stderr = (result.stderr or "").strip()
92
+ stdout = (result.stdout or "").strip()
93
+ detail = stderr or stdout
94
+ message = f"Docker image '{request.image}' failed with exit code {result.returncode}"
95
+ if detail:
96
+ message = f"{message}: {detail}"
97
+ raise TransformerExecutionError(message)
98
+
99
+ return result
100
+
101
+ def _build_run_command(self, request: DockerRunRequest) -> list[str]:
102
+ executable = self._resolve_executable(optional=False)
103
+ assert executable is not None
104
+ command: list[str] = [executable, "run"]
105
+
106
+ if request.remove:
107
+ command.append("--rm")
108
+
109
+ if request.extra_args:
110
+ command.extend(request.extra_args)
111
+
112
+ user = request.user or (self._resolve_host_user() if request.use_host_user else None)
113
+ if user:
114
+ command.extend(["--user", user])
115
+
116
+ if request.environment:
117
+ for key in sorted(request.environment):
118
+ value = request.environment[key]
119
+ command.extend(["-e", f"{key}={value}"])
120
+
121
+ if request.workdir:
122
+ command.extend(["--workdir", request.workdir])
123
+
124
+ if request.network:
125
+ command.extend(["--network", request.network])
126
+
127
+ command.extend(self._build_mounts(request.mounts))
128
+ command.extend(self._build_limits(request.limits))
129
+
130
+ command.append(request.image)
131
+ command.extend(request.args)
132
+ return command
133
+
134
+ def _build_mounts(self, mounts: Sequence[VolumeMount]) -> list[str]:
135
+ flags: list[str] = []
136
+ for mount in mounts:
137
+ host = Path(mount.source).expanduser()
138
+ if not host.exists():
139
+ raise TransformerExecutionError(f"Docker mount source '{host}' does not exist.")
140
+ try:
141
+ resolved = host.resolve(strict=True)
142
+ except (OSError, RuntimeError):
143
+ resolved = host.absolute()
144
+
145
+ parts = [
146
+ "type=bind",
147
+ f"src={resolved}",
148
+ f"dst={mount.target}",
149
+ ]
150
+
151
+ if mount.read_only:
152
+ parts.append("readonly")
153
+
154
+ flags.extend(["--mount", ",".join(parts)])
155
+ return flags
156
+
157
+ def _build_limits(self, limits: DockerLimits | None) -> list[str]:
158
+ if limits is None:
159
+ return []
160
+
161
+ flags: list[str] = []
162
+ if limits.cpus is not None:
163
+ flags.extend(["--cpus", str(limits.cpus)])
164
+ if limits.memory:
165
+ flags.extend(["--memory", str(limits.memory)])
166
+ if limits.pids_limit is not None:
167
+ flags.extend(["--pids-limit", str(limits.pids_limit)])
168
+ return flags
169
+
170
+ def _resolve_executable(self, *, optional: bool) -> str | None:
171
+ if self._explicit_executable:
172
+ return self._explicit_executable
173
+
174
+ if self._cached_executable:
175
+ return self._cached_executable
176
+
177
+ try:
178
+ executable = shutil.which("docker")
179
+ except (AssertionError, OSError, ValueError):
180
+ executable = None
181
+
182
+ if executable:
183
+ self._cached_executable = executable
184
+ return executable
185
+
186
+ if optional:
187
+ return None
188
+
189
+ raise TransformerExecutionError("Docker is required but was not found on PATH.")
190
+
191
+ def _resolve_host_user(self) -> str | None:
192
+ getuid = getattr(os, "getuid", None)
193
+ getgid = getattr(os, "getgid", None)
194
+
195
+ if callable(getuid) and callable(getgid):
196
+ try:
197
+ uid = getuid()
198
+ gid = getgid()
199
+ except OSError:
200
+ return None
201
+ return f"{uid}:{gid}"
202
+
203
+ return None
204
+
205
+
206
+ _default_runner = DockerRunner()
207
+
208
+
209
+ def is_docker_available() -> bool:
210
+ """Check if Docker can be executed."""
211
+ return _default_runner.is_available()
212
+
213
+
214
+ def run_container(
215
+ image: str,
216
+ args: Sequence[str] = (),
217
+ *,
218
+ mounts: Sequence[VolumeMount] = (),
219
+ environment: Mapping[str, str] | None = None,
220
+ workdir: str | None = None,
221
+ user: str | None = None,
222
+ use_host_user: bool = True,
223
+ limits: DockerLimits | None = None,
224
+ network: str | None = None,
225
+ remove: bool = True,
226
+ extra_args: Sequence[str] = (),
227
+ capture_output: bool = True,
228
+ text: bool = True,
229
+ ) -> subprocess.CompletedProcess[str]:
230
+ """Execute Docker using the shared runner."""
231
+ request = DockerRunRequest(
232
+ image=image,
233
+ args=tuple(args),
234
+ mounts=tuple(mounts),
235
+ environment=environment or {},
236
+ workdir=workdir,
237
+ user=user,
238
+ use_host_user=use_host_user,
239
+ remove=remove,
240
+ limits=limits,
241
+ network=network,
242
+ extra_args=tuple(extra_args),
243
+ )
244
+ return _default_runner.run(
245
+ request,
246
+ capture_output=capture_output,
247
+ text=text,
248
+ )
249
+
250
+
251
+ __all__ = [
252
+ "DockerLimits",
253
+ "DockerRunRequest",
254
+ "DockerRunner",
255
+ "VolumeMount",
256
+ "is_docker_available",
257
+ "run_container",
258
+ ]
@@ -0,0 +1,3 @@
1
+ """Built-in handler collections."""
2
+
3
+ from . import admonitions, basic, blocks, code, inline, links, media