python-hwpx-automation 7.3.2__py3-none-any.whl → 7.3.3__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_automation/core/content.py +18 -2
- hwpx_automation/core/document.py +2 -3
- hwpx_automation/core/transactions.py +15 -3
- hwpx_automation/errors.py +54 -1
- hwpx_automation/handlers/agent_document.py +5 -0
- hwpx_automation/handlers/read_export.py +14 -10
- hwpx_automation/identity.json +2 -2
- hwpx_automation/mixed_form.py +3 -1
- hwpx_automation/office/rendering/oracle.py +15 -0
- hwpx_automation/ops_services/context.py +49 -3
- hwpx_automation/ops_services/form_fields.py +3 -0
- hwpx_automation/ops_services/package_validation.py +1 -1
- hwpx_automation/ops_services/planning.py +1 -1
- hwpx_automation/ops_services/read_query.py +21 -16
- hwpx_automation/ops_services/save_policy.py +3 -0
- hwpx_automation/ops_services/transactions.py +1 -0
- hwpx_automation/runtime.py +26 -3
- hwpx_automation/storage.py +116 -10
- hwpx_automation/upstream.py +56 -2
- {python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/METADATA +3 -3
- {python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/RECORD +26 -26
- {python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/WHEEL +0 -0
- {python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/entry_points.txt +0 -0
- {python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/licenses/LICENSE +0 -0
- {python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/licenses/NOTICE +0 -0
- {python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/top_level.txt +0 -0
hwpx_automation/core/content.py
CHANGED
|
@@ -14,8 +14,15 @@ from uuid import uuid4
|
|
|
14
14
|
from xml.etree import ElementTree as ET
|
|
15
15
|
|
|
16
16
|
from ..compat import patch_python_hwpx
|
|
17
|
-
from ..storage import build_hwpx_open_safety_report
|
|
18
|
-
from ..upstream import
|
|
17
|
+
from ..storage import build_hwpx_open_safety_report, open_local_document
|
|
18
|
+
from ..upstream import (
|
|
19
|
+
HP_NS as _HP_NS,
|
|
20
|
+
HwpxDocument,
|
|
21
|
+
is_hwp5_source,
|
|
22
|
+
is_hwp5_target,
|
|
23
|
+
repair_pathological_text_spacing,
|
|
24
|
+
)
|
|
25
|
+
from .document import save_doc
|
|
19
26
|
from .formatting import resolve_style_id
|
|
20
27
|
from .locations import resolve_paragraph_reference
|
|
21
28
|
|
|
@@ -500,6 +507,15 @@ def copy_document_file(source: str, destination: str = None) -> str:
|
|
|
500
507
|
destination = f"{stem}_copy.{ext}"
|
|
501
508
|
source_path = Path(source)
|
|
502
509
|
destination_path = Path(destination)
|
|
510
|
+
if is_hwp5_source(source_path) != is_hwp5_target(destination_path):
|
|
511
|
+
# The destination extension picks the format, so a copy between .hwp
|
|
512
|
+
# and .hwpx is written through the document model, not byte-copied.
|
|
513
|
+
document = open_local_document(source_path, role="copy source")
|
|
514
|
+
try:
|
|
515
|
+
save_doc(document, str(destination_path))
|
|
516
|
+
finally:
|
|
517
|
+
document.close()
|
|
518
|
+
return str(destination_path)
|
|
503
519
|
if source_path.suffix.lower() != ".hwpx" and destination_path.suffix.lower() != ".hwpx":
|
|
504
520
|
shutil.copy2(source_path, destination_path)
|
|
505
521
|
return str(destination_path)
|
hwpx_automation/core/document.py
CHANGED
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from ..configuration import env_value
|
|
8
|
-
from ..storage import LocalDocumentStorage,
|
|
8
|
+
from ..storage import LocalDocumentStorage, open_local_document
|
|
9
9
|
from ..upstream import HwpxDocument, blank_document_template_bytes, open_document
|
|
10
10
|
|
|
11
11
|
|
|
@@ -18,8 +18,7 @@ def _local_storage() -> LocalDocumentStorage:
|
|
|
18
18
|
def open_doc(path: str) -> HwpxDocument:
|
|
19
19
|
if not os.path.exists(path):
|
|
20
20
|
raise FileNotFoundError(f"파일을 찾을 수 없습니다: {path}")
|
|
21
|
-
|
|
22
|
-
return open_document(path)
|
|
21
|
+
return open_local_document(Path(path), role="local HWPX open")
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def save_doc(doc: HwpxDocument, path: str, *, quality: Any = None) -> dict[str, Any]:
|
|
@@ -12,8 +12,12 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any, Iterable
|
|
13
13
|
|
|
14
14
|
from .. import quality as quality_contract
|
|
15
|
-
from ..storage import
|
|
16
|
-
|
|
15
|
+
from ..storage import (
|
|
16
|
+
build_hwpx_verification_report,
|
|
17
|
+
require_hwpx_editor_open_safe,
|
|
18
|
+
require_native_hwp5,
|
|
19
|
+
)
|
|
20
|
+
from ..upstream import HwpxDocument, hwp5_document_bytes, is_hwp5_target, open_document
|
|
17
21
|
|
|
18
22
|
_MAX_SUMMARY_ITEMS = 20
|
|
19
23
|
_MAX_SNIPPET = 80
|
|
@@ -341,9 +345,13 @@ def save_dry_run(
|
|
|
341
345
|
document: HwpxDocument, target: str | Path, *, quality: Any = None
|
|
342
346
|
) -> dict[str, Any]:
|
|
343
347
|
target_path = Path(target)
|
|
348
|
+
hwp_target = is_hwp5_target(target_path)
|
|
349
|
+
if hwp_target:
|
|
350
|
+
require_native_hwp5()
|
|
351
|
+
# The gate checks HWPX parts, so an .hwp target is staged as HWPX.
|
|
344
352
|
fd, tmp_name = tempfile.mkstemp(
|
|
345
353
|
prefix=f".{target_path.stem}.dry-run.",
|
|
346
|
-
suffix=target_path.suffix or ".hwpx",
|
|
354
|
+
suffix=".hwpx" if hwp_target else (target_path.suffix or ".hwpx"),
|
|
347
355
|
dir=str(target_path.parent),
|
|
348
356
|
)
|
|
349
357
|
tmp_path = Path(tmp_name)
|
|
@@ -366,6 +374,10 @@ def save_dry_run(
|
|
|
366
374
|
"dry-run HWPX failed open-safety verification: "
|
|
367
375
|
+ verification["openSafety"]["summary"]
|
|
368
376
|
)
|
|
377
|
+
if hwp_target:
|
|
378
|
+
# Raises Hwp5Error (hwp5-write-unsupported) as the real save would.
|
|
379
|
+
hwp5_document_bytes(document)
|
|
380
|
+
verification["format"] = "hwp"
|
|
369
381
|
diff = semantic_diff(target_path, tmp_path) if target_path.exists() else None
|
|
370
382
|
verification["filePath"] = str(target_path)
|
|
371
383
|
verification["dryRunTempDeleted"] = True
|
hwpx_automation/errors.py
CHANGED
|
@@ -73,6 +73,7 @@ DEFAULT_ERROR_SUGGESTIONS: dict[str, str] = {
|
|
|
73
73
|
"EMPTY_MATCH": "찾을 텍스트나 앵커 조건을 실제 문서 내용에 맞게 수정하세요.",
|
|
74
74
|
"HWP_CONVERSION_FAILED": "원본 HWP가 손상되지 않았는지 확인하고 변환 로그의 원인을 기준으로 다시 시도하세요.",
|
|
75
75
|
"HWP_TEXT_EXTRACT_FAILED": "HWP 파일을 다른 뷰어에서 열어 유효성을 확인한 뒤 텍스트 추출을 다시 실행하세요.",
|
|
76
|
+
"HWPX_PACKAGE_REQUIRED": "copy_document로 .hwpx 사본을 만들어 이 도구를 쓰고, .hwp가 필요하면 결과를 copy_document로 .hwp 경로에 복사하세요. search_and_replace·set_table_cell_text 같은 문서 편집 도구는 .hwp를 그대로 편집합니다.",
|
|
76
77
|
"IDEMPOTENT_REPLAY": "이미 적용된 planId입니다. 최신 preview/plan을 새로 만든 뒤 apply를 다시 호출하세요.",
|
|
77
78
|
"MEMO_NOT_FOUND": "list_memos 또는 문서 리소스에서 memoId를 다시 확인한 뒤 호출하세요.",
|
|
78
79
|
"MISSING_NODE": "대상 섹션, 문단, 표, 셀이 아직 존재하는지 조회한 뒤 새 앵커로 다시 실행하세요.",
|
|
@@ -81,7 +82,10 @@ DEFAULT_ERROR_SUGGESTIONS: dict[str, str] = {
|
|
|
81
82
|
"PIPELINE_ERROR": "details.pipelineCode와 hint를 확인한 뒤 preview부터 다시 생성하세요.",
|
|
82
83
|
"PLAN_RECORD_MISSING": "preview_edit 또는 plan_edit으로 planId를 새로 발급받은 뒤 apply를 다시 호출하세요.",
|
|
83
84
|
"PREVIEW_REQUIRED": "preview_edit으로 변경 내용을 확인한 뒤 반환된 planId로 apply_edit을 호출하세요.",
|
|
84
|
-
"
|
|
85
|
+
"HWP_DOCUMENT_UNREADABLE": "한/글에서 문서를 열어 다시 저장한 뒤 사용하세요. details.hwp5Code가 원인입니다.",
|
|
86
|
+
"HWP_ENCRYPTED_DOCUMENT": "한/글에서 문서 암호·배포용 보호를 풀고 다시 저장한 뒤 사용하세요. DRM 문서는 열 수 없습니다.",
|
|
87
|
+
"HWP_WRITE_UNSUPPORTED": "HWP 5.0으로 쓸 수 없는 내용이 있습니다. 출력 경로를 .hwpx로 지정해 저장하세요.",
|
|
88
|
+
"READ_ONLY_HWP_DOCUMENT": "python-hwpx 6.6.0 이상을 설치하면 .hwp 문서를 그대로 열고 편집·저장할 수 있습니다.",
|
|
85
89
|
"RENDER_PREVIEW_INVALID_MODE": "mode는 'pages' 또는 'long'으로 지정한 뒤 다시 실행하세요.",
|
|
86
90
|
"RENDER_PREVIEW_INVALID_SCREENSHOT_MODE": "screenshot은 'auto', 'require', 'off' 중 하나로 지정하세요.",
|
|
87
91
|
"RENDER_PREVIEW_UNAVAILABLE": "matching python-hwpx 버전을 설치하고 MCP 서버를 새로 시작한 뒤 다시 실행하세요.",
|
|
@@ -175,3 +179,52 @@ def build_error_payload(
|
|
|
175
179
|
suggestion=suggestion
|
|
176
180
|
or suggestion_for_error(code, details=details, hint=hint),
|
|
177
181
|
).model_dump(exclude_none=True, by_alias=True)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class HwpDocumentError(RuntimeError):
|
|
185
|
+
"""An ``.hwp`` request the installed engine cannot carry out."""
|
|
186
|
+
|
|
187
|
+
def __init__(
|
|
188
|
+
self, message: str, *, code: str, details: Optional[Dict[str, Any]] = None
|
|
189
|
+
) -> None:
|
|
190
|
+
super().__init__(message)
|
|
191
|
+
self.code = code
|
|
192
|
+
self.message = message
|
|
193
|
+
self.details = details
|
|
194
|
+
|
|
195
|
+
def to_payload(self) -> Dict[str, Any]:
|
|
196
|
+
return build_error_payload(
|
|
197
|
+
code=self.code, message=self.message, details=self.details
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_HWP5_ENCRYPTED_CODES = frozenset({"hwp5-password", "hwp5-distribution", "hwp5-drm"})
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def hwp5_error_payload(exc: BaseException) -> Optional[Dict[str, Any]]:
|
|
205
|
+
"""The MCP error for python-hwpx's ``Hwp5Error``; None for any other exception.
|
|
206
|
+
|
|
207
|
+
Matched by its ``hwp5-*`` code so this module needs no core import.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
hwp5_code = getattr(exc, "code", None)
|
|
211
|
+
if not isinstance(hwp5_code, str) or not hwp5_code.startswith("hwp5-"):
|
|
212
|
+
return None
|
|
213
|
+
details = {"hwp5Code": hwp5_code}
|
|
214
|
+
if hwp5_code == "hwp5-write-unsupported":
|
|
215
|
+
return build_error_payload(
|
|
216
|
+
code="HWP_WRITE_UNSUPPORTED",
|
|
217
|
+
message="문서에 HWP 5.0(.hwp)으로 쓸 수 없는 내용이 있어 아무것도 저장하지 않았습니다.",
|
|
218
|
+
details=details,
|
|
219
|
+
)
|
|
220
|
+
if hwp5_code in _HWP5_ENCRYPTED_CODES:
|
|
221
|
+
return build_error_payload(
|
|
222
|
+
code="HWP_ENCRYPTED_DOCUMENT",
|
|
223
|
+
message="암호·배포용·DRM으로 보호된 HWP 문서는 열 수 없습니다.",
|
|
224
|
+
details=details,
|
|
225
|
+
)
|
|
226
|
+
return build_error_payload(
|
|
227
|
+
code="HWP_DOCUMENT_UNREADABLE",
|
|
228
|
+
message="HWP 5.0(.hwp) 문서를 읽을 수 없습니다.",
|
|
229
|
+
details=details,
|
|
230
|
+
)
|
|
@@ -9,6 +9,8 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
from ..document_state import document_state_payload
|
|
12
|
+
from ..errors import HwpDocumentError
|
|
13
|
+
from ..storage import require_hwpx_package
|
|
12
14
|
from ..agent_document import (
|
|
13
15
|
AgentChildLimit,
|
|
14
16
|
AgentCommandList,
|
|
@@ -100,6 +102,7 @@ def apply_document_commands(
|
|
|
100
102
|
try:
|
|
101
103
|
resolved_input = resolve_path(filename)
|
|
102
104
|
resolved_output = resolve_path(output)
|
|
105
|
+
require_hwpx_package(resolved_input, output=resolved_output)
|
|
103
106
|
quality_contract.assert_write_capability()
|
|
104
107
|
store = ScopedIdempotencyStore(
|
|
105
108
|
RUNTIME_SERVICES.idempotency_cache,
|
|
@@ -132,6 +135,8 @@ def apply_document_commands(
|
|
|
132
135
|
if Path(state_path).exists():
|
|
133
136
|
payload.update(document_state_payload(state_path))
|
|
134
137
|
return payload
|
|
138
|
+
except HwpDocumentError:
|
|
139
|
+
raise
|
|
135
140
|
except Exception as exc:
|
|
136
141
|
return agent_batch_error_payload(
|
|
137
142
|
exc,
|
|
@@ -35,6 +35,7 @@ from ..office.compliance import DEFAULT_POLICY, mask_pii
|
|
|
35
35
|
from ..runtime_services import RUNTIME_SERVICES
|
|
36
36
|
from ..upstream import (
|
|
37
37
|
HP_NS,
|
|
38
|
+
hwp5_conversion_report,
|
|
38
39
|
open_document,
|
|
39
40
|
)
|
|
40
41
|
from ..utils.helpers import default_max_chars, resolve_path, truncate_response
|
|
@@ -777,16 +778,19 @@ def get_document_info(filename: str) -> dict:
|
|
|
777
778
|
path = resolve_path(filename)
|
|
778
779
|
doc = open_doc(path)
|
|
779
780
|
file_size = Path(path).stat().st_size
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
781
|
+
info: dict[str, Any] = {
|
|
782
|
+
"filename": filename,
|
|
783
|
+
"sections": len(doc.sections),
|
|
784
|
+
"paragraphs": _paragraph_count(doc),
|
|
785
|
+
"tables": _table_count(doc),
|
|
786
|
+
"file_size": str(file_size),
|
|
787
|
+
}
|
|
788
|
+
conversion = hwp5_conversion_report(doc)
|
|
789
|
+
if conversion is not None:
|
|
790
|
+
# An .hwp is edited through the document model; this is what it left out.
|
|
791
|
+
info["format"] = "hwp"
|
|
792
|
+
info["hwpConversion"] = conversion
|
|
793
|
+
return _with_document_state(info, path)
|
|
790
794
|
|
|
791
795
|
|
|
792
796
|
def get_document_text(
|
hwpx_automation/identity.json
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
"candidate": {
|
|
8
8
|
"pythonHwpx": "6.6.0",
|
|
9
9
|
"canonicalDistribution": "python-hwpx-automation",
|
|
10
|
-
"canonicalAutomation": "7.3.
|
|
10
|
+
"canonicalAutomation": "7.3.3",
|
|
11
11
|
"compatibilityDistribution": "hwpx-mcp-server",
|
|
12
|
-
"compatibility": "7.3.
|
|
12
|
+
"compatibility": "7.3.3",
|
|
13
13
|
"plugin": "2.4.0",
|
|
14
14
|
"contractHash": "5e5c23651f92785a"
|
|
15
15
|
},
|
hwpx_automation/mixed_form.py
CHANGED
|
@@ -41,7 +41,7 @@ from pydantic import (
|
|
|
41
41
|
from .execution_lock import PUBLIC_MUTATION_LOCK
|
|
42
42
|
from .office.agent._batch_publication import GuardedSavePipeline
|
|
43
43
|
from .office.rendering import resolve_hancom_backend
|
|
44
|
-
from .storage import build_hwpx_open_safety_report
|
|
44
|
+
from .storage import build_hwpx_open_safety_report, require_hwpx_package
|
|
45
45
|
from .utils.helpers import resolve_path
|
|
46
46
|
from .workspace import (
|
|
47
47
|
WorkspaceMissingParentGuard,
|
|
@@ -1374,6 +1374,7 @@ def analyze_mixed_form_plan(
|
|
|
1374
1374
|
|
|
1375
1375
|
request = _authorize_public_plan(_payload(plan, apply=False))
|
|
1376
1376
|
source = Path(request["source"])
|
|
1377
|
+
require_hwpx_package(source, output=request["output"])
|
|
1377
1378
|
source_snapshot = _capture_path_snapshot(source)
|
|
1378
1379
|
if source_snapshot.data is None:
|
|
1379
1380
|
raise FileNotFoundError(source)
|
|
@@ -1494,6 +1495,7 @@ def _apply_canonical_mixed_form_plan_locked(
|
|
|
1494
1495
|
|
|
1495
1496
|
compiled = validate_mixed_form_plan(compiled).to_dict()
|
|
1496
1497
|
source, output = _authorize_compiled_plan(compiled)
|
|
1498
|
+
require_hwpx_package(source, output=output)
|
|
1497
1499
|
source_snapshot = _capture_path_snapshot(source)
|
|
1498
1500
|
if source_snapshot.data is None: # pragma: no cover - compiled plan invariant
|
|
1499
1501
|
raise FileNotFoundError(source)
|
|
@@ -149,6 +149,16 @@ def _complete_package(path: str) -> bool:
|
|
|
149
149
|
return False
|
|
150
150
|
|
|
151
151
|
|
|
152
|
+
def _unchanged(path: str, before: os.stat_result) -> bool:
|
|
153
|
+
"""True when ``path`` still has the size and modification time in ``before``."""
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
now = os.stat(path)
|
|
157
|
+
except OSError:
|
|
158
|
+
return False
|
|
159
|
+
return (now.st_size, now.st_mtime_ns) == (before.st_size, before.st_mtime_ns)
|
|
160
|
+
|
|
161
|
+
|
|
152
162
|
def _promote_file(staged: str, target: str) -> None:
|
|
153
163
|
"""Move a finished file to ``target`` without leaving a partial ``target``.
|
|
154
164
|
|
|
@@ -442,6 +452,8 @@ class WindowsComOracle(RenderBackend):
|
|
|
442
452
|
by a complete package: a failed refresh leaves it as it was. Returns
|
|
443
453
|
True when the file was re-saved. Only an ``.hwpx`` file is refreshed
|
|
444
454
|
(Hancom saves it as HWPX), and a link is followed to the file it names.
|
|
455
|
+
A document that changed while Hancom ran (size or modification time)
|
|
456
|
+
is not overwritten: the refresh returns False and the change stays.
|
|
445
457
|
"""
|
|
446
458
|
|
|
447
459
|
src = os.path.realpath(hwpx_path)
|
|
@@ -459,6 +471,7 @@ class WindowsComOracle(RenderBackend):
|
|
|
459
471
|
try:
|
|
460
472
|
os.makedirs(os.path.dirname(job_src))
|
|
461
473
|
os.makedirs(os.path.dirname(job_out))
|
|
474
|
+
before = os.stat(src)
|
|
462
475
|
shutil.copyfile(src, job_src)
|
|
463
476
|
except OSError:
|
|
464
477
|
return False
|
|
@@ -467,6 +480,8 @@ class WindowsComOracle(RenderBackend):
|
|
|
467
480
|
entry = entries[0] if entries else None
|
|
468
481
|
if not (isinstance(entry, dict) and entry.get("saved") and _complete_package(job_out)):
|
|
469
482
|
return False
|
|
483
|
+
if not _unchanged(src, before):
|
|
484
|
+
return False
|
|
470
485
|
try:
|
|
471
486
|
_promote_file(job_out, src)
|
|
472
487
|
except OSError:
|
|
@@ -4,13 +4,16 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import logging
|
|
7
|
+
from io import BytesIO
|
|
7
8
|
from pathlib import Path
|
|
9
|
+
from zipfile import ZipFile
|
|
8
10
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
9
11
|
|
|
12
|
+
from ..errors import HwpDocumentError, hwp5_error_payload
|
|
10
13
|
from ..hwp_support import HwpBinaryError, extract_hwp_text
|
|
11
14
|
from ..core.locator import RegisteredHandle
|
|
12
15
|
from ..core.context import default_session_lifecycle_policy
|
|
13
|
-
from ..storage import DocumentStorage, LocalDocumentStorage
|
|
16
|
+
from ..storage import DocumentStorage, LocalDocumentStorage, require_hwpx_package
|
|
14
17
|
from ..workspace import (
|
|
15
18
|
WorkspacePathError,
|
|
16
19
|
)
|
|
@@ -18,6 +21,8 @@ from ..upstream import (
|
|
|
18
21
|
HwpxDocument,
|
|
19
22
|
HwpxOxmlParagraph,
|
|
20
23
|
HwpxOxmlTable,
|
|
24
|
+
hwpx_view_bytes,
|
|
25
|
+
native_hwp5_supported,
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
logger = logging.getLogger("hwpx_automation.hwpx_ops")
|
|
@@ -158,12 +163,50 @@ class DocumentContext:
|
|
|
158
163
|
def _relative_path(self, path: Path) -> str:
|
|
159
164
|
return self.storage.relative_path(path)
|
|
160
165
|
|
|
166
|
+
def _is_legacy_hwp(self, resolved: Path) -> bool:
|
|
167
|
+
"""An ``.hwp`` the installed core cannot open; only its preview text is readable."""
|
|
168
|
+
|
|
169
|
+
return resolved.suffix.lower() == ".hwp" and not native_hwp5_supported()
|
|
170
|
+
|
|
171
|
+
def _text_source(self, resolved: Path) -> Path | ZipFile:
|
|
172
|
+
"""What the text extractor reads: the HWPX file, or an ``.hwp``'s HWPX model."""
|
|
173
|
+
|
|
174
|
+
if resolved.suffix.lower() != ".hwp":
|
|
175
|
+
return resolved
|
|
176
|
+
try:
|
|
177
|
+
return ZipFile(BytesIO(hwpx_view_bytes(resolved)))
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
hwp_error = self._hwp_error(exc)
|
|
180
|
+
if hwp_error is None:
|
|
181
|
+
raise
|
|
182
|
+
raise hwp_error from exc
|
|
183
|
+
|
|
184
|
+
def _hwp_error(self, exc: BaseException) -> Optional[RuntimeError]:
|
|
185
|
+
"""The operation error for an ``.hwp`` refusal, or None for any other failure."""
|
|
186
|
+
|
|
187
|
+
if isinstance(exc, HwpDocumentError):
|
|
188
|
+
return self._new_error(exc.code, exc.message, details=exc.details)
|
|
189
|
+
payload = hwp5_error_payload(exc)
|
|
190
|
+
if payload is None:
|
|
191
|
+
return None
|
|
192
|
+
return self._new_error(
|
|
193
|
+
payload["code"], payload["message"], details=payload.get("details")
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _require_hwpx_package(self, source: Path, output: Optional[str] = None) -> None:
|
|
197
|
+
"""Refuse ``.hwp`` for editors that patch HWPX package bytes."""
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
require_hwpx_package(source, output=output)
|
|
201
|
+
except HwpDocumentError as exc:
|
|
202
|
+
raise self._new_error(exc.code, exc.message, details=exc.details) from exc
|
|
203
|
+
|
|
161
204
|
def _open_document(self, path: str) -> Tuple[HwpxDocument, Path]:
|
|
162
205
|
resolved = self._resolve_path(path)
|
|
163
|
-
if
|
|
206
|
+
if self._is_legacy_hwp(resolved):
|
|
164
207
|
raise self._new_error(
|
|
165
208
|
"READ_ONLY_HWP_DOCUMENT",
|
|
166
|
-
"
|
|
209
|
+
"설치된 python-hwpx는 HWP 5.0(.hwp) 문서를 열거나 저장하지 못합니다.",
|
|
167
210
|
)
|
|
168
211
|
try:
|
|
169
212
|
document, resolved = self.storage.open_document(path)
|
|
@@ -186,6 +229,9 @@ class DocumentContext:
|
|
|
186
229
|
details={"requestedName": Path(path).name},
|
|
187
230
|
) from exc
|
|
188
231
|
except Exception as exc: # pragma: no cover - delegated to backend
|
|
232
|
+
hwp_error = self._hwp_error(exc)
|
|
233
|
+
if hwp_error is not None:
|
|
234
|
+
raise hwp_error from exc
|
|
189
235
|
raise self._new_error(
|
|
190
236
|
"DOCUMENT_OPEN_FAILED",
|
|
191
237
|
f"failed to open '{path}': {exc}",
|
|
@@ -299,6 +299,7 @@ class FormFieldService:
|
|
|
299
299
|
) from exc
|
|
300
300
|
|
|
301
301
|
source_path = self._context._resolve_path(path)
|
|
302
|
+
self._context._require_hwpx_package(source_path, output)
|
|
302
303
|
output_precondition = None
|
|
303
304
|
if isinstance(self._context.storage, LocalDocumentStorage):
|
|
304
305
|
output_precondition = (
|
|
@@ -468,6 +469,7 @@ class FormFieldService:
|
|
|
468
469
|
"installed python-hwpx does not provide hwpx.body_patch.apply_body_ops",
|
|
469
470
|
) from exc
|
|
470
471
|
source_path = self._context._resolve_path(path)
|
|
472
|
+
self._context._require_hwpx_package(source_path, output)
|
|
471
473
|
output_precondition = None
|
|
472
474
|
if isinstance(self._context.storage, LocalDocumentStorage):
|
|
473
475
|
output_precondition = (
|
|
@@ -633,6 +635,7 @@ class FormFieldService:
|
|
|
633
635
|
) from exc
|
|
634
636
|
|
|
635
637
|
blank = self._context._resolve_path(path)
|
|
638
|
+
self._context._require_hwpx_package(blank, output)
|
|
636
639
|
md = self._context._resolve_path(review_md)
|
|
637
640
|
output_precondition = None
|
|
638
641
|
if isinstance(self._context.storage, LocalDocumentStorage):
|
|
@@ -135,7 +135,7 @@ class PackageValidationService:
|
|
|
135
135
|
resolved = self._context._resolve_path(path)
|
|
136
136
|
patterns = [re.compile(pat) for pat in (forbid_patterns or [])]
|
|
137
137
|
warnings: List[Dict[str, Any]] = []
|
|
138
|
-
with create_text_extractor(resolved) as extractor:
|
|
138
|
+
with create_text_extractor(self._context._text_source(resolved)) as extractor:
|
|
139
139
|
for paragraph in extractor.iter_document_paragraphs():
|
|
140
140
|
text = paragraph.text()
|
|
141
141
|
if max_line_len is not None and len(text) > max_line_len:
|
|
@@ -39,7 +39,7 @@ class PlanningService:
|
|
|
39
39
|
def _ensure_planner_document(self, doc_id: str, path: str) -> None:
|
|
40
40
|
resolved = self._context._resolve_path(path)
|
|
41
41
|
paragraphs: List[str] = []
|
|
42
|
-
with create_text_extractor(resolved) as extractor:
|
|
42
|
+
with create_text_extractor(self._context._text_source(resolved)) as extractor:
|
|
43
43
|
for paragraph in extractor.iter_document_paragraphs():
|
|
44
44
|
paragraphs.append(paragraph.text(preserve_breaks=True))
|
|
45
45
|
self._manager.register_document(doc_id, "\n".join(paragraphs))
|
|
@@ -18,6 +18,7 @@ from ..upstream import (
|
|
|
18
18
|
AnnotationOptions,
|
|
19
19
|
create_object_finder,
|
|
20
20
|
create_text_extractor,
|
|
21
|
+
hwp5_conversion_report,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
from .context import DocumentContext
|
|
@@ -45,7 +46,7 @@ class ReadQueryService:
|
|
|
45
46
|
def get_paragraphs_by_handle(self, handle_id: str) -> Dict[str, Any]:
|
|
46
47
|
handle = self._context.get_registered_handle(handle_id)
|
|
47
48
|
resolved = self._context._resolve_path(handle.path)
|
|
48
|
-
if
|
|
49
|
+
if self._context._is_legacy_hwp(resolved):
|
|
49
50
|
paragraphs, _, _ = self._context._read_only_hwp_paragraphs(handle.path)
|
|
50
51
|
hwp_serialized = [
|
|
51
52
|
ParagraphResourceEntry(paragraphIndex=index, text=text)
|
|
@@ -57,7 +58,7 @@ class ReadQueryService:
|
|
|
57
58
|
return model.model_dump(by_alias=True)
|
|
58
59
|
|
|
59
60
|
serialized: List[ParagraphResourceEntry] = []
|
|
60
|
-
with create_text_extractor(resolved) as extractor:
|
|
61
|
+
with create_text_extractor(self._context._text_source(resolved)) as extractor:
|
|
61
62
|
for paragraph in extractor.iter_document_paragraphs():
|
|
62
63
|
serialized.append(
|
|
63
64
|
ParagraphResourceEntry(
|
|
@@ -73,7 +74,7 @@ class ReadQueryService:
|
|
|
73
74
|
def get_tables_by_handle(self, handle_id: str) -> Dict[str, Any]:
|
|
74
75
|
handle = self._context.get_registered_handle(handle_id)
|
|
75
76
|
resolved = self._context._resolve_path(handle.path)
|
|
76
|
-
if
|
|
77
|
+
if self._context._is_legacy_hwp(resolved):
|
|
77
78
|
model = DocumentTablesResource(handleId=handle.handle_id, tables=[])
|
|
78
79
|
return model.model_dump(by_alias=True)
|
|
79
80
|
|
|
@@ -92,7 +93,7 @@ class ReadQueryService:
|
|
|
92
93
|
|
|
93
94
|
def open_info(self, path: str) -> Dict[str, Any]:
|
|
94
95
|
resolved = self._context._resolve_path(path)
|
|
95
|
-
if
|
|
96
|
+
if self._context._is_legacy_hwp(resolved):
|
|
96
97
|
paragraphs, _, source = self._context._read_only_hwp_paragraphs(path)
|
|
97
98
|
stat = resolved.stat()
|
|
98
99
|
meta = {
|
|
@@ -123,6 +124,10 @@ class ReadQueryService:
|
|
|
123
124
|
"size": stat.st_size,
|
|
124
125
|
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
|
125
126
|
}
|
|
127
|
+
conversion = hwp5_conversion_report(document)
|
|
128
|
+
if conversion is not None:
|
|
129
|
+
meta["format"] = "hwp"
|
|
130
|
+
meta["hwpConversion"] = conversion
|
|
126
131
|
return {
|
|
127
132
|
"meta": meta,
|
|
128
133
|
"sectionCount": section_count,
|
|
@@ -169,7 +174,7 @@ class ReadQueryService:
|
|
|
169
174
|
with_footnotes: bool = False,
|
|
170
175
|
) -> Dict[str, Any]:
|
|
171
176
|
resolved = self._context._resolve_path(path)
|
|
172
|
-
if
|
|
177
|
+
if self._context._is_legacy_hwp(resolved):
|
|
173
178
|
hwp_paragraphs, _, _ = self._context._read_only_hwp_paragraphs(path)
|
|
174
179
|
effective_limit = (
|
|
175
180
|
self._context.paging_limit if limit is None else max(1, limit)
|
|
@@ -195,7 +200,7 @@ class ReadQueryService:
|
|
|
195
200
|
paragraphs: List[str] = []
|
|
196
201
|
next_offset: Optional[int] = None
|
|
197
202
|
start = max(0, offset)
|
|
198
|
-
with create_text_extractor(resolved) as extractor:
|
|
203
|
+
with create_text_extractor(self._context._text_source(resolved)) as extractor:
|
|
199
204
|
paragraph_iter = extractor.iter_document_paragraphs()
|
|
200
205
|
sentinel = object()
|
|
201
206
|
|
|
@@ -239,7 +244,7 @@ class ReadQueryService:
|
|
|
239
244
|
unique_indexes.add(int(index))
|
|
240
245
|
|
|
241
246
|
resolved = self._context._resolve_path(path)
|
|
242
|
-
if
|
|
247
|
+
if self._context._is_legacy_hwp(resolved):
|
|
243
248
|
paragraphs, _, _ = self._context._read_only_hwp_paragraphs(path)
|
|
244
249
|
hwp_collected = {
|
|
245
250
|
idx: paragraphs[idx] for idx in unique_indexes if idx < len(paragraphs)
|
|
@@ -266,7 +271,7 @@ class ReadQueryService:
|
|
|
266
271
|
)
|
|
267
272
|
|
|
268
273
|
collected: Dict[int, str] = {}
|
|
269
|
-
with create_text_extractor(resolved) as extractor:
|
|
274
|
+
with create_text_extractor(self._context._text_source(resolved)) as extractor:
|
|
270
275
|
for paragraph in extractor.iter_document_paragraphs():
|
|
271
276
|
para_index = paragraph.index
|
|
272
277
|
if para_index in unique_indexes and para_index not in collected:
|
|
@@ -292,7 +297,7 @@ class ReadQueryService:
|
|
|
292
297
|
|
|
293
298
|
def text_extract_report(self, path: str, mode: str = "plain") -> Dict[str, Any]:
|
|
294
299
|
resolved = self._context._resolve_path(path)
|
|
295
|
-
if
|
|
300
|
+
if self._context._is_legacy_hwp(resolved):
|
|
296
301
|
paragraphs, _, source = self._context._read_only_hwp_paragraphs(path)
|
|
297
302
|
return {
|
|
298
303
|
"content": "\n".join(paragraphs)
|
|
@@ -307,7 +312,7 @@ class ReadQueryService:
|
|
|
307
312
|
endnote="inline",
|
|
308
313
|
control="placeholder",
|
|
309
314
|
)
|
|
310
|
-
with create_text_extractor(resolved) as extractor:
|
|
315
|
+
with create_text_extractor(self._context._text_source(resolved)) as extractor:
|
|
311
316
|
content = extractor.extract_text(
|
|
312
317
|
annotations=annotations,
|
|
313
318
|
include_nested=True,
|
|
@@ -322,11 +327,11 @@ class ReadQueryService:
|
|
|
322
327
|
lock_keywords: Optional[Sequence[str]] = None,
|
|
323
328
|
) -> Dict[str, Any]:
|
|
324
329
|
resolved = self._context._resolve_path(path)
|
|
325
|
-
if
|
|
330
|
+
if self._context._is_legacy_hwp(resolved):
|
|
326
331
|
paragraphs, _, source = self._context._read_only_hwp_paragraphs(path)
|
|
327
332
|
else:
|
|
328
333
|
paragraphs = []
|
|
329
|
-
with create_text_extractor(resolved) as extractor:
|
|
334
|
+
with create_text_extractor(self._context._text_source(resolved)) as extractor:
|
|
330
335
|
for paragraph in extractor.iter_document_paragraphs():
|
|
331
336
|
paragraphs.append(paragraph.text(preserve_breaks=True))
|
|
332
337
|
source = "hwpx.text_extractor"
|
|
@@ -474,7 +479,7 @@ class ReadQueryService:
|
|
|
474
479
|
return snippet
|
|
475
480
|
|
|
476
481
|
pattern = re.compile(query) if is_regex else None
|
|
477
|
-
if
|
|
482
|
+
if self._context._is_legacy_hwp(resolved):
|
|
478
483
|
paragraphs, _, _ = self._context._read_only_hwp_paragraphs(path)
|
|
479
484
|
for para_index, text in enumerate(paragraphs):
|
|
480
485
|
if is_regex:
|
|
@@ -512,7 +517,7 @@ class ReadQueryService:
|
|
|
512
517
|
start = found + len(query)
|
|
513
518
|
return {"matches": matches}
|
|
514
519
|
|
|
515
|
-
with create_text_extractor(resolved) as extractor:
|
|
520
|
+
with create_text_extractor(self._context._text_source(resolved)) as extractor:
|
|
516
521
|
for paragraph in extractor.iter_document_paragraphs():
|
|
517
522
|
text = paragraph.text()
|
|
518
523
|
if is_regex:
|
|
@@ -558,7 +563,7 @@ class ReadQueryService:
|
|
|
558
563
|
max_results: int = 200,
|
|
559
564
|
) -> Dict[str, Any]:
|
|
560
565
|
resolved = self._context._resolve_path(path)
|
|
561
|
-
finder = create_object_finder(resolved)
|
|
566
|
+
finder = create_object_finder(self._context._text_source(resolved))
|
|
562
567
|
objects = []
|
|
563
568
|
for found in finder.iter(tag=tag_name, limit=max_results):
|
|
564
569
|
element = found.element
|
|
@@ -582,7 +587,7 @@ class ReadQueryService:
|
|
|
582
587
|
max_results: int = 200,
|
|
583
588
|
) -> Dict[str, Any]:
|
|
584
589
|
resolved = self._context._resolve_path(path)
|
|
585
|
-
finder = create_object_finder(resolved)
|
|
590
|
+
finder = create_object_finder(self._context._text_source(resolved))
|
|
586
591
|
tag_filter = None if element_type in {None, "", "*"} else element_type
|
|
587
592
|
attr_matcher: Any = value if value is not None else (lambda _: True)
|
|
588
593
|
objects = []
|
|
@@ -108,6 +108,9 @@ class SavePolicy:
|
|
|
108
108
|
details={"path": str(target)},
|
|
109
109
|
) from exc
|
|
110
110
|
except Exception as exc: # pragma: no cover - delegated to backend
|
|
111
|
+
hwp_error = self._context._hwp_error(exc)
|
|
112
|
+
if hwp_error is not None:
|
|
113
|
+
raise hwp_error from exc
|
|
111
114
|
raise self._context._new_error(
|
|
112
115
|
"DOCUMENT_SAVE_FAILED",
|
|
113
116
|
f"failed to save '{target}': {exc}",
|
|
@@ -427,6 +427,7 @@ class TransactionService:
|
|
|
427
427
|
) from exc
|
|
428
428
|
|
|
429
429
|
source_path = self._context._resolve_path(path)
|
|
430
|
+
self._context._require_hwpx_package(source_path, output)
|
|
430
431
|
output_precondition = None
|
|
431
432
|
if isinstance(self._context.storage, LocalDocumentStorage):
|
|
432
433
|
output_precondition = self._context.storage.capture_output_precondition(
|
hwpx_automation/runtime.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
|
+
import zipfile
|
|
8
9
|
from typing import Any, cast
|
|
9
10
|
|
|
10
11
|
import mcp.types as mcp_types
|
|
@@ -17,7 +18,12 @@ from hwpx_automation.office.agent.model import RECOVERABILITY
|
|
|
17
18
|
from . import __version__
|
|
18
19
|
from . import quality as quality_contract
|
|
19
20
|
from .configuration import env_value
|
|
20
|
-
from .errors import
|
|
21
|
+
from .errors import (
|
|
22
|
+
HwpDocumentError,
|
|
23
|
+
build_error_payload,
|
|
24
|
+
hwp5_error_payload,
|
|
25
|
+
mcp_code_for_error,
|
|
26
|
+
)
|
|
21
27
|
from .fastmcp_adapter import configure_runtime
|
|
22
28
|
from .hwpx_ops import HwpxOperationError
|
|
23
29
|
from .network_policy import NetworkPolicyError
|
|
@@ -81,11 +87,28 @@ def _exception_chain(exc: BaseException) -> list[BaseException]:
|
|
|
81
87
|
return chain
|
|
82
88
|
|
|
83
89
|
|
|
90
|
+
def _document_error_payload(item: BaseException) -> dict[str, Any] | None:
|
|
91
|
+
"""The payload for an operation, ``.hwp`` or non-package failure; else None."""
|
|
92
|
+
|
|
93
|
+
if isinstance(item, (HwpxOperationError, HwpDocumentError)):
|
|
94
|
+
return item.to_payload()
|
|
95
|
+
hwp5_payload = hwp5_error_payload(item)
|
|
96
|
+
if hwp5_payload is not None:
|
|
97
|
+
return hwp5_payload
|
|
98
|
+
if isinstance(item, zipfile.BadZipFile):
|
|
99
|
+
return build_error_payload(
|
|
100
|
+
code="HWPX_PACKAGE_REQUIRED",
|
|
101
|
+
message="이 도구는 HWPX 패키지(ZIP)를 읽는데, 대상 파일은 HWPX 패키지가 아닙니다.",
|
|
102
|
+
)
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
84
106
|
def _classified_error_payload(exc: BaseException | None) -> dict[str, Any]:
|
|
85
107
|
if exc is not None:
|
|
86
108
|
for item in _exception_chain(exc):
|
|
87
|
-
|
|
88
|
-
|
|
109
|
+
document_payload = _document_error_payload(item)
|
|
110
|
+
if document_payload is not None:
|
|
111
|
+
return document_payload
|
|
89
112
|
if isinstance(item, AgentContractError):
|
|
90
113
|
return build_error_payload(
|
|
91
114
|
code=item.code,
|
hwpx_automation/storage.py
CHANGED
|
@@ -32,7 +32,18 @@ else:
|
|
|
32
32
|
_OPEN_SAFETY_CLASSIFIER_IMPORT_ERROR = None
|
|
33
33
|
|
|
34
34
|
from . import quality as quality_contract
|
|
35
|
-
from .
|
|
35
|
+
from .errors import HwpDocumentError
|
|
36
|
+
from .upstream import (
|
|
37
|
+
HwpxDocument,
|
|
38
|
+
hwp5_conversion_report,
|
|
39
|
+
hwp5_document_bytes,
|
|
40
|
+
hwpx_view_bytes,
|
|
41
|
+
is_hwp5_source,
|
|
42
|
+
is_hwp5_target,
|
|
43
|
+
native_hwp5_supported,
|
|
44
|
+
open_document,
|
|
45
|
+
validate_document_path,
|
|
46
|
+
)
|
|
36
47
|
from .workspace import (
|
|
37
48
|
LEGACY_SANDBOX_ROOT_ENV,
|
|
38
49
|
LEGACY_WORKSPACE_ROOTS_ENV,
|
|
@@ -231,8 +242,7 @@ class LocalDocumentStorage:
|
|
|
231
242
|
|
|
232
243
|
def open_document(self, path: str) -> Tuple[HwpxDocument, Path]:
|
|
233
244
|
resolved = self.resolve_path(path)
|
|
234
|
-
|
|
235
|
-
document = open_document(resolved)
|
|
245
|
+
document = open_local_document(resolved, role="local HWPX open")
|
|
236
246
|
return document, resolved
|
|
237
247
|
|
|
238
248
|
def save_document(
|
|
@@ -241,12 +251,15 @@ class LocalDocumentStorage:
|
|
|
241
251
|
# General document saves use the SavePipeline gate. Byte-preserving
|
|
242
252
|
# form writers have their own guarded open-safety publication path.
|
|
243
253
|
quality_contract.assert_write_capability()
|
|
254
|
+
if is_hwp5_target(target):
|
|
255
|
+
require_native_hwp5()
|
|
244
256
|
guard = self.capture_output_guard(target)
|
|
245
257
|
self.maybe_backup(target)
|
|
246
258
|
pre_save_snapshot = build_hwpx_presave_snapshot(target)
|
|
247
259
|
# Validate in an isolated temp, then publish through the identity-bound
|
|
248
260
|
# workspace guard. Candidate creation never follows the output parent.
|
|
249
|
-
|
|
261
|
+
# The gate always checks HWPX parts, so an .hwp target is staged as HWPX.
|
|
262
|
+
tmp_fd, tmp_path_str = tempfile.mkstemp(suffix=_staging_suffix(target))
|
|
250
263
|
tmp_path = Path(tmp_path_str)
|
|
251
264
|
try:
|
|
252
265
|
os.close(tmp_fd)
|
|
@@ -259,7 +272,8 @@ class LocalDocumentStorage:
|
|
|
259
272
|
"saved HWPX failed open-safety verification: "
|
|
260
273
|
+ verification_report["openSafety"]["summary"]
|
|
261
274
|
)
|
|
262
|
-
|
|
275
|
+
data = _published_bytes(document, target, tmp_path, verification_report)
|
|
276
|
+
self.atomic_write_bytes(guard, data)
|
|
263
277
|
verification_report["filePath"] = str(target)
|
|
264
278
|
verification_report["visualComplete"] = quality_contract.visual_complete_block(report)
|
|
265
279
|
return verification_report
|
|
@@ -304,6 +318,75 @@ def require_hwpx_editor_open_safe(
|
|
|
304
318
|
return open_safety
|
|
305
319
|
|
|
306
320
|
|
|
321
|
+
def require_native_hwp5() -> None:
|
|
322
|
+
"""Refuse ``.hwp`` work clearly when the installed core cannot read or write it."""
|
|
323
|
+
|
|
324
|
+
if not native_hwp5_supported():
|
|
325
|
+
raise HwpDocumentError(
|
|
326
|
+
"설치된 python-hwpx는 HWP 5.0(.hwp) 문서를 열거나 저장하지 못합니다.",
|
|
327
|
+
code="READ_ONLY_HWP_DOCUMENT",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def open_local_document(path: Path, *, role: str) -> HwpxDocument:
|
|
332
|
+
"""Open a local HWPX or HWP 5.0 document for editing.
|
|
333
|
+
|
|
334
|
+
An HWPX package must pass the editor open-safety check first. An ``.hwp``
|
|
335
|
+
file is converted by python-hwpx, which refuses damaged, encrypted and
|
|
336
|
+
DRM files itself (``Hwp5Error``).
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
if is_hwp5_source(path):
|
|
340
|
+
require_native_hwp5()
|
|
341
|
+
return open_document(path)
|
|
342
|
+
require_hwpx_editor_open_safe(path, role=role)
|
|
343
|
+
return open_document(path)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def require_hwpx_package(
|
|
347
|
+
source: Path | str, *, output: Path | str | None = None
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Refuse an ``.hwp`` source or output for editors that patch HWPX package bytes."""
|
|
350
|
+
|
|
351
|
+
if is_hwp5_source(source) or (output is not None and is_hwp5_target(output)):
|
|
352
|
+
raise HwpDocumentError(
|
|
353
|
+
"이 도구는 HWPX 패키지의 바이트를 직접 고치므로 HWP 5.0(.hwp) 문서를 입력이나 출력으로 쓸 수 없습니다.",
|
|
354
|
+
code="HWPX_PACKAGE_REQUIRED",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _staging_suffix(target: Path) -> str:
|
|
359
|
+
return ".hwpx" if is_hwp5_target(target) else (target.suffix or ".hwpx")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _published_bytes(
|
|
363
|
+
document: HwpxDocument,
|
|
364
|
+
target: Path,
|
|
365
|
+
staged: Path,
|
|
366
|
+
verification_report: Dict[str, Any],
|
|
367
|
+
) -> bytes:
|
|
368
|
+
"""The bytes to publish at *target*: the verified HWPX, or HWP 5.0 for ``.hwp``.
|
|
369
|
+
|
|
370
|
+
The HWP 5.0 bytes are reopened before anything is written, and the report
|
|
371
|
+
records what an ``.hwp`` source could not carry into the document model.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
conversion = hwp5_conversion_report(document)
|
|
375
|
+
if conversion is not None and (conversion["unconverted"] or conversion["dropped"]):
|
|
376
|
+
verification_report["hwpConversion"] = conversion
|
|
377
|
+
verification_report["warnings"].append(
|
|
378
|
+
"content the .hwp source could not carry into the document model is not in the saved file"
|
|
379
|
+
)
|
|
380
|
+
if not is_hwp5_target(target):
|
|
381
|
+
return staged.read_bytes()
|
|
382
|
+
data = hwp5_document_bytes(document)
|
|
383
|
+
reopened = open_document(data)
|
|
384
|
+
reopened.close()
|
|
385
|
+
verification_report["format"] = "hwp"
|
|
386
|
+
verification_report["fileSizeBytes"] = len(data)
|
|
387
|
+
return data
|
|
388
|
+
|
|
389
|
+
|
|
307
390
|
@dataclass(slots=True)
|
|
308
391
|
class _RestDocumentClient:
|
|
309
392
|
"""Default HTTP client used by :class:`HttpDocumentStorage`."""
|
|
@@ -423,7 +506,10 @@ class HttpDocumentStorage:
|
|
|
423
506
|
try:
|
|
424
507
|
with os.fdopen(tmp_fd, "wb") as tmp_fh:
|
|
425
508
|
tmp_fh.write(payload)
|
|
426
|
-
|
|
509
|
+
if is_hwp5_source(tmp_path):
|
|
510
|
+
require_native_hwp5()
|
|
511
|
+
else:
|
|
512
|
+
require_hwpx_editor_open_safe(tmp_path, role="HTTP storage open")
|
|
427
513
|
os.replace(tmp_path, local_path)
|
|
428
514
|
except Exception:
|
|
429
515
|
tmp_path.unlink(missing_ok=True)
|
|
@@ -437,6 +523,8 @@ class HttpDocumentStorage:
|
|
|
437
523
|
self, document: HwpxDocument, target: Path, *, quality: Any = None
|
|
438
524
|
) -> Dict[str, Any]:
|
|
439
525
|
quality_contract.assert_write_capability()
|
|
526
|
+
if is_hwp5_target(target):
|
|
527
|
+
require_native_hwp5()
|
|
440
528
|
remote_key = str(target)
|
|
441
529
|
cache_path = self._cache.get(remote_key)
|
|
442
530
|
if cache_path is None:
|
|
@@ -445,7 +533,7 @@ class HttpDocumentStorage:
|
|
|
445
533
|
|
|
446
534
|
pre_save_snapshot = build_hwpx_presave_snapshot(cache_path if cache_path.exists() else None)
|
|
447
535
|
tmp_fd, tmp_path_str = tempfile.mkstemp(
|
|
448
|
-
suffix=cache_path
|
|
536
|
+
suffix=_staging_suffix(cache_path),
|
|
449
537
|
dir=str(cache_path.parent),
|
|
450
538
|
)
|
|
451
539
|
tmp_path = Path(tmp_path_str)
|
|
@@ -460,7 +548,9 @@ class HttpDocumentStorage:
|
|
|
460
548
|
+ verification_report["openSafety"]["summary"]
|
|
461
549
|
)
|
|
462
550
|
verification_report["visualComplete"] = quality_contract.visual_complete_block(report)
|
|
463
|
-
payload = tmp_path
|
|
551
|
+
payload = _published_bytes(document, target, tmp_path, verification_report)
|
|
552
|
+
if is_hwp5_target(target):
|
|
553
|
+
tmp_path.write_bytes(payload)
|
|
464
554
|
except quality_contract.QualityGateError:
|
|
465
555
|
tmp_path.unlink(missing_ok=True)
|
|
466
556
|
raise
|
|
@@ -491,6 +581,19 @@ def build_hwpx_presave_snapshot(
|
|
|
491
581
|
return _collect_hwpx_snapshot(source)
|
|
492
582
|
|
|
493
583
|
|
|
584
|
+
def _hwpx_view(source: HwpxVerificationSource) -> HwpxVerificationSource:
|
|
585
|
+
"""*source* itself, or for an ``.hwp`` the HWPX package of its document model.
|
|
586
|
+
|
|
587
|
+
HWPX verification reads package parts, so an HWP 5.0 file is checked
|
|
588
|
+
through the model python-hwpx opens it to.
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
if is_hwp5_source(source):
|
|
592
|
+
require_native_hwp5()
|
|
593
|
+
return hwpx_view_bytes(source)
|
|
594
|
+
return source
|
|
595
|
+
|
|
596
|
+
|
|
494
597
|
def _issue_messages(report: Any, attr_name: str = "issues") -> List[str]:
|
|
495
598
|
return [str(issue) for issue in getattr(report, attr_name, ())]
|
|
496
599
|
|
|
@@ -520,6 +623,7 @@ def build_hwpx_open_safety_report(
|
|
|
520
623
|
document_payload: Dict[str, Any]
|
|
521
624
|
reopen_payload: Dict[str, Any]
|
|
522
625
|
|
|
626
|
+
source = _hwpx_view(source)
|
|
523
627
|
dependency_error = _open_safety_dependency_error()
|
|
524
628
|
if dependency_error is not None:
|
|
525
629
|
package_payload = {
|
|
@@ -606,8 +710,9 @@ def build_hwpx_verification_report(
|
|
|
606
710
|
*,
|
|
607
711
|
file_path: Path | None = None,
|
|
608
712
|
) -> Dict[str, Any]:
|
|
609
|
-
|
|
610
|
-
|
|
713
|
+
view = _hwpx_view(source)
|
|
714
|
+
snapshot = _collect_hwpx_snapshot(view)
|
|
715
|
+
open_safety = build_hwpx_open_safety_report(view)
|
|
611
716
|
displayed_path = file_path or (source if isinstance(source, Path) else None)
|
|
612
717
|
file_size = len(source) if isinstance(source, bytes) else source.stat().st_size
|
|
613
718
|
totals = snapshot["totals"]
|
|
@@ -681,6 +786,7 @@ def _collect_hwpx_snapshot(source: HwpxVerificationSource) -> Dict[str, Any]:
|
|
|
681
786
|
"suspiciousPatterns": 0,
|
|
682
787
|
}
|
|
683
788
|
|
|
789
|
+
source = _hwpx_view(source)
|
|
684
790
|
archive_source = BytesIO(source) if isinstance(source, bytes) else source
|
|
685
791
|
with zipfile.ZipFile(archive_source) as archive:
|
|
686
792
|
names = set(archive.namelist())
|
hwpx_automation/upstream.py
CHANGED
|
@@ -12,6 +12,7 @@ from copy import deepcopy
|
|
|
12
12
|
from os import PathLike
|
|
13
13
|
from typing import Any, Iterable, Literal
|
|
14
14
|
from xml.etree import ElementTree as ET
|
|
15
|
+
from zipfile import ZipFile
|
|
15
16
|
|
|
16
17
|
from .compat import patch_python_hwpx
|
|
17
18
|
|
|
@@ -59,6 +60,59 @@ def open_document(source: str | PathLike[str] | Any) -> HwpxDocument:
|
|
|
59
60
|
return HwpxDocument.open(source)
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
# HWP 5.0 (.hwp) files are OLE2 compound files; HWPX files are ZIP packages.
|
|
64
|
+
_OLE2_MAGIC = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def native_hwp5_supported() -> bool:
|
|
68
|
+
"""Whether the installed python-hwpx opens and writes ``.hwp`` itself (6.6.0+)."""
|
|
69
|
+
|
|
70
|
+
return hasattr(HwpxDocument, "conversion_report")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_hwp5_source(source: str | PathLike[str] | bytes) -> bool:
|
|
74
|
+
"""Whether *source* holds an HWP 5.0 file, judged by its leading bytes."""
|
|
75
|
+
|
|
76
|
+
if isinstance(source, bytes):
|
|
77
|
+
return source[:8] == _OLE2_MAGIC
|
|
78
|
+
try:
|
|
79
|
+
with open(source, "rb") as stream:
|
|
80
|
+
return stream.read(8) == _OLE2_MAGIC
|
|
81
|
+
except OSError:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_hwp5_target(path: str | PathLike[str]) -> bool:
|
|
86
|
+
"""Whether saving to *path* writes HWP 5.0; the extension picks the format."""
|
|
87
|
+
|
|
88
|
+
return str(path).lower().endswith(".hwp")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def hwp5_conversion_report(document: HwpxDocument) -> dict[str, dict[str, int]] | None:
|
|
92
|
+
"""What opening an ``.hwp`` left out of the model; None for an HWPX source."""
|
|
93
|
+
|
|
94
|
+
report = getattr(document, "conversion_report", None)
|
|
95
|
+
if report is None:
|
|
96
|
+
return None
|
|
97
|
+
return {"unconverted": dict(report.unconverted), "dropped": dict(report.dropped)}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def hwp5_document_bytes(document: HwpxDocument) -> bytes:
|
|
101
|
+
"""Serialize *document* as HWP 5.0; core raises ``Hwp5Error`` before writing anything."""
|
|
102
|
+
|
|
103
|
+
return document.to_bytes(format="hwp")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def hwpx_view_bytes(source: str | PathLike[str] | bytes) -> bytes:
|
|
107
|
+
"""The HWPX package of the document model an ``.hwp`` source opens to."""
|
|
108
|
+
|
|
109
|
+
document = HwpxDocument.open(source)
|
|
110
|
+
try:
|
|
111
|
+
return document.to_bytes()
|
|
112
|
+
finally:
|
|
113
|
+
document.close()
|
|
114
|
+
|
|
115
|
+
|
|
62
116
|
def new_document() -> HwpxDocument:
|
|
63
117
|
return HwpxDocument.new()
|
|
64
118
|
|
|
@@ -71,11 +125,11 @@ def open_package(path: str | PathLike[str]) -> HwpxPackage:
|
|
|
71
125
|
return HwpxPackage.open(path)
|
|
72
126
|
|
|
73
127
|
|
|
74
|
-
def create_text_extractor(path: str | PathLike[str]) -> TextExtractor:
|
|
128
|
+
def create_text_extractor(path: str | PathLike[str] | ZipFile) -> TextExtractor:
|
|
75
129
|
return TextExtractor(path)
|
|
76
130
|
|
|
77
131
|
|
|
78
|
-
def create_object_finder(path: str | PathLike[str]) -> ObjectFinder:
|
|
132
|
+
def create_object_finder(path: str | PathLike[str] | ZipFile) -> ObjectFinder:
|
|
79
133
|
return ObjectFinder(path)
|
|
80
134
|
|
|
81
135
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx-automation
|
|
3
|
-
Version: 7.3.
|
|
3
|
+
Version: 7.3.3
|
|
4
4
|
Summary: Task-oriented HWPX document automation for Python, with an optional MCP adapter.
|
|
5
5
|
Author: Kohkyuhyun
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -74,7 +74,7 @@ Dynamic: license-file
|
|
|
74
74
|
|
|
75
75
|
<!-- release-state: release-approved -->
|
|
76
76
|
> [!NOTE]
|
|
77
|
-
> 발행이 승인된 7.3.
|
|
77
|
+
> 발행이 승인된 7.3.3 후보 (release-approved; 원격 발행 대기): `python-hwpx 6.6.0 → python-hwpx-automation 7.3.3 → hwpx-plugin 2.4.0` (`5e5c23651f92785a`).
|
|
78
78
|
> 공개 트레인: `python-hwpx 6.6.0 → python-hwpx-automation 7.3.1 → hwpx-plugin 2.4.0`.
|
|
79
79
|
> 공개 좌표는 core·automation PyPI와 plugin GitHub
|
|
80
80
|
> Release·marketplace·실제 marketplace 설치 관찰 후에만 승격합니다.
|
|
@@ -162,7 +162,7 @@ PDF와 쪽 PNG로 렌더합니다. 별도 환경에서 실행하므로 호출하
|
|
|
162
162
|
고정 버전을 건드리지 않습니다.
|
|
163
163
|
|
|
164
164
|
```bash
|
|
165
|
-
uvx --from "python-hwpx-automation[oracle]==7.3.
|
|
165
|
+
uvx --from "python-hwpx-automation[oracle]==7.3.3" \
|
|
166
166
|
hwpx render-pdf in.hwpx out.pdf --png out/page --json
|
|
167
167
|
```
|
|
168
168
|
|
|
@@ -8,7 +8,7 @@ hwpx_automation/capabilities.py,sha256=6arb_5qH-ZvFcKftDW0r4jWeJI24QfameTZRzLQbz
|
|
|
8
8
|
hwpx_automation/compat.py,sha256=c08KFbs1IzbXAZbVAUOzuoWoR8BXnBad313rL8e-WcM,1582
|
|
9
9
|
hwpx_automation/configuration.py,sha256=kZmkeDPTDgvl1l-QMkdD2vg4rwZsvfk9pMESrpgoMPI,1371
|
|
10
10
|
hwpx_automation/document_state.py,sha256=18ihSR9i062BaVnni24BEBUPyDIBVwPvKGWFMIVDmjw,2979
|
|
11
|
-
hwpx_automation/errors.py,sha256=
|
|
11
|
+
hwpx_automation/errors.py,sha256=EJzNnYaJ4cbYZEqp-zCc0nofVSJJGeYjdRcD_cIX0Ro,13277
|
|
12
12
|
hwpx_automation/execution_lock.py,sha256=-KEYeY7iNaGVV6MwKHA7uacmnLkm_BPzSODLLGnBgMY,459
|
|
13
13
|
hwpx_automation/fastmcp_adapter.py,sha256=fLCm1hOay3LiQFf0fGFMSCb3Dq36weuJHvOb7ScYUd8,27858
|
|
14
14
|
hwpx_automation/form_fill.py,sha256=m_e3Awxa2wCPwPNSlTgGH7ytylxV0u8FEqDdAeyyFsw,48717
|
|
@@ -16,12 +16,12 @@ hwpx_automation/form_output_models.py,sha256=qRNtqNjsmTynMWRU6p7SWIfcqglWyAxffmO
|
|
|
16
16
|
hwpx_automation/hwp_converter.py,sha256=6i1dVntfbAl1esGicvIhquJKDldg0PxGaIdfiE2VzK8,7611
|
|
17
17
|
hwpx_automation/hwp_support.py,sha256=EbM5PQiK9QDJDwFhA1dCxh3I7-5wst5pJXgcr4VK3tY,3371
|
|
18
18
|
hwpx_automation/hwpx_ops.py,sha256=39oO_S_dTQHny1qJhJzUjoQTblAKLTQBnIzITlBwyLY,50991
|
|
19
|
-
hwpx_automation/identity.json,sha256=
|
|
19
|
+
hwpx_automation/identity.json,sha256=VcotkeMb4PwBX8t8zD5yRSYeRARrcb3quUBILYFi6kQ,9820
|
|
20
20
|
hwpx_automation/identity.py,sha256=g62OiZ_Bvo3wL8NCHFPmAEfwDSJY4AG_cc6aBipU9zI,473
|
|
21
21
|
hwpx_automation/ingest_adapters.py,sha256=7Lm1Pc__tPKdso0rRORLyy1pmUScr8VVmnTnleSYMXo,2726
|
|
22
22
|
hwpx_automation/markdown_plan.py,sha256=n2H_vhRM-el6Y5KLi21tz-4WzK78-wlE8i8hsUVWu2Y,6886
|
|
23
23
|
hwpx_automation/mcp_cli.py,sha256=rgLgj4bhOyYMHCpSGMy-KgryVieH4nFbV1Y4sV98Slo,798
|
|
24
|
-
hwpx_automation/mixed_form.py,sha256=
|
|
24
|
+
hwpx_automation/mixed_form.py,sha256=EML3LWACXxEp3Z2BJAG9jG7ARik0Zj8UT1zSeVRRs0Y,108853
|
|
25
25
|
hwpx_automation/mutation_models.py,sha256=SEIZYPxlp2FvnFClOsWS08KWBIL7kiMsvX1n6STpJTE,11690
|
|
26
26
|
hwpx_automation/network_policy.py,sha256=ZemScVr_7w1XbAtuBjVVhO3XMiditjyn5FzQjcaER40,7512
|
|
27
27
|
hwpx_automation/preview_output_models.py,sha256=eKreT-GLLxsUnz3I7nqRjAvMOwbBAbHJsACSVPr5ma0,2490
|
|
@@ -30,20 +30,20 @@ hwpx_automation/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
|
30
30
|
hwpx_automation/quality.py,sha256=D4S1Zx57V2yEzLPADGk9n_PxuNnxgC5Qw0gnu_72AEQ,13052
|
|
31
31
|
hwpx_automation/quality_generation.py,sha256=pakrQ2K0dLC0faCAi7KVr2k09fEBCz8K-M9Qtjr8Mp4,30427
|
|
32
32
|
hwpx_automation/resources.py,sha256=ksAU7yLkp66h7XOizFwVVXnQ7DBdTx7qYJ81-lnJygY,4859
|
|
33
|
-
hwpx_automation/runtime.py,sha256=
|
|
33
|
+
hwpx_automation/runtime.py,sha256=QRby9BiHIJfAJogsDsApBlXcLS8oSKVwB3fVFKSdYrw,13789
|
|
34
34
|
hwpx_automation/runtime_services.py,sha256=E74uv1RWasAF93hk-g1zIcWZVvgsOB1nL77zmTmFhYM,3567
|
|
35
35
|
hwpx_automation/server.py,sha256=U2IxqrGJ1EBjNnY8QzJah7ZG0MdXEVZcR0NM1FJ7Qhg,12224
|
|
36
|
-
hwpx_automation/storage.py,sha256=
|
|
36
|
+
hwpx_automation/storage.py,sha256=DfHKo8Q7sRtV44SVCV2V5jkexnkpY7oCZgfUXXg5rFQ,32652
|
|
37
37
|
hwpx_automation/tool_bindings.py,sha256=aHl8N8yTtFrmGKRjdKlNMZEed1qVvC27xF9Y-cfY10I,9816
|
|
38
38
|
hwpx_automation/tool_contract.py,sha256=z8W2RIUSAgiNH8F3TTZhSIKz_L2AYThy6I4uULYzR7c,38794
|
|
39
|
-
hwpx_automation/upstream.py,sha256=
|
|
39
|
+
hwpx_automation/upstream.py,sha256=4JPxC0BzhfVDyLNpnnYGwQ5NNXNXy0YSLu23oPLQP3c,27569
|
|
40
40
|
hwpx_automation/visual_qa.py,sha256=ei2WnH0YvXrCQKa0WmnxaWNX_whRQfSLyDoClEHk0Qs,29813
|
|
41
41
|
hwpx_automation/workspace.py,sha256=WogK_yXmGG42kRMhtruLncRJ6WoKqVh3eMfnItEWtLs,67525
|
|
42
42
|
hwpx_automation/core/__init__.py,sha256=OHgM3he4rGageZEFB4iNv2ni7c6mr1xNNVfMGO1bzz4,74
|
|
43
|
-
hwpx_automation/core/content.py,sha256=
|
|
43
|
+
hwpx_automation/core/content.py,sha256=K3iogv6Qsb9xy3wPud6QYSZZRJvPtWKuWvK8rG76qqc,31789
|
|
44
44
|
hwpx_automation/core/context.py,sha256=cG6XC5TpCB-_D8QDnqRsOwpxdstQWfb0gtmYBDzNV50,2966
|
|
45
45
|
hwpx_automation/core/diff.py,sha256=9_YLUyO88Yepvq-Hdr35fakjJ0plc1Krkc0IErMvGMo,1522
|
|
46
|
-
hwpx_automation/core/document.py,sha256=
|
|
46
|
+
hwpx_automation/core/document.py,sha256=3PradsOWv6akKEsA1cM8XaUwp9NDI5aqIEtD1yxkYuY,1135
|
|
47
47
|
hwpx_automation/core/formatting.py,sha256=FsomTQeTC385wreflLhwzP18kCD-jLfP9j1Xu0fHYVI,15770
|
|
48
48
|
hwpx_automation/core/handles.py,sha256=v7HCAW2cWVu3OarPiDFRSixNncP1FIVRcTIPp5MeWAA,705
|
|
49
49
|
hwpx_automation/core/locations.py,sha256=FVbgLhGka9jaUeU2lG8illML15cxi31ouWP05oQBzI4,7634
|
|
@@ -51,17 +51,17 @@ hwpx_automation/core/locator.py,sha256=Q3CfVniPaMSXQ2ABV0JXmTNROkHtKZLz5VR4mCmlJ
|
|
|
51
51
|
hwpx_automation/core/plan.py,sha256=sNFIko5USDhvs7NA_gy4hSUV40q4vIQTc_dZ2d-nFXE,22744
|
|
52
52
|
hwpx_automation/core/resources.py,sha256=PNPhcGEwq4BrRdMrytgIfeT2_4nrDFB9z97OfxCaQBc,1198
|
|
53
53
|
hwpx_automation/core/search.py,sha256=OmUs0wLPxMrmkhbBiiiQQC-YKG8zjz4bHvJbmCae_eU,12280
|
|
54
|
-
hwpx_automation/core/transactions.py,sha256=
|
|
54
|
+
hwpx_automation/core/transactions.py,sha256=4Dn13n3fmDl7PHZ3AGRyf09gioNLcC-TgD6-TrqHqzQ,16909
|
|
55
55
|
hwpx_automation/core/txn.py,sha256=gXbv3JFRnYnWY3L57YeTdpFyQaBBMlvLd-dix7NVFog,1449
|
|
56
56
|
hwpx_automation/handlers/__init__.py,sha256=iLzNXNd-ernCvKTi4V8h2XzvFDoM4biZp34KIL0fgsg,83
|
|
57
57
|
hwpx_automation/handlers/_shared.py,sha256=DShvk2vhvtc6Ufy-gVol_S9yP-u_2Gj-iuxArQVne4o,13009
|
|
58
|
-
hwpx_automation/handlers/agent_document.py,sha256=
|
|
58
|
+
hwpx_automation/handlers/agent_document.py,sha256=RBA3A8braZ3B2zMqSavUHZ_Uk_M1Olwm2O4-SwTVS5U,8388
|
|
59
59
|
hwpx_automation/handlers/authoring.py,sha256=7qcFFViTUZQyjhpvNbE6zD8QOsMpDOOMTV8sUKV9MZs,26209
|
|
60
60
|
hwpx_automation/handlers/content_edit.py,sha256=qZH5TcMrSt4zNYCI6Wy_oCiilLLi7d1RYvHQNDjPVKo,42291
|
|
61
61
|
hwpx_automation/handlers/form_fill.py,sha256=oAalDItZwIinHWYIvMUpEIZTPKXpasGSgWoKyQQLYVw,22382
|
|
62
62
|
hwpx_automation/handlers/layout_style.py,sha256=j-vivR4KHbrSAfslx1c2Lba__b8zv-S6-epTjmwbBRs,23705
|
|
63
63
|
hwpx_automation/handlers/quality_render.py,sha256=JK6xedmHYtf8KWrYzI5A2L69pI_Dz0nfQqibz2L5SM8,24763
|
|
64
|
-
hwpx_automation/handlers/read_export.py,sha256=
|
|
64
|
+
hwpx_automation/handlers/read_export.py,sha256=f8phMXVUlUIsY1WWgXonKCLpnqqXmNpa0kFNfBrayws,45085
|
|
65
65
|
hwpx_automation/handlers/specialized.py,sha256=wvq0P-7MiZCN36Ba4Q1V-9Tl_T7MB3Cn0rC4RGjxt_k,29223
|
|
66
66
|
hwpx_automation/handlers/tracked_changes.py,sha256=bYlVFVsuF6DSvlxOdvv4MavPI2IByudTkYl8_NDoN8U,20598
|
|
67
67
|
hwpx_automation/handlers/workflow.py,sha256=xjkuocWyFKFFdPYX2sQDxtwyGZwn1hqX2SZ6e-TpIBI,2988
|
|
@@ -179,7 +179,7 @@ hwpx_automation/office/rendering/detectors.py,sha256=k7t79nWIBSqStNNdwPohmUG2zwr
|
|
|
179
179
|
hwpx_automation/office/rendering/diff.py,sha256=SJW7HgDyBFbJDL2yHM1cCnCSkGP7CoYuqonABoWh_eo,5743
|
|
180
180
|
hwpx_automation/office/rendering/fixture_corpus.py,sha256=fqx9rRwLCBZ0unhgWCm8V-_xs5597AbBdiDaW5ey5M8,7595
|
|
181
181
|
hwpx_automation/office/rendering/mac_session.py,sha256=DwNpzWwhxIhu_v5PBlLZd6YcPuMlcXREhswKsvIV82k,4021
|
|
182
|
-
hwpx_automation/office/rendering/oracle.py,sha256=
|
|
182
|
+
hwpx_automation/office/rendering/oracle.py,sha256=dzsRIEmmC7NVzWY0ZhScT0L8jyDlz5HQJXES9U_BKUs,44782
|
|
183
183
|
hwpx_automation/office/rendering/page_qa.py,sha256=BG5kqboMg7SyAi6tAi78LYq1ZmRh30uo72JpPN4Hk2Q,7352
|
|
184
184
|
hwpx_automation/office/rendering/qa_contracts.py,sha256=1cjoiRTAWgi7NgE8SPc6bKxdoUfKx9nyou1mgC47TxY,10506
|
|
185
185
|
hwpx_automation/office/rendering/qa_metrics.py,sha256=ijskhoieoEGk4bwos70XAvA8qDVTOOu4CJUH783cB9w,9859
|
|
@@ -190,17 +190,17 @@ hwpx_automation/ops_services/__init__.py,sha256=OUtolsBwPX7kzfmOmanHZK98HFU5ylYw
|
|
|
190
190
|
hwpx_automation/ops_services/_border_fill.py,sha256=2ruoojd7xLvbC829O95mYnE75RgjbAHcYRZ0HymTMWw,9143
|
|
191
191
|
hwpx_automation/ops_services/composition.py,sha256=xMWDquck6Ii8YQlUZwS3oW5F753FgN64tbSv8O32xWA,1944
|
|
192
192
|
hwpx_automation/ops_services/content_layout.py,sha256=2iq1V2UbTJy299Vl-iTPBvvN8VETAs0fDVUu4SJVKGw,19488
|
|
193
|
-
hwpx_automation/ops_services/context.py,sha256=
|
|
194
|
-
hwpx_automation/ops_services/form_fields.py,sha256=
|
|
193
|
+
hwpx_automation/ops_services/context.py,sha256=LT7rAio0MgChQ5PDt50sZH_XlrPbpV6QTKoyKS8bXZ4,10240
|
|
194
|
+
hwpx_automation/ops_services/form_fields.py,sha256=uc_1a-M1JClRGVbRfxhxWNDI1lsy29nJVAFZmxbRf20,31448
|
|
195
195
|
hwpx_automation/ops_services/media.py,sha256=ewqIPMZVFxesh9Sr1Uc7HcX6Es2GIdHRhh2J9IQq6LM,7945
|
|
196
196
|
hwpx_automation/ops_services/memo_style.py,sha256=j5y5gRjkrLRNnH7q_u855raKfHUAcCURpqHTe09eFso,16585
|
|
197
|
-
hwpx_automation/ops_services/package_validation.py,sha256=
|
|
198
|
-
hwpx_automation/ops_services/planning.py,sha256=
|
|
197
|
+
hwpx_automation/ops_services/package_validation.py,sha256=vPxHG174N8MIbkEVareO73speR-rYNtve9zSpWVChAI,6349
|
|
198
|
+
hwpx_automation/ops_services/planning.py,sha256=3yN3e-5zdD6pFuIKoB5IvwuVX0hLVGX6hGoppHJfYDI,6653
|
|
199
199
|
hwpx_automation/ops_services/preview_export.py,sha256=1NAQT3j1Ej98MIv4pCDDnYd83w4tTemHmoFWTidRWHw,23504
|
|
200
|
-
hwpx_automation/ops_services/read_query.py,sha256=
|
|
201
|
-
hwpx_automation/ops_services/save_policy.py,sha256
|
|
200
|
+
hwpx_automation/ops_services/read_query.py,sha256=z13Exfvweu_qhTFW0EBR8sn5GaVWhe9HlLuFkuNUaug,23655
|
|
201
|
+
hwpx_automation/ops_services/save_policy.py,sha256=sOh6FDy9E6niI6KnwpzWbIf-hnGrmxGw9oOG15vnsi4,23819
|
|
202
202
|
hwpx_automation/ops_services/tables.py,sha256=psGpwWY8B2Gx611sRri4X7xIQaiZ0DtZvOadj8n1t14,19089
|
|
203
|
-
hwpx_automation/ops_services/transactions.py,sha256=
|
|
203
|
+
hwpx_automation/ops_services/transactions.py,sha256=iLSrZOj8a2q3tGcCeVQu0-2xH-NqPt5YoQgz37qkNIo,26382
|
|
204
204
|
hwpx_automation/utils/__init__.py,sha256=MIE-UrcJ-Trz80UxicViKiGCXNqe3L5lC1f0acVZwUM,65
|
|
205
205
|
hwpx_automation/utils/helpers.py,sha256=nTuzGdXmhDJFODSs2pQZNNOdvrKeU457rJq4Vo3W5cY,762
|
|
206
206
|
hwpx_automation/utils/read_budget.py,sha256=K71qu5D9fblRXFvSQqe8e7yFOMRK0UuWR5xSpq350Jk,4416
|
|
@@ -219,10 +219,10 @@ hwpx_automation/workflow/rendering.py,sha256=MAuL3vSS0vfo-R8ws55E52ddnBrtoYmYikT
|
|
|
219
219
|
hwpx_automation/workflow/service.py,sha256=dKhcWQVHVBh8752pIDPSvHPXpsQsmB25hrJD7IsgObc,35945
|
|
220
220
|
hwpx_automation/workflow/state_machine.py,sha256=8nFj3WPoqBj0H2j9WRbdAUCT7tm3q_ieQsaEX7LPnE4,2230
|
|
221
221
|
hwpx_automation/workflow/store.py,sha256=S2BTR5g21WgFYfi8ZwvgWQCv7wxUSA8ttlMS9-zNeoU,31663
|
|
222
|
-
python_hwpx_automation-7.3.
|
|
223
|
-
python_hwpx_automation-7.3.
|
|
224
|
-
python_hwpx_automation-7.3.
|
|
225
|
-
python_hwpx_automation-7.3.
|
|
226
|
-
python_hwpx_automation-7.3.
|
|
227
|
-
python_hwpx_automation-7.3.
|
|
228
|
-
python_hwpx_automation-7.3.
|
|
222
|
+
python_hwpx_automation-7.3.3.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
|
|
223
|
+
python_hwpx_automation-7.3.3.dist-info/licenses/NOTICE,sha256=B6vl9SzBdZ0LlAeEd89Q3P6r_rlXZWD6ZJK6Ntz3s6A,644
|
|
224
|
+
python_hwpx_automation-7.3.3.dist-info/METADATA,sha256=6SO3powknTW6MECFSnJpEXFYHYQTn85iFMVdDcKnabo,13819
|
|
225
|
+
python_hwpx_automation-7.3.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
226
|
+
python_hwpx_automation-7.3.3.dist-info/entry_points.txt,sha256=6YDtwFKwyk9BNTR_iCVV0XTwtDRVwENVNvv3wHM81KQ,114
|
|
227
|
+
python_hwpx_automation-7.3.3.dist-info/top_level.txt,sha256=jQZZqF1u9rVIJvragSUf8HCHkm0gFVaCViNDZEKfh1A,16
|
|
228
|
+
python_hwpx_automation-7.3.3.dist-info/RECORD,,
|
|
File without changes
|
{python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/licenses/NOTICE
RENAMED
|
File without changes
|
{python_hwpx_automation-7.3.2.dist-info → python_hwpx_automation-7.3.3.dist-info}/top_level.txt
RENAMED
|
File without changes
|