python-hwpx 3.8.0__py3-none-any.whl → 4.1.1__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 CHANGED
@@ -1,6 +1,18 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
- """High-level utilities for working with HWPX documents."""
2
+ """High-level utilities for working with HWPX documents.
3
3
 
4
+ 최상위 ``from hwpx import ...`` 표면은 세 계층으로 나뉩니다(정책·전수 목록:
5
+ ``docs/stable-api.md``):
6
+
7
+ - **stable** — ``__all__``에 있는 이름. 계약이 굳었고 major 경계에서만 깨질 수 있음.
8
+ - **experimental** — 계약이 유동적. ``from hwpx.experimental import ...``로 쓰세요.
9
+ 최상위 재내보내기는 하위 호환을 위해 유지하되 접근 시 ``DeprecationWarning``이 나며
10
+ 다음 major에서 제거될 예정입니다.
11
+ - **deprecated** — 대체 경로로 이전하세요. 접근 시 ``DeprecationWarning``이 납니다.
12
+ """
13
+
14
+ import importlib
15
+ import warnings
4
16
  from importlib.metadata import PackageNotFoundError, version as _metadata_version
5
17
 
6
18
 
@@ -11,13 +23,94 @@ def _resolve_version() -> str:
11
23
  except PackageNotFoundError:
12
24
  return "0+unknown"
13
25
 
26
+
27
+ # --- experimental / deprecated 최상위 표면 (지연 접근, 접근 시 경고) ---------------
28
+ #
29
+ # stable 이름은 아래에서 eager import 되어 모듈 전역에 존재하므로 ``__getattr__``이
30
+ # 호출되지 않습니다(=경고 없음). 여기 등록된 이름만 지연 해석되어 경고를 냅니다.
31
+ # 4.0.0에서 제거되는 이름은 0개 — 모두 계속 import 가능합니다.
32
+
33
+ _EXPERIMENTAL_EXPORTS = {
34
+ # 문서 ingestion 프레임워크(임의 포맷 -> HWPX). 계약 유동.
35
+ "ConversionAttempt": "hwpx.ingest",
36
+ "DocumentConverter": "hwpx.ingest",
37
+ "DocumentIngestError": "hwpx.ingest",
38
+ "DocumentIngestResult": "hwpx.ingest",
39
+ "DocumentIngestor": "hwpx.ingest",
40
+ "DocumentSourceInfo": "hwpx.ingest",
41
+ "UnsupportedDocumentFormat": "hwpx.ingest",
42
+ # 레이아웃 프리뷰(한컴 없는 정직 근사). 계약 유동.
43
+ "LayoutPreview": "hwpx.tools.layout_preview",
44
+ "PreviewPage": "hwpx.tools.layout_preview",
45
+ "render_layout_preview": "hwpx.tools.layout_preview",
46
+ # 문서 프리뷰 뷰어(3.8.0 신규). 계약 유동.
47
+ "DocumentViewer": "hwpx.tools.document_viewer",
48
+ "render_document_viewer": "hwpx.tools.document_viewer",
49
+ }
50
+
51
+ _DEPRECATED_EXPORTS = {
52
+ "analyze_template_formfit": "hwpx.template_formfit",
53
+ "apply_template_formfit": "hwpx.template_formfit",
54
+ "TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION": "hwpx.template_formfit",
55
+ "TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION": "hwpx.template_formfit",
56
+ }
57
+
58
+ # deprecated formfit 표면의 공통 대체 경로 안내.
59
+ _FORMFIT_REPLACEMENT = (
60
+ "구조적 form-fill 경로를 사용하세요: 라이브러리는 "
61
+ "hwpx.table_patch.fill_cells 계열, MCP는 analyze_form_fill/apply_form_fill/"
62
+ "verify_form_fill."
63
+ )
64
+
65
+
66
+ def _experimental_message(name: str) -> str:
67
+ return (
68
+ f"'hwpx.{name}'는 실험적(experimental) 표면입니다. 계약이 유동적이므로 "
69
+ f"'from hwpx.experimental import {name}'로 import하세요. 최상위 재내보내기는 "
70
+ f"다음 major에서 제거될 예정입니다."
71
+ )
72
+
73
+
74
+ def _deprecated_message(name: str) -> str:
75
+ return (
76
+ f"'hwpx.{name}'는 deprecated입니다. {_FORMFIT_REPLACEMENT} 이 이름은 "
77
+ f"다음 major에서 제거될 예정입니다."
78
+ )
79
+
80
+
14
81
  def __getattr__(name: str) -> object:
15
- """Resolve dynamic module attributes."""
82
+ """Resolve dynamic module attributes.
83
+
84
+ ``__version__``은 경고 없이 지연 해석하고, experimental/deprecated 이름은
85
+ 해석 시 ``DeprecationWarning``을 냅니다.
86
+ """
16
87
 
17
88
  if name == "__version__":
18
89
  return _resolve_version()
90
+
91
+ module_name = _EXPERIMENTAL_EXPORTS.get(name)
92
+ if module_name is not None:
93
+ warnings.warn(_experimental_message(name), DeprecationWarning, stacklevel=2)
94
+ return getattr(importlib.import_module(module_name), name)
95
+
96
+ module_name = _DEPRECATED_EXPORTS.get(name)
97
+ if module_name is not None:
98
+ warnings.warn(_deprecated_message(name), DeprecationWarning, stacklevel=2)
99
+ return getattr(importlib.import_module(module_name), name)
100
+
19
101
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
20
102
 
103
+
104
+ def __dir__() -> list[str]:
105
+ return sorted(
106
+ set(globals())
107
+ | set(__all__)
108
+ | set(_EXPERIMENTAL_EXPORTS)
109
+ | set(_DEPRECATED_EXPORTS)
110
+ )
111
+
112
+
113
+ # --- stable 최상위 표면 (eager import) ------------------------------------------
21
114
  from .tools.text_extractor import (
22
115
  DEFAULT_NAMESPACES,
23
116
  ParagraphInfo,
@@ -70,25 +163,8 @@ from .tools.style_profile import (
70
163
  placeholder_fill_report,
71
164
  register_template,
72
165
  )
73
- from .tools.layout_preview import (
74
- LayoutPreview,
75
- PreviewPage,
76
- render_layout_preview,
77
- )
78
- from .tools.document_viewer import (
79
- DocumentViewer,
80
- render_document_viewer,
81
- )
82
- from .ingest import (
83
- ConversionAttempt,
84
- DocumentConverter,
85
- DocumentIngestError,
86
- DocumentIngestResult,
87
- DocumentIngestor,
88
- DocumentSourceInfo,
89
- HwpxMarkdownConverter,
90
- UnsupportedDocumentFormat,
91
- )
166
+ from .ingest import HwpxMarkdownConverter
167
+ from .errors import HwpxError
92
168
  from .mutation_report import (
93
169
  MutationReport,
94
170
  PreservationDowngradeError,
@@ -124,12 +200,6 @@ from .quality import (
124
200
  SavePipeline,
125
201
  VisualCompleteReport,
126
202
  )
127
- from .template_formfit import (
128
- TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION,
129
- TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION,
130
- analyze_template_formfit,
131
- apply_template_formfit,
132
- )
133
203
 
134
204
  __all__ = [
135
205
  "QualityPolicy",
@@ -141,30 +211,21 @@ __all__ = [
141
211
  "DEFAULT_STYLE_PRESET",
142
212
  "DOCUMENT_PLAN_SCHEMA_VERSION",
143
213
  "get_document_plan_schema",
144
- "ConversionAttempt",
145
214
  "DocumentBlock",
146
- "DocumentConverter",
147
- "DocumentIngestError",
148
- "DocumentIngestResult",
149
- "DocumentIngestor",
150
215
  "DocumentPlan",
151
- "DocumentSourceInfo",
152
216
  "DocumentStylePreset",
153
217
  "EditorOpenSafetyReport",
154
218
  "ParagraphInfo",
155
219
  "PackageValidationReport",
156
220
  "BytePreservingPatchResult",
221
+ "HwpxError",
157
222
  "MutationReport",
158
223
  "PreservationDowngradeError",
159
224
  "PlanValidationReport",
160
225
  "ParagraphTextPatch",
161
226
  "PatchApplied",
162
227
  "PatchSkipped",
163
- "LayoutPreview",
164
- "PreviewPage",
165
228
  "SectionInfo",
166
- "TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION",
167
- "TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION",
168
229
  "TextExtractor",
169
230
  "FoundElement",
170
231
  "HwpxMarkdownConverter",
@@ -177,7 +238,6 @@ __all__ = [
177
238
  "STYLE_PROFILE_SCHEMA_VERSION",
178
239
  "TEMPLATE_REGISTRY_SCHEMA_VERSION",
179
240
  "TABLE_COMPUTE_REPORT_VERSION",
180
- "UnsupportedDocumentFormat",
181
241
  "apply_style_profile_to_plan",
182
242
  "build_comparison_table_plan",
183
243
  "build_image_grid",
@@ -190,8 +250,6 @@ __all__ = [
190
250
  "HwpxDocument",
191
251
  "HwpxPackage",
192
252
  "create_document_from_plan",
193
- "analyze_template_formfit",
194
- "apply_template_formfit",
195
253
  "approval_box",
196
254
  "describe_template",
197
255
  "extract_style_profile",
@@ -209,9 +267,6 @@ __all__ = [
209
267
  "validate_editor_open_safety",
210
268
  "validate_package",
211
269
  "paragraph_patch",
212
- "render_layout_preview",
213
- "render_document_viewer",
214
- "DocumentViewer",
215
270
  "register_template",
216
271
  "table_compute",
217
272
  ]
@@ -3,10 +3,10 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- import warnings
7
6
  from os import PathLike
8
7
  from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Sequence
9
8
 
9
+ from ..errors import SaveError
10
10
  from ..mutation_report import (
11
11
  Fallback,
12
12
  Mode,
@@ -84,7 +84,15 @@ def _run_pre_save_validation(doc: "HwpxDocument") -> None:
84
84
  report = doc.validate()
85
85
  if not report.ok:
86
86
  msgs = _summarize_validation_issues(report.issues)
87
- raise ValueError(f"Document validation failed: {msgs}")
87
+ raise SaveError(
88
+ f"Document validation failed: {msgs}",
89
+ code="document-validation-failed",
90
+ context={"issueCount": len(report.issues), "issues": msgs},
91
+ suggestion=(
92
+ "Fix the reported schema issues, or disable validate_on_save if "
93
+ "the validation is a false positive for your document."
94
+ ),
95
+ )
88
96
 
89
97
 
90
98
  def _run_open_safety_validation(doc: "HwpxDocument", archive_bytes: bytes) -> None:
@@ -94,9 +102,15 @@ def _run_open_safety_validation(doc: "HwpxDocument", archive_bytes: bytes) -> No
94
102
 
95
103
  report = validate_editor_open_safety(archive_bytes)
96
104
  if not report.ok:
97
- raise ValueError(
105
+ raise SaveError(
98
106
  "Generated HWPX package failed open-safety validation: "
99
- + report.summary
107
+ + report.summary,
108
+ code="open-safety-failed",
109
+ context={"summary": report.summary},
110
+ suggestion=(
111
+ "The serialized package would not reopen in an HWPX editor; "
112
+ "inspect validate_editor_open_safety(...) for the failing checks."
113
+ ),
100
114
  )
101
115
 
102
116
 
@@ -126,7 +140,15 @@ def _gate_and_write(
126
140
  )
127
141
  if not report.ok:
128
142
  detail = "; ".join(str(error) for error in report.errors)
129
- raise ValueError(f"Document save failed the quality gate: {detail}")
143
+ raise SaveError(
144
+ f"Document save failed the quality gate: {detail}",
145
+ code="quality-gate-failed",
146
+ context={"errors": [str(error) for error in report.errors]},
147
+ suggestion=(
148
+ "The SavePipeline quality gate rejected the archive; inspect the "
149
+ "returned report's errors before retrying."
150
+ ),
151
+ )
130
152
  return report
131
153
 
132
154
 
@@ -420,29 +442,3 @@ def _mark_save_clean(doc: "HwpxDocument") -> None:
420
442
  package = doc._package
421
443
  package._opened_members = dict(package._files)
422
444
  package._opened_zip_infos = dict(package._zip_infos)
423
-
424
-
425
- def save(
426
- doc: "HwpxDocument",
427
- path_or_stream: str | PathLike[str] | BinaryIO | None = None,
428
- ) -> str | PathLike[str] | BinaryIO | bytes:
429
- """Deprecated compatibility wrapper around save_to_path/save_to_stream/to_bytes.
430
-
431
- Deprecated:
432
- ``save()``는 하위 호환을 위해 유지되며 향후 제거될 수 있습니다.
433
- - 경로 저장: ``save_to_path(path)``
434
- - 스트림 저장: ``save_to_stream(stream)``
435
- - 바이트 반환: ``to_bytes()``
436
- """
437
-
438
- warnings.warn(
439
- "HwpxDocument.save()는 deprecated 예정입니다. "
440
- "save_to_path()/save_to_stream()/to_bytes() 사용을 권장합니다.",
441
- DeprecationWarning,
442
- stacklevel=2,
443
- )
444
- if path_or_stream is None:
445
- return doc.to_bytes()
446
- if isinstance(path_or_stream, (str, PathLike)):
447
- return doc.save_to_path(path_or_stream)
448
- return doc.save_to_stream(path_or_stream)
hwpx/document.py CHANGED
@@ -1844,28 +1844,3 @@ class HwpxDocument:
1844
1844
  self,
1845
1845
  reset_dirty=reset_dirty,
1846
1846
  )
1847
-
1848
-
1849
- @overload
1850
- def save(self, path_or_stream: None = None) -> bytes: ...
1851
-
1852
- @overload
1853
- def save(self, path_or_stream: str | PathLike[str]) -> str | PathLike[str]: ...
1854
-
1855
- @overload
1856
- def save(self, path_or_stream: BinaryIO) -> BinaryIO: ...
1857
-
1858
- def save(
1859
- self,
1860
- path_or_stream: str | PathLike[str] | BinaryIO | None = None,
1861
- ) -> str | PathLike[str] | BinaryIO | bytes:
1862
- """Deprecated compatibility wrapper around save_to_path/save_to_stream/to_bytes.
1863
-
1864
- Deprecated:
1865
- ``save()``는 하위 호환을 위해 유지되며 향후 제거될 수 있습니다.
1866
- - 경로 저장: ``save_to_path(path)``
1867
- - 스트림 저장: ``save_to_stream(stream)``
1868
- - 바이트 반환: ``to_bytes()``
1869
- """
1870
-
1871
- return _persistence.save(self, path_or_stream=path_or_stream)
hwpx/errors.py ADDED
@@ -0,0 +1,70 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Structured exception base for python-hwpx (S-091 P2).
3
+
4
+ Every fail-closed public contract raises a :class:`HwpxError` (or a subclass).
5
+ The base carries three machine-readable fields on top of the human-readable
6
+ message, so a caller can branch on a stable ``code``, read the measured
7
+ ``context``, and surface an actionable ``suggestion`` without parsing prose:
8
+
9
+ - ``code`` — a stable, kebab-case identifier for the failure class. Callers may
10
+ switch on it; it is part of the contract and changes only on a major boundary.
11
+ - ``context`` — a JSON-serialisable dict of the measured values that triggered
12
+ the failure (offending parts, indices, counts…). Empty when there is nothing
13
+ to measure.
14
+ - ``suggestion`` — one actionable next step, or ``None`` when there is nothing
15
+ specific to advise.
16
+
17
+ ``str(exc)`` stays the human sentence (the ``message``), so existing ``except``
18
+ handlers and log lines are unchanged. Subclasses set :attr:`default_code`, which
19
+ lets a historical ``raise Subclass("message")`` site keep working while gaining
20
+ the structured fields with no raise-site churn (§11 — no bulk raise rewrites).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, Mapping
26
+
27
+
28
+ class HwpxError(Exception):
29
+ """Base for structured, fail-closed python-hwpx errors."""
30
+
31
+ #: Stable ``code`` used when a raise site does not pass an explicit ``code``.
32
+ default_code: str = "hwpx-error"
33
+
34
+ def __init__(
35
+ self,
36
+ message: str,
37
+ *,
38
+ code: str | None = None,
39
+ context: Mapping[str, Any] | None = None,
40
+ suggestion: str | None = None,
41
+ ) -> None:
42
+ super().__init__(message)
43
+ self.message = message
44
+ self.code = code if code is not None else self.default_code
45
+ self.context: dict[str, Any] = dict(context) if context else {}
46
+ self.suggestion = suggestion
47
+
48
+ def to_dict(self) -> dict[str, Any]:
49
+ """The structured envelope: ``code`` / ``message`` / ``context`` / ``suggestion``."""
50
+
51
+ return {
52
+ "code": self.code,
53
+ "message": self.message,
54
+ "context": dict(self.context),
55
+ "suggestion": self.suggestion,
56
+ }
57
+
58
+
59
+ class SaveError(HwpxError, ValueError):
60
+ """A representative save path (``save_to_path`` / ``save_to_stream`` /
61
+ ``to_bytes``) failed closed before writing any output.
62
+
63
+ Subclasses :class:`ValueError` for backward compatibility: pre-4.0 callers
64
+ caught ``ValueError`` from these paths and must keep working.
65
+ """
66
+
67
+ default_code = "save-failed"
68
+
69
+
70
+ __all__ = ["HwpxError", "SaveError"]
hwpx/experimental.py ADDED
@@ -0,0 +1,48 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """실험적(experimental) 공개 표면.
3
+
4
+ 여기 노출되는 이름들은 **계약이 유동적**입니다 — minor 릴리스에서 시그니처,
5
+ 동작, 반환 스키마가 예고 없이 바뀔 수 있습니다. 안정 계약이 필요하면 stable 표면
6
+ (:mod:`hwpx` 최상위 ``__all__`` / ``docs/stable-api.md``)만 사용하세요.
7
+
8
+ 이 모듈은 실제 구현 모듈에서 이름을 **재내보내기(re-export)** 만 합니다(모듈을
9
+ 옮기지 않음). 최상위 ``from hwpx import ...`` 경로도 하위 호환을 위해 당분간
10
+ 동작하지만 ``DeprecationWarning``을 내며 다음 major에서 제거될 예정입니다 —
11
+ 새 코드는 ``from hwpx.experimental import ...``를 쓰세요.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from .ingest import (
17
+ ConversionAttempt,
18
+ DocumentConverter,
19
+ DocumentIngestError,
20
+ DocumentIngestResult,
21
+ DocumentIngestor,
22
+ DocumentSourceInfo,
23
+ UnsupportedDocumentFormat,
24
+ )
25
+ from .tools.document_viewer import (
26
+ DocumentViewer,
27
+ render_document_viewer,
28
+ )
29
+ from .tools.layout_preview import (
30
+ LayoutPreview,
31
+ PreviewPage,
32
+ render_layout_preview,
33
+ )
34
+
35
+ __all__ = [
36
+ "ConversionAttempt",
37
+ "DocumentConverter",
38
+ "DocumentIngestError",
39
+ "DocumentIngestResult",
40
+ "DocumentIngestor",
41
+ "DocumentSourceInfo",
42
+ "UnsupportedDocumentFormat",
43
+ "LayoutPreview",
44
+ "PreviewPage",
45
+ "render_layout_preview",
46
+ "DocumentViewer",
47
+ "render_document_viewer",
48
+ ]
hwpx/form_fit/wordbox.py CHANGED
@@ -681,6 +681,14 @@ def extract_cell_clips(pdf_path: str, *, page: int | None = None) -> list[Rect]:
681
681
  These are the overflow clips — same coordinate space as the glyph boxes — so
682
682
  no HWPX→PDF transform is needed. ``find_tables`` can raise on odd pages; such
683
683
  a page contributes no clips rather than crashing.
684
+
685
+ Borders-only (``strategy="lines_strict"``): a cell rectangle is defined by its
686
+ *drawn borders*, not by the text inside it. The ``find_tables`` default snaps
687
+ edges to glyph positions, so a filled value can appear to *escape* a clip that
688
+ was mis-sized around the very text it holds — a false overflow. S-094 P3
689
+ measured this: the only 3 corpus pairs that flagged ``newOverflow`` (the nts
690
+ forms) all had the fill value escaping a text-snapped clip under the default,
691
+ and 0 escapes under ``lines_strict``; there were no real escapes to mask.
684
692
  """
685
693
 
686
694
  if not fitz_available():
@@ -695,7 +703,7 @@ def extract_cell_clips(pdf_path: str, *, page: int | None = None) -> list[Rect]:
695
703
  with doc:
696
704
  for pno in _page_indices(doc, page):
697
705
  try:
698
- tables = doc[pno].find_tables().tables
706
+ tables = doc[pno].find_tables(strategy="lines_strict").tables
699
707
  except Exception:
700
708
  continue # layout analysis failed on this page -> no clips, no crash
701
709
  for ti, table in enumerate(tables):
@@ -839,8 +847,21 @@ class LayoutSignature:
839
847
  lesson that absolute glyph-overlap is unusable on real forms). Page count is
840
848
  the dominant, high-confidence signal — a fill that spills onto a new page is
841
849
  the canonical layout collapse. Per-table ``(rows, cols)`` is the finer
842
- row/column signal; ``find_tables`` is content-sensitive, so it is judged as a
843
- multiset *delta* (blank vs filled), not an absolute count.
850
+ row/column signal, extracted from *drawn table borders only*
851
+ (``find_tables(strategy="lines_strict")``): a structural fingerprint must be
852
+ text-independent, so it is judged as a multiset *delta* (blank vs filled),
853
+ not an absolute count.
854
+
855
+ Why ``lines_strict`` and not the ``find_tables`` default (``"lines"``): the
856
+ default snaps table edges to *text* positions, so injecting a value into a
857
+ previously-empty cell makes it hallucinate a phantom ``(2,2)``/``(1,2)`` table
858
+ or wobble a real one by ±1 row/col even when the drawn grid and page count are
859
+ unchanged. S-094 P3 measured this on the frozen corpus (all 6 "table-shape"
860
+ differential failures were page-stable phantoms; under ``lines_strict`` blank
861
+ and filled agree, 0 regressions across 31 pairs; ``sen-24`` has byte-identical
862
+ drawn rectangles yet the default strategy still invented a ``(1,2)`` table from
863
+ the 3 added glyphs). Borders-only detection measures the grid the fill did not
864
+ touch.
844
865
  """
845
866
 
846
867
  page_count: int
@@ -879,7 +900,12 @@ def extract_layout_signature(pdf_path: str) -> LayoutSignature:
879
900
  for page in doc:
880
901
  sizes.append((float(page.rect.width), float(page.rect.height)))
881
902
  try:
882
- tables = page.find_tables().tables
903
+ # Borders-only ("lines_strict"): a *structural* fingerprint must
904
+ # not react to cell text. The default strategy snaps to glyph
905
+ # positions and invents phantom tables from a fill's added text
906
+ # (S-094 P3). Bordered form grids are detected identically either
907
+ # way; the text-sensitivity is the only difference we drop.
908
+ tables = page.find_tables(strategy="lines_strict").tables
883
909
  except Exception:
884
910
  continue # layout analysis failed on this page -> no tables, no crash
885
911
  for table in tables:
hwpx/mutation_report.py CHANGED
@@ -22,6 +22,8 @@ from io import BytesIO
22
22
  from typing import Any, Literal, Mapping, Sequence
23
23
  from zipfile import ZipInfo
24
24
 
25
+ from .errors import HwpxError
26
+
25
27
  MUTATION_REPORT_SCHEMA = "hwpx.mutation-report/v1"
26
28
  COORDINATE_SPACE = "uncompressed-part-bytes"
27
29
 
@@ -229,9 +231,19 @@ class MutationReport:
229
231
  }
230
232
 
231
233
 
232
- class PreservationDowngradeError(Exception):
234
+ class PreservationDowngradeError(HwpxError):
233
235
  """Raised when a requested preservation grade is not achieved and
234
- ``fallback="error"`` — before any output is written (specs/032 §1)."""
236
+ ``fallback="error"`` — before any output is written (specs/032 §1).
237
+
238
+ Structured on the :class:`~hwpx.errors.HwpxError` base: ``code`` is the stable
239
+ ``"preservation-downgrade"`` identifier, ``context`` carries the measured
240
+ modes and offending parts, and ``suggestion`` is the actionable next step.
241
+ The historical positional attributes (``requested_mode``/``achieved_grade``/
242
+ ``offending_parts``/``suggestion``) are preserved so existing handlers still
243
+ read them.
244
+ """
245
+
246
+ default_code = "preservation-downgrade"
235
247
 
236
248
  def __init__(
237
249
  self,
@@ -244,12 +256,18 @@ class PreservationDowngradeError(Exception):
244
256
  self.requested_mode = requested_mode
245
257
  self.achieved_grade = achieved_grade
246
258
  self.offending_parts = offending_parts
247
- self.suggestion = suggestion
248
259
  parts = ", ".join(offending_parts) if offending_parts else "(none)"
249
260
  super().__init__(
250
261
  f"requested mode {requested_mode!r} needs patch-grade preservation but "
251
262
  f"the save achieved {achieved_grade!r}; offending parts: {parts}. "
252
- f"{suggestion}"
263
+ f"{suggestion}",
264
+ code=self.default_code,
265
+ context={
266
+ "requestedMode": requested_mode,
267
+ "achievedGrade": achieved_grade,
268
+ "offendingParts": list(offending_parts),
269
+ },
270
+ suggestion=suggestion,
253
271
  )
254
272
 
255
273
 
hwpx/table_patch.py CHANGED
@@ -28,6 +28,7 @@ from dataclasses import dataclass
28
28
  from pathlib import Path
29
29
  from typing import Any, Iterable, Mapping, Sequence
30
30
 
31
+ from .errors import HwpxError
31
32
  from .mutation_report import MutationReport, project_byte_splice
32
33
  from .patch import (
33
34
  _apply_edits,
@@ -803,8 +804,16 @@ _S_TR = re.compile(r"<hp:tr\b.*?</hp:tr>", re.DOTALL)
803
804
  _S_TC = re.compile(r"<hp:tc\b.*?</hp:tc>", re.DOTALL)
804
805
 
805
806
 
806
- class TableStructureError(ValueError):
807
- """A structure edit was refused (fail-closed) or is unsupported."""
807
+ class TableStructureError(HwpxError, ValueError):
808
+ """A structure edit was refused (fail-closed) or is unsupported.
809
+
810
+ Structured on :class:`~hwpx.errors.HwpxError` (``code="table-structure"``)
811
+ while still subclassing :class:`ValueError` for backward compatibility. The
812
+ many string-only raise sites keep working and gain the ``code``/``context``/
813
+ ``suggestion`` fields via the base default.
814
+ """
815
+
816
+ default_code = "table-structure"
808
817
 
809
818
 
810
819
  def _si(chunk: str, tag: str, attr: str) -> int | None:
@@ -1645,8 +1654,14 @@ def apply_table_ops(
1645
1654
 
1646
1655
  # --- P3: real-Hancom oracle gate for form-fill (FR-005) -----------------------
1647
1656
 
1648
- class RenderCheckRequired(RuntimeError):
1649
- """``verify_fill(require=True)`` but no real Hancom oracle rendered."""
1657
+ class RenderCheckRequired(HwpxError, RuntimeError):
1658
+ """``verify_fill(require=True)`` but no real Hancom oracle rendered.
1659
+
1660
+ Structured on :class:`~hwpx.errors.HwpxError` (``code="render-check-required"``)
1661
+ while still subclassing :class:`RuntimeError` for backward compatibility.
1662
+ """
1663
+
1664
+ default_code = "render-check-required"
1650
1665
 
1651
1666
 
1652
1667
  def verify_fill(
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-hwpx
3
+ Version: 4.1.1
4
+ Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
5
+ Author: python-hwpx Maintainers
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/airmang/python-hwpx
8
+ Project-URL: Documentation, https://airmang.github.io/python-hwpx/
9
+ Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
10
+ Keywords: hwp,hwpx,hancom,opc,xml,document-automation,validation,template
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Text Processing :: Markup :: XML
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ License-File: NOTICE
23
+ Requires-Dist: lxml<7,>=4.9
24
+ Provides-Extra: visual
25
+ Requires-Dist: pymupdf>=1.24; extra == "visual"
26
+ Requires-Dist: pillow>=10.0; extra == "visual"
27
+ Requires-Dist: numpy>=1.26; extra == "visual"
28
+ Provides-Extra: xlsx
29
+ Requires-Dist: openpyxl>=3.1; extra == "xlsx"
30
+ Provides-Extra: preview
31
+ Requires-Dist: latex2mathml>=3.77; extra == "preview"
32
+ Provides-Extra: dev
33
+ Requires-Dist: build>=1.0; extra == "dev"
34
+ Requires-Dist: twine>=4.0; extra == "dev"
35
+ Requires-Dist: pytest>=7.4; extra == "dev"
36
+ Requires-Dist: latex2mathml>=3.77; extra == "dev"
37
+ Provides-Extra: test
38
+ Requires-Dist: build>=1.0; extra == "test"
39
+ Requires-Dist: numpy>=1.26; extra == "test"
40
+ Requires-Dist: pillow>=10.0; extra == "test"
41
+ Requires-Dist: pytest>=7.4; extra == "test"
42
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
43
+ Requires-Dist: ruff>=0.12; extra == "test"
44
+ Requires-Dist: latex2mathml>=3.77; extra == "test"
45
+ Provides-Extra: typecheck
46
+ Requires-Dist: mypy>=1.10; extra == "typecheck"
47
+ Requires-Dist: pyright>=1.1.390; extra == "typecheck"
48
+ Dynamic: license-file
49
+
50
+ <p align="center">
51
+ <h1 align="center">python-hwpx</h1>
52
+ <p align="center">
53
+ <strong>한컴 없이 HWPX를 읽고, 고치고, 만드는 순수 파이썬 라이브러리</strong>
54
+ </p>
55
+ <p align="center">
56
+ <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
57
+ <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
58
+ <a href="https://github.com/airmang/python-hwpx/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/airmang/python-hwpx/tests.yml?branch=main&label=tests" alt="Tests"></a>
59
+ <a href="https://airmang.github.io/python-hwpx/corpus-metrics.html"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fairmang.github.io%2Fpython-hwpx%2F_static%2Fbadge-hancom-open.json" alt="Hancom open"></a>
60
+ <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License"></a>
61
+ </p>
62
+ </p>
63
+
64
+ <p align="center">한국어 | <a href="README_EN.md">English</a></p>
65
+
66
+ 기존 문서는 손댄 곳만 고치고(미수정 영역은 바이트 그대로), 새 문서는 실제
67
+ 한컴오피스가 받아들이는 형태로 만듭니다. HWPX는 ZIP+XML(OWPML/OPC) 구조라
68
+ Windows·macOS·Linux·CI 어디서든 순수 파이썬으로 동작합니다.
69
+
70
+ | | 레포 | 역할 |
71
+ |---|---|---|
72
+ | 📦 | **`python-hwpx`** | 순수 파이썬 HWPX 코어 (이 레포) |
73
+ | 🔌 | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | MCP 클라이언트(Claude Desktop 등)에서 HWPX 조작 |
74
+ | 🎯 | [`hwpx-plugin`](https://github.com/airmang/hwpx-plugins) | 에이전트용 플러그인·스킬 번들 |
75
+
76
+ ## 시작하기
77
+
78
+ ```bash
79
+ pip install python-hwpx # Python 3.10+
80
+ ```
81
+
82
+ ```python
83
+ from hwpx import HwpxDocument
84
+
85
+ doc = HwpxDocument.open("보고서.hwpx")
86
+ doc.add_paragraph("자동화로 추가한 문단입니다.")
87
+ doc.save_to_path("보고서-수정.hwpx")
88
+ ```
89
+
90
+ ## 무엇을 하나
91
+
92
+ - **읽기·추출** — 텍스트/HTML/rich Markdown 내보내기(서식·중첩 표·각주 보존), XPath 객체 탐색
93
+ - **편집** — 문단·표·이미지·머리글/바닥글·메모·각주, 줄간격·여백·쪽번호 등 서식
94
+ - **양식 채우기** — 라벨·경로 기반 셀 채움, 바이트 보존 구조 편집(행·열·오토핏·shrink-to-fit)
95
+ - **생성** — 조립형 builder, 공문 lint·결재란, 사진대지·명패·조직도, mail merge, 신구대조표
96
+ - **변경추적·목차** — redline 저작, 네이티브 목차·상호참조
97
+ - **검증·안전** — XSD·패키지 검증 CLI, 열림 안전 게이트, 모든 쓰기에 영수증(`MutationReport`)
98
+
99
+ 자세한 내용: [사용 가이드](docs/usage.md) · [API 레퍼런스](https://airmang.github.io/python-hwpx/) · [안정 API 표면](docs/stable-api.md) · [예제](docs/examples.md)
100
+
101
+ ### 양식 채우기 — 서식은 그대로, 값만
102
+
103
+ ```python
104
+ doc = HwpxDocument.open("신청서.hwpx")
105
+ result = doc.fill_by_path({
106
+ "성명 > right": "홍길동",
107
+ "소속 > right": "플랫폼팀",
108
+ })
109
+ doc.save_to_path("신청서-작성완료.hwpx")
110
+ ```
111
+
112
+ 라벨 기준으로 셀을 찾아 채우고, 손대지 않은 영역은 원본 바이트가 그대로 유지됩니다.
113
+
114
+ ### 저장에는 영수증이 따라옵니다
115
+
116
+ ```python
117
+ report = doc.save_to_path("결과.hwpx", return_report=True)
118
+ print(report.actual_mode) # "patch" — 문서 재조립 없이 저장됨
119
+ print(report.preservation.untouched_part_payloads.to_dict())
120
+ # {"verified": 17, "changed": 0}
121
+ ```
122
+
123
+ 요청한 보존 등급을 지킬 수 없으면 아무것도 쓰지 않고 실패합니다(fail-closed).
124
+ 전체 규칙은 [안전한 쓰기 계약](docs/safe-write-contract.md)에 있습니다.
125
+
126
+ ## 실측으로 말합니다
127
+
128
+ 산출물 전수를 실제 한컴오피스로 측정해 그대로 공개합니다(동결 코퍼스 N=497):
129
+
130
+ - **한컴 오픈 476/476 all-pass** — 우리가 만든 파일을 실한컴이 전부 엽니다
131
+ - **미수정 영역 바이트 보존 497/497** · 개인정보 0-leak
132
+ - **렌더 검증 416/476** + 정직 버킷 43 — 한컴 자체가 PDF export를 거부한 케이스도 숨기지 않고 집계
133
+ - 낮은 숫자도 그대로 발행합니다 — 전체 수치·주의사항: [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html)
134
+
135
+ 기능별로 되는 것과 안 되는 것은 [지원 매트릭스](docs/support-matrix.md)에 등급으로
136
+ 명시되어 있습니다. 현재 개발 상태는 Alpha입니다 — API는 바뀔 수 있습니다.
137
+
138
+ > 위 수치는 *생성물 수용률* 축입니다(만든 파일을 실한컴이 받는가). 문서 *파싱 recall*과는
139
+ > 다른 축이므로 파서 프로젝트 수치와 병치 비교하지 마세요.
140
+
141
+ ## 비교
142
+
143
+ | | python-hwpx | pyhwpx | pyhwp |
144
+ |---|---|---|---|
145
+ | **대상 포맷** | `.hwpx` (OWPML/OPC) | `.hwpx` | `.hwp` (v5 바이너리) |
146
+ | **한/글 설치** | 불필요 | 필요 (Windows COM) | 불필요 |
147
+ | **크로스 플랫폼** | ✅ Linux / macOS / Windows / CI | ❌ Windows 전용 | ✅ |
148
+ | **편집/생성 API** | ✅ | ✅ (COM) | ❌ 대부분 읽기 |
149
+ | **AI 에이전트 연동 (MCP)** | ✅ | ❌ | ❌ |
150
+
151
+ > HWP(v5 바이너리)는 지원하지 않습니다. 한컴오피스에서 HWPX로 변환 후 사용하세요.
152
+
153
+ ## 알려진 제약
154
+
155
+ - `add_shape()` / `add_control()`은 한/글이 요구하는 모든 하위 요소를 생성하지 않습니다.
156
+ - `<hp:pic>` 그림 개체의 완전 자동 생성은 제공하지 않습니다.
157
+ - 암호화된 HWPX는 지원하지 않습니다.
158
+
159
+ ## 기여하기
160
+
161
+ [help wanted](https://github.com/airmang/python-hwpx/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) ·
162
+ [로드맵](https://github.com/airmang/python-hwpx/milestones) ·
163
+ [Discussions](https://github.com/airmang/python-hwpx/discussions) ·
164
+ [내부 실전 가이드](docs/internals/) ·
165
+ [CONTRIBUTING](CONTRIBUTING.md)
166
+
167
+ HWPX 내부 구조가 처음이라면 [내부 실전 가이드](docs/internals/)부터 — 실제 한/글
168
+ 동작에서 확인된 조판 캐시·목차 필드·OPC 재패킹 같은 실전 지식을 정리해 두었습니다.
169
+
170
+ ## 감사의 말
171
+
172
+ 아래 공개 표준·프로젝트에 빚지고 있습니다.
173
+
174
+ - **[OWPML — 개방형 워드프로세서 마크업 언어 (KS X 6101)](https://www.kssn.net/search/stddetail.do?itemNo=K001010119985)** — HWPX가 기반하는 한국 산업 표준
175
+ - **[hancom-io/hwpx-owpml-model](https://github.com/hancom-io/hwpx-owpml-model)** — OWPML 요소 구조 참조 모델 · **[neolord0/hwpxlib](https://github.com/neolord0/hwpxlib)** — 오라클 샘플 코퍼스
176
+ - **[edwardkim/rhwp](https://github.com/edwardkim/rhwp)** — 멱등성·검증 게이트 설계 영감
177
+ - **범정부오피스** — 공무 문서 편집 워크플로 아이디어
178
+
179
+ ## License · Maintainer
180
+
181
+ Apache-2.0 ([LICENSE](LICENSE) · [NOTICE](NOTICE)) — **Kohkyuhyun** [@airmang](https://github.com/airmang) · [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
@@ -1,17 +1,19 @@
1
- hwpx/__init__.py,sha256=HisWQIT_6hib7mZizJnOixVZ04CtEb0Jb7ldWpEOy5U,5774
1
+ hwpx/__init__.py,sha256=7o5kxSumBqqFnhLrYojfCrvRbq3I0cE5DxxIp3cFeB8,8600
2
2
  hwpx/authoring.py,sha256=BoWGNc50ALMSuyjFUGl1iMCLYb1rFllCKVcuugvsaOw,129001
3
3
  hwpx/body_patch.py,sha256=yB3X0L3Jns38CpRFOvjvPMnovJ5a2SoVefh6FIy3uYQ,25736
4
- hwpx/document.py,sha256=grSkZFmRAIIy6arVrP9DVbKrjKzsKopvSE8TFFmBVD8,60037
4
+ hwpx/document.py,sha256=_a0snPKzdsdIL9AmzvQzvOloqXB0CVq89vx6GjnKeHw,59186
5
+ hwpx/errors.py,sha256=BgWhEFHiiiz9DoCNRwovHt95dz8HVzQx1C29PGfXMwQ,2619
5
6
  hwpx/evalplan_fill.py,sha256=UmXj6uo63Bo1iBI9lB3_e2G-z8dT_8nQBywwvfrepfM,83578
7
+ hwpx/experimental.py,sha256=H8fBrmGf0BzoDS6dqwBpUChUestUgOpO7XjMEBXsssg,1499
6
8
  hwpx/fill_residue.py,sha256=Uq4DbwFNlbHrO3FQA7Yyk1I0gAsvPw6O8mPD82rmBws,8286
7
9
  hwpx/form_fill.py,sha256=VUIU53Qa9Ho2aP72biDvJwnDW7ngdAzu3PSd5A7d1JM,9908
8
10
  hwpx/formfill_quality.py,sha256=pRxZYb1cg95F2O78va5glMJ3UqPh1rewJB1Qz-LOlsc,38521
9
11
  hwpx/guidance_scan.py,sha256=62ltZsuaebWaDxDaMbbq870J5PQVZoz0ZCSZ39otBkE,25193
10
- hwpx/mutation_report.py,sha256=a5l9TtkfwOluXhTof88pVesjNr-7pD1vQ3G0wh0iuhI,18683
12
+ hwpx/mutation_report.py,sha256=6hurhDdgGiONeLIeWZJT8lwLtWJZ1VHp1l6G0zy0mQo,19409
11
13
  hwpx/package.py,sha256=0rKjGCJbPQvrVBIy07Jpjsu3fI7HhbqFCGWTiTDsJpo,1141
12
14
  hwpx/patch.py,sha256=6MEVGAjpaWKK7Oyxydfvk52-VzSJfHi6I-eUCGJHZQM,24846
13
15
  hwpx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- hwpx/table_patch.py,sha256=_i220iggNfrIXmQxq4eKO0OZWqZeUFRwAlev9FL1xKw,78208
16
+ hwpx/table_patch.py,sha256=R5DBCgImIU8e53nq9X3wqGPJlZ7pbZoiretgU2E__a8,78805
15
17
  hwpx/template_formfit.py,sha256=dlxf98FatT5Nl4hN15TuMXqj5NlDTGSnLaqXVo_PoBo,23424
16
18
  hwpx/templates.py,sha256=28bYqeJVeDb1Cq8G9NZG9Mhnu4K2GamAKC4QhxvUZyA,1187
17
19
  hwpx/_document/__init__.py,sha256=gJ7N5W4EANv_341NJCtbxSh0QrNIBtArRAnjJUe_PE4,98
@@ -20,7 +22,7 @@ hwpx/_document/fields.py,sha256=YvrbWyjcDtZDrWSMQkR31maCpYvhwYEWkNtxTFVxXGY,1969
20
22
  hwpx/_document/layout.py,sha256=2Y3JUnN5-_zZHg0CxfXq4nl8MjqFoqQbSlgszCCzJ6g,23097
21
23
  hwpx/_document/media.py,sha256=F3iquwS7V-luiBVg7H8Aq1DpNXNClPDmO1ug99dLyhw,11600
22
24
  hwpx/_document/memos.py,sha256=LBr5OPKLQMm4cHFIg9HohcMyKbf8DPpdmR3jeiyRNMg,7489
23
- hwpx/_document/persistence.py,sha256=dZDXRLdKjjN6JAjVnJAZngXhMvwJp4WgYcZGgd0vvX8,15978
25
+ hwpx/_document/persistence.py,sha256=gPxo7alBywBPBu3iACeQs_gK3w3ruFd2K7J90cr4Keo,16011
24
26
  hwpx/_document/shapes.py,sha256=RoDtvlGvLshIKMsqDbiKfGz0pin63qkLHVtWGbdzb-0,6165
25
27
  hwpx/_document/tracked.py,sha256=ni4tglO1LuukTnRz6I__9kW0P8uYCSy9BEuxZMrinqc,3423
26
28
  hwpx/agent/__init__.py,sha256=jMMxl9cvGrRtdX669yNmLzrk_CK3UlhXGjXQHy4ERpo,3400
@@ -106,7 +108,7 @@ hwpx/form_fit/measure.py,sha256=gnSfqccm7_3oTKv3HH6Zs77r2bToxzmfvUWN2zshDE8,2232
106
108
  hwpx/form_fit/policy.py,sha256=iXAatjS7ZycOdKgMCL4Q_kYg0qnogFPdRNlr_ecOvjc,3106
107
109
  hwpx/form_fit/report.py,sha256=h1NzQfbj1JYvZtKUi0DckJNWGnUHMZtQ3iCzbUxUjHA,3305
108
110
  hwpx/form_fit/seal.py,sha256=QsPzFagaEi7UKAXS-iFwiZs8rLG-_Fo9RixLc2OaqbU,17640
109
- hwpx/form_fit/wordbox.py,sha256=bStyzpcK4PF3pihRUSKiStwW_XwYHDeU0D3Or_Eh0nM,48301
111
+ hwpx/form_fit/wordbox.py,sha256=n86BegyBZMybf6Qxol6hWCG1r0EWiaJRyZEno_YkpIE,50170
110
112
  hwpx/ingest/__init__.py,sha256=cjDQwMdaqbq2xiD32zmaFAyzZbENga_6MLbgo0sLGZE,636
111
113
  hwpx/ingest/base.py,sha256=95jXJ70Hkay70BgyryGOzI3UVTixUpR-6f71iF3eqn4,8134
112
114
  hwpx/ingest/hwpx_converter.py,sha256=HOUCFYO-HK4WRpWiN5sA_kZIvdv439mkZ6PMCFU3hLg,4269
@@ -207,10 +209,10 @@ hwpx/visual/page_qa.py,sha256=AuJCySfLIdXjPeekhAc3YnrIWOsN0sI35Pnz8BZ6Pro,7278
207
209
  hwpx/visual/qa_contracts.py,sha256=1cjoiRTAWgi7NgE8SPc6bKxdoUfKx9nyou1mgC47TxY,10506
208
210
  hwpx/visual/qa_metrics.py,sha256=I7RdybN-f1Fvcv2j-ceDPoek51nOkMui1Zrd7TEX_C8,9838
209
211
  hwpx/visual/report.py,sha256=2RhXN1KBYOZTim9FNpeUhaaDHR7oFxI6Z2DLUkDIiwE,1717
210
- python_hwpx-3.8.0.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
211
- python_hwpx-3.8.0.dist-info/licenses/NOTICE,sha256=KJgtwIIzrXoA0j3PUhwp78ZtnucocEoB7jiuwoL4i7o,3060
212
- python_hwpx-3.8.0.dist-info/METADATA,sha256=O2fepIIHxJZFjeuuJZzzAVtvg6WvxiWxLdT03-vYCg0,20512
213
- python_hwpx-3.8.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
214
- python_hwpx-3.8.0.dist-info/entry_points.txt,sha256=PVUBTBQtBHiflQ9XBjfd004w-LrDibgZjwzxgu8Tx7w,480
215
- python_hwpx-3.8.0.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
216
- python_hwpx-3.8.0.dist-info/RECORD,,
212
+ python_hwpx-4.1.1.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
213
+ python_hwpx-4.1.1.dist-info/licenses/NOTICE,sha256=KJgtwIIzrXoA0j3PUhwp78ZtnucocEoB7jiuwoL4i7o,3060
214
+ python_hwpx-4.1.1.dist-info/METADATA,sha256=5R5nvsB-mwhZxnwU0KQG3wXscXzEBa6TjdvA4iknZhg,9164
215
+ python_hwpx-4.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
216
+ python_hwpx-4.1.1.dist-info/entry_points.txt,sha256=PVUBTBQtBHiflQ9XBjfd004w-LrDibgZjwzxgu8Tx7w,480
217
+ python_hwpx-4.1.1.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
218
+ python_hwpx-4.1.1.dist-info/RECORD,,
@@ -1,344 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-hwpx
3
- Version: 3.8.0
4
- Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
5
- Author: python-hwpx Maintainers
6
- License-Expression: Apache-2.0
7
- Project-URL: Homepage, https://github.com/airmang/python-hwpx
8
- Project-URL: Documentation, https://airmang.github.io/python-hwpx/
9
- Project-URL: Issues, https://github.com/airmang/python-hwpx/issues
10
- Keywords: hwp,hwpx,hancom,opc,xml,document-automation,validation,template
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Topic :: Software Development :: Libraries
18
- Classifier: Topic :: Text Processing :: Markup :: XML
19
- Requires-Python: >=3.10
20
- Description-Content-Type: text/markdown
21
- License-File: LICENSE
22
- License-File: NOTICE
23
- Requires-Dist: lxml<7,>=4.9
24
- Provides-Extra: visual
25
- Requires-Dist: pymupdf>=1.24; extra == "visual"
26
- Requires-Dist: pillow>=10.0; extra == "visual"
27
- Requires-Dist: numpy>=1.26; extra == "visual"
28
- Provides-Extra: xlsx
29
- Requires-Dist: openpyxl>=3.1; extra == "xlsx"
30
- Provides-Extra: preview
31
- Requires-Dist: latex2mathml>=3.77; extra == "preview"
32
- Provides-Extra: dev
33
- Requires-Dist: build>=1.0; extra == "dev"
34
- Requires-Dist: twine>=4.0; extra == "dev"
35
- Requires-Dist: pytest>=7.4; extra == "dev"
36
- Requires-Dist: latex2mathml>=3.77; extra == "dev"
37
- Provides-Extra: test
38
- Requires-Dist: build>=1.0; extra == "test"
39
- Requires-Dist: numpy>=1.26; extra == "test"
40
- Requires-Dist: pillow>=10.0; extra == "test"
41
- Requires-Dist: pytest>=7.4; extra == "test"
42
- Requires-Dist: pytest-cov>=5.0; extra == "test"
43
- Requires-Dist: ruff>=0.12; extra == "test"
44
- Requires-Dist: latex2mathml>=3.77; extra == "test"
45
- Provides-Extra: typecheck
46
- Requires-Dist: mypy>=1.10; extra == "typecheck"
47
- Requires-Dist: pyright>=1.1.390; extra == "typecheck"
48
- Dynamic: license-file
49
-
50
- <p align="center">
51
- <h1 align="center">python-hwpx</h1>
52
- <p align="center">
53
- <strong>한컴 없이 HWPX를 안전하게 자동화하는 Python 계층 — 최소 범위 편집, 검증된 저작, 모든 쓰기에 영수증.</strong>
54
- </p>
55
- <p align="center">
56
- <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/v/python-hwpx?color=blue&label=PyPI" alt="PyPI"></a>
57
- <a href="https://pypi.org/project/python-hwpx/"><img src="https://img.shields.io/pypi/pyversions/python-hwpx" alt="Python"></a>
58
- <a href="https://github.com/airmang/python-hwpx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License"></a>
59
- <a href="https://airmang.github.io/python-hwpx/"><img src="https://img.shields.io/badge/docs-Sphinx-8CA1AF" alt="Docs"></a>
60
- <a href="https://github.com/airmang/python-hwpx/actions/workflows/tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/airmang/python-hwpx/tests.yml?branch=main&label=tests" alt="Tests"></a>
61
- <a href="https://airmang.github.io/python-hwpx/corpus-metrics.html"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fairmang.github.io%2Fpython-hwpx%2F_static%2Fbadge-hancom-open.json" alt="Hancom open"></a>
62
- </p>
63
- </p>
64
-
65
- <p align="center">한국어 | <a href="README_EN.md">English</a></p>
66
-
67
- ---
68
-
69
- > **python-hwpx는 한컴 없이 HWPX를 안전하게 자동화하는 Python 계층입니다.** 기존
70
- > 문서는 최소 범위만 수정하고, 새 문서는 실제 한컴 수용이 검증된 형태로 생성하며,
71
- > 모든 쓰기에 변경·보존·검증 영수증을 남기고, 완전한 해석과 렌더링은 전문 백엔드에
72
- > 위임할 수 있습니다.
73
-
74
- - **최소 범위 편집** — 미수정 part는 저장 시 바이트 그대로 유지됩니다(patch 경로
75
- 바이트 보존 497/497, 동결 코퍼스 v2 · 2026-07-19).
76
- - **검증된 저작** — 밑바닥 생성도 실제 한컴이 받아들이는 형태로 냅니다(산출물 한컴
77
- 오픈 476/476 all-pass, 실저작 품질 게이트 58/58).
78
- - **모든 쓰기에 영수증** — 대표 저장 경로는 [Safe Write Contract](docs/safe-write-contract.md)의
79
- `MutationReport`(`hwpx.mutation-report/v1`)로 실제 쓰기 모드·보존 등급·검증 결과를
80
- **측정해** 반환합니다.
81
-
82
- ---
83
-
84
- ## 🧩 HWPX Stack (3종)
85
-
86
- | 계층 | 레포 | 역할 |
87
- |---|---|---|
88
- | 📦 라이브러리 | **[`python-hwpx`](https://github.com/airmang/python-hwpx)** | 순수 파이썬 HWPX 파싱·편집·생성 코어 |
89
- | 🔌 MCP 서버 | [`hwpx-mcp-server`](https://github.com/airmang/hwpx-mcp-server) | MCP 클라이언트(Claude Desktop, VS Code 등)에서 HWPX 조작 |
90
- | 🎯 에이전트 스킬 | [`hwpx-plugin`](https://github.com/airmang/hwpx-plugins) | 에이전트가 HWPX를 바로 쓰게 해주는 first-party 플러그인·스킬 번들 |
91
-
92
- `hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동
93
- 구성요소입니다(유지보수 관계를 뜻하며, 한컴 등 외부 기관의 인증을 뜻하지 않습니다).
94
-
95
- 현재 PyPI 공개 릴리스는 `python-hwpx 3.8.0`입니다. 일반
96
- `pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
97
- 현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
98
- 성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
99
-
100
- ---
101
-
102
- ## 실측으로 말합니다 — Published Corpus
103
-
104
- 이 스택의 산출물은 주장 대신 **실제 한컴오피스 전수 측정**으로 검증됩니다
105
- (동결 코퍼스 v2, N=497 산출물, 2026-07-19, 실한컴 12.0.0.3288 COM/GUI 오라클;
106
- 상세·주의사항은
107
- [실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html)):
108
-
109
- - **한컴 오픈 수용률 476/476 all-pass** (동결 코퍼스 v2 · 2026-07-19 · 실한컴 COM
110
- `Open()` 판정 · rule-of-three 하한 99.37%) · 파싱 96.2%(458/476)
111
- - **미수정 영역 바이트 보존 497/497** (patch 경로 한정, zip-part diff · 오라클 불요)
112
- · **개인정보 0-leak** (35문서/합성 140값)
113
- - 렌더 검증 416/476 (실한컴 `SaveAs("PDF")`) + 정직 버킷 43건(변경추적 문서의 PDF
114
- export는 한컴 자체가 거부 — 실측 한계로 발행) + 미검증 17건
115
- - wild 공개 양식 채움은 구조결함 픽스 후 **무음 서식파괴 16.7%**(판정 66조합, 못 담는 타깃은 typed 거부 35건·산출분 pass 17/28) — **낮은 숫자도 그대로 발행**하고 잔여(페이지 리플·표 shape)를 명명합니다
116
-
117
- <p align="center">
118
- <img src="https://raw.githubusercontent.com/airmang/python-hwpx/main/docs/images/redline-hancom.png" alt="python-hwpx가 남긴 변경추적과 AI 에이전트 메모가 실제 한/글에서 열린 화면" width="720">
119
- </p>
120
- <p align="center"><sub>python-hwpx로 작성한 변경추적(취소선·삽입)과 AI 에이전트 메모 — 실제 한/글에서 연 화면입니다.</sub></p>
121
-
122
- > 이 숫자들은 *생성물 수용률* 축입니다(우리가 만든 파일을 실제 한컴이 받아들이는가).
123
- > 문서 *파싱 recall*과는 다른 축이므로 파서 프로젝트 수치와 병치 비교하지 마세요.
124
-
125
- ---
126
-
127
- ## 왜 python-hwpx인가
128
-
129
- - **코어 편집에 한컴오피스 설치 불필요** — HWPX는 ZIP+XML(OWPML/OPC) 구조라, 순수 파이썬으로 Windows·macOS·Linux·CI 어디서나 읽고 씁니다.
130
- - **읽기부터 생성까지 한 코어** — 텍스트/서식 추출, 문단·표·양식 편집, 새 문서 생성, XSD 스키마 검증을 하나의 API로 처리합니다.
131
- - **에이전트·자동화 친화** — 같은 프로젝트가 유지보수하는 `hwpx-mcp-server`와 `hwpx-plugin`이 코어에 연결됩니다.
132
-
133
- 문서 파싱·편집·생성은 순수 Python으로 수행할 수 있습니다. 다만 페이지 나눔, 표 넘침,
134
- 글꼴 대체 등 최종 시각 품질을 확언하려면 필요에 따라 실제 한컴 렌더 오라클을 별도로 사용합니다.
135
-
136
- ## 빠른 시작
137
-
138
- ```bash
139
- pip install python-hwpx # Python 3.10+ · lxml ≥ 4.9
140
- ```
141
-
142
- ```python
143
- from hwpx import HwpxDocument
144
-
145
- # 기존 문서 열기 → 편집 → 저장
146
- doc = HwpxDocument.open("보고서.hwpx")
147
- doc.add_paragraph("자동화로 추가한 문단입니다.")
148
- doc.save_to_path("보고서-수정.hwpx")
149
-
150
- # 새 문서 만들기
151
- new = HwpxDocument.new()
152
- new.add_paragraph("python-hwpx로 만든 새 문서")
153
- new.save_to_path("새문서.hwpx")
154
- ```
155
-
156
- > 💡 컨텍스트 매니저도 지원합니다 — `with` 블록을 벗어나면 리소스가 자동 정리됩니다:
157
- > ```python
158
- > with HwpxDocument.open("보고서.hwpx") as doc:
159
- > doc.add_paragraph("자동으로 리소스가 정리됩니다.")
160
- > doc.save_to_path("결과물.hwpx")
161
- > ```
162
-
163
- `open`/`new` → `edit`/`extract` → `save_to_path` 흐름만 잡으면 나머지는 필요할 때 확장하면 됩니다.
164
-
165
- ## 무엇을 하나
166
-
167
- ### 🔍 읽기 · 추출
168
- - 텍스트/HTML/Markdown 내보내기 — `export_text()` · `export_html()` · `export_markdown()`
169
- - **풍부한 Markdown** — `export_rich_markdown()`은 인라인 서식(`**굵게**`·`*기울임*`·`~~취소선~~`), 중첩 표(colspan/rowspan 안전), 도형 텍스트, 이미지, 각주/미주, 하이퍼링크, 제목(`#`/`##`) 자동 감지까지 보존
170
- - **문서 ingest 게이트웨이** — `hwpx.ingest.DocumentIngestor`가 HWPX를 감지해 rich Markdown과 섹션/표 메타데이터로 정규화
171
- - `TextExtractor` / `ObjectFinder` — 섹션·문단 순회, 태그·속성·XPath로 객체 탐색 (`hp:tab`은 `\t`로 보존, roundtrip 안전)
172
-
173
- ```python
174
- doc = HwpxDocument.open("보고서.hwpx")
175
- md = doc.export_rich_markdown(
176
- image_dir="out/images", # BinData 이미지를 디스크에 추출
177
- image_ref_prefix="images/", # 마크다운 내 ![](images/...) 경로 접두
178
- detect_headings=True, # Ⅰ./1. 패턴 기반 #/## 자동
179
- )
180
- ```
181
-
182
- ### ✏️ 편집
183
- - 문단 추가/삭제/서식, Run 단위 볼드·이탤릭·밑줄·색상
184
- - 섹션 추가/삭제(`add_section(after=)`·`remove_section()`, manifest 자동 관리)
185
- - 표 생성·셀 텍스트·병합/분할·중첩 테이블, 이미지 임베드, 머리글/바닥글, 메모(앵커 기반), 각주/미주, 북마크/하이퍼링크, 다단 편집
186
- - **기존 문서 서식 편집** — 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 쪽번호, 불릿/번호
187
- - **스타일 기반 치환** — 색상·밑줄·`charPrIDRef`로 Run을 필터링해 선택 교체(`replace_text_in_runs`·`find_runs_by_style`)
188
-
189
- ```python
190
- # 빨간색 텍스트만 찾아서 치환
191
- doc.replace_text_in_runs("임시", "확정", text_color="#FF0000")
192
- ```
193
-
194
- ### 🖊️ 양식 채우기 (byte-preserving)
195
- - 누름틀(클릭히어) 필드 조회·서식 보존 채움, 라벨 기반 셀 탐색(`find_cell_by_label`)·경로 채우기(`fill_by_path`)
196
- - **바이트 보존 구조 편집** — 셀 채우기 / 행·열·표 삭제·삽입 / 열 너비 오토핏 / 폰트 shrink-to-fit 을 문서 재조립 없이 수행해 양식 서식을 그대로 보존. 미수정 영역은 `hwpx.patch`가 section XML 바이트를 splice해 손대지 않음
197
-
198
- ```python
199
- doc = HwpxDocument.open("신청서.hwpx")
200
- result = doc.fill_by_path({
201
- "성명 > right": "홍길동",
202
- "소속 > right": "플랫폼팀",
203
- })
204
- doc.save_to_path("신청서-작성완료.hwpx")
205
- print(result["applied_count"], result["failed_count"])
206
- ```
207
-
208
- ### 🏗️ 생성 · 공문서 도구
209
- - `hwpx.builder` — Section/Heading/Table/Image/Header 조립형 생성 + 하드게이트 저장 리포트
210
- - 공문서 도구 — `official_lint`(항목기호 위계·"끝." 표시·붙임·날짜 lint), 결재란 프리셋
211
- - `advanced_generators` — 사진대지(image_grid)·회의 명패·표 기반 조직도
212
- - `mail_merge` — 템플릿+데이터 N부 대량 생성, 표 합계·평균 계산
213
- - `doc_diff` — 문단 LCS diff·신구대조표·참조 정합 lint
214
- - `style_profile` — 참조 문서 프로파일 추출·적용, 템플릿 레지스트리
215
-
216
- ### ✅ 검증 · 안전 · 저수준
217
- - XSD 스키마 + 패키지 구조 검증 — CLI `hwpx-validate` · `hwpx-validate-package`, `hwpx-analyze-template`
218
- - `validate_editor_open_safety` — 저장/팩/리페어/빌더 출력 게이트, `openSafety` 증거 반환
219
- - `hwpx.tools.fuzz`(시드 결정적 시나리오·3중 오라클) · `hwpx.tools.layout_preview`(페이지 박스 근사 HTML/PNG 자기검증) · `opc.security`(XML entity·ZIP 압축 폭탄 가드)
220
- - `hwpx.oxml` 데이터클래스로 OWPML 스키마 ↔ Python 객체 직접 조작, HWPML 2016→2011 네임스페이스 자동 정규화
221
-
222
- ```bash
223
- hwpx-validate-package 보고서.hwpx
224
- hwpx-analyze-template 보고서.hwpx
225
- ```
226
-
227
- > 전체 기능·클래스·메서드 목록은 [사용 가이드](docs/usage.md)와 [API 레퍼런스](https://airmang.github.io/python-hwpx/api_reference.html)를 참고하세요.
228
-
229
- ## 안전한 쓰기 계약 (Safe Write Contract)
230
-
231
- 대표 저장 경로(`save_to_path` · `save_to_stream` · `to_bytes`)는 **요청한 보존 등급을
232
- 쓰기 전에 판정하고, 실제로 무엇을 바꿨는지 측정한 영수증**을 돌려줍니다.
233
-
234
- ```python
235
- from hwpx.mutation_report import PreservationDowngradeError
236
-
237
- # 영수증과 함께 저장 — 달성 가능한 가장 강한 보존 등급 자동 선택(mode="auto" 기본)
238
- report = doc.save_to_path("결과.hwpx", return_report=True)
239
- print(report.actual_mode) # "patch" | "rebuild"
240
- print(report.preservation.untouched_part_payloads.to_dict()) # {"verified": 17, "changed": 0}
241
-
242
- # patch 등급 강제 — 미달이면 아무것도 쓰지 않고 예외(fail-closed)
243
- try:
244
- doc.save_to_path("결과.hwpx", mode="patch", fallback="error")
245
- except PreservationDowngradeError as exc:
246
- print(exc.offending_parts, exc.suggestion)
247
- ```
248
-
249
- - `mode="patch" | "rebuild" | "auto"`(기본 `auto`) · `fallback="error" | "rebuild"`(기본 `error`)
250
- - `mode="patch"` + `fallback="error"`에서 미수정 part의 바이트 동일성을 지킬 수 없으면
251
- **아무것도 쓰지 않고** `PreservationDowngradeError`를 던집니다(무음 rebuild 없음).
252
- - `MutationReport`는 `requestedMode`/`actualMode`/`fallbackUsed`, 변경 part와 좌표 명시
253
- 범위, 보존 3층(part 페이로드·ZIP 레코드·전체 패키지), 검증 3항목(`passed`/`failed`/`not_performed`)을
254
- **측정해** 반환합니다.
255
-
256
- > 파라미터 전체와 `MutationReport` 스키마는 [안전한 쓰기 계약 문서](docs/safe-write-contract.md)를 참고하세요.
257
-
258
- ## 지원 매트릭스
259
-
260
- 능력 영역별 실제 등급입니다(동결 코퍼스 v2 · 2026-07-19 · 실한컴 12.0.0.3288 오라클).
261
- 등급 어휘: **Parse / Preserve / Edit / Create / Render-verified /
262
- Unsupported-but-preserved / Unsupported-and-rejected**.
263
-
264
- | 능력 영역 | 상태 | 증거 |
265
- |---|---|---|
266
- | 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | 오픈 476/476 · 실저작 게이트 58/58 · 렌더 416 |
267
- | 표 구조 변경(행·열·표, 오토핏) | Preserve·Edit | `hwpx.table_patch` · 바이트 보존 497/497 |
268
- | 양식 채움(byte-splice) | Preserve·Edit | `hwpx.patch`·`table_patch`·`body_patch` · 보존 497/497 (wild 무음 서식파괴 16.7%·typed 거부 35/66, 잔여 명명) |
269
- | 그림 삽입/치환 | Edit·Create | `add_picture`·`replace_picture` (복잡 개체는 한컴 확인 권장) |
270
- | 차트 | Unsupported-but-preserved | 생성 API 없음 · 기존 차트 part는 patch 보존 |
271
- | 수식 | Parse·Unsupported-but-preserved | 저작 API 없음 · 기존 수식 파싱·patch 보존 |
272
- | 변경추적(redline) | Edit·Create | `add_tracked_*` · 실한컴 `IsTrackChange=1` (한컴이 PDF export 거부 → `render_unavailable` 정직 집계) |
273
- | 메모(코멘트) | Edit·Create·Render-verified | `add_memo*` · 실 Windows 한컴 검증 |
274
- | 각주/미주 | Edit·Create | `add_footnote`·`add_endnote` (렌더 독립 게이트 미측정) |
275
- | 네이티브 목차/상호참조 | Create·Render-verified | `add_native_toc`·`toc_verify` · 구조 15/15 · 페이지 정합 5/5 |
276
- | 암호화 HWPX | Unsupported-and-rejected | 복호화 없음 · 암호화 part는 파싱 단계 예외로 거부 |
277
- | HWP 5.x 바이너리 | Unsupported-and-rejected | ZIP 아님 → 열기 시 `BadZipFile` (HWPX로 변환 후 사용) |
278
- | 누름틀(form field) 생성 | Parse·Edit | 기존 필드 조회·서식보존 채움 · **신규 누름틀 생성 도구는 미제공** |
279
-
280
- > 각 등급의 판정 근거와 상세 증거 포인터는 [지원 매트릭스 문서](docs/support-matrix.md)를 참고하세요.
281
-
282
- ## 대항 라이브러리 비교
283
-
284
- | | python-hwpx | pyhwpx | pyhwp |
285
- |---|---|---|---|
286
- | **대상 포맷** | `.hwpx` (OWPML/OPC) | `.hwpx` | `.hwp` (v5 바이너리) |
287
- | **한/글 설치** | 불필요 | 필요 (Windows COM) | 불필요 |
288
- | **크로스 플랫폼** | ✅ Linux / macOS / Windows / CI | ❌ Windows 전용 | ✅ |
289
- | **편집/생성 API** | ✅ | ✅ (COM) | ❌ 대부분 읽기 |
290
- | **스키마 검증** | ✅ | ❌ | ❌ |
291
- | **AI 에이전트 연동 (MCP)** | ✅ `hwpx-mcp-server` | ❌ | ❌ |
292
-
293
- > HWP(v5 바이너리) 파일은 지원하지 않습니다. 한컴오피스에서 HWPX로 변환 후 사용하세요.
294
-
295
- ## 알려진 제약
296
-
297
- - `add_shape()` / `add_control()`은 한/글이 요구하는 모든 하위 요소를 생성하지 않습니다. 복잡한 개체 추가 시 한/글에서 열어 검증하세요.
298
- - 이미지 바이너리 임베드는 지원하지만 `<hp:pic>` 요소의 완전 자동 생성은 제공하지 않습니다.
299
- - 암호화된 HWPX 파일의 암복호화는 지원하지 않습니다.
300
-
301
- ## 더 보기
302
-
303
- - **[🚀 빠른 시작](docs/quickstart.md)** · **[📚 사용 가이드](docs/usage.md)** — 첫 파일 열기부터 문단·표·메모·섹션 편집, 텍스트 추출·검증까지
304
- - **[💡 예제 모음](docs/examples.md)** · [`examples/`](examples/) — `build_release_checklist.py`(메모·스타일 편집 HWPX 생성), `extract_text.py`(CLI 텍스트 추출), `find_objects.py`(OWPML 노드 추적) 등
305
- - **[📐 스키마 개요](docs/schema-overview.md)** · **[🔧 설치 검증](docs/installation.md)**
306
- - **[🔬 HWPX 내부 실전 가이드](docs/internals/)** — 실제 한/글 동작에서 확인된 HWPUNIT·조판 캐시·목차 필드·OPC 재패킹·메모·오라클 한계
307
- - **[📖 전체 문서 (Sphinx)](https://airmang.github.io/python-hwpx/)** — API 레퍼런스·50+ 실전 패턴·FAQ
308
- - **[📝 CHANGELOG](CHANGELOG.md)** · **[🤝 CONTRIBUTING](CONTRIBUTING.md)** · **[👥 CONTRIBUTORS](CONTRIBUTORS.md)**
309
-
310
- ## 기여하기
311
-
312
- 버그 리포트, 기능 제안, PR 모두 환영합니다.
313
-
314
- - **[help wanted 이슈](https://github.com/airmang/python-hwpx/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)** — 지금 들어오기 좋은 입구입니다. 시작 전에 이슈에 코멘트로 방향을 남겨주세요.
315
- - **[마일스톤](https://github.com/airmang/python-hwpx/milestones)** — 공개 로드맵입니다. 프로젝트가 어디로 가는지 여기서 보입니다.
316
- - **[Discussions](https://github.com/airmang/python-hwpx/discussions)** — 질문·아이디어는 이슈 대신 여기로.
317
- - **[내부 실전 가이드](docs/internals/)** — HWPX 내부 구조가 처음이라면 여기부터. 개발 흐름은 [CONTRIBUTING.md](CONTRIBUTING.md)에 있습니다.
318
-
319
- ```bash
320
- git clone https://github.com/airmang/python-hwpx.git
321
- cd python-hwpx
322
- pip install -e ".[dev]"
323
- pytest
324
- ```
325
-
326
- ## 감사의 말
327
-
328
- 아래 공개 표준·프로젝트에 빚지고 있습니다.
329
-
330
- - **[OWPML — 개방형 워드프로세서 마크업 언어 (KS X 6101)](https://www.kssn.net/search/stddetail.do?itemNo=K001010119985)** — HWPX가 기반하는 한국 산업 표준
331
- - **[hancom-io/hwpx-owpml-model](https://github.com/hancom-io/hwpx-owpml-model)** — OWPML 요소 구조 참조 모델 · **[neolord0/hwpxlib](https://github.com/neolord0/hwpxlib)** — 오라클 샘플 코퍼스
332
- - **[edwardkim/rhwp](https://github.com/edwardkim/rhwp)** — 멱등성·검증 게이트 설계 영감
333
- - **범정부오피스** — 공무 문서 편집 워크플로 아이디어
334
-
335
- ## License
336
-
337
- Apache License 2.0. See LICENSE and NOTICE.
338
-
339
- ## Maintainer
340
-
341
- Primary maintainer/contact: **Kohkyuhyun** ([@airmang](https://github.com/airmang))
342
-
343
- - ✉️ [kokyuhyun@hotmail.com](mailto:kokyuhyun@hotmail.com)
344
- - 🐙 [@airmang](https://github.com/airmang)