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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Conversion registry exposing high-level helpers for assets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from texsmith.core.exceptions import TransformerExecutionError
|
|
9
|
+
|
|
10
|
+
from .base import ConverterStrategy
|
|
11
|
+
from .strategies import (
|
|
12
|
+
DrawioToPdfStrategy,
|
|
13
|
+
FetchImageStrategy,
|
|
14
|
+
ImageToPdfStrategy,
|
|
15
|
+
MermaidToPdfStrategy,
|
|
16
|
+
PdfMetadataStrategy,
|
|
17
|
+
SvgToPdfStrategy,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConverterRegistry:
|
|
22
|
+
"""Registry storing converter strategies."""
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
self._strategies: dict[str, ConverterStrategy] = {}
|
|
26
|
+
|
|
27
|
+
def register(self, name: str, strategy: ConverterStrategy) -> None:
|
|
28
|
+
"""Register a converter strategy under a unique name."""
|
|
29
|
+
self._strategies[name] = strategy
|
|
30
|
+
|
|
31
|
+
def get(self, name: str) -> ConverterStrategy:
|
|
32
|
+
"""Return a registered converter strategy or raise an execution error."""
|
|
33
|
+
try:
|
|
34
|
+
return self._strategies[name]
|
|
35
|
+
except KeyError as exc: # pragma: no cover - defensive
|
|
36
|
+
raise TransformerExecutionError(f"No converter registered for '{name}'") from exc
|
|
37
|
+
|
|
38
|
+
def is_registered(self, name: str) -> bool:
|
|
39
|
+
"""Return True when a converter has been registered under the given name."""
|
|
40
|
+
return name in self._strategies
|
|
41
|
+
|
|
42
|
+
def convert(
|
|
43
|
+
self,
|
|
44
|
+
name: str,
|
|
45
|
+
source: Path | str,
|
|
46
|
+
*,
|
|
47
|
+
output_dir: Path,
|
|
48
|
+
**options: Any,
|
|
49
|
+
) -> Any:
|
|
50
|
+
"""Execute a converter strategy with the provided arguments."""
|
|
51
|
+
strategy = self.get(name)
|
|
52
|
+
return strategy(source, output_dir=output_dir, **options)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
registry = ConverterRegistry()
|
|
56
|
+
|
|
57
|
+
# Built-in strategies
|
|
58
|
+
registry.register("svg", SvgToPdfStrategy())
|
|
59
|
+
registry.register("image", ImageToPdfStrategy())
|
|
60
|
+
registry.register("fetch-image", FetchImageStrategy())
|
|
61
|
+
registry.register("pdf-metadata", PdfMetadataStrategy())
|
|
62
|
+
registry.register("drawio", DrawioToPdfStrategy())
|
|
63
|
+
registry.register("mermaid", MermaidToPdfStrategy())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def register_converter(name: str, strategy: ConverterStrategy) -> None:
|
|
67
|
+
"""Expose a helper to register external strategies."""
|
|
68
|
+
registry.register(name, strategy)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def has_converter(name: str) -> bool:
|
|
72
|
+
"""Return True when a converter strategy is currently registered."""
|
|
73
|
+
return registry.is_registered(name)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def svg2pdf(source: Path | str, output_dir: Path, **options: Any) -> Path:
|
|
77
|
+
"""Convert SVG assets to PDF."""
|
|
78
|
+
return registry.convert("svg", source, output_dir=output_dir, **options)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def image2pdf(source: Path | str, output_dir: Path, **options: Any) -> Path:
|
|
82
|
+
"""Convert bitmap images to PDF."""
|
|
83
|
+
return registry.convert("image", source, output_dir=output_dir, **options)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def drawio2pdf(source: Path | str, output_dir: Path, **options: Any) -> Path:
|
|
87
|
+
"""Convert draw.io diagrams to PDF."""
|
|
88
|
+
return registry.convert("drawio", source, output_dir=output_dir, **options)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def mermaid2pdf(source: Path | str, output_dir: Path, **options: Any) -> Path:
|
|
92
|
+
"""Convert Mermaid diagrams to PDF."""
|
|
93
|
+
return registry.convert("mermaid", source, output_dir=output_dir, **options)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def fetch_image(url: str, output_dir: Path, **options: Any) -> Path:
|
|
97
|
+
"""Fetch a remote image and normalise it to PDF."""
|
|
98
|
+
return registry.convert("fetch-image", url, output_dir=output_dir, **options)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_pdf_page_sizes(source: Path | str, **options: Any) -> dict[str, Any]:
|
|
102
|
+
"""Inspect a PDF and return structured metadata."""
|
|
103
|
+
output_dir = options.pop(
|
|
104
|
+
"output_dir", Path(source).parent if isinstance(source, Path) else Path.cwd()
|
|
105
|
+
)
|
|
106
|
+
return registry.convert("pdf-metadata", source, output_dir=output_dir, **options)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
"ConverterRegistry",
|
|
111
|
+
"ConverterStrategy",
|
|
112
|
+
"DrawioToPdfStrategy",
|
|
113
|
+
"MermaidToPdfStrategy",
|
|
114
|
+
"drawio2pdf",
|
|
115
|
+
"fetch_image",
|
|
116
|
+
"get_pdf_page_sizes",
|
|
117
|
+
"has_converter",
|
|
118
|
+
"image2pdf",
|
|
119
|
+
"mermaid2pdf",
|
|
120
|
+
"register_converter",
|
|
121
|
+
"svg2pdf",
|
|
122
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Primitives used by asset converter strategies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Protocol
|
|
11
|
+
|
|
12
|
+
from texsmith.core.exceptions import TransformerExecutionError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConverterStrategy(Protocol):
|
|
16
|
+
"""Protocol implemented by concrete converter strategies."""
|
|
17
|
+
|
|
18
|
+
def __call__(self, source: Path | str, *, output_dir: Path, **options: Any) -> Any: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def exponential_backoff(
|
|
22
|
+
base_delay: float = 0.5, factor: float = 2.0, max_delay: float = 5.0
|
|
23
|
+
) -> Callable[[int], float]:
|
|
24
|
+
"""Return a simple exponential backoff policy."""
|
|
25
|
+
|
|
26
|
+
def policy(attempt: int) -> float:
|
|
27
|
+
delay = base_delay * (factor ** (attempt - 1))
|
|
28
|
+
return min(delay, max_delay)
|
|
29
|
+
|
|
30
|
+
return policy
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CachedConversionStrategy:
|
|
34
|
+
"""Base class that adds caching and retry/backoff policies."""
|
|
35
|
+
|
|
36
|
+
suffix: str = ".pdf"
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
namespace: str,
|
|
41
|
+
*,
|
|
42
|
+
max_attempts: int = 3,
|
|
43
|
+
backoff: Callable[[int], float] | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.namespace = namespace
|
|
46
|
+
self.max_attempts = max_attempts
|
|
47
|
+
self.backoff = backoff or exponential_backoff()
|
|
48
|
+
|
|
49
|
+
def __call__(self, source: Path | str, *, output_dir: Path, **options: Any) -> Path:
|
|
50
|
+
output_dir = Path(output_dir)
|
|
51
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
cacheable_options = {key: value for key, value in options.items() if key != "emitter"}
|
|
54
|
+
|
|
55
|
+
cache_key = self._make_cache_key(source, cacheable_options)
|
|
56
|
+
target = self._resolve_target_path(output_dir, cache_key, source, options)
|
|
57
|
+
|
|
58
|
+
if target.exists() and not options.get("force", False):
|
|
59
|
+
return target
|
|
60
|
+
|
|
61
|
+
cache_dir = output_dir / ".cache" / self.namespace
|
|
62
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
last_error: Exception | None = None
|
|
65
|
+
for attempt in range(1, self.max_attempts + 1):
|
|
66
|
+
try:
|
|
67
|
+
return self._perform_conversion(
|
|
68
|
+
source, target=target, cache_dir=cache_dir, **options
|
|
69
|
+
)
|
|
70
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
71
|
+
last_error = exc
|
|
72
|
+
should_retry = attempt < self.max_attempts and not isinstance(
|
|
73
|
+
exc, TransformerExecutionError
|
|
74
|
+
)
|
|
75
|
+
if not should_retry:
|
|
76
|
+
raise
|
|
77
|
+
delay = self.backoff(attempt)
|
|
78
|
+
if delay > 0:
|
|
79
|
+
time.sleep(delay)
|
|
80
|
+
|
|
81
|
+
if isinstance(last_error, TransformerExecutionError):
|
|
82
|
+
raise last_error
|
|
83
|
+
|
|
84
|
+
message = f"Conversion failed for '{self.namespace}' after {self.max_attempts} attempts"
|
|
85
|
+
raise TransformerExecutionError(message) from last_error
|
|
86
|
+
|
|
87
|
+
# --------------------------------------------------------------------- helpers
|
|
88
|
+
|
|
89
|
+
def _perform_conversion(
|
|
90
|
+
self,
|
|
91
|
+
source: Path | str,
|
|
92
|
+
*,
|
|
93
|
+
target: Path,
|
|
94
|
+
cache_dir: Path,
|
|
95
|
+
**options: Any,
|
|
96
|
+
) -> Path:
|
|
97
|
+
"""Sub-classes must implement actual conversion logic."""
|
|
98
|
+
raise NotImplementedError
|
|
99
|
+
|
|
100
|
+
def _resolve_target_path(
|
|
101
|
+
self,
|
|
102
|
+
output_dir: Path,
|
|
103
|
+
cache_key: str,
|
|
104
|
+
source: Path | str,
|
|
105
|
+
options: dict[str, Any],
|
|
106
|
+
) -> Path:
|
|
107
|
+
suffix = self.output_suffix(source=source, options=options)
|
|
108
|
+
return output_dir / f"{cache_key}{suffix}"
|
|
109
|
+
|
|
110
|
+
def output_suffix(self, source: Any, options: dict[str, Any]) -> str:
|
|
111
|
+
"""Allow subclasses to customise the output suffix."""
|
|
112
|
+
return self.suffix
|
|
113
|
+
|
|
114
|
+
def _make_cache_key(self, source: Path | str, options: dict[str, Any]) -> str:
|
|
115
|
+
digest = sha256()
|
|
116
|
+
digest.update(self._serialise_source(source))
|
|
117
|
+
digest.update(self._serialise_options(options))
|
|
118
|
+
return digest.hexdigest()
|
|
119
|
+
|
|
120
|
+
def _serialise_source(self, source: Path | str) -> bytes:
|
|
121
|
+
if isinstance(source, Path):
|
|
122
|
+
if source.exists():
|
|
123
|
+
return source.read_bytes()
|
|
124
|
+
return str(source.resolve()).encode("utf-8")
|
|
125
|
+
return source.encode("utf-8")
|
|
126
|
+
|
|
127
|
+
def _serialise_options(self, options: dict[str, Any]) -> bytes:
|
|
128
|
+
normalised = {key: self._normalise_option(value) for key, value in options.items()}
|
|
129
|
+
return json.dumps(normalised, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
130
|
+
|
|
131
|
+
def _normalise_option(self, value: Any) -> Any:
|
|
132
|
+
match value:
|
|
133
|
+
case Path():
|
|
134
|
+
return value.as_posix()
|
|
135
|
+
case list() | tuple():
|
|
136
|
+
return [self._normalise_option(item) for item in value]
|
|
137
|
+
case dict():
|
|
138
|
+
return {str(k): self._normalise_option(v) for k, v in value.items()}
|
|
139
|
+
case _:
|
|
140
|
+
return value
|