python-hwpx 3.5.0__tar.gz → 3.7.0__tar.gz
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.
- {python_hwpx-3.5.0/src/python_hwpx.egg-info → python_hwpx-3.7.0}/PKG-INFO +83 -8
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/README.md +82 -7
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/pyproject.toml +3 -1
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/__init__.py +6 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/persistence.py +196 -15
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/commands.py +4 -20
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/model.py +56 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/body_patch.py +16 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/document.py +102 -8
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/engine.py +43 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/measure.py +52 -0
- python_hwpx-3.7.0/src/hwpx/mutation_report.py +524 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/opc/package.py +66 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/document_parts.py +8 -4
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/paragraph.py +3 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/run.py +4 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/table.py +11 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/patch.py +24 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/table_patch.py +16 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0/src/python_hwpx.egg-info}/PKG-INFO +83 -8
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/SOURCES.txt +4 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_formatting.py +9 -2
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_save_api.py +4 -1
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit.py +65 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_kordoc_absorption.py +8 -6
- python_hwpx-3.7.0/tests/test_layout_cache_scope.py +122 -0
- python_hwpx-3.7.0/tests/test_mutation_report.py +323 -0
- python_hwpx-3.7.0/tests/test_mutation_report_projections.py +318 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_opc_package.py +38 -6
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_oxml_modularization.py +3 -1
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/LICENSE +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/NOTICE +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/setup.cfg +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/fields.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/bundle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/catalog.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/dump.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/mapping.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/model.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/native.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/blueprint/replay.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/catalog.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/cli.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/document.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/form_plan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/path.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/query.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/agent/story.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/authoring.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/benchmark/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/benchmark/blind_eval.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/builder/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/builder/core.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/builder/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/badges.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/conformance/runner.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/_support.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/composer.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/harvest.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/plan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/profile.json +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/design/validator.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/evalplan_fill.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/compose.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/ir.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/measure.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/exam/profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/fill_residue.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/seal.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/form_fit/wordbox.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/formfill_quality.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/guidance_scan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/package.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/template_formfit.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/advanced_generators.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/generator.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/fuzz/runner.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/official_lint.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/pii.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/style_profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/table_compute.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/__init__.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/_refresh_hwpx_mac.applescript +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/detectors.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/diff.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/fixture_corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/hancom_worker.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/masks.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/oracle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/page_qa.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/qa_contracts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/qa_metrics.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/hwpx/visual/report.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_advanced_generators.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_blueprint_contracts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_blueprint_dump.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_blueprint_replay.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_catalog.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_cli.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_commands.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_contracts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_document.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_mixed_form.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_path_query.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_agent_story_commands.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_native_toc.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_profile_routing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_authoring_render_check.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_body_patch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_builder_core.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_conformance.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_corpus_toc_verify.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_design_builder.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_plan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_evalplan_fill.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_compose.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_compose_oracle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_fixtures.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_ir.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_measure.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_exam_profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_fill_residue.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit_integration.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit_seal.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit_seal_placement.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_form_fit_wordbox.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_formfill_differential_driver.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_formfill_quality.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_guidance_scan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_hancom_render_worker.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_ingest.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_m9_p0_spike_tools.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_mail_merge_fit.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_mail_merge_table_compute.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_mixed_form_plan.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_new_features.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_official_document_style.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_official_lint_gongmun_gate.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_official_lint_tableaware.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_openrate.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_pii.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_pii_leak_sweep.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_pii_structural.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_pii_wiring.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_question_split_detector.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_report_parser.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_rhwp_t1_gates.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_rhwp_t2_verification.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_save_pipeline_no_bypass.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_style_profile.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_template_formfit.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_version_metadata.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_fixture_corpus.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_oracle.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_oracle_budget.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_qa_contracts.py +0 -0
- {python_hwpx-3.5.0 → python_hwpx-3.7.0}/tests/test_visual_qa_metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -46,7 +46,7 @@ Dynamic: license-file
|
|
|
46
46
|
<p align="center">
|
|
47
47
|
<h1 align="center">python-hwpx</h1>
|
|
48
48
|
<p align="center">
|
|
49
|
-
<strong
|
|
49
|
+
<strong>한컴 없이 HWPX를 안전하게 자동화하는 Python 계층 — 최소 범위 편집, 검증된 저작, 모든 쓰기에 영수증.</strong>
|
|
50
50
|
</p>
|
|
51
51
|
<p align="center">
|
|
52
52
|
<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>
|
|
@@ -56,6 +56,23 @@ Dynamic: license-file
|
|
|
56
56
|
</p>
|
|
57
57
|
</p>
|
|
58
58
|
|
|
59
|
+
<p align="center">한국어 | <a href="README_EN.md">English</a></p>
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
> **python-hwpx는 한컴 없이 HWPX를 안전하게 자동화하는 Python 계층입니다.** 기존
|
|
64
|
+
> 문서는 최소 범위만 수정하고, 새 문서는 실제 한컴 수용이 검증된 형태로 생성하며,
|
|
65
|
+
> 모든 쓰기에 변경·보존·검증 영수증을 남기고, 완전한 해석과 렌더링은 전문 백엔드에
|
|
66
|
+
> 위임할 수 있습니다.
|
|
67
|
+
|
|
68
|
+
- **최소 범위 편집** — 미수정 part는 저장 시 바이트 그대로 유지됩니다(patch 경로
|
|
69
|
+
바이트 보존 497/497, 동결 코퍼스 v2 · 2026-07-19).
|
|
70
|
+
- **검증된 저작** — 밑바닥 생성도 실제 한컴이 받아들이는 형태로 냅니다(산출물 한컴
|
|
71
|
+
오픈 476/476 all-pass, 실저작 품질 게이트 58/58).
|
|
72
|
+
- **모든 쓰기에 영수증** — 대표 저장 경로는 [Safe Write Contract](docs/safe-write-contract.md)의
|
|
73
|
+
`MutationReport`(`hwpx.mutation-report/v1`)로 실제 쓰기 모드·보존 등급·검증 결과를
|
|
74
|
+
**측정해** 반환합니다.
|
|
75
|
+
|
|
59
76
|
---
|
|
60
77
|
|
|
61
78
|
## 🧩 HWPX Stack (3종)
|
|
@@ -70,7 +87,7 @@ Dynamic: license-file
|
|
|
70
87
|
`hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
|
|
71
88
|
“first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
|
|
72
89
|
|
|
73
|
-
현재 PyPI 공개 릴리스는 `python-hwpx 3.
|
|
90
|
+
현재 PyPI 공개 릴리스는 `python-hwpx 3.7.0`입니다. 일반
|
|
74
91
|
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
75
92
|
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
76
93
|
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
@@ -80,13 +97,17 @@ Dynamic: license-file
|
|
|
80
97
|
## 실측으로 말합니다 — Published Corpus
|
|
81
98
|
|
|
82
99
|
이 스택의 산출물은 주장 대신 **실제 한컴오피스 전수 측정**으로 검증됩니다
|
|
83
|
-
(동결 코퍼스 N=497
|
|
100
|
+
(동결 코퍼스 v2, N=497 산출물, 2026-07-19, 실한컴 12.0.0.3288 COM/GUI 오라클;
|
|
101
|
+
상세·주의사항은
|
|
84
102
|
[실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html)):
|
|
85
103
|
|
|
86
|
-
- **한컴 오픈 수용률
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
104
|
+
- **한컴 오픈 수용률 476/476 all-pass** (동결 코퍼스 v2 · 2026-07-19 · 실한컴 COM
|
|
105
|
+
`Open()` 판정 · rule-of-three 하한 99.37%) · 파싱 96.2%(458/476)
|
|
106
|
+
- **미수정 영역 바이트 보존 497/497** (patch 경로 한정, zip-part diff · 오라클 불요)
|
|
107
|
+
· **개인정보 0-leak** (35문서/합성 140값)
|
|
108
|
+
- 렌더 검증 416/476 (실한컴 `SaveAs("PDF")`) + 정직 버킷 43건(변경추적 문서의 PDF
|
|
109
|
+
export는 한컴 자체가 거부 — 실측 한계로 발행) + 미검증 17건
|
|
110
|
+
- wild 공개 양식 채움은 구조결함 픽스 후 **무음 서식파괴 16.7%**(판정 66조합, 못 담는 타깃은 typed 거부 35건·산출분 pass 17/28) — **낮은 숫자도 그대로 발행**하고 잔여(페이지 리플·표 shape)를 명명합니다
|
|
90
111
|
|
|
91
112
|
> 이 숫자들은 *생성물 수용률* 축입니다(우리가 만든 파일을 실제 한컴이 받아들이는가).
|
|
92
113
|
> 문서 *파싱 recall*과는 다른 축이므로 파서 프로젝트 수치와 병치 비교하지 마세요.
|
|
@@ -195,6 +216,59 @@ hwpx-analyze-template 보고서.hwpx
|
|
|
195
216
|
|
|
196
217
|
> 전체 기능·클래스·메서드 목록은 [사용 가이드](docs/usage.md)와 [API 레퍼런스](https://airmang.github.io/python-hwpx/api_reference.html)를 참고하세요.
|
|
197
218
|
|
|
219
|
+
## 안전한 쓰기 계약 (Safe Write Contract)
|
|
220
|
+
|
|
221
|
+
대표 저장 경로(`save_to_path` · `save_to_stream` · `to_bytes`)는 **요청한 보존 등급을
|
|
222
|
+
쓰기 전에 판정하고, 실제로 무엇을 바꿨는지 측정한 영수증**을 돌려줍니다.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from hwpx.mutation_report import PreservationDowngradeError
|
|
226
|
+
|
|
227
|
+
# 영수증과 함께 저장 — 달성 가능한 가장 강한 보존 등급 자동 선택(mode="auto" 기본)
|
|
228
|
+
report = doc.save_to_path("결과.hwpx", return_report=True)
|
|
229
|
+
print(report.actual_mode) # "patch" | "rebuild"
|
|
230
|
+
print(report.preservation.untouched_part_payloads.to_dict()) # {"verified": 17, "changed": 0}
|
|
231
|
+
|
|
232
|
+
# patch 등급 강제 — 미달이면 아무것도 쓰지 않고 예외(fail-closed)
|
|
233
|
+
try:
|
|
234
|
+
doc.save_to_path("결과.hwpx", mode="patch", fallback="error")
|
|
235
|
+
except PreservationDowngradeError as exc:
|
|
236
|
+
print(exc.offending_parts, exc.suggestion)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- `mode="patch" | "rebuild" | "auto"`(기본 `auto`) · `fallback="error" | "rebuild"`(기본 `error`)
|
|
240
|
+
- `mode="patch"` + `fallback="error"`에서 미수정 part의 바이트 동일성을 지킬 수 없으면
|
|
241
|
+
**아무것도 쓰지 않고** `PreservationDowngradeError`를 던집니다(무음 rebuild 없음).
|
|
242
|
+
- `MutationReport`는 `requestedMode`/`actualMode`/`fallbackUsed`, 변경 part와 좌표 명시
|
|
243
|
+
범위, 보존 3층(part 페이로드·ZIP 레코드·전체 패키지), 검증 3항목(`passed`/`failed`/`not_performed`)을
|
|
244
|
+
**측정해** 반환합니다.
|
|
245
|
+
|
|
246
|
+
> 파라미터 전체와 `MutationReport` 스키마는 [안전한 쓰기 계약 문서](docs/safe-write-contract.md)를 참고하세요.
|
|
247
|
+
|
|
248
|
+
## 지원 매트릭스
|
|
249
|
+
|
|
250
|
+
능력 영역별 실제 등급입니다(동결 코퍼스 v2 · 2026-07-19 · 실한컴 12.0.0.3288 오라클).
|
|
251
|
+
등급 어휘: **Parse / Preserve / Edit / Create / Render-verified /
|
|
252
|
+
Unsupported-but-preserved / Unsupported-and-rejected**.
|
|
253
|
+
|
|
254
|
+
| 능력 영역 | 상태 | 증거 |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | 오픈 476/476 · 실저작 게이트 58/58 · 렌더 416 |
|
|
257
|
+
| 표 구조 변경(행·열·표, 오토핏) | Preserve·Edit | `hwpx.table_patch` · 바이트 보존 497/497 |
|
|
258
|
+
| 양식 채움(byte-splice) | Preserve·Edit | `hwpx.patch`·`table_patch`·`body_patch` · 보존 497/497 (wild 무음 서식파괴 16.7%·typed 거부 35/66, 잔여 명명) |
|
|
259
|
+
| 그림 삽입/치환 | Edit·Create | `add_picture`·`replace_picture` (복잡 개체는 한컴 확인 권장) |
|
|
260
|
+
| 차트 | Unsupported-but-preserved | 생성 API 없음 · 기존 차트 part는 patch 보존 |
|
|
261
|
+
| 수식 | Parse·Unsupported-but-preserved | 저작 API 없음 · 기존 수식 파싱·patch 보존 |
|
|
262
|
+
| 변경추적(redline) | Edit·Create | `add_tracked_*` · 실한컴 `IsTrackChange=1` (한컴이 PDF export 거부 → `render_unavailable` 정직 집계) |
|
|
263
|
+
| 메모(코멘트) | Edit·Create·Render-verified | `add_memo*` · 실 Windows 한컴 검증 |
|
|
264
|
+
| 각주/미주 | Edit·Create | `add_footnote`·`add_endnote` (렌더 독립 게이트 미측정) |
|
|
265
|
+
| 네이티브 목차/상호참조 | Create·Render-verified | `add_native_toc`·`toc_verify` · 구조 15/15 · 페이지 정합 5/5 |
|
|
266
|
+
| 암호화 HWPX | Unsupported-and-rejected | 복호화 없음 · 암호화 part는 파싱 단계 예외로 거부 |
|
|
267
|
+
| HWP 5.x 바이너리 | Unsupported-and-rejected | ZIP 아님 → 열기 시 `BadZipFile` (HWPX로 변환 후 사용) |
|
|
268
|
+
| 누름틀(form field) 생성 | Parse·Edit | 기존 필드 조회·서식보존 채움 · **신규 누름틀 생성 도구는 미제공** |
|
|
269
|
+
|
|
270
|
+
> 각 등급의 판정 근거와 상세 증거 포인터는 [지원 매트릭스 문서](docs/support-matrix.md)를 참고하세요.
|
|
271
|
+
|
|
198
272
|
## 대항 라이브러리 비교
|
|
199
273
|
|
|
200
274
|
| | python-hwpx | pyhwpx | pyhwp |
|
|
@@ -219,6 +293,7 @@ hwpx-analyze-template 보고서.hwpx
|
|
|
219
293
|
- **[🚀 빠른 시작](docs/quickstart.md)** · **[📚 사용 가이드](docs/usage.md)** — 첫 파일 열기부터 문단·표·메모·섹션 편집, 텍스트 추출·검증까지
|
|
220
294
|
- **[💡 예제 모음](docs/examples.md)** · [`examples/`](examples/) — `build_release_checklist.py`(메모·스타일 편집 HWPX 생성), `extract_text.py`(CLI 텍스트 추출), `find_objects.py`(OWPML 노드 추적) 등
|
|
221
295
|
- **[📐 스키마 개요](docs/schema-overview.md)** · **[🔧 설치 검증](docs/installation.md)**
|
|
296
|
+
- **[🔬 HWPX 내부 실전 가이드](docs/internals/)** — 실제 한/글 동작에서 확인된 HWPUNIT·조판 캐시·목차 필드·OPC 재패킹·메모·오라클 한계
|
|
222
297
|
- **[📖 전체 문서 (Sphinx)](https://airmang.github.io/python-hwpx/)** — API 레퍼런스·50+ 실전 패턴·FAQ
|
|
223
298
|
- **[📝 CHANGELOG](CHANGELOG.md)** · **[🤝 CONTRIBUTING](CONTRIBUTING.md)** · **[👥 CONTRIBUTORS](CONTRIBUTORS.md)**
|
|
224
299
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<h1 align="center">python-hwpx</h1>
|
|
3
3
|
<p align="center">
|
|
4
|
-
<strong
|
|
4
|
+
<strong>한컴 없이 HWPX를 안전하게 자동화하는 Python 계층 — 최소 범위 편집, 검증된 저작, 모든 쓰기에 영수증.</strong>
|
|
5
5
|
</p>
|
|
6
6
|
<p align="center">
|
|
7
7
|
<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>
|
|
@@ -11,6 +11,23 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
|
+
<p align="center">한국어 | <a href="README_EN.md">English</a></p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
> **python-hwpx는 한컴 없이 HWPX를 안전하게 자동화하는 Python 계층입니다.** 기존
|
|
19
|
+
> 문서는 최소 범위만 수정하고, 새 문서는 실제 한컴 수용이 검증된 형태로 생성하며,
|
|
20
|
+
> 모든 쓰기에 변경·보존·검증 영수증을 남기고, 완전한 해석과 렌더링은 전문 백엔드에
|
|
21
|
+
> 위임할 수 있습니다.
|
|
22
|
+
|
|
23
|
+
- **최소 범위 편집** — 미수정 part는 저장 시 바이트 그대로 유지됩니다(patch 경로
|
|
24
|
+
바이트 보존 497/497, 동결 코퍼스 v2 · 2026-07-19).
|
|
25
|
+
- **검증된 저작** — 밑바닥 생성도 실제 한컴이 받아들이는 형태로 냅니다(산출물 한컴
|
|
26
|
+
오픈 476/476 all-pass, 실저작 품질 게이트 58/58).
|
|
27
|
+
- **모든 쓰기에 영수증** — 대표 저장 경로는 [Safe Write Contract](docs/safe-write-contract.md)의
|
|
28
|
+
`MutationReport`(`hwpx.mutation-report/v1`)로 실제 쓰기 모드·보존 등급·검증 결과를
|
|
29
|
+
**측정해** 반환합니다.
|
|
30
|
+
|
|
14
31
|
---
|
|
15
32
|
|
|
16
33
|
## 🧩 HWPX Stack (3종)
|
|
@@ -25,7 +42,7 @@
|
|
|
25
42
|
`hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
|
|
26
43
|
“first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
|
|
27
44
|
|
|
28
|
-
현재 PyPI 공개 릴리스는 `python-hwpx 3.
|
|
45
|
+
현재 PyPI 공개 릴리스는 `python-hwpx 3.7.0`입니다. 일반
|
|
29
46
|
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
30
47
|
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
31
48
|
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
@@ -35,13 +52,17 @@
|
|
|
35
52
|
## 실측으로 말합니다 — Published Corpus
|
|
36
53
|
|
|
37
54
|
이 스택의 산출물은 주장 대신 **실제 한컴오피스 전수 측정**으로 검증됩니다
|
|
38
|
-
(동결 코퍼스 N=497
|
|
55
|
+
(동결 코퍼스 v2, N=497 산출물, 2026-07-19, 실한컴 12.0.0.3288 COM/GUI 오라클;
|
|
56
|
+
상세·주의사항은
|
|
39
57
|
[실측 코퍼스 메트릭](https://airmang.github.io/python-hwpx/corpus-metrics.html)):
|
|
40
58
|
|
|
41
|
-
- **한컴 오픈 수용률
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
59
|
+
- **한컴 오픈 수용률 476/476 all-pass** (동결 코퍼스 v2 · 2026-07-19 · 실한컴 COM
|
|
60
|
+
`Open()` 판정 · rule-of-three 하한 99.37%) · 파싱 96.2%(458/476)
|
|
61
|
+
- **미수정 영역 바이트 보존 497/497** (patch 경로 한정, zip-part diff · 오라클 불요)
|
|
62
|
+
· **개인정보 0-leak** (35문서/합성 140값)
|
|
63
|
+
- 렌더 검증 416/476 (실한컴 `SaveAs("PDF")`) + 정직 버킷 43건(변경추적 문서의 PDF
|
|
64
|
+
export는 한컴 자체가 거부 — 실측 한계로 발행) + 미검증 17건
|
|
65
|
+
- wild 공개 양식 채움은 구조결함 픽스 후 **무음 서식파괴 16.7%**(판정 66조합, 못 담는 타깃은 typed 거부 35건·산출분 pass 17/28) — **낮은 숫자도 그대로 발행**하고 잔여(페이지 리플·표 shape)를 명명합니다
|
|
45
66
|
|
|
46
67
|
> 이 숫자들은 *생성물 수용률* 축입니다(우리가 만든 파일을 실제 한컴이 받아들이는가).
|
|
47
68
|
> 문서 *파싱 recall*과는 다른 축이므로 파서 프로젝트 수치와 병치 비교하지 마세요.
|
|
@@ -150,6 +171,59 @@ hwpx-analyze-template 보고서.hwpx
|
|
|
150
171
|
|
|
151
172
|
> 전체 기능·클래스·메서드 목록은 [사용 가이드](docs/usage.md)와 [API 레퍼런스](https://airmang.github.io/python-hwpx/api_reference.html)를 참고하세요.
|
|
152
173
|
|
|
174
|
+
## 안전한 쓰기 계약 (Safe Write Contract)
|
|
175
|
+
|
|
176
|
+
대표 저장 경로(`save_to_path` · `save_to_stream` · `to_bytes`)는 **요청한 보존 등급을
|
|
177
|
+
쓰기 전에 판정하고, 실제로 무엇을 바꿨는지 측정한 영수증**을 돌려줍니다.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from hwpx.mutation_report import PreservationDowngradeError
|
|
181
|
+
|
|
182
|
+
# 영수증과 함께 저장 — 달성 가능한 가장 강한 보존 등급 자동 선택(mode="auto" 기본)
|
|
183
|
+
report = doc.save_to_path("결과.hwpx", return_report=True)
|
|
184
|
+
print(report.actual_mode) # "patch" | "rebuild"
|
|
185
|
+
print(report.preservation.untouched_part_payloads.to_dict()) # {"verified": 17, "changed": 0}
|
|
186
|
+
|
|
187
|
+
# patch 등급 강제 — 미달이면 아무것도 쓰지 않고 예외(fail-closed)
|
|
188
|
+
try:
|
|
189
|
+
doc.save_to_path("결과.hwpx", mode="patch", fallback="error")
|
|
190
|
+
except PreservationDowngradeError as exc:
|
|
191
|
+
print(exc.offending_parts, exc.suggestion)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- `mode="patch" | "rebuild" | "auto"`(기본 `auto`) · `fallback="error" | "rebuild"`(기본 `error`)
|
|
195
|
+
- `mode="patch"` + `fallback="error"`에서 미수정 part의 바이트 동일성을 지킬 수 없으면
|
|
196
|
+
**아무것도 쓰지 않고** `PreservationDowngradeError`를 던집니다(무음 rebuild 없음).
|
|
197
|
+
- `MutationReport`는 `requestedMode`/`actualMode`/`fallbackUsed`, 변경 part와 좌표 명시
|
|
198
|
+
범위, 보존 3층(part 페이로드·ZIP 레코드·전체 패키지), 검증 3항목(`passed`/`failed`/`not_performed`)을
|
|
199
|
+
**측정해** 반환합니다.
|
|
200
|
+
|
|
201
|
+
> 파라미터 전체와 `MutationReport` 스키마는 [안전한 쓰기 계약 문서](docs/safe-write-contract.md)를 참고하세요.
|
|
202
|
+
|
|
203
|
+
## 지원 매트릭스
|
|
204
|
+
|
|
205
|
+
능력 영역별 실제 등급입니다(동결 코퍼스 v2 · 2026-07-19 · 실한컴 12.0.0.3288 오라클).
|
|
206
|
+
등급 어휘: **Parse / Preserve / Edit / Create / Render-verified /
|
|
207
|
+
Unsupported-but-preserved / Unsupported-and-rejected**.
|
|
208
|
+
|
|
209
|
+
| 능력 영역 | 상태 | 증거 |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | 오픈 476/476 · 실저작 게이트 58/58 · 렌더 416 |
|
|
212
|
+
| 표 구조 변경(행·열·표, 오토핏) | Preserve·Edit | `hwpx.table_patch` · 바이트 보존 497/497 |
|
|
213
|
+
| 양식 채움(byte-splice) | Preserve·Edit | `hwpx.patch`·`table_patch`·`body_patch` · 보존 497/497 (wild 무음 서식파괴 16.7%·typed 거부 35/66, 잔여 명명) |
|
|
214
|
+
| 그림 삽입/치환 | Edit·Create | `add_picture`·`replace_picture` (복잡 개체는 한컴 확인 권장) |
|
|
215
|
+
| 차트 | Unsupported-but-preserved | 생성 API 없음 · 기존 차트 part는 patch 보존 |
|
|
216
|
+
| 수식 | Parse·Unsupported-but-preserved | 저작 API 없음 · 기존 수식 파싱·patch 보존 |
|
|
217
|
+
| 변경추적(redline) | Edit·Create | `add_tracked_*` · 실한컴 `IsTrackChange=1` (한컴이 PDF export 거부 → `render_unavailable` 정직 집계) |
|
|
218
|
+
| 메모(코멘트) | Edit·Create·Render-verified | `add_memo*` · 실 Windows 한컴 검증 |
|
|
219
|
+
| 각주/미주 | Edit·Create | `add_footnote`·`add_endnote` (렌더 독립 게이트 미측정) |
|
|
220
|
+
| 네이티브 목차/상호참조 | Create·Render-verified | `add_native_toc`·`toc_verify` · 구조 15/15 · 페이지 정합 5/5 |
|
|
221
|
+
| 암호화 HWPX | Unsupported-and-rejected | 복호화 없음 · 암호화 part는 파싱 단계 예외로 거부 |
|
|
222
|
+
| HWP 5.x 바이너리 | Unsupported-and-rejected | ZIP 아님 → 열기 시 `BadZipFile` (HWPX로 변환 후 사용) |
|
|
223
|
+
| 누름틀(form field) 생성 | Parse·Edit | 기존 필드 조회·서식보존 채움 · **신규 누름틀 생성 도구는 미제공** |
|
|
224
|
+
|
|
225
|
+
> 각 등급의 판정 근거와 상세 증거 포인터는 [지원 매트릭스 문서](docs/support-matrix.md)를 참고하세요.
|
|
226
|
+
|
|
153
227
|
## 대항 라이브러리 비교
|
|
154
228
|
|
|
155
229
|
| | python-hwpx | pyhwpx | pyhwp |
|
|
@@ -174,6 +248,7 @@ hwpx-analyze-template 보고서.hwpx
|
|
|
174
248
|
- **[🚀 빠른 시작](docs/quickstart.md)** · **[📚 사용 가이드](docs/usage.md)** — 첫 파일 열기부터 문단·표·메모·섹션 편집, 텍스트 추출·검증까지
|
|
175
249
|
- **[💡 예제 모음](docs/examples.md)** · [`examples/`](examples/) — `build_release_checklist.py`(메모·스타일 편집 HWPX 생성), `extract_text.py`(CLI 텍스트 추출), `find_objects.py`(OWPML 노드 추적) 등
|
|
176
250
|
- **[📐 스키마 개요](docs/schema-overview.md)** · **[🔧 설치 검증](docs/installation.md)**
|
|
251
|
+
- **[🔬 HWPX 내부 실전 가이드](docs/internals/)** — 실제 한/글 동작에서 확인된 HWPUNIT·조판 캐시·목차 필드·OPC 재패킹·메모·오라클 한계
|
|
177
252
|
- **[📖 전체 문서 (Sphinx)](https://airmang.github.io/python-hwpx/)** — API 레퍼런스·50+ 실전 패턴·FAQ
|
|
178
253
|
- **[📝 CHANGELOG](CHANGELOG.md)** · **[🤝 CONTRIBUTING](CONTRIBUTING.md)** · **[👥 CONTRIBUTORS](CONTRIBUTORS.md)**
|
|
179
254
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.7.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -109,6 +109,7 @@ python_version = "3.10"
|
|
|
109
109
|
follow_imports = "skip"
|
|
110
110
|
files = [
|
|
111
111
|
"src/hwpx/document.py",
|
|
112
|
+
"src/hwpx/mutation_report.py",
|
|
112
113
|
"src/hwpx/_document/_units.py",
|
|
113
114
|
"src/hwpx/_document/fields.py",
|
|
114
115
|
"src/hwpx/_document/layout.py",
|
|
@@ -156,6 +157,7 @@ ignore_missing_imports = true
|
|
|
156
157
|
# both central facades and every extracted/touched runtime owner to the gate.
|
|
157
158
|
include = [
|
|
158
159
|
"src/hwpx/document.py",
|
|
160
|
+
"src/hwpx/mutation_report.py",
|
|
159
161
|
"src/hwpx/_document/_units.py",
|
|
160
162
|
"src/hwpx/_document/fields.py",
|
|
161
163
|
"src/hwpx/_document/layout.py",
|
|
@@ -85,6 +85,10 @@ from .ingest import (
|
|
|
85
85
|
HwpxMarkdownConverter,
|
|
86
86
|
UnsupportedDocumentFormat,
|
|
87
87
|
)
|
|
88
|
+
from .mutation_report import (
|
|
89
|
+
MutationReport,
|
|
90
|
+
PreservationDowngradeError,
|
|
91
|
+
)
|
|
88
92
|
from .patch import (
|
|
89
93
|
BytePreservingPatchResult,
|
|
90
94
|
ParagraphTextPatch,
|
|
@@ -146,6 +150,8 @@ __all__ = [
|
|
|
146
150
|
"ParagraphInfo",
|
|
147
151
|
"PackageValidationReport",
|
|
148
152
|
"BytePreservingPatchResult",
|
|
153
|
+
"MutationReport",
|
|
154
|
+
"PreservationDowngradeError",
|
|
149
155
|
"PlanValidationReport",
|
|
150
156
|
"ParagraphTextPatch",
|
|
151
157
|
"PatchApplied",
|
|
@@ -5,8 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import warnings
|
|
7
7
|
from os import PathLike
|
|
8
|
-
from typing import TYPE_CHECKING, Any, BinaryIO, Sequence
|
|
9
|
-
|
|
8
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Sequence
|
|
9
|
+
|
|
10
|
+
from ..mutation_report import (
|
|
11
|
+
Fallback,
|
|
12
|
+
Mode,
|
|
13
|
+
MutationReport,
|
|
14
|
+
PreservationDowngradeError,
|
|
15
|
+
PreservationMeasurement,
|
|
16
|
+
VerificationSummary,
|
|
17
|
+
VerificationValue,
|
|
18
|
+
measure_save,
|
|
19
|
+
)
|
|
10
20
|
from ..opc.package import _UNCHECKED_SAVE_TOKEN
|
|
11
21
|
from ..quality import QualityPolicy
|
|
12
22
|
from ..quality.report import OpenSafetyReport
|
|
@@ -120,27 +130,178 @@ def _gate_and_write(
|
|
|
120
130
|
return report
|
|
121
131
|
|
|
122
132
|
|
|
123
|
-
def
|
|
124
|
-
""
|
|
133
|
+
def _build_measured(
|
|
134
|
+
doc: "HwpxDocument", *, reset_dirty: bool
|
|
135
|
+
) -> tuple[bytes, PreservationMeasurement]:
|
|
136
|
+
"""Serialize + open-safety validate exactly like ``_to_bytes_raw`` and measure
|
|
137
|
+
the built archive's preservation against the pre-save part payloads.
|
|
138
|
+
|
|
139
|
+
The pre-save snapshot is captured *before* the ``serialize()`` updates are
|
|
140
|
+
applied so a per-save normalizer touching a part the editor never declared
|
|
141
|
+
dirty surfaces as an ``unexpected`` change (Safe Write Contract §2).
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
package = doc._package
|
|
145
|
+
# Measure against the OPEN-TIME baseline, not the current package state: a
|
|
146
|
+
# low-level ``set_part`` applied before this save already lives in
|
|
147
|
+
# ``_files``, and measuring against it would silently launder that change
|
|
148
|
+
# through a ``mode="patch"`` grade (Safe Write Contract §1, fail-closed).
|
|
149
|
+
source_members = dict(
|
|
150
|
+
getattr(package, "_opened_members", None) or package._files
|
|
151
|
+
)
|
|
152
|
+
source_infos = dict(
|
|
153
|
+
getattr(package, "_opened_zip_infos", None) or package._zip_infos
|
|
154
|
+
)
|
|
155
|
+
updates = doc._root.serialize()
|
|
156
|
+
predeclared = set(updates)
|
|
157
|
+
if updates:
|
|
158
|
+
for part_name, payload in updates.items():
|
|
159
|
+
package.set_part(part_name, payload)
|
|
160
|
+
result = package._save_to_bytes(verify_open_safety=True, mark_clean=False)
|
|
161
|
+
if not isinstance(result, bytes):
|
|
162
|
+
raise TypeError("package.save(None) must return bytes")
|
|
163
|
+
doc._run_open_safety_validation(result)
|
|
164
|
+
if reset_dirty:
|
|
165
|
+
_mark_save_clean(doc)
|
|
166
|
+
measurement = measure_save(source_members, source_infos, result, predeclared)
|
|
167
|
+
return result, measurement
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _resolve_grade(
|
|
171
|
+
mode: Mode,
|
|
172
|
+
fallback: Fallback,
|
|
173
|
+
measurement: PreservationMeasurement,
|
|
174
|
+
) -> tuple[Literal["patch", "rebuild"], bool]:
|
|
175
|
+
"""Return ``(actual_mode, fallback_used)``; raise before any write when a
|
|
176
|
+
requested ``patch`` grade is unmet and ``fallback="error"`` (§1)."""
|
|
177
|
+
|
|
178
|
+
if mode == "patch" and not measurement.patch_grade_ok:
|
|
179
|
+
if fallback == "error":
|
|
180
|
+
raise PreservationDowngradeError(
|
|
181
|
+
requested_mode=mode,
|
|
182
|
+
achieved_grade=measurement.achieved_grade,
|
|
183
|
+
offending_parts=measurement.offending_parts,
|
|
184
|
+
suggestion=(
|
|
185
|
+
"route these edits through a byte-preserving primitive "
|
|
186
|
+
"(hwpx.patch/table_patch/body_patch) or pass fallback='rebuild'."
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
return "rebuild", True
|
|
190
|
+
if mode == "auto":
|
|
191
|
+
return measurement.achieved_grade, False
|
|
192
|
+
if mode == "patch":
|
|
193
|
+
return "patch", False
|
|
194
|
+
return "rebuild", False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _verification_summary(report: "VisualCompleteReport") -> VerificationSummary:
|
|
198
|
+
"""Reflect what the pipeline actually ran; never fabricate a pass (§2)."""
|
|
199
|
+
|
|
200
|
+
package: VerificationValue = "passed" if report.ok else "failed"
|
|
201
|
+
open_safety: VerificationValue = "passed" if report.open_safety.ok else "failed"
|
|
202
|
+
# Open-safety validation (which includes the reopen probe) already ran during
|
|
203
|
+
# serialize and raised on failure, so reaching here means reopen passed.
|
|
204
|
+
reopen: VerificationValue = "passed"
|
|
205
|
+
status = report.visual_complete_status
|
|
206
|
+
visual: VerificationValue = (
|
|
207
|
+
"passed"
|
|
208
|
+
if status == "verified"
|
|
209
|
+
else "failed"
|
|
210
|
+
if status == "failed"
|
|
211
|
+
else "not_performed"
|
|
212
|
+
)
|
|
213
|
+
return VerificationSummary(
|
|
214
|
+
package=package, open_safety=open_safety, reopen=reopen, visual=visual
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _compose_mutation_report(
|
|
219
|
+
*,
|
|
220
|
+
path: str | None,
|
|
221
|
+
requested_mode: Mode,
|
|
222
|
+
actual_mode: Literal["patch", "rebuild"],
|
|
223
|
+
fallback_used: bool,
|
|
224
|
+
measurement: PreservationMeasurement,
|
|
225
|
+
report: "VisualCompleteReport",
|
|
226
|
+
) -> MutationReport:
|
|
227
|
+
return MutationReport(
|
|
228
|
+
requested_mode=requested_mode,
|
|
229
|
+
actual_mode=actual_mode,
|
|
230
|
+
fallback_used=fallback_used,
|
|
231
|
+
changed_parts=measurement.changed_parts,
|
|
232
|
+
preservation=measurement.preservation,
|
|
233
|
+
verification=_verification_summary(report),
|
|
234
|
+
path=path,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def save_to_path(
|
|
239
|
+
doc: "HwpxDocument",
|
|
240
|
+
path: str | PathLike[str],
|
|
241
|
+
*,
|
|
242
|
+
mode: Mode = "auto",
|
|
243
|
+
fallback: Fallback = "error",
|
|
244
|
+
return_report: bool = False,
|
|
245
|
+
) -> str | PathLike[str] | MutationReport:
|
|
246
|
+
"""Persist pending changes to *path*.
|
|
247
|
+
|
|
248
|
+
Returns *path* by default; ``return_report=True`` returns the
|
|
249
|
+
:class:`~hwpx.mutation_report.MutationReport` receipt instead. ``mode`` and
|
|
250
|
+
``fallback`` are the Safe Write Contract grade controls — ``mode="patch"``
|
|
251
|
+
with ``fallback="error"`` raises :class:`PreservationDowngradeError` and
|
|
252
|
+
writes nothing when the save cannot keep every untouched part byte-identical.
|
|
253
|
+
"""
|
|
125
254
|
|
|
126
255
|
_run_pre_save_validation(doc)
|
|
127
|
-
archive_bytes = doc
|
|
128
|
-
|
|
129
|
-
|
|
256
|
+
archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
|
|
257
|
+
actual_mode, fallback_used = _resolve_grade(mode, fallback, measurement)
|
|
258
|
+
report = _gate_and_write(
|
|
259
|
+
doc, archive_bytes, output_path=path, source_label="document.save_to_path"
|
|
130
260
|
)
|
|
131
261
|
_mark_save_clean(doc)
|
|
262
|
+
if return_report:
|
|
263
|
+
return _compose_mutation_report(
|
|
264
|
+
path=str(path),
|
|
265
|
+
requested_mode=mode,
|
|
266
|
+
actual_mode=actual_mode,
|
|
267
|
+
fallback_used=fallback_used,
|
|
268
|
+
measurement=measurement,
|
|
269
|
+
report=report,
|
|
270
|
+
)
|
|
132
271
|
return path
|
|
133
272
|
|
|
134
273
|
|
|
135
|
-
def save_to_stream(
|
|
136
|
-
|
|
274
|
+
def save_to_stream(
|
|
275
|
+
doc: "HwpxDocument",
|
|
276
|
+
stream: BinaryIO,
|
|
277
|
+
*,
|
|
278
|
+
mode: Mode = "auto",
|
|
279
|
+
fallback: Fallback = "error",
|
|
280
|
+
return_report: bool = False,
|
|
281
|
+
) -> BinaryIO | MutationReport:
|
|
282
|
+
"""Persist pending changes to *stream*.
|
|
283
|
+
|
|
284
|
+
Returns *stream* by default; ``return_report=True`` returns the
|
|
285
|
+
:class:`~hwpx.mutation_report.MutationReport` receipt instead. See
|
|
286
|
+
:func:`save_to_path` for the ``mode``/``fallback`` grade semantics.
|
|
287
|
+
"""
|
|
137
288
|
|
|
138
289
|
_run_pre_save_validation(doc)
|
|
139
|
-
archive_bytes = doc
|
|
140
|
-
|
|
141
|
-
|
|
290
|
+
archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
|
|
291
|
+
actual_mode, fallback_used = _resolve_grade(mode, fallback, measurement)
|
|
292
|
+
report = _gate_and_write(
|
|
293
|
+
doc, archive_bytes, output_stream=stream, source_label="document.save_to_stream"
|
|
142
294
|
)
|
|
143
295
|
_mark_save_clean(doc)
|
|
296
|
+
if return_report:
|
|
297
|
+
return _compose_mutation_report(
|
|
298
|
+
path=None,
|
|
299
|
+
requested_mode=mode,
|
|
300
|
+
actual_mode=actual_mode,
|
|
301
|
+
fallback_used=fallback_used,
|
|
302
|
+
measurement=measurement,
|
|
303
|
+
report=report,
|
|
304
|
+
)
|
|
144
305
|
return stream
|
|
145
306
|
|
|
146
307
|
|
|
@@ -193,11 +354,26 @@ def save_report(
|
|
|
193
354
|
return report
|
|
194
355
|
|
|
195
356
|
|
|
196
|
-
def to_bytes(
|
|
197
|
-
|
|
357
|
+
def to_bytes(
|
|
358
|
+
doc: "HwpxDocument",
|
|
359
|
+
*,
|
|
360
|
+
mode: Mode = "auto",
|
|
361
|
+
fallback: Fallback = "error",
|
|
362
|
+
) -> bytes:
|
|
363
|
+
"""Serialize pending changes and return the HWPX archive as bytes.
|
|
364
|
+
|
|
365
|
+
``mode``/``fallback`` enforce the Safe Write Contract grade the same way as
|
|
366
|
+
:func:`save_to_path`: ``mode="patch"`` with ``fallback="error"`` raises
|
|
367
|
+
:class:`PreservationDowngradeError` before returning when the archive is not
|
|
368
|
+
patch-grade. There is no ``return_report`` — the enforcement is via the typed
|
|
369
|
+
exception, so the byte return stays unchanged.
|
|
370
|
+
"""
|
|
198
371
|
|
|
199
372
|
_run_pre_save_validation(doc)
|
|
200
|
-
|
|
373
|
+
archive_bytes, measurement = _build_measured(doc, reset_dirty=False)
|
|
374
|
+
_resolve_grade(mode, fallback, measurement)
|
|
375
|
+
_mark_save_clean(doc)
|
|
376
|
+
return archive_bytes
|
|
201
377
|
|
|
202
378
|
|
|
203
379
|
def _to_bytes_raw(
|
|
@@ -239,6 +415,11 @@ def _to_bytes_for_validation(doc: "HwpxDocument") -> bytes:
|
|
|
239
415
|
def _mark_save_clean(doc: "HwpxDocument") -> None:
|
|
240
416
|
doc._root.reset_dirty()
|
|
241
417
|
doc._package.version_info.mark_clean()
|
|
418
|
+
# The published archive is the new preservation baseline for any further
|
|
419
|
+
# save on this document instance.
|
|
420
|
+
package = doc._package
|
|
421
|
+
package._opened_members = dict(package._files)
|
|
422
|
+
package._opened_zip_infos = dict(package._zip_infos)
|
|
242
423
|
|
|
243
424
|
|
|
244
425
|
def save(
|
|
@@ -14,9 +14,7 @@ import copy
|
|
|
14
14
|
import hashlib
|
|
15
15
|
import json
|
|
16
16
|
import re
|
|
17
|
-
import zipfile
|
|
18
17
|
from collections.abc import Callable, Mapping, MutableMapping
|
|
19
|
-
from io import BytesIO
|
|
20
18
|
from pathlib import Path
|
|
21
19
|
from typing import Any
|
|
22
20
|
from xml.etree import ElementTree as ET
|
|
@@ -24,6 +22,7 @@ from xml.etree import ElementTree as ET
|
|
|
24
22
|
from lxml import etree as LET # type: ignore[reportAttributeAccessIssue] # lxml has no complete bundled typing
|
|
25
23
|
|
|
26
24
|
from hwpx.document import HwpxDocument
|
|
25
|
+
from hwpx.mutation_report import member_diff_bytes
|
|
27
26
|
from hwpx.oxml import HwpxOxmlTable
|
|
28
27
|
from hwpx.quality import QualityPolicy, SavePipeline
|
|
29
28
|
from hwpx.tools.package_validator import validate_editor_open_safety
|
|
@@ -1172,24 +1171,9 @@ def _quality_policy(value: str | Mapping[str, Any] | None) -> QualityPolicy:
|
|
|
1172
1171
|
|
|
1173
1172
|
|
|
1174
1173
|
def _member_diff(before: bytes, after: bytes) -> dict[str, Any]:
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
new_names = set(new_zip.namelist())
|
|
1179
|
-
shared = sorted(old_names & new_names)
|
|
1180
|
-
changed = [name for name in shared if old_zip.read(name) != new_zip.read(name)]
|
|
1181
|
-
unchanged = len(shared) - len(changed)
|
|
1182
|
-
return {
|
|
1183
|
-
"ok": True,
|
|
1184
|
-
"changedMembers": changed,
|
|
1185
|
-
"addedMembers": sorted(new_names - old_names),
|
|
1186
|
-
"removedMembers": sorted(old_names - new_names),
|
|
1187
|
-
"unchangedMemberCount": unchanged,
|
|
1188
|
-
"beforeMemberCount": len(old_names),
|
|
1189
|
-
"afterMemberCount": len(new_names),
|
|
1190
|
-
}
|
|
1191
|
-
except (OSError, zipfile.BadZipFile) as exc:
|
|
1192
|
-
return {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
1174
|
+
# Shared with the Safe Write Contract's MutationReport spine: one uncompressed
|
|
1175
|
+
# member comparison, one home for the diff shape (mutation_report.py).
|
|
1176
|
+
return member_diff_bytes(before, after)
|
|
1193
1177
|
|
|
1194
1178
|
|
|
1195
1179
|
def _verify_header_story_candidates(
|