zenfmt 0.1.0__py3-none-win_amd64.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.
- zenfmt/__init__.py +72 -0
- zenfmt/_capabilities.py +102 -0
- zenfmt/_convert.py +374 -0
- zenfmt/_diagnostics.py +422 -0
- zenfmt/_errors.py +147 -0
- zenfmt/_ffi.py +139 -0
- zenfmt/_limits.py +174 -0
- zenfmt/_loader.py +378 -0
- zenfmt/_marshal.py +372 -0
- zenfmt/_models.py +402 -0
- zenfmt/_native/README.md +14 -0
- zenfmt/_native/zenfmt_py.dll +0 -0
- zenfmt/py.typed +0 -0
- zenfmt-0.1.0.dist-info/METADATA +119 -0
- zenfmt-0.1.0.dist-info/RECORD +17 -0
- zenfmt-0.1.0.dist-info/WHEEL +4 -0
- zenfmt-0.1.0.dist-info/licenses/LICENSE +21 -0
zenfmt/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""zenfmt: universal document conversion for Python (ZDS 0014).
|
|
2
|
+
|
|
3
|
+
The common operation is one call::
|
|
4
|
+
|
|
5
|
+
import zenfmt
|
|
6
|
+
|
|
7
|
+
conversion = zenfmt.convert("report.docx")
|
|
8
|
+
print(conversion.text)
|
|
9
|
+
for report in conversion.reports:
|
|
10
|
+
print(report.code, report.problem)
|
|
11
|
+
|
|
12
|
+
Importing this package defines Python objects only; the bundled native
|
|
13
|
+
engine loads and verifies itself on the first capability query or
|
|
14
|
+
conversion. There is no global configuration, no environment lookup, and
|
|
15
|
+
no network access.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib.metadata as _metadata
|
|
21
|
+
|
|
22
|
+
from ._convert import Converter, convert, formats
|
|
23
|
+
from ._errors import (
|
|
24
|
+
ConversionError,
|
|
25
|
+
DestinationExistsError,
|
|
26
|
+
Direction,
|
|
27
|
+
InputReadError,
|
|
28
|
+
LimitExceededError,
|
|
29
|
+
NativeLibraryError,
|
|
30
|
+
UnknownFormatError,
|
|
31
|
+
UnsupportedPlatformError,
|
|
32
|
+
ZenfmtError,
|
|
33
|
+
)
|
|
34
|
+
from ._limits import Limits
|
|
35
|
+
from ._models import (
|
|
36
|
+
Context,
|
|
37
|
+
Conversion,
|
|
38
|
+
Format,
|
|
39
|
+
Manifest,
|
|
40
|
+
Report,
|
|
41
|
+
Resource,
|
|
42
|
+
Strictness,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
__version__ = _metadata.version("zenfmt")
|
|
47
|
+
except _metadata.PackageNotFoundError: # not installed; source tree use
|
|
48
|
+
__version__ = "0.0.0"
|
|
49
|
+
|
|
50
|
+
__all__ = (
|
|
51
|
+
"Context",
|
|
52
|
+
"Conversion",
|
|
53
|
+
"ConversionError",
|
|
54
|
+
"Converter",
|
|
55
|
+
"DestinationExistsError",
|
|
56
|
+
"Direction",
|
|
57
|
+
"Format",
|
|
58
|
+
"InputReadError",
|
|
59
|
+
"LimitExceededError",
|
|
60
|
+
"Limits",
|
|
61
|
+
"Manifest",
|
|
62
|
+
"NativeLibraryError",
|
|
63
|
+
"Report",
|
|
64
|
+
"Resource",
|
|
65
|
+
"Strictness",
|
|
66
|
+
"UnknownFormatError",
|
|
67
|
+
"UnsupportedPlatformError",
|
|
68
|
+
"ZenfmtError",
|
|
69
|
+
"__version__",
|
|
70
|
+
"convert",
|
|
71
|
+
"formats",
|
|
72
|
+
)
|
zenfmt/_capabilities.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Capability discovery from native metadata (ZDS 0014).
|
|
2
|
+
|
|
3
|
+
The bridge's compile-time capability JSON is the only format table: it
|
|
4
|
+
drives :func:`zenfmt.formats`, alias resolution, artifact naming, and the
|
|
5
|
+
``Conversion.text`` decision. No hard-coded Python format list exists
|
|
6
|
+
anywhere in this package.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
from . import _diagnostics
|
|
14
|
+
from ._loader import Bridge
|
|
15
|
+
from ._models import Format
|
|
16
|
+
|
|
17
|
+
_cache: dict[bytes, Capabilities] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Capabilities:
|
|
21
|
+
__slots__ = ("_aliases", "default_output_format", "formats", "limits")
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
formats: tuple[Format, ...],
|
|
26
|
+
default_output_format: str,
|
|
27
|
+
limits: dict[str, int],
|
|
28
|
+
) -> None:
|
|
29
|
+
self.formats = formats
|
|
30
|
+
self.default_output_format = default_output_format
|
|
31
|
+
self.limits = limits
|
|
32
|
+
aliases: dict[str, str] = {}
|
|
33
|
+
for entry in formats:
|
|
34
|
+
aliases[entry.name] = entry.name
|
|
35
|
+
for extension in entry.extensions:
|
|
36
|
+
aliases.setdefault(extension, entry.name)
|
|
37
|
+
self._aliases = aliases
|
|
38
|
+
|
|
39
|
+
def resolve(self, alias: str) -> str:
|
|
40
|
+
"""Canonical format id for a name or extension-like alias.
|
|
41
|
+
|
|
42
|
+
ASCII case-insensitive; one leading dot is ignored. Unknown
|
|
43
|
+
aliases pass through unchanged so the engine produces the
|
|
44
|
+
canonical unknown-format failure with its suggestions — the
|
|
45
|
+
wrapper never maintains a second table of judgments.
|
|
46
|
+
"""
|
|
47
|
+
normalized = alias.strip().lower().removeprefix(".")
|
|
48
|
+
return self._aliases.get(normalized, normalized)
|
|
49
|
+
|
|
50
|
+
def by_name(self, name: str) -> Format | None:
|
|
51
|
+
for entry in self.formats:
|
|
52
|
+
if entry.name == name:
|
|
53
|
+
return entry
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def writer_emits_text(self, name: str) -> bool:
|
|
57
|
+
entry = self.by_name(name)
|
|
58
|
+
return bool(entry and entry.text_writer)
|
|
59
|
+
|
|
60
|
+
def primary_extension(self, name: str) -> str | None:
|
|
61
|
+
entry = self.by_name(name)
|
|
62
|
+
return entry.primary_extension if entry else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def capabilities(bridge: Bridge) -> Capabilities:
|
|
66
|
+
"""The parsed capability view for a verified bridge, cached by the
|
|
67
|
+
exact capability bytes (identical metadata parses identically)."""
|
|
68
|
+
key = bridge.capability_json
|
|
69
|
+
cached = _cache.get(key)
|
|
70
|
+
if cached is not None:
|
|
71
|
+
return cached
|
|
72
|
+
parsed = _parse(key)
|
|
73
|
+
_cache.clear()
|
|
74
|
+
_cache[key] = parsed
|
|
75
|
+
return parsed
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse(raw: bytes) -> Capabilities:
|
|
79
|
+
try:
|
|
80
|
+
document = json.loads(raw)
|
|
81
|
+
formats = tuple(
|
|
82
|
+
Format(
|
|
83
|
+
name=str(entry["format"]),
|
|
84
|
+
plugin_id=str(entry["plugin_id"]),
|
|
85
|
+
extensions=tuple(str(e) for e in entry["extensions"]),
|
|
86
|
+
can_read=bool(entry["read"]),
|
|
87
|
+
can_write=bool(entry["write"]),
|
|
88
|
+
primary_extension=entry["primary_extension"],
|
|
89
|
+
seekable_input=bool(entry["seekable_input"]),
|
|
90
|
+
text_writer=entry["text_writer"],
|
|
91
|
+
)
|
|
92
|
+
for entry in document["formats"]
|
|
93
|
+
)
|
|
94
|
+
default_output = str(document["default_output_format"])
|
|
95
|
+
limits = {str(name): int(value) for name, value in document["limits"].items()}
|
|
96
|
+
except (KeyError, TypeError, ValueError) as error:
|
|
97
|
+
raise _diagnostics.capabilities_invalid(
|
|
98
|
+
f"{type(error).__name__}: {error}"
|
|
99
|
+
) from error
|
|
100
|
+
if not formats:
|
|
101
|
+
raise _diagnostics.capabilities_invalid("no formats are declared")
|
|
102
|
+
return Capabilities(formats, default_output, limits)
|
zenfmt/_convert.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Argument validation, reusable policy, and the public conversion call
|
|
2
|
+
(ZDS 0014). Contains no document-format logic: format identity comes from
|
|
3
|
+
native capability metadata only."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path, PurePath
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from . import _capabilities, _diagnostics, _ffi, _loader, _marshal
|
|
12
|
+
from ._limits import LIMIT_TABLE, Limits
|
|
13
|
+
from ._models import Conversion, Format, Strictness
|
|
14
|
+
|
|
15
|
+
_UNSET: Any = object()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _normalize_strict(value: Any) -> Strictness:
|
|
19
|
+
if value is False or value is None:
|
|
20
|
+
return Strictness.OFF
|
|
21
|
+
if value is True:
|
|
22
|
+
# Bare `--strict` means content, and so does `strict=True`.
|
|
23
|
+
return Strictness.CONTENT
|
|
24
|
+
if isinstance(value, Strictness):
|
|
25
|
+
return value
|
|
26
|
+
if isinstance(value, str):
|
|
27
|
+
try:
|
|
28
|
+
return Strictness(value.lower())
|
|
29
|
+
except ValueError:
|
|
30
|
+
raise _diagnostics.invalid_strict(value) from None
|
|
31
|
+
raise _diagnostics.invalid_strict(value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_limits(value: Any) -> Limits | None:
|
|
35
|
+
if value is None:
|
|
36
|
+
return None
|
|
37
|
+
if isinstance(value, Limits):
|
|
38
|
+
return value
|
|
39
|
+
raise _diagnostics.invalid_limits_argument(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _normalize_bool(argument: str, value: Any) -> bool:
|
|
43
|
+
if isinstance(value, bool):
|
|
44
|
+
return value
|
|
45
|
+
raise _diagnostics.invalid_bool_argument(argument, value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _normalize_format(argument: str, value: Any) -> str | None:
|
|
49
|
+
if value is None:
|
|
50
|
+
return None
|
|
51
|
+
if isinstance(value, Format):
|
|
52
|
+
return value.name
|
|
53
|
+
if isinstance(value, str):
|
|
54
|
+
return value
|
|
55
|
+
raise _diagnostics.invalid_format_argument(argument, value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _validate_name(name: str) -> str:
|
|
59
|
+
if not isinstance(name, str):
|
|
60
|
+
raise _diagnostics.invalid_name(repr(name), "must be a str display basename")
|
|
61
|
+
if not name:
|
|
62
|
+
raise _diagnostics.invalid_name(name, "must not be empty")
|
|
63
|
+
if any(ch in name for ch in ("/", "\\")):
|
|
64
|
+
raise _diagnostics.invalid_name(name, "must not contain a directory separator")
|
|
65
|
+
if any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in name):
|
|
66
|
+
raise _diagnostics.invalid_name(name, "must not contain control characters")
|
|
67
|
+
return name
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _sanitize_stream_name(value: Any) -> str | None:
|
|
71
|
+
"""The optional display-only ``.name`` attribute of a reader: used
|
|
72
|
+
only when it is already a str, reduced to a basename, never opened."""
|
|
73
|
+
if not isinstance(value, str) or not value:
|
|
74
|
+
return None
|
|
75
|
+
base = PurePath(value).name
|
|
76
|
+
if not base:
|
|
77
|
+
return None
|
|
78
|
+
try:
|
|
79
|
+
return _validate_name(base)
|
|
80
|
+
except ValueError:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _Source:
|
|
85
|
+
"""One classified conversion source."""
|
|
86
|
+
|
|
87
|
+
__slots__ = ("data", "display_name", "kind", "path")
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
kind: str,
|
|
92
|
+
*,
|
|
93
|
+
path: str | bytes | None = None,
|
|
94
|
+
data: bytes | None = None,
|
|
95
|
+
display_name: str | None = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
self.kind = kind
|
|
98
|
+
self.path = path
|
|
99
|
+
self.data = data
|
|
100
|
+
self.display_name = display_name
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _classify_source(source: Any, name: str | None, max_input_bytes: int) -> _Source:
|
|
104
|
+
if isinstance(source, (str, PurePath, os.PathLike)):
|
|
105
|
+
# A Python str is always a filesystem path, never inline text.
|
|
106
|
+
if name is not None:
|
|
107
|
+
raise _diagnostics.name_with_path_source()
|
|
108
|
+
try:
|
|
109
|
+
path = os.fspath(source)
|
|
110
|
+
except (KeyboardInterrupt, SystemExit, GeneratorExit):
|
|
111
|
+
raise
|
|
112
|
+
except BaseException as error:
|
|
113
|
+
raise _diagnostics.path_protocol_failed("source", error) from error
|
|
114
|
+
display = os.path.basename(path) if isinstance(path, str) else None
|
|
115
|
+
if display is None:
|
|
116
|
+
display = os.path.basename(os.fsdecode(path))
|
|
117
|
+
return _Source("path", path=path, display_name=display)
|
|
118
|
+
|
|
119
|
+
display = _validate_name(name) if name is not None else None
|
|
120
|
+
if isinstance(source, (bytes, bytearray, memoryview)):
|
|
121
|
+
# Mutable or non-contiguous buffers are copied before the GIL is
|
|
122
|
+
# released; immutable bytes pass through.
|
|
123
|
+
data = source if isinstance(source, bytes) else bytes(source)
|
|
124
|
+
return _Source("bytes", data=data, display_name=display or "<memory>")
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
read = getattr(source, "read", None)
|
|
128
|
+
except (KeyboardInterrupt, SystemExit, GeneratorExit):
|
|
129
|
+
raise
|
|
130
|
+
except BaseException as error:
|
|
131
|
+
raise _diagnostics.reader_failed(error) from error
|
|
132
|
+
if callable(read):
|
|
133
|
+
data = _marshal.read_bounded(source, max_input_bytes)
|
|
134
|
+
try:
|
|
135
|
+
stream_name = getattr(source, "name", None)
|
|
136
|
+
except (KeyboardInterrupt, SystemExit, GeneratorExit):
|
|
137
|
+
raise
|
|
138
|
+
except BaseException:
|
|
139
|
+
stream_name = None
|
|
140
|
+
fallback = _sanitize_stream_name(stream_name)
|
|
141
|
+
return _Source(
|
|
142
|
+
"bytes",
|
|
143
|
+
data=data,
|
|
144
|
+
display_name=display or fallback or "<stream>",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
raise _diagnostics.invalid_source_type(source)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _artifact_name(display_name: str, extension: str) -> str:
|
|
151
|
+
"""The memory-mode artifact basename: source stem plus the writer's
|
|
152
|
+
primary extension, or ``artifact.<extension>`` when no usable stem
|
|
153
|
+
exists."""
|
|
154
|
+
stem = display_name
|
|
155
|
+
dot = stem.rfind(".")
|
|
156
|
+
if dot > 0:
|
|
157
|
+
stem = stem[:dot]
|
|
158
|
+
if not stem or stem.startswith("<"):
|
|
159
|
+
stem = "artifact"
|
|
160
|
+
return f"{stem}.{extension}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Converter:
|
|
164
|
+
"""Immutable, reusable conversion policy over the shared native
|
|
165
|
+
bridge. Thread-safe: independent calls may run concurrently on one
|
|
166
|
+
instance, and the GIL is released while native conversion runs."""
|
|
167
|
+
|
|
168
|
+
__slots__ = ("_limits", "_preserve_facets", "_strict")
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
*,
|
|
173
|
+
strict: Any = False,
|
|
174
|
+
limits: Limits | None = None,
|
|
175
|
+
preserve_facets: bool = False,
|
|
176
|
+
) -> None:
|
|
177
|
+
object.__setattr__(self, "_strict", _normalize_strict(strict))
|
|
178
|
+
object.__setattr__(self, "_limits", _normalize_limits(limits))
|
|
179
|
+
object.__setattr__(
|
|
180
|
+
self,
|
|
181
|
+
"_preserve_facets",
|
|
182
|
+
_normalize_bool("preserve_facets", preserve_facets),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
186
|
+
raise AttributeError("Converter is immutable")
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def strict(self) -> Strictness:
|
|
190
|
+
return self._strict
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def limits(self) -> Limits | None:
|
|
194
|
+
return self._limits
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def preserve_facets(self) -> bool:
|
|
198
|
+
return self._preserve_facets
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def formats(self) -> tuple[Format, ...]:
|
|
202
|
+
"""The native reader and writer capabilities of this release."""
|
|
203
|
+
return _capabilities.capabilities(_loader.bridge()).formats
|
|
204
|
+
|
|
205
|
+
def convert(
|
|
206
|
+
self,
|
|
207
|
+
source: Any,
|
|
208
|
+
*,
|
|
209
|
+
to: Any = None,
|
|
210
|
+
from_: Any = None,
|
|
211
|
+
output: Any = None,
|
|
212
|
+
name: str | None = None,
|
|
213
|
+
strict: Any = _UNSET,
|
|
214
|
+
limits: Any = _UNSET,
|
|
215
|
+
overwrite: bool = False,
|
|
216
|
+
preserve_facets: Any = _UNSET,
|
|
217
|
+
) -> Conversion:
|
|
218
|
+
"""One conversion using this converter's policy; each keyword
|
|
219
|
+
explicitly supplied here overrides the stored policy for this
|
|
220
|
+
call only."""
|
|
221
|
+
effective_strict = (
|
|
222
|
+
self._strict if strict is _UNSET else _normalize_strict(strict)
|
|
223
|
+
)
|
|
224
|
+
effective_limits = (
|
|
225
|
+
self._limits if limits is _UNSET else _normalize_limits(limits)
|
|
226
|
+
)
|
|
227
|
+
effective_facets = (
|
|
228
|
+
self._preserve_facets
|
|
229
|
+
if preserve_facets is _UNSET
|
|
230
|
+
else _normalize_bool("preserve_facets", preserve_facets)
|
|
231
|
+
)
|
|
232
|
+
effective_overwrite = _normalize_bool("overwrite", overwrite)
|
|
233
|
+
to_value = _normalize_format("to", to)
|
|
234
|
+
from_value = _normalize_format("from_", from_)
|
|
235
|
+
output_path = _normalize_output(output)
|
|
236
|
+
|
|
237
|
+
bridge = _loader.bridge()
|
|
238
|
+
caps = _capabilities.capabilities(bridge)
|
|
239
|
+
|
|
240
|
+
resolved_from = caps.resolve(from_value) if from_value is not None else None
|
|
241
|
+
resolved_to = caps.resolve(to_value) if to_value is not None else None
|
|
242
|
+
if resolved_to is None and output_path is not None:
|
|
243
|
+
# An output suffix may select the writer when `to` is absent.
|
|
244
|
+
suffix = output_path.suffix.removeprefix(".")
|
|
245
|
+
if suffix:
|
|
246
|
+
candidate = caps.resolve(suffix)
|
|
247
|
+
entry = caps.by_name(candidate)
|
|
248
|
+
if entry is not None and entry.can_write:
|
|
249
|
+
resolved_to = candidate
|
|
250
|
+
|
|
251
|
+
max_input = (
|
|
252
|
+
effective_limits.max_input_bytes
|
|
253
|
+
if effective_limits is not None
|
|
254
|
+
else caps.limits.get("max_input_bytes", 512 * 1024 * 1024)
|
|
255
|
+
)
|
|
256
|
+
classified = _classify_source(source, name, max_input)
|
|
257
|
+
|
|
258
|
+
artifact_name = None
|
|
259
|
+
if output_path is None:
|
|
260
|
+
writer = resolved_to or caps.default_output_format
|
|
261
|
+
extension = caps.primary_extension(writer) or "out"
|
|
262
|
+
artifact_name = _artifact_name(
|
|
263
|
+
classified.display_name or "<memory>", extension
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
options = _marshal.options_json(
|
|
267
|
+
input_kind=classified.kind,
|
|
268
|
+
input_name=(
|
|
269
|
+
classified.display_name if classified.kind == "bytes" else None
|
|
270
|
+
),
|
|
271
|
+
output_kind="path" if output_path is not None else "memory",
|
|
272
|
+
artifact_name=artifact_name,
|
|
273
|
+
from_=resolved_from,
|
|
274
|
+
to=resolved_to,
|
|
275
|
+
strict=effective_strict.value,
|
|
276
|
+
overwrite=effective_overwrite,
|
|
277
|
+
preserve_facets=effective_facets,
|
|
278
|
+
limits=(
|
|
279
|
+
effective_limits.to_dict() if effective_limits is not None else None
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
raw = _marshal.perform(
|
|
283
|
+
bridge,
|
|
284
|
+
options=options,
|
|
285
|
+
input_bytes=classified.data,
|
|
286
|
+
input_path=(
|
|
287
|
+
_marshal.encode_native_path(classified.path)
|
|
288
|
+
if classified.kind == "path"
|
|
289
|
+
else None
|
|
290
|
+
),
|
|
291
|
+
output_path=(
|
|
292
|
+
_marshal.encode_native_path(os.fspath(output_path))
|
|
293
|
+
if output_path is not None
|
|
294
|
+
else None
|
|
295
|
+
),
|
|
296
|
+
copy_limits=(
|
|
297
|
+
effective_limits.to_dict()
|
|
298
|
+
if effective_limits is not None
|
|
299
|
+
else {
|
|
300
|
+
name: caps.limits.get(name, default)
|
|
301
|
+
for name, (default, _) in LIMIT_TABLE.items()
|
|
302
|
+
}
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
if raw.status == _ffi.STATUS_INVALID_REQUEST:
|
|
306
|
+
raise _diagnostics.invalid_request()
|
|
307
|
+
if raw.status == _ffi.STATUS_FAILED:
|
|
308
|
+
raise _diagnostics.conversion_error(raw.reports)
|
|
309
|
+
return _marshal.build_conversion(raw, caps, output_path=output_path)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _normalize_output(output: Any) -> Path | None:
|
|
313
|
+
if output is None:
|
|
314
|
+
return None
|
|
315
|
+
if isinstance(output, (str, PurePath, os.PathLike)):
|
|
316
|
+
# The caller's spelling is preserved rather than silently
|
|
317
|
+
# resolved.
|
|
318
|
+
try:
|
|
319
|
+
return Path(os.fsdecode(os.fspath(output)))
|
|
320
|
+
except (KeyboardInterrupt, SystemExit, GeneratorExit):
|
|
321
|
+
raise
|
|
322
|
+
except BaseException as error:
|
|
323
|
+
raise _diagnostics.path_protocol_failed("output", error) from error
|
|
324
|
+
raise _diagnostics.invalid_output_argument(output)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
_default_converter: Converter | None = None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _default() -> Converter:
|
|
331
|
+
global _default_converter
|
|
332
|
+
if _default_converter is None:
|
|
333
|
+
_default_converter = Converter()
|
|
334
|
+
return _default_converter
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def convert(
|
|
338
|
+
source: Any,
|
|
339
|
+
*,
|
|
340
|
+
to: Any = None,
|
|
341
|
+
from_: Any = None,
|
|
342
|
+
output: Any = None,
|
|
343
|
+
name: str | None = None,
|
|
344
|
+
strict: Any = False,
|
|
345
|
+
limits: Limits | None = None,
|
|
346
|
+
overwrite: bool = False,
|
|
347
|
+
preserve_facets: bool = False,
|
|
348
|
+
) -> Conversion:
|
|
349
|
+
"""Converts one document with safe defaults (ZDS 0014).
|
|
350
|
+
|
|
351
|
+
``source`` is a path, bytes-like object, or binary reader; a Python
|
|
352
|
+
str is always a filesystem path, never inline text. With no
|
|
353
|
+
``output`` the complete artifact ensemble is returned in memory;
|
|
354
|
+
an ``output`` path selects transactional publication. Failures raise
|
|
355
|
+
:class:`zenfmt.ConversionError` (or a subclass) carrying every native
|
|
356
|
+
report.
|
|
357
|
+
"""
|
|
358
|
+
return _default().convert(
|
|
359
|
+
source,
|
|
360
|
+
to=to,
|
|
361
|
+
from_=from_,
|
|
362
|
+
output=output,
|
|
363
|
+
name=name,
|
|
364
|
+
strict=strict,
|
|
365
|
+
limits=limits,
|
|
366
|
+
overwrite=overwrite,
|
|
367
|
+
preserve_facets=preserve_facets,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def formats() -> tuple[Format, ...]:
|
|
372
|
+
"""Every reader and writer compiled into this release, in native
|
|
373
|
+
registry order, from capability metadata only."""
|
|
374
|
+
return _capabilities.capabilities(_loader.bridge()).formats
|