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.
- texsmith/__init__.py +107 -0
- texsmith/_alias.py +59 -0
- texsmith/adapters/__init__.py +6 -0
- texsmith/adapters/docker.py +258 -0
- texsmith/adapters/handlers/__init__.py +3 -0
- texsmith/adapters/handlers/_assets.py +363 -0
- texsmith/adapters/handlers/_helpers.py +62 -0
- texsmith/adapters/handlers/_mermaid.py +122 -0
- texsmith/adapters/handlers/admonitions.py +267 -0
- texsmith/adapters/handlers/basic.py +228 -0
- texsmith/adapters/handlers/blocks.py +851 -0
- texsmith/adapters/handlers/code.py +309 -0
- texsmith/adapters/handlers/inline.py +937 -0
- texsmith/adapters/handlers/links.py +239 -0
- texsmith/adapters/handlers/media.py +380 -0
- texsmith/adapters/latex/__init__.py +10 -0
- texsmith/adapters/latex/engines/__init__.py +726 -0
- texsmith/adapters/latex/engines/latex/__init__.py +26 -0
- texsmith/adapters/latex/engines/latex/log.py +720 -0
- texsmith/adapters/latex/engines/latex/runner.py +28 -0
- texsmith/adapters/latex/engines/tectonic/__init__.py +38 -0
- texsmith/adapters/latex/formatter.py +297 -0
- texsmith/adapters/latex/latexmk.py +148 -0
- texsmith/adapters/latex/partials/acronym.tex +1 -0
- texsmith/adapters/latex/partials/add.tex +1 -0
- texsmith/adapters/latex/partials/addition.tex +1 -0
- texsmith/adapters/latex/partials/blockquote.tex +3 -0
- texsmith/adapters/latex/partials/callout.tex +3 -0
- texsmith/adapters/latex/partials/choices.tex +5 -0
- texsmith/adapters/latex/partials/citation.tex +1 -0
- texsmith/adapters/latex/partials/codeblock.tex +7 -0
- texsmith/adapters/latex/partials/codeblock_listings.tex +5 -0
- texsmith/adapters/latex/partials/codeblock_pygments.tex +5 -0
- texsmith/adapters/latex/partials/codeblock_verbatim.tex +12 -0
- texsmith/adapters/latex/partials/codeinline.tex +1 -0
- texsmith/adapters/latex/partials/codeinlinett.tex +1 -0
- texsmith/adapters/latex/partials/comment.tex +1 -0
- texsmith/adapters/latex/partials/del.tex +1 -0
- texsmith/adapters/latex/partials/deletion.tex +1 -0
- texsmith/adapters/latex/partials/description_list.tex +5 -0
- texsmith/adapters/latex/partials/enquote.tex +1 -0
- texsmith/adapters/latex/partials/epigraph.tex +1 -0
- texsmith/adapters/latex/partials/exercises_solutions.tex +7 -0
- texsmith/adapters/latex/partials/figure.tex +33 -0
- texsmith/adapters/latex/partials/figure_tcolorbox.tex +15 -0
- texsmith/adapters/latex/partials/footnote.tex +3 -0
- texsmith/adapters/latex/partials/glossary.tex +1 -0
- texsmith/adapters/latex/partials/heading.tex +14 -0
- texsmith/adapters/latex/partials/highlight.tex +25 -0
- texsmith/adapters/latex/partials/horizontal_rule.tex +1 -0
- texsmith/adapters/latex/partials/href.tex +1 -0
- texsmith/adapters/latex/partials/icon.tex +1 -0
- texsmith/adapters/latex/partials/include.tex +1 -0
- texsmith/adapters/latex/partials/index.tex +1 -0
- texsmith/adapters/latex/partials/italic.tex +1 -0
- texsmith/adapters/latex/partials/keystroke.tex +46 -0
- texsmith/adapters/latex/partials/label.tex +1 -0
- texsmith/adapters/latex/partials/list_acronyms.tex +5 -0
- texsmith/adapters/latex/partials/list_glossary.tex +8 -0
- texsmith/adapters/latex/partials/multicolumn.tex +3 -0
- texsmith/adapters/latex/partials/ordered_list.tex +5 -0
- texsmith/adapters/latex/partials/pagestyle.tex +1 -0
- texsmith/adapters/latex/partials/ref.tex +1 -0
- texsmith/adapters/latex/partials/regex.tex +1 -0
- texsmith/adapters/latex/partials/smallcaps.tex +1 -0
- texsmith/adapters/latex/partials/strikethrough.tex +1 -0
- texsmith/adapters/latex/partials/strong.tex +1 -0
- texsmith/adapters/latex/partials/subscript.tex +1 -0
- texsmith/adapters/latex/partials/substitution.tex +1 -0
- texsmith/adapters/latex/partials/superscript.tex +1 -0
- texsmith/adapters/latex/partials/tabbed.tex +3 -0
- texsmith/adapters/latex/partials/table.tex +48 -0
- texsmith/adapters/latex/partials/underline.tex +1 -0
- texsmith/adapters/latex/partials/unordered_list.tex +5 -0
- texsmith/adapters/latex/partials/url.tex +1 -0
- texsmith/adapters/latex/pygments.py +98 -0
- texsmith/adapters/latex/pyxindy.py +64 -0
- texsmith/adapters/latex/renderer.py +220 -0
- texsmith/adapters/latex/tectonic.py +366 -0
- texsmith/adapters/latex/utils.py +86 -0
- texsmith/adapters/markdown/__init__.py +342 -0
- texsmith/adapters/plugins/__init__.py +8 -0
- texsmith/adapters/plugins/material.py +174 -0
- texsmith/adapters/plugins/snippet.py +1734 -0
- texsmith/adapters/transformers/__init__.py +122 -0
- texsmith/adapters/transformers/base.py +140 -0
- texsmith/adapters/transformers/strategies.py +1456 -0
- texsmith/adapters/transformers/utils.py +59 -0
- texsmith/api/__init__.py +80 -0
- texsmith/api/_utils.py +56 -0
- texsmith/api/document.py +645 -0
- texsmith/api/pipeline.py +229 -0
- texsmith/api/service.py +648 -0
- texsmith/api/templates.py +287 -0
- texsmith/core/__init__.py +6 -0
- texsmith/core/bibliography/__init__.py +57 -0
- texsmith/core/bibliography/collection.py +354 -0
- texsmith/core/bibliography/doi.py +194 -0
- texsmith/core/bibliography/issues.py +15 -0
- texsmith/core/bibliography/parsing.py +45 -0
- texsmith/core/callouts.py +99 -0
- texsmith/core/config.py +191 -0
- texsmith/core/context.py +242 -0
- texsmith/core/conversion/__init__.py +103 -0
- texsmith/core/conversion/core.py +780 -0
- texsmith/core/conversion/debug.py +87 -0
- texsmith/core/conversion/inputs.py +474 -0
- texsmith/core/conversion/renderer.py +578 -0
- texsmith/core/conversion/templates.py +646 -0
- texsmith/core/conversion_contexts.py +95 -0
- texsmith/core/diagnostics.py +118 -0
- texsmith/core/exceptions.py +41 -0
- texsmith/core/fonts/__init__.py +3 -0
- texsmith/core/fragments/__init__.py +716 -0
- texsmith/core/fragments/base.py +117 -0
- texsmith/core/metadata.py +280 -0
- texsmith/core/mustache.py +86 -0
- texsmith/core/partials.py +20 -0
- texsmith/core/rules.py +394 -0
- texsmith/core/templates/__init__.py +58 -0
- texsmith/core/templates/base.py +343 -0
- texsmith/core/templates/builtins.py +68 -0
- texsmith/core/templates/context_usage.py +137 -0
- texsmith/core/templates/loader.py +303 -0
- texsmith/core/templates/manifest.py +861 -0
- texsmith/core/templates/runtime.py +347 -0
- texsmith/core/templates/text.py +14 -0
- texsmith/core/templates/wrapper.py +340 -0
- texsmith/core/user_dir.py +179 -0
- texsmith/devtools.py +28 -0
- texsmith/extensions/__init__.py +162 -0
- texsmith/extensions/index/__init__.py +21 -0
- texsmith/extensions/index/markdown.py +94 -0
- texsmith/extensions/index/mkdocs_plugin.py +136 -0
- texsmith/extensions/index/registry.py +57 -0
- texsmith/extensions/index/renderer.py +183 -0
- texsmith/extensions/index/templates/index.tex +1 -0
- texsmith/extensions/latex_raw.py +115 -0
- texsmith/extensions/latex_text.py +117 -0
- texsmith/extensions/mermaid.py +267 -0
- texsmith/extensions/missing_footnotes.py +125 -0
- texsmith/extensions/multi_citations.py +54 -0
- texsmith/extensions/progressbar/__init__.py +9 -0
- texsmith/extensions/progressbar/markdown.py +212 -0
- texsmith/extensions/progressbar/renderer.py +117 -0
- texsmith/extensions/smallcaps.py +48 -0
- texsmith/extensions/texlogos/__init__.py +10 -0
- texsmith/extensions/texlogos/markdown.py +236 -0
- texsmith/extensions/texlogos/renderer.py +81 -0
- texsmith/extensions/texlogos/specs.py +66 -0
- texsmith/fonts/__init__.py +57 -0
- texsmith/fonts/cache.py +46 -0
- texsmith/fonts/constants.py +60 -0
- texsmith/fonts/coverage.py +275 -0
- texsmith/fonts/downloader.py +103 -0
- texsmith/fonts/fallback.py +438 -0
- texsmith/fonts/html_scripts.py +168 -0
- texsmith/fonts/logging.py +127 -0
- texsmith/fonts/pipeline.py +338 -0
- texsmith/fonts/scripts.py +493 -0
- texsmith/fonts/ucharclasses.py +164 -0
- texsmith/fragments/__init__.py +11 -0
- texsmith/fragments/bibliography/__init__.py +65 -0
- texsmith/fragments/bibliography/fragment.toml +3 -0
- texsmith/fragments/bibliography/ts-bibliography-backmatter.jinja.tex +35 -0
- texsmith/fragments/bibliography/ts-bibliography.jinja.tex +10 -0
- texsmith/fragments/callouts/__init__.py +88 -0
- texsmith/fragments/callouts/fragment.toml +3 -0
- texsmith/fragments/callouts/ts-callouts.jinja.sty +129 -0
- texsmith/fragments/code/__init__.py +82 -0
- texsmith/fragments/code/fragment.toml +3 -0
- texsmith/fragments/code/ts-code.jinja.sty +140 -0
- texsmith/fragments/extra/__init__.py +180 -0
- texsmith/fragments/extra/fragment.toml +3 -0
- texsmith/fragments/extra/ts-extra.jinja.tex +13 -0
- texsmith/fragments/fonts/__init__.py +880 -0
- texsmith/fragments/fonts/fragment.toml +3 -0
- texsmith/fragments/fonts/ts-fonts.jinja.sty +292 -0
- texsmith/fragments/frame/__init__.py +170 -0
- texsmith/fragments/frame/fragment.toml +3 -0
- texsmith/fragments/frame/ts-frame.tex.jinja +49 -0
- texsmith/fragments/geometry/__init__.py +169 -0
- texsmith/fragments/geometry/fragment.toml +3 -0
- texsmith/fragments/geometry/paper.py +526 -0
- texsmith/fragments/geometry/ts_geometry.tex.jinja +53 -0
- texsmith/fragments/glossary/__init__.py +67 -0
- texsmith/fragments/glossary/fragment.toml +3 -0
- texsmith/fragments/glossary/ts-glossary-backmatter.jinja.tex +5 -0
- texsmith/fragments/glossary/ts-glossary.jinja.sty +28 -0
- texsmith/fragments/index/__init__.py +66 -0
- texsmith/fragments/index/fragment.toml +3 -0
- texsmith/fragments/index/ts-index-backmatter.jinja.tex +3 -0
- texsmith/fragments/index/ts-index.jinja.sty +12 -0
- texsmith/fragments/keystrokes/__init__.py +69 -0
- texsmith/fragments/keystrokes/fragment.toml +3 -0
- texsmith/fragments/keystrokes/ts-keystrokes.jinja.sty +20 -0
- texsmith/fragments/todolist/__init__.py +71 -0
- texsmith/fragments/todolist/fragment.toml +3 -0
- texsmith/fragments/todolist/ts-todolist.jinja.sty +21 -0
- texsmith/fragments/typesetting/__init__.py +214 -0
- texsmith/fragments/typesetting/fragment.toml +3 -0
- texsmith/fragments/typesetting/ts-typesetting.tex.jinja +81 -0
- texsmith/index.py +26 -0
- texsmith/plugins/__init__.py +14 -0
- texsmith/progressbar.py +8 -0
- texsmith/quotes.py +40 -0
- texsmith/smart_dashes.py +66 -0
- texsmith/templates/__init__.py +3 -0
- texsmith/templates/article/README.md +33 -0
- texsmith/templates/article/__init__.py +281 -0
- texsmith/templates/article/template/manifest.toml +125 -0
- texsmith/templates/article/template/mermaid-config.json +18 -0
- texsmith/templates/article/template/template.tex +74 -0
- texsmith/templates/book/README.md +26 -0
- texsmith/templates/book/__init__.py +75 -0
- texsmith/templates/book/overrides/codeblock.tex +7 -0
- texsmith/templates/book/overrides/codeinline.tex +1 -0
- texsmith/templates/book/template/fixtoc.sty +81 -0
- texsmith/templates/book/template/manifest.toml +198 -0
- texsmith/templates/book/template/template.tex +314 -0
- texsmith/templates/common/__init__.py +1 -0
- texsmith/templates/common/latexmkrc +46 -0
- texsmith/templates/letter/README.md +59 -0
- texsmith/templates/letter/__init__.py +495 -0
- texsmith/templates/letter/demo.md +31 -0
- texsmith/templates/letter/fonts/modernline bold.otf +0 -0
- texsmith/templates/letter/fonts/modernline.otf +0 -0
- texsmith/templates/letter/manifest.toml +197 -0
- texsmith/templates/letter/template/callouts.jinja.sty +290 -0
- texsmith/templates/letter/template/template.tex +123 -0
- texsmith/templates/snippet/README.md +21 -0
- texsmith/templates/snippet/__init__.py +80 -0
- texsmith/templates/snippet/template/manifest.toml +66 -0
- texsmith/templates/snippet/template/template.tex +47 -0
- texsmith/texlogos.py +15 -0
- texsmith/ui/__init__.py +6 -0
- texsmith/ui/cli/__init__.py +22 -0
- texsmith/ui/cli/_options.py +338 -0
- texsmith/ui/cli/app.py +65 -0
- texsmith/ui/cli/bibliography.py +300 -0
- texsmith/ui/cli/commands/__init__.py +14 -0
- texsmith/ui/cli/commands/render.py +1128 -0
- texsmith/ui/cli/commands/templates.py +397 -0
- texsmith/ui/cli/diagnostics.py +36 -0
- texsmith/ui/cli/presenter.py +663 -0
- texsmith/ui/cli/state.py +263 -0
- texsmith/ui/cli/utils.py +235 -0
- texsmith-0.0.2.dev0.dist-info/METADATA +187 -0
- texsmith-0.0.2.dev0.dist-info/RECORD +252 -0
- texsmith-0.0.2.dev0.dist-info/WHEEL +4 -0
- texsmith-0.0.2.dev0.dist-info/entry_points.txt +22 -0
- 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,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
|
+
]
|