python-hwpx 3.6.0__py3-none-any.whl → 3.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hwpx/__init__.py +6 -0
- hwpx/_document/persistence.py +196 -15
- hwpx/agent/commands.py +4 -20
- hwpx/agent/model.py +56 -0
- hwpx/body_patch.py +16 -0
- hwpx/document.py +102 -8
- hwpx/mutation_report.py +524 -0
- hwpx/opc/package.py +7 -0
- hwpx/patch.py +24 -0
- hwpx/table_patch.py +16 -0
- {python_hwpx-3.6.0.dist-info → python_hwpx-3.7.0.dist-info}/METADATA +83 -8
- {python_hwpx-3.6.0.dist-info → python_hwpx-3.7.0.dist-info}/RECORD +17 -16
- {python_hwpx-3.6.0.dist-info → python_hwpx-3.7.0.dist-info}/WHEEL +0 -0
- {python_hwpx-3.6.0.dist-info → python_hwpx-3.7.0.dist-info}/entry_points.txt +0 -0
- {python_hwpx-3.6.0.dist-info → python_hwpx-3.7.0.dist-info}/licenses/LICENSE +0 -0
- {python_hwpx-3.6.0.dist-info → python_hwpx-3.7.0.dist-info}/licenses/NOTICE +0 -0
- {python_hwpx-3.6.0.dist-info → python_hwpx-3.7.0.dist-info}/top_level.txt +0 -0
hwpx/__init__.py
CHANGED
|
@@ -85,6 +85,10 @@ from .ingest import (
|
|
|
85
85
|
HwpxMarkdownConverter,
|
|
86
86
|
UnsupportedDocumentFormat,
|
|
87
87
|
)
|
|
88
|
+
from .mutation_report import (
|
|
89
|
+
MutationReport,
|
|
90
|
+
PreservationDowngradeError,
|
|
91
|
+
)
|
|
88
92
|
from .patch import (
|
|
89
93
|
BytePreservingPatchResult,
|
|
90
94
|
ParagraphTextPatch,
|
|
@@ -146,6 +150,8 @@ __all__ = [
|
|
|
146
150
|
"ParagraphInfo",
|
|
147
151
|
"PackageValidationReport",
|
|
148
152
|
"BytePreservingPatchResult",
|
|
153
|
+
"MutationReport",
|
|
154
|
+
"PreservationDowngradeError",
|
|
149
155
|
"PlanValidationReport",
|
|
150
156
|
"ParagraphTextPatch",
|
|
151
157
|
"PatchApplied",
|
hwpx/_document/persistence.py
CHANGED
|
@@ -5,8 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import warnings
|
|
7
7
|
from os import PathLike
|
|
8
|
-
from typing import TYPE_CHECKING, Any, BinaryIO, Sequence
|
|
9
|
-
|
|
8
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Sequence
|
|
9
|
+
|
|
10
|
+
from ..mutation_report import (
|
|
11
|
+
Fallback,
|
|
12
|
+
Mode,
|
|
13
|
+
MutationReport,
|
|
14
|
+
PreservationDowngradeError,
|
|
15
|
+
PreservationMeasurement,
|
|
16
|
+
VerificationSummary,
|
|
17
|
+
VerificationValue,
|
|
18
|
+
measure_save,
|
|
19
|
+
)
|
|
10
20
|
from ..opc.package import _UNCHECKED_SAVE_TOKEN
|
|
11
21
|
from ..quality import QualityPolicy
|
|
12
22
|
from ..quality.report import OpenSafetyReport
|
|
@@ -120,27 +130,178 @@ def _gate_and_write(
|
|
|
120
130
|
return report
|
|
121
131
|
|
|
122
132
|
|
|
123
|
-
def
|
|
124
|
-
""
|
|
133
|
+
def _build_measured(
|
|
134
|
+
doc: "HwpxDocument", *, reset_dirty: bool
|
|
135
|
+
) -> tuple[bytes, PreservationMeasurement]:
|
|
136
|
+
"""Serialize + open-safety validate exactly like ``_to_bytes_raw`` and measure
|
|
137
|
+
the built archive's preservation against the pre-save part payloads.
|
|
138
|
+
|
|
139
|
+
The pre-save snapshot is captured *before* the ``serialize()`` updates are
|
|
140
|
+
applied so a per-save normalizer touching a part the editor never declared
|
|
141
|
+
dirty surfaces as an ``unexpected`` change (Safe Write Contract §2).
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
package = doc._package
|
|
145
|
+
# Measure against the OPEN-TIME baseline, not the current package state: a
|
|
146
|
+
# low-level ``set_part`` applied before this save already lives in
|
|
147
|
+
# ``_files``, and measuring against it would silently launder that change
|
|
148
|
+
# through a ``mode="patch"`` grade (Safe Write Contract §1, fail-closed).
|
|
149
|
+
source_members = dict(
|
|
150
|
+
getattr(package, "_opened_members", None) or package._files
|
|
151
|
+
)
|
|
152
|
+
source_infos = dict(
|
|
153
|
+
getattr(package, "_opened_zip_infos", None) or package._zip_infos
|
|
154
|
+
)
|
|
155
|
+
updates = doc._root.serialize()
|
|
156
|
+
predeclared = set(updates)
|
|
157
|
+
if updates:
|
|
158
|
+
for part_name, payload in updates.items():
|
|
159
|
+
package.set_part(part_name, payload)
|
|
160
|
+
result = package._save_to_bytes(verify_open_safety=True, mark_clean=False)
|
|
161
|
+
if not isinstance(result, bytes):
|
|
162
|
+
raise TypeError("package.save(None) must return bytes")
|
|
163
|
+
doc._run_open_safety_validation(result)
|
|
164
|
+
if reset_dirty:
|
|
165
|
+
_mark_save_clean(doc)
|
|
166
|
+
measurement = measure_save(source_members, source_infos, result, predeclared)
|
|
167
|
+
return result, measurement
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _resolve_grade(
|
|
171
|
+
mode: Mode,
|
|
172
|
+
fallback: Fallback,
|
|
173
|
+
measurement: PreservationMeasurement,
|
|
174
|
+
) -> tuple[Literal["patch", "rebuild"], bool]:
|
|
175
|
+
"""Return ``(actual_mode, fallback_used)``; raise before any write when a
|
|
176
|
+
requested ``patch`` grade is unmet and ``fallback="error"`` (§1)."""
|
|
177
|
+
|
|
178
|
+
if mode == "patch" and not measurement.patch_grade_ok:
|
|
179
|
+
if fallback == "error":
|
|
180
|
+
raise PreservationDowngradeError(
|
|
181
|
+
requested_mode=mode,
|
|
182
|
+
achieved_grade=measurement.achieved_grade,
|
|
183
|
+
offending_parts=measurement.offending_parts,
|
|
184
|
+
suggestion=(
|
|
185
|
+
"route these edits through a byte-preserving primitive "
|
|
186
|
+
"(hwpx.patch/table_patch/body_patch) or pass fallback='rebuild'."
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
return "rebuild", True
|
|
190
|
+
if mode == "auto":
|
|
191
|
+
return measurement.achieved_grade, False
|
|
192
|
+
if mode == "patch":
|
|
193
|
+
return "patch", False
|
|
194
|
+
return "rebuild", False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _verification_summary(report: "VisualCompleteReport") -> VerificationSummary:
|
|
198
|
+
"""Reflect what the pipeline actually ran; never fabricate a pass (§2)."""
|
|
199
|
+
|
|
200
|
+
package: VerificationValue = "passed" if report.ok else "failed"
|
|
201
|
+
open_safety: VerificationValue = "passed" if report.open_safety.ok else "failed"
|
|
202
|
+
# Open-safety validation (which includes the reopen probe) already ran during
|
|
203
|
+
# serialize and raised on failure, so reaching here means reopen passed.
|
|
204
|
+
reopen: VerificationValue = "passed"
|
|
205
|
+
status = report.visual_complete_status
|
|
206
|
+
visual: VerificationValue = (
|
|
207
|
+
"passed"
|
|
208
|
+
if status == "verified"
|
|
209
|
+
else "failed"
|
|
210
|
+
if status == "failed"
|
|
211
|
+
else "not_performed"
|
|
212
|
+
)
|
|
213
|
+
return VerificationSummary(
|
|
214
|
+
package=package, open_safety=open_safety, reopen=reopen, visual=visual
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _compose_mutation_report(
|
|
219
|
+
*,
|
|
220
|
+
path: str | None,
|
|
221
|
+
requested_mode: Mode,
|
|
222
|
+
actual_mode: Literal["patch", "rebuild"],
|
|
223
|
+
fallback_used: bool,
|
|
224
|
+
measurement: PreservationMeasurement,
|
|
225
|
+
report: "VisualCompleteReport",
|
|
226
|
+
) -> MutationReport:
|
|
227
|
+
return MutationReport(
|
|
228
|
+
requested_mode=requested_mode,
|
|
229
|
+
actual_mode=actual_mode,
|
|
230
|
+
fallback_used=fallback_used,
|
|
231
|
+
changed_parts=measurement.changed_parts,
|
|
232
|
+
preservation=measurement.preservation,
|
|
233
|
+
verification=_verification_summary(report),
|
|
234
|
+
path=path,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def save_to_path(
|
|
239
|
+
doc: "HwpxDocument",
|
|
240
|
+
path: str | PathLike[str],
|
|
241
|
+
*,
|
|
242
|
+
mode: Mode = "auto",
|
|
243
|
+
fallback: Fallback = "error",
|
|
244
|
+
return_report: bool = False,
|
|
245
|
+
) -> str | PathLike[str] | MutationReport:
|
|
246
|
+
"""Persist pending changes to *path*.
|
|
247
|
+
|
|
248
|
+
Returns *path* by default; ``return_report=True`` returns the
|
|
249
|
+
:class:`~hwpx.mutation_report.MutationReport` receipt instead. ``mode`` and
|
|
250
|
+
``fallback`` are the Safe Write Contract grade controls — ``mode="patch"``
|
|
251
|
+
with ``fallback="error"`` raises :class:`PreservationDowngradeError` and
|
|
252
|
+
writes nothing when the save cannot keep every untouched part byte-identical.
|
|
253
|
+
"""
|
|
125
254
|
|
|
126
255
|
_run_pre_save_validation(doc)
|
|
127
|
-
archive_bytes = doc
|
|
128
|
-
|
|
129
|
-
|
|
256
|
+
archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
|
|
257
|
+
actual_mode, fallback_used = _resolve_grade(mode, fallback, measurement)
|
|
258
|
+
report = _gate_and_write(
|
|
259
|
+
doc, archive_bytes, output_path=path, source_label="document.save_to_path"
|
|
130
260
|
)
|
|
131
261
|
_mark_save_clean(doc)
|
|
262
|
+
if return_report:
|
|
263
|
+
return _compose_mutation_report(
|
|
264
|
+
path=str(path),
|
|
265
|
+
requested_mode=mode,
|
|
266
|
+
actual_mode=actual_mode,
|
|
267
|
+
fallback_used=fallback_used,
|
|
268
|
+
measurement=measurement,
|
|
269
|
+
report=report,
|
|
270
|
+
)
|
|
132
271
|
return path
|
|
133
272
|
|
|
134
273
|
|
|
135
|
-
def save_to_stream(
|
|
136
|
-
|
|
274
|
+
def save_to_stream(
|
|
275
|
+
doc: "HwpxDocument",
|
|
276
|
+
stream: BinaryIO,
|
|
277
|
+
*,
|
|
278
|
+
mode: Mode = "auto",
|
|
279
|
+
fallback: Fallback = "error",
|
|
280
|
+
return_report: bool = False,
|
|
281
|
+
) -> BinaryIO | MutationReport:
|
|
282
|
+
"""Persist pending changes to *stream*.
|
|
283
|
+
|
|
284
|
+
Returns *stream* by default; ``return_report=True`` returns the
|
|
285
|
+
:class:`~hwpx.mutation_report.MutationReport` receipt instead. See
|
|
286
|
+
:func:`save_to_path` for the ``mode``/``fallback`` grade semantics.
|
|
287
|
+
"""
|
|
137
288
|
|
|
138
289
|
_run_pre_save_validation(doc)
|
|
139
|
-
archive_bytes = doc
|
|
140
|
-
|
|
141
|
-
|
|
290
|
+
archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
|
|
291
|
+
actual_mode, fallback_used = _resolve_grade(mode, fallback, measurement)
|
|
292
|
+
report = _gate_and_write(
|
|
293
|
+
doc, archive_bytes, output_stream=stream, source_label="document.save_to_stream"
|
|
142
294
|
)
|
|
143
295
|
_mark_save_clean(doc)
|
|
296
|
+
if return_report:
|
|
297
|
+
return _compose_mutation_report(
|
|
298
|
+
path=None,
|
|
299
|
+
requested_mode=mode,
|
|
300
|
+
actual_mode=actual_mode,
|
|
301
|
+
fallback_used=fallback_used,
|
|
302
|
+
measurement=measurement,
|
|
303
|
+
report=report,
|
|
304
|
+
)
|
|
144
305
|
return stream
|
|
145
306
|
|
|
146
307
|
|
|
@@ -193,11 +354,26 @@ def save_report(
|
|
|
193
354
|
return report
|
|
194
355
|
|
|
195
356
|
|
|
196
|
-
def to_bytes(
|
|
197
|
-
|
|
357
|
+
def to_bytes(
|
|
358
|
+
doc: "HwpxDocument",
|
|
359
|
+
*,
|
|
360
|
+
mode: Mode = "auto",
|
|
361
|
+
fallback: Fallback = "error",
|
|
362
|
+
) -> bytes:
|
|
363
|
+
"""Serialize pending changes and return the HWPX archive as bytes.
|
|
364
|
+
|
|
365
|
+
``mode``/``fallback`` enforce the Safe Write Contract grade the same way as
|
|
366
|
+
:func:`save_to_path`: ``mode="patch"`` with ``fallback="error"`` raises
|
|
367
|
+
:class:`PreservationDowngradeError` before returning when the archive is not
|
|
368
|
+
patch-grade. There is no ``return_report`` — the enforcement is via the typed
|
|
369
|
+
exception, so the byte return stays unchanged.
|
|
370
|
+
"""
|
|
198
371
|
|
|
199
372
|
_run_pre_save_validation(doc)
|
|
200
|
-
|
|
373
|
+
archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
|
|
374
|
+
_resolve_grade(mode, fallback, measurement)
|
|
375
|
+
_mark_save_clean(doc)
|
|
376
|
+
return archive_bytes
|
|
201
377
|
|
|
202
378
|
|
|
203
379
|
def _to_bytes_raw(
|
|
@@ -239,6 +415,11 @@ def _to_bytes_for_validation(doc: "HwpxDocument") -> bytes:
|
|
|
239
415
|
def _mark_save_clean(doc: "HwpxDocument") -> None:
|
|
240
416
|
doc._root.reset_dirty()
|
|
241
417
|
doc._package.version_info.mark_clean()
|
|
418
|
+
# The published archive is the new preservation baseline for any further
|
|
419
|
+
# save on this document instance.
|
|
420
|
+
package = doc._package
|
|
421
|
+
package._opened_members = dict(package._files)
|
|
422
|
+
package._opened_zip_infos = dict(package._zip_infos)
|
|
242
423
|
|
|
243
424
|
|
|
244
425
|
def save(
|
hwpx/agent/commands.py
CHANGED
|
@@ -14,9 +14,7 @@ import copy
|
|
|
14
14
|
import hashlib
|
|
15
15
|
import json
|
|
16
16
|
import re
|
|
17
|
-
import zipfile
|
|
18
17
|
from collections.abc import Callable, Mapping, MutableMapping
|
|
19
|
-
from io import BytesIO
|
|
20
18
|
from pathlib import Path
|
|
21
19
|
from typing import Any
|
|
22
20
|
from xml.etree import ElementTree as ET
|
|
@@ -24,6 +22,7 @@ from xml.etree import ElementTree as ET
|
|
|
24
22
|
from lxml import etree as LET # type: ignore[reportAttributeAccessIssue] # lxml has no complete bundled typing
|
|
25
23
|
|
|
26
24
|
from hwpx.document import HwpxDocument
|
|
25
|
+
from hwpx.mutation_report import member_diff_bytes
|
|
27
26
|
from hwpx.oxml import HwpxOxmlTable
|
|
28
27
|
from hwpx.quality import QualityPolicy, SavePipeline
|
|
29
28
|
from hwpx.tools.package_validator import validate_editor_open_safety
|
|
@@ -1172,24 +1171,9 @@ def _quality_policy(value: str | Mapping[str, Any] | None) -> QualityPolicy:
|
|
|
1172
1171
|
|
|
1173
1172
|
|
|
1174
1173
|
def _member_diff(before: bytes, after: bytes) -> dict[str, Any]:
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
new_names = set(new_zip.namelist())
|
|
1179
|
-
shared = sorted(old_names & new_names)
|
|
1180
|
-
changed = [name for name in shared if old_zip.read(name) != new_zip.read(name)]
|
|
1181
|
-
unchanged = len(shared) - len(changed)
|
|
1182
|
-
return {
|
|
1183
|
-
"ok": True,
|
|
1184
|
-
"changedMembers": changed,
|
|
1185
|
-
"addedMembers": sorted(new_names - old_names),
|
|
1186
|
-
"removedMembers": sorted(old_names - new_names),
|
|
1187
|
-
"unchangedMemberCount": unchanged,
|
|
1188
|
-
"beforeMemberCount": len(old_names),
|
|
1189
|
-
"afterMemberCount": len(new_names),
|
|
1190
|
-
}
|
|
1191
|
-
except (OSError, zipfile.BadZipFile) as exc:
|
|
1192
|
-
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
1174
|
+
# Shared with the Safe Write Contract's MutationReport spine: one uncompressed
|
|
1175
|
+
# member comparison, one home for the diff shape (mutation_report.py).
|
|
1176
|
+
return member_diff_bytes(before, after)
|
|
1193
1177
|
|
|
1194
1178
|
|
|
1195
1179
|
def _verify_header_story_candidates(
|
hwpx/agent/model.py
CHANGED
|
@@ -15,6 +15,15 @@ from collections.abc import Mapping, Sequence
|
|
|
15
15
|
from dataclasses import dataclass, field
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
+
from ..mutation_report import (
|
|
19
|
+
ChangedPart,
|
|
20
|
+
MutationReport,
|
|
21
|
+
PreservationCounts,
|
|
22
|
+
PreservationSummary,
|
|
23
|
+
verification_from_open_safety,
|
|
24
|
+
visual_value_from_status,
|
|
25
|
+
)
|
|
26
|
+
|
|
18
27
|
AGENT_NODE_SCHEMA = "hwpx.agent-node/v1"
|
|
19
28
|
AGENT_COMMAND_SCHEMA = "hwpx.agent-command/v1"
|
|
20
29
|
AGENT_BATCH_SCHEMA = "hwpx.agent-batch/v1"
|
|
@@ -667,6 +676,53 @@ class AgentBatchResult:
|
|
|
667
676
|
"error": None if self.error is None else self.error.to_dict(),
|
|
668
677
|
}
|
|
669
678
|
|
|
679
|
+
def as_mutation_report(self) -> MutationReport:
|
|
680
|
+
"""Project this transaction result onto ``hwpx.mutation-report/v1`` (specs/032
|
|
681
|
+
§3). Additive — the fields above are untouched.
|
|
682
|
+
|
|
683
|
+
The agent write is a family-A rebuild (``document.to_bytes()``), so
|
|
684
|
+
``actualMode`` is ``"rebuild"`` and changed parts carry no ranges (a rebuilt
|
|
685
|
+
part is re-serialized whole). Preservation and verification are read from the
|
|
686
|
+
already-measured ``verification_report`` (``bytePreservation`` = the shared
|
|
687
|
+
``_member_diff``, ``openSafety``, ``realHancom``); layers that report never
|
|
688
|
+
measured — local ZIP records, an absent oracle — stay zero-verified /
|
|
689
|
+
``not_performed`` rather than being promoted to a pass.
|
|
690
|
+
"""
|
|
691
|
+
|
|
692
|
+
report = self.verification_report
|
|
693
|
+
byte = report.get("bytePreservation") or {}
|
|
694
|
+
changed_names = [
|
|
695
|
+
*byte.get("changedMembers", ()),
|
|
696
|
+
*byte.get("addedMembers", ()),
|
|
697
|
+
*byte.get("removedMembers", ()),
|
|
698
|
+
]
|
|
699
|
+
changed_parts = tuple(
|
|
700
|
+
ChangedPart(path=str(name), reason="dirty-part", ranges=None)
|
|
701
|
+
for name in changed_names
|
|
702
|
+
)
|
|
703
|
+
unchanged = byte.get("unchangedMemberCount")
|
|
704
|
+
preservation = PreservationSummary(
|
|
705
|
+
untouched_part_payloads=PreservationCounts(
|
|
706
|
+
verified=unchanged if isinstance(unchanged, int) else 0, changed=0
|
|
707
|
+
),
|
|
708
|
+
untouched_local_zip_records=PreservationCounts(verified=0, changed=0),
|
|
709
|
+
whole_package_identical=bool(byte) and not changed_names,
|
|
710
|
+
)
|
|
711
|
+
real_hancom = report.get("realHancom") or {}
|
|
712
|
+
verification = verification_from_open_safety(
|
|
713
|
+
report.get("openSafety"),
|
|
714
|
+
visual=visual_value_from_status(real_hancom.get("status")),
|
|
715
|
+
)
|
|
716
|
+
return MutationReport(
|
|
717
|
+
requested_mode="rebuild",
|
|
718
|
+
actual_mode="rebuild",
|
|
719
|
+
fallback_used=False,
|
|
720
|
+
changed_parts=changed_parts,
|
|
721
|
+
preservation=preservation,
|
|
722
|
+
verification=verification,
|
|
723
|
+
path=self.output_filename,
|
|
724
|
+
)
|
|
725
|
+
|
|
670
726
|
|
|
671
727
|
def agent_contract_manifest() -> dict[str, Any]:
|
|
672
728
|
"""Return the deterministic, inspectable v1 contract manifest."""
|
hwpx/body_patch.py
CHANGED
|
@@ -35,6 +35,7 @@ from dataclasses import dataclass
|
|
|
35
35
|
from pathlib import Path
|
|
36
36
|
from typing import Any, Mapping, Sequence
|
|
37
37
|
|
|
38
|
+
from .mutation_report import MutationReport, project_byte_splice
|
|
38
39
|
from .patch import (
|
|
39
40
|
_finalize,
|
|
40
41
|
_patch_zip_entries,
|
|
@@ -113,6 +114,21 @@ class BodyOpsResult:
|
|
|
113
114
|
"openSafety": self.open_safety,
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
def as_mutation_report(self, *, source: bytes | str | Path | None = None) -> MutationReport:
|
|
118
|
+
"""Project this body-ops result onto ``hwpx.mutation-report/v1`` (specs/032
|
|
119
|
+
§3). Additive — the fields above are untouched. This path never renders,
|
|
120
|
+
so the visual verdict stays ``not_performed``. Pass the original *source*
|
|
121
|
+
for real ranges and a measured preservation summary.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
return project_byte_splice(
|
|
125
|
+
data=self.data,
|
|
126
|
+
changed_part_names=self.changed_parts,
|
|
127
|
+
byte_identical=self.byte_identical,
|
|
128
|
+
open_safety=self.open_safety,
|
|
129
|
+
source=source,
|
|
130
|
+
)
|
|
131
|
+
|
|
116
132
|
|
|
117
133
|
def _op_replace_text(xml: str, op: Mapping[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
118
134
|
find = str(op["find"])
|
hwpx/document.py
CHANGED
|
@@ -9,7 +9,17 @@ from datetime import datetime
|
|
|
9
9
|
import logging
|
|
10
10
|
|
|
11
11
|
from os import PathLike
|
|
12
|
-
from typing import
|
|
12
|
+
from typing import (
|
|
13
|
+
TYPE_CHECKING,
|
|
14
|
+
Any,
|
|
15
|
+
BinaryIO,
|
|
16
|
+
Iterator,
|
|
17
|
+
Literal,
|
|
18
|
+
Mapping,
|
|
19
|
+
Sequence,
|
|
20
|
+
cast,
|
|
21
|
+
overload,
|
|
22
|
+
)
|
|
13
23
|
|
|
14
24
|
|
|
15
25
|
from .oxml import (
|
|
@@ -40,6 +50,7 @@ from .opc.package import (
|
|
|
40
50
|
HwpxPackage,
|
|
41
51
|
)
|
|
42
52
|
from .oxml.namespaces import register_owpml_namespaces
|
|
53
|
+
from .mutation_report import Fallback, Mode, MutationReport
|
|
43
54
|
from .quality import QualityPolicy, SavePipeline, VisualCompleteReport
|
|
44
55
|
from .templates import blank_document_bytes
|
|
45
56
|
|
|
@@ -1682,20 +1693,92 @@ class HwpxDocument:
|
|
|
1682
1693
|
)
|
|
1683
1694
|
|
|
1684
1695
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1696
|
+
@overload
|
|
1697
|
+
def save_to_path(
|
|
1698
|
+
self,
|
|
1699
|
+
path: str | PathLike[str],
|
|
1700
|
+
*,
|
|
1701
|
+
mode: Mode = ...,
|
|
1702
|
+
fallback: Fallback = ...,
|
|
1703
|
+
return_report: Literal[False] = ...,
|
|
1704
|
+
) -> str | PathLike[str]: ...
|
|
1705
|
+
|
|
1706
|
+
@overload
|
|
1707
|
+
def save_to_path(
|
|
1708
|
+
self,
|
|
1709
|
+
path: str | PathLike[str],
|
|
1710
|
+
*,
|
|
1711
|
+
mode: Mode = ...,
|
|
1712
|
+
fallback: Fallback = ...,
|
|
1713
|
+
return_report: Literal[True],
|
|
1714
|
+
) -> MutationReport: ...
|
|
1715
|
+
|
|
1716
|
+
def save_to_path(
|
|
1717
|
+
self,
|
|
1718
|
+
path: str | PathLike[str],
|
|
1719
|
+
*,
|
|
1720
|
+
mode: Mode = "auto",
|
|
1721
|
+
fallback: Fallback = "error",
|
|
1722
|
+
return_report: bool = False,
|
|
1723
|
+
) -> str | PathLike[str] | MutationReport:
|
|
1724
|
+
"""Persist pending changes to *path* and return the same path.
|
|
1725
|
+
|
|
1726
|
+
``return_report=True`` returns the Safe Write Contract
|
|
1727
|
+
:class:`~hwpx.mutation_report.MutationReport` instead. ``mode="patch"``
|
|
1728
|
+
with ``fallback="error"`` raises
|
|
1729
|
+
:class:`~hwpx.mutation_report.PreservationDowngradeError` and writes
|
|
1730
|
+
nothing when an untouched part would not stay byte-identical.
|
|
1731
|
+
"""
|
|
1687
1732
|
|
|
1688
1733
|
return _persistence.save_to_path(
|
|
1689
1734
|
self,
|
|
1690
1735
|
path=path,
|
|
1736
|
+
mode=mode,
|
|
1737
|
+
fallback=fallback,
|
|
1738
|
+
return_report=return_report,
|
|
1691
1739
|
)
|
|
1692
1740
|
|
|
1693
|
-
|
|
1694
|
-
|
|
1741
|
+
@overload
|
|
1742
|
+
def save_to_stream(
|
|
1743
|
+
self,
|
|
1744
|
+
stream: BinaryIO,
|
|
1745
|
+
*,
|
|
1746
|
+
mode: Mode = ...,
|
|
1747
|
+
fallback: Fallback = ...,
|
|
1748
|
+
return_report: Literal[False] = ...,
|
|
1749
|
+
) -> BinaryIO: ...
|
|
1750
|
+
|
|
1751
|
+
@overload
|
|
1752
|
+
def save_to_stream(
|
|
1753
|
+
self,
|
|
1754
|
+
stream: BinaryIO,
|
|
1755
|
+
*,
|
|
1756
|
+
mode: Mode = ...,
|
|
1757
|
+
fallback: Fallback = ...,
|
|
1758
|
+
return_report: Literal[True],
|
|
1759
|
+
) -> MutationReport: ...
|
|
1760
|
+
|
|
1761
|
+
def save_to_stream(
|
|
1762
|
+
self,
|
|
1763
|
+
stream: BinaryIO,
|
|
1764
|
+
*,
|
|
1765
|
+
mode: Mode = "auto",
|
|
1766
|
+
fallback: Fallback = "error",
|
|
1767
|
+
return_report: bool = False,
|
|
1768
|
+
) -> BinaryIO | MutationReport:
|
|
1769
|
+
"""Persist pending changes to *stream* and return the same stream.
|
|
1770
|
+
|
|
1771
|
+
``return_report=True`` returns the Safe Write Contract
|
|
1772
|
+
:class:`~hwpx.mutation_report.MutationReport` instead. See
|
|
1773
|
+
:meth:`save_to_path` for the ``mode``/``fallback`` grade semantics.
|
|
1774
|
+
"""
|
|
1695
1775
|
|
|
1696
1776
|
return _persistence.save_to_stream(
|
|
1697
1777
|
self,
|
|
1698
1778
|
stream=stream,
|
|
1779
|
+
mode=mode,
|
|
1780
|
+
fallback=fallback,
|
|
1781
|
+
return_report=return_report,
|
|
1699
1782
|
)
|
|
1700
1783
|
|
|
1701
1784
|
def save_report(
|
|
@@ -1730,10 +1813,21 @@ class HwpxDocument:
|
|
|
1730
1813
|
ledger=ledger,
|
|
1731
1814
|
)
|
|
1732
1815
|
|
|
1733
|
-
def to_bytes(
|
|
1734
|
-
|
|
1816
|
+
def to_bytes(
|
|
1817
|
+
self,
|
|
1818
|
+
*,
|
|
1819
|
+
mode: Mode = "auto",
|
|
1820
|
+
fallback: Fallback = "error",
|
|
1821
|
+
) -> bytes:
|
|
1822
|
+
"""Serialize pending changes and return the HWPX archive as bytes.
|
|
1823
|
+
|
|
1824
|
+
``mode="patch"`` with ``fallback="error"`` raises
|
|
1825
|
+
:class:`~hwpx.mutation_report.PreservationDowngradeError` before
|
|
1826
|
+
returning when the archive is not patch-grade; the byte return itself is
|
|
1827
|
+
unchanged.
|
|
1828
|
+
"""
|
|
1735
1829
|
|
|
1736
|
-
return _persistence.to_bytes(self)
|
|
1830
|
+
return _persistence.to_bytes(self, mode=mode, fallback=fallback)
|
|
1737
1831
|
|
|
1738
1832
|
def _to_bytes_raw(
|
|
1739
1833
|
self,
|