python-hwpx 2.10.3__tar.gz → 2.11.1__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-2.10.3/src/python_hwpx.egg-info → python_hwpx-2.11.1}/PKG-INFO +14 -1
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/README.md +13 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/pyproject.toml +1 -1
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/__init__.py +88 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/authoring.py +139 -6
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/builder/__init__.py +2 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/builder/core.py +66 -2
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/builder/report.py +10 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/document.py +938 -8
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/opc/package.py +12 -1
- python_hwpx-2.11.1/src/hwpx/opc/security.py +134 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/opc/xml_utils.py +15 -2
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/__init__.py +7 -20
- python_hwpx-2.10.3/src/hwpx/oxml/document.py → python_hwpx-2.11.1/src/hwpx/oxml/_document_impl.py +244 -22
- python_hwpx-2.11.1/src/hwpx/oxml/document.py +40 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/header_part.py +1 -1
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/memo.py +2 -2
- python_hwpx-2.11.1/src/hwpx/oxml/numbering.py +8 -0
- python_hwpx-2.11.1/src/hwpx/oxml/objects.py +8 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/paragraph.py +1 -1
- python_hwpx-2.11.1/src/hwpx/oxml/run.py +8 -0
- python_hwpx-2.11.1/src/hwpx/oxml/section.py +11 -0
- python_hwpx-2.11.1/src/hwpx/oxml/simple_parts.py +8 -0
- python_hwpx-2.11.1/src/hwpx/oxml/table.py +11 -0
- python_hwpx-2.11.1/src/hwpx/patch.py +653 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/__init__.py +74 -0
- python_hwpx-2.11.1/src/hwpx/tools/advanced_generators.py +154 -0
- python_hwpx-2.11.1/src/hwpx/tools/doc_diff.py +349 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/exporter.py +4 -1
- python_hwpx-2.11.1/src/hwpx/tools/fuzz/__init__.py +36 -0
- python_hwpx-2.11.1/src/hwpx/tools/fuzz/__main__.py +80 -0
- python_hwpx-2.11.1/src/hwpx/tools/fuzz/catalog.py +299 -0
- python_hwpx-2.11.1/src/hwpx/tools/fuzz/generator.py +195 -0
- python_hwpx-2.11.1/src/hwpx/tools/fuzz/minimize.py +33 -0
- python_hwpx-2.11.1/src/hwpx/tools/fuzz/runner.py +503 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/id_integrity.py +156 -4
- python_hwpx-2.11.1/src/hwpx/tools/layout_preview.py +573 -0
- python_hwpx-2.11.1/src/hwpx/tools/mail_merge.py +282 -0
- python_hwpx-2.11.1/src/hwpx/tools/official_lint.py +373 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/package_validator.py +189 -7
- python_hwpx-2.11.1/src/hwpx/tools/style_profile.py +437 -0
- python_hwpx-2.11.1/src/hwpx/tools/table_compute.py +477 -0
- python_hwpx-2.11.1/src/hwpx/tools/template_analyzer.py +656 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/text_extractor.py +10 -5
- {python_hwpx-2.10.3 → python_hwpx-2.11.1/src/python_hwpx.egg-info}/PKG-INFO +14 -1
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/python_hwpx.egg-info/SOURCES.txt +33 -0
- python_hwpx-2.11.1/tests/test_advanced_generators.py +117 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_builder_core.py +9 -0
- python_hwpx-2.11.1/tests/test_doc_diff.py +102 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_document_plan.py +48 -0
- python_hwpx-2.11.1/tests/test_existing_document_format_editing.py +167 -0
- python_hwpx-2.11.1/tests/test_form_fields.py +100 -0
- python_hwpx-2.11.1/tests/test_fuzz_loop.py +118 -0
- python_hwpx-2.11.1/tests/test_fuzz_regressions.py +45 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_gap_closure_tools.py +1 -1
- python_hwpx-2.11.1/tests/test_image_object_workflow.py +124 -0
- python_hwpx-2.11.1/tests/test_kordoc_absorption.py +225 -0
- python_hwpx-2.11.1/tests/test_layout_preview.py +59 -0
- python_hwpx-2.11.1/tests/test_mail_merge_table_compute.py +143 -0
- python_hwpx-2.11.1/tests/test_official_document_style.py +134 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_opc_package.py +45 -3
- python_hwpx-2.11.1/tests/test_style_profile.py +114 -0
- python_hwpx-2.11.1/tests/test_template_analyzer_enrichment.py +110 -0
- python_hwpx-2.10.3/src/hwpx/oxml/section.py +0 -11
- python_hwpx-2.10.3/src/hwpx/oxml/table.py +0 -11
- python_hwpx-2.10.3/src/hwpx/tools/template_analyzer.py +0 -235
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/LICENSE +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/NOTICE +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/setup.cfg +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/package.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/py.typed +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/template_formfit.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/templates.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_document_formatting.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_document_save_api.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_id_integrity.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_inline_models.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_markdown_export.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_new_features.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_repair_repack.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_report_parser.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_report_utils.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_section_headers.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_table_navigation.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_template_formfit.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_validation_severity.py +0 -0
- {python_hwpx-2.10.3 → python_hwpx-2.11.1}/tests/test_version_metadata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.1
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -261,6 +261,19 @@ HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python
|
|
|
261
261
|
| 🧰 **작업 도구** | unpack/pack/분석/비교 | pack-ready 작업 디렉터리 추출과 재구성 점검 |
|
|
262
262
|
| 🏗️ **저수준 XML** | 데이터클래스 매핑 | OWPML 스키마 ↔ Python 객체 직접 조작 |
|
|
263
263
|
| 🔄 **네임스페이스 호환** | 자동 정규화 | HWPML 2016 → 2011 자동 변환 |
|
|
264
|
+
| 🏗️ **빌더** | 조립형 생성 | `hwpx.builder` — Section/Heading/Table/Image/Header 조립, 하드게이트 저장 리포트 |
|
|
265
|
+
| ✅ **편집기 오픈 안전** | `validate_editor_open_safety` | 저장/팩/리페어/빌더 출력 게이트, `openSafety` 증거 반환 |
|
|
266
|
+
| 🧪 **퍼징 수렴 루프** | `hwpx.tools.fuzz` | 시드 결정적 시나리오 생성 · 3중 오라클 러너 · 회귀 fixture 박제 |
|
|
267
|
+
| 🖥️ **레이아웃 프리뷰** | `hwpx.tools.layout_preview` | 페이지 박스·표·여백 근사 HTML/PNG (에이전트 자기검증용) |
|
|
268
|
+
| 🧷 **바이트 보존 패치** | `hwpx.patch` | section XML 바이트 splice — 미수정 영역 바이트 보존 |
|
|
269
|
+
| 📐 **기존 문서 서식 편집** | 문단·페이지 | 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 머리말/쪽번호, 불릿/번호 |
|
|
270
|
+
| 🖊️ **누름틀** | 양식 필드 | 클릭히어 필드 조회·서식 보존 채움 |
|
|
271
|
+
| 🏛️ **공문서 도구** | `official_lint` · 결재란 | 항목기호 위계·"끝." 표시·붙임·날짜 표기 lint, 결재란 프리셋 |
|
|
272
|
+
| 📷 **고급 생성기** | `advanced_generators` | 사진대지(image_grid)·회의 명패·표 기반 조직도 |
|
|
273
|
+
| 🆚 **신구대조** | `doc_diff` | 문단 LCS diff·신구대조표 생성·참조 정합 lint |
|
|
274
|
+
| 📨 **메일머지·표 계산** | `mail_merge` | 템플릿+데이터 N부 대량 생성, 표 합계·평균 |
|
|
275
|
+
| 🪄 **서식 이식** | `style_profile` | 참조 문서 프로파일 추출·적용, 템플릿 레지스트리 |
|
|
276
|
+
| 🛡️ **입력 강건화** | `opc.security` | XML entity 폭탄·ZIP 압축 폭탄 가드 |
|
|
264
277
|
|
|
265
278
|
## 기능 상세
|
|
266
279
|
|
|
@@ -225,6 +225,19 @@ HWPX 파일은 **ZIP + XML** 구조이므로, 한/글 프로그램 없이 Python
|
|
|
225
225
|
| 🧰 **작업 도구** | unpack/pack/분석/비교 | pack-ready 작업 디렉터리 추출과 재구성 점검 |
|
|
226
226
|
| 🏗️ **저수준 XML** | 데이터클래스 매핑 | OWPML 스키마 ↔ Python 객체 직접 조작 |
|
|
227
227
|
| 🔄 **네임스페이스 호환** | 자동 정규화 | HWPML 2016 → 2011 자동 변환 |
|
|
228
|
+
| 🏗️ **빌더** | 조립형 생성 | `hwpx.builder` — Section/Heading/Table/Image/Header 조립, 하드게이트 저장 리포트 |
|
|
229
|
+
| ✅ **편집기 오픈 안전** | `validate_editor_open_safety` | 저장/팩/리페어/빌더 출력 게이트, `openSafety` 증거 반환 |
|
|
230
|
+
| 🧪 **퍼징 수렴 루프** | `hwpx.tools.fuzz` | 시드 결정적 시나리오 생성 · 3중 오라클 러너 · 회귀 fixture 박제 |
|
|
231
|
+
| 🖥️ **레이아웃 프리뷰** | `hwpx.tools.layout_preview` | 페이지 박스·표·여백 근사 HTML/PNG (에이전트 자기검증용) |
|
|
232
|
+
| 🧷 **바이트 보존 패치** | `hwpx.patch` | section XML 바이트 splice — 미수정 영역 바이트 보존 |
|
|
233
|
+
| 📐 **기존 문서 서식 편집** | 문단·페이지 | 정렬·줄간격·들여쓰기·문단 간격, 용지·여백·방향, 머리말/쪽번호, 불릿/번호 |
|
|
234
|
+
| 🖊️ **누름틀** | 양식 필드 | 클릭히어 필드 조회·서식 보존 채움 |
|
|
235
|
+
| 🏛️ **공문서 도구** | `official_lint` · 결재란 | 항목기호 위계·"끝." 표시·붙임·날짜 표기 lint, 결재란 프리셋 |
|
|
236
|
+
| 📷 **고급 생성기** | `advanced_generators` | 사진대지(image_grid)·회의 명패·표 기반 조직도 |
|
|
237
|
+
| 🆚 **신구대조** | `doc_diff` | 문단 LCS diff·신구대조표 생성·참조 정합 lint |
|
|
238
|
+
| 📨 **메일머지·표 계산** | `mail_merge` | 템플릿+데이터 N부 대량 생성, 표 합계·평균 |
|
|
239
|
+
| 🪄 **서식 이식** | `style_profile` | 참조 문서 프로파일 추출·적용, 템플릿 레지스트리 |
|
|
240
|
+
| 🛡️ **입력 강건화** | `opc.security` | XML entity 폭탄·ZIP 압축 폭탄 가드 |
|
|
228
241
|
|
|
229
242
|
## 기능 상세
|
|
230
243
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.11.1"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -25,12 +25,63 @@ from .tools.text_extractor import (
|
|
|
25
25
|
TextExtractor,
|
|
26
26
|
)
|
|
27
27
|
from .tools.object_finder import FoundElement, ObjectFinder
|
|
28
|
+
from .tools.advanced_generators import (
|
|
29
|
+
build_image_grid,
|
|
30
|
+
build_meeting_nameplates,
|
|
31
|
+
build_organization_chart,
|
|
32
|
+
)
|
|
33
|
+
from .tools.doc_diff import (
|
|
34
|
+
DOC_DIFF_REPORT_VERSION,
|
|
35
|
+
REFERENCE_CONSISTENCY_REPORT_VERSION,
|
|
36
|
+
build_comparison_table_plan,
|
|
37
|
+
diff_paragraphs,
|
|
38
|
+
doc_diff,
|
|
39
|
+
inspect_reference_consistency,
|
|
40
|
+
)
|
|
41
|
+
from .tools.mail_merge import (
|
|
42
|
+
MAIL_MERGE_REPORT_VERSION,
|
|
43
|
+
inspect_mail_merge_placeholders,
|
|
44
|
+
load_mail_merge_rows,
|
|
45
|
+
mail_merge,
|
|
46
|
+
)
|
|
47
|
+
from .tools.table_compute import (
|
|
48
|
+
TABLE_COMPUTE_REPORT_VERSION,
|
|
49
|
+
table_compute,
|
|
50
|
+
)
|
|
51
|
+
from .tools.official_lint import (
|
|
52
|
+
OFFICIAL_DOCUMENT_STYLE_REPORT_VERSION,
|
|
53
|
+
inspect_official_document_style,
|
|
54
|
+
)
|
|
28
55
|
from .tools.package_validator import (
|
|
29
56
|
EditorOpenSafetyReport,
|
|
30
57
|
PackageValidationReport,
|
|
31
58
|
validate_editor_open_safety,
|
|
32
59
|
validate_package,
|
|
33
60
|
)
|
|
61
|
+
from .tools.style_profile import (
|
|
62
|
+
STYLE_PROFILE_COMPARISON_SCHEMA_VERSION,
|
|
63
|
+
STYLE_PROFILE_SCHEMA_VERSION,
|
|
64
|
+
TEMPLATE_REGISTRY_SCHEMA_VERSION,
|
|
65
|
+
apply_style_profile_to_plan,
|
|
66
|
+
compare_style_profiles,
|
|
67
|
+
describe_template,
|
|
68
|
+
extract_style_profile,
|
|
69
|
+
list_templates,
|
|
70
|
+
placeholder_fill_report,
|
|
71
|
+
register_template,
|
|
72
|
+
)
|
|
73
|
+
from .tools.layout_preview import (
|
|
74
|
+
LayoutPreview,
|
|
75
|
+
PreviewPage,
|
|
76
|
+
render_layout_preview,
|
|
77
|
+
)
|
|
78
|
+
from .patch import (
|
|
79
|
+
BytePreservingPatchResult,
|
|
80
|
+
ParagraphTextPatch,
|
|
81
|
+
PatchApplied,
|
|
82
|
+
PatchSkipped,
|
|
83
|
+
paragraph_patch,
|
|
84
|
+
)
|
|
34
85
|
from .document import HwpxDocument
|
|
35
86
|
from .package import HwpxPackage
|
|
36
87
|
from .authoring import (
|
|
@@ -48,6 +99,7 @@ from .authoring import (
|
|
|
48
99
|
normalize_document_plan,
|
|
49
100
|
validate_document_plan,
|
|
50
101
|
)
|
|
102
|
+
from .builder import approval_box
|
|
51
103
|
from .template_formfit import (
|
|
52
104
|
TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION,
|
|
53
105
|
TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION,
|
|
@@ -67,23 +119,59 @@ __all__ = [
|
|
|
67
119
|
"EditorOpenSafetyReport",
|
|
68
120
|
"ParagraphInfo",
|
|
69
121
|
"PackageValidationReport",
|
|
122
|
+
"BytePreservingPatchResult",
|
|
70
123
|
"PlanValidationReport",
|
|
124
|
+
"ParagraphTextPatch",
|
|
125
|
+
"PatchApplied",
|
|
126
|
+
"PatchSkipped",
|
|
127
|
+
"LayoutPreview",
|
|
128
|
+
"PreviewPage",
|
|
71
129
|
"SectionInfo",
|
|
72
130
|
"TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION",
|
|
73
131
|
"TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION",
|
|
74
132
|
"TextExtractor",
|
|
75
133
|
"FoundElement",
|
|
76
134
|
"ObjectFinder",
|
|
135
|
+
"OFFICIAL_DOCUMENT_STYLE_REPORT_VERSION",
|
|
136
|
+
"DOC_DIFF_REPORT_VERSION",
|
|
137
|
+
"REFERENCE_CONSISTENCY_REPORT_VERSION",
|
|
138
|
+
"MAIL_MERGE_REPORT_VERSION",
|
|
139
|
+
"STYLE_PROFILE_COMPARISON_SCHEMA_VERSION",
|
|
140
|
+
"STYLE_PROFILE_SCHEMA_VERSION",
|
|
141
|
+
"TEMPLATE_REGISTRY_SCHEMA_VERSION",
|
|
142
|
+
"TABLE_COMPUTE_REPORT_VERSION",
|
|
143
|
+
"apply_style_profile_to_plan",
|
|
144
|
+
"build_comparison_table_plan",
|
|
145
|
+
"build_image_grid",
|
|
146
|
+
"build_meeting_nameplates",
|
|
147
|
+
"build_organization_chart",
|
|
148
|
+
"diff_paragraphs",
|
|
149
|
+
"doc_diff",
|
|
150
|
+
"compare_style_profiles",
|
|
77
151
|
"PlanValidationIssue",
|
|
78
152
|
"HwpxDocument",
|
|
79
153
|
"HwpxPackage",
|
|
80
154
|
"create_document_from_plan",
|
|
81
155
|
"analyze_template_formfit",
|
|
82
156
|
"apply_template_formfit",
|
|
157
|
+
"approval_box",
|
|
158
|
+
"describe_template",
|
|
159
|
+
"extract_style_profile",
|
|
83
160
|
"inspect_document_authoring_quality",
|
|
161
|
+
"inspect_mail_merge_placeholders",
|
|
162
|
+
"inspect_official_document_style",
|
|
163
|
+
"inspect_reference_consistency",
|
|
84
164
|
"inspect_operating_plan_quality",
|
|
165
|
+
"list_templates",
|
|
166
|
+
"load_mail_merge_rows",
|
|
167
|
+
"mail_merge",
|
|
85
168
|
"normalize_document_plan",
|
|
169
|
+
"placeholder_fill_report",
|
|
86
170
|
"validate_document_plan",
|
|
87
171
|
"validate_editor_open_safety",
|
|
88
172
|
"validate_package",
|
|
173
|
+
"paragraph_patch",
|
|
174
|
+
"render_layout_preview",
|
|
175
|
+
"register_template",
|
|
176
|
+
"table_compute",
|
|
89
177
|
]
|
|
@@ -26,11 +26,13 @@ from .builder import (
|
|
|
26
26
|
Run as BuilderRun,
|
|
27
27
|
Section as BuilderSection,
|
|
28
28
|
Table as BuilderTable,
|
|
29
|
+
approval_box as BuilderApprovalBox,
|
|
29
30
|
)
|
|
30
31
|
from .builder.core import Toc as BuilderToc
|
|
31
32
|
from .document import HwpxDocument
|
|
32
33
|
from .tools.package_validator import validate_package
|
|
33
34
|
from .tools.table_cleanup import normalize_cell_text
|
|
35
|
+
from .tools.advanced_generators import build_image_grid
|
|
34
36
|
from .tools.report_utils import (
|
|
35
37
|
calculate_age,
|
|
36
38
|
calculate_ratios,
|
|
@@ -166,16 +168,30 @@ class DocumentStylePreset:
|
|
|
166
168
|
heading_bold: bool = True
|
|
167
169
|
heading_underline: bool = True
|
|
168
170
|
table_header_bold: bool = True
|
|
171
|
+
title_size: int = 18
|
|
172
|
+
subtitle_size: int = 12
|
|
173
|
+
heading_size: int = 14
|
|
174
|
+
font: str = "함초롬바탕"
|
|
169
175
|
|
|
170
176
|
def ensure_tokens(self, document: HwpxDocument) -> dict[str, str]:
|
|
171
177
|
"""Create/reuse run styles and return semantic token IDs."""
|
|
172
178
|
|
|
173
179
|
return {
|
|
174
|
-
"title": document.ensure_run_style(
|
|
175
|
-
|
|
180
|
+
"title": document.ensure_run_style(
|
|
181
|
+
bold=self.title_bold,
|
|
182
|
+
size=self.title_size,
|
|
183
|
+
font=self.font,
|
|
184
|
+
),
|
|
185
|
+
"subtitle": document.ensure_run_style(
|
|
186
|
+
italic=self.subtitle_italic,
|
|
187
|
+
size=self.subtitle_size,
|
|
188
|
+
font=self.font,
|
|
189
|
+
),
|
|
176
190
|
"heading": document.ensure_run_style(
|
|
177
191
|
bold=self.heading_bold,
|
|
178
192
|
underline=self.heading_underline,
|
|
193
|
+
size=self.heading_size,
|
|
194
|
+
font=self.font,
|
|
179
195
|
),
|
|
180
196
|
"body": document.ensure_run_style(),
|
|
181
197
|
"bullet": document.ensure_run_style(),
|
|
@@ -184,6 +200,25 @@ class DocumentStylePreset:
|
|
|
184
200
|
}
|
|
185
201
|
|
|
186
202
|
|
|
203
|
+
def _outline_style_refs(document: HwpxDocument, level: int) -> dict[str, str | int]:
|
|
204
|
+
"""Return paragraph style refs for a HWP outline heading level, if available."""
|
|
205
|
+
|
|
206
|
+
safe_level = min(10, max(1, int(level)))
|
|
207
|
+
for style in document.styles.values():
|
|
208
|
+
name = str(style.name or "")
|
|
209
|
+
eng_name = str(style.eng_name or "")
|
|
210
|
+
if name == f"개요 {safe_level}" or eng_name == f"Outline {safe_level}":
|
|
211
|
+
refs: dict[str, str | int] = {}
|
|
212
|
+
style_id = style.raw_id if style.raw_id is not None else style.id
|
|
213
|
+
if style_id is None:
|
|
214
|
+
continue
|
|
215
|
+
refs["style_id_ref"] = style_id
|
|
216
|
+
if style.para_pr_id_ref is not None:
|
|
217
|
+
refs["para_pr_id_ref"] = int(style.para_pr_id_ref)
|
|
218
|
+
return refs
|
|
219
|
+
return {}
|
|
220
|
+
|
|
221
|
+
|
|
187
222
|
def _plan_issue(
|
|
188
223
|
code: str,
|
|
189
224
|
path: str,
|
|
@@ -603,9 +638,13 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
|
|
|
603
638
|
"numberedList",
|
|
604
639
|
"table",
|
|
605
640
|
"image",
|
|
641
|
+
"image_grid",
|
|
642
|
+
"imageGrid",
|
|
606
643
|
"toc",
|
|
607
644
|
"page_break",
|
|
608
645
|
"pageBreak",
|
|
646
|
+
"approval_box",
|
|
647
|
+
"approvalBox",
|
|
609
648
|
}
|
|
610
649
|
if block_type not in supported:
|
|
611
650
|
return [
|
|
@@ -627,6 +666,27 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
|
|
|
627
666
|
suggestion=f"Add non-empty {text_key}.",
|
|
628
667
|
)
|
|
629
668
|
]
|
|
669
|
+
if block_type in {"image_grid", "imageGrid"}:
|
|
670
|
+
images = raw_block.get("images")
|
|
671
|
+
if not isinstance(images, list) or not images:
|
|
672
|
+
return [
|
|
673
|
+
_plan_issue(
|
|
674
|
+
"missing_image_grid_images",
|
|
675
|
+
f"{path}.images",
|
|
676
|
+
f"{path}.images must be a non-empty list",
|
|
677
|
+
suggestion="Add image items with path and optional caption fields.",
|
|
678
|
+
)
|
|
679
|
+
]
|
|
680
|
+
for image_index, image in enumerate(images):
|
|
681
|
+
if not isinstance(image, Mapping) or not str(image.get("path") or "").strip():
|
|
682
|
+
return [
|
|
683
|
+
_plan_issue(
|
|
684
|
+
"missing_image_path",
|
|
685
|
+
f"{path}.images[{image_index}].path",
|
|
686
|
+
f"{path}.images[{image_index}].path is required",
|
|
687
|
+
suggestion="Set a non-empty image path.",
|
|
688
|
+
)
|
|
689
|
+
]
|
|
630
690
|
if block_type in {"bullets", "bullet", "numbered_list", "numberedList"}:
|
|
631
691
|
if not _string_list(raw_block.get("items")):
|
|
632
692
|
return [
|
|
@@ -672,6 +732,14 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
|
|
|
672
732
|
if isinstance(row, (list, tuple)):
|
|
673
733
|
for col_index, value in enumerate(row):
|
|
674
734
|
issues.extend(_computed_field_issues(value, path=f"{path}.rows[{row_index}][{col_index}]"))
|
|
735
|
+
elif block_type in {"image_grid", "imageGrid"}:
|
|
736
|
+
for image_index, image in enumerate(raw_block.get("images") or []):
|
|
737
|
+
if isinstance(image, Mapping):
|
|
738
|
+
issues.extend(_computed_field_issues(image.get("caption"), path=f"{path}.images[{image_index}].caption"))
|
|
739
|
+
elif block_type in {"approval_box", "approvalBox"}:
|
|
740
|
+
for label_index, label in enumerate(raw_block.get("labels") or []):
|
|
741
|
+
issues.extend(_computed_field_issues(label, path=f"{path}.labels[{label_index}]"))
|
|
742
|
+
issues.extend(_computed_field_issues(raw_block.get("delegated"), path=f"{path}.delegated"))
|
|
675
743
|
elif block_type == "toc":
|
|
676
744
|
issues.extend(_computed_field_issues(raw_block.get("title"), path=f"{path}.title"))
|
|
677
745
|
for entry_index, entry in enumerate(raw_block.get("entries") or []):
|
|
@@ -1325,11 +1393,15 @@ def _normalize_v2_section(raw_section: Any, *, index: int) -> BuilderSection:
|
|
|
1325
1393
|
if not isinstance(raw_section, Mapping):
|
|
1326
1394
|
raise TypeError(f"sections[{index}] must be a mapping")
|
|
1327
1395
|
raw_blocks = raw_section.get("blocks", raw_section.get("children"))
|
|
1396
|
+
children: list[Any] = []
|
|
1397
|
+
for block_index, raw_block in enumerate(raw_blocks or []):
|
|
1398
|
+
normalized = _normalize_v2_block(raw_block, path=f"sections[{index}].blocks[{block_index}]")
|
|
1399
|
+
if isinstance(normalized, tuple):
|
|
1400
|
+
children.extend(normalized)
|
|
1401
|
+
else:
|
|
1402
|
+
children.append(normalized)
|
|
1328
1403
|
return BuilderSection(
|
|
1329
|
-
children=tuple(
|
|
1330
|
-
_normalize_v2_block(raw_block, path=f"sections[{index}].blocks[{block_index}]")
|
|
1331
|
-
for block_index, raw_block in enumerate(raw_blocks or [])
|
|
1332
|
-
),
|
|
1404
|
+
children=tuple(children),
|
|
1333
1405
|
page=_normalize_v2_page(raw_section.get("page")),
|
|
1334
1406
|
margins=_normalize_v2_margins(raw_section.get("margins")),
|
|
1335
1407
|
header=_normalize_v2_header_footer(raw_section.get("header"), kind="header"),
|
|
@@ -1455,6 +1527,23 @@ def _normalize_v2_block(raw_block: Any, *, path: str) -> Any:
|
|
|
1455
1527
|
for item in raw_block.get("columnWidths", raw_block.get("column_widths")) or ()
|
|
1456
1528
|
),
|
|
1457
1529
|
)
|
|
1530
|
+
if block_type in {"approval_box", "approvalBox"}:
|
|
1531
|
+
labels = tuple(replace_computed_fields(str(item)) for item in _string_list(raw_block.get("labels")))
|
|
1532
|
+
return BuilderApprovalBox(
|
|
1533
|
+
labels=labels or None,
|
|
1534
|
+
approver_rows=_int_value(
|
|
1535
|
+
raw_block.get("approverRows", raw_block.get("approver_rows")),
|
|
1536
|
+
default=2,
|
|
1537
|
+
),
|
|
1538
|
+
delegated=(
|
|
1539
|
+
replace_computed_fields(str(raw_block.get("delegated")))
|
|
1540
|
+
if raw_block.get("delegated") is not None
|
|
1541
|
+
else None
|
|
1542
|
+
),
|
|
1543
|
+
header_shading=str(raw_block.get("headerShading", raw_block.get("header_shading")) or "EAF1FB"),
|
|
1544
|
+
)
|
|
1545
|
+
if block_type in {"image_grid", "imageGrid"}:
|
|
1546
|
+
return _image_grid_builder_nodes(raw_block)
|
|
1458
1547
|
if block_type == "image":
|
|
1459
1548
|
return BuilderImage(
|
|
1460
1549
|
path=str(raw_block.get("path") or ""),
|
|
@@ -1481,6 +1570,49 @@ def _normalize_v2_block(raw_block: Any, *, path: str) -> Any:
|
|
|
1481
1570
|
raise ValueError(f"{path}.type is unsupported: {block_type!r}")
|
|
1482
1571
|
|
|
1483
1572
|
|
|
1573
|
+
def _image_grid_builder_nodes(raw_block: Mapping[str, Any]) -> tuple[Any, ...]:
|
|
1574
|
+
block = build_image_grid(
|
|
1575
|
+
[
|
|
1576
|
+
{
|
|
1577
|
+
"path": str(image.get("path") or ""),
|
|
1578
|
+
"caption": replace_computed_fields(str(image.get("caption") or "")),
|
|
1579
|
+
}
|
|
1580
|
+
for image in raw_block.get("images") or ()
|
|
1581
|
+
if isinstance(image, Mapping)
|
|
1582
|
+
],
|
|
1583
|
+
columns=_int_value(raw_block.get("columns"), default=2),
|
|
1584
|
+
image_width_mm=_optional_number(raw_block.get("imageWidthMm", raw_block.get("image_width_mm"))),
|
|
1585
|
+
)
|
|
1586
|
+
columns = int(block["columns"])
|
|
1587
|
+
images = list(block["images"])
|
|
1588
|
+
rows: list[tuple[str, ...]] = []
|
|
1589
|
+
for offset in range(0, len(images), columns):
|
|
1590
|
+
row = []
|
|
1591
|
+
for image_index, image in enumerate(images[offset : offset + columns], start=offset + 1):
|
|
1592
|
+
row.append(f"{image_index}. {image['caption']} ({Path(str(image['path'])).name})")
|
|
1593
|
+
row.extend("" for _ in range(columns - len(row)))
|
|
1594
|
+
rows.append(tuple(row))
|
|
1595
|
+
image_width = _optional_number(raw_block.get("imageWidthMm", raw_block.get("image_width_mm")))
|
|
1596
|
+
nodes: list[Any] = [
|
|
1597
|
+
BuilderTable(
|
|
1598
|
+
header=tuple(f"사진 {index + 1}" for index in range(columns)),
|
|
1599
|
+
rows=tuple(rows),
|
|
1600
|
+
header_shading=_optional_str(raw_block.get("headerShading", raw_block.get("header_shading"))) or "EAF1FB",
|
|
1601
|
+
column_widths=tuple(1 for _ in range(columns)),
|
|
1602
|
+
)
|
|
1603
|
+
]
|
|
1604
|
+
for image_index, image in enumerate(images, start=1):
|
|
1605
|
+
nodes.append(
|
|
1606
|
+
BuilderImage(
|
|
1607
|
+
path=str(image["path"]),
|
|
1608
|
+
width_mm=image_width,
|
|
1609
|
+
align=_optional_str(raw_block.get("align")) or "center",
|
|
1610
|
+
caption=f"{image_index}. {image['caption']}",
|
|
1611
|
+
)
|
|
1612
|
+
)
|
|
1613
|
+
return tuple(nodes)
|
|
1614
|
+
|
|
1615
|
+
|
|
1484
1616
|
def _lower_plan_to_builder_document(plan: DocumentPlan) -> BuilderDocument:
|
|
1485
1617
|
"""Lower a normalized document plan to builder nodes.
|
|
1486
1618
|
|
|
@@ -1618,6 +1750,7 @@ def _render_block(
|
|
|
1618
1750
|
block.text,
|
|
1619
1751
|
char_pr_id_ref=tokens["heading"],
|
|
1620
1752
|
inherit_style=False,
|
|
1753
|
+
**_outline_style_refs(document, block.level),
|
|
1621
1754
|
)
|
|
1622
1755
|
return
|
|
1623
1756
|
if isinstance(block, BuilderParagraph):
|
|
@@ -25,6 +25,25 @@ _A4_HWP_SIZE = (59528, 84188)
|
|
|
25
25
|
# changing default node contracts or the plan-v1 authoring style-token path.
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def _outline_style_refs(document: HwpxDocument, level: int) -> dict[str, str | int]:
|
|
29
|
+
"""Return paragraph style refs for the built-in HWP outline level, if present."""
|
|
30
|
+
|
|
31
|
+
safe_level = min(10, max(1, int(level)))
|
|
32
|
+
for style in document.styles.values():
|
|
33
|
+
name = str(style.name or "")
|
|
34
|
+
eng_name = str(style.eng_name or "")
|
|
35
|
+
if name == f"개요 {safe_level}" or eng_name == f"Outline {safe_level}":
|
|
36
|
+
refs: dict[str, str | int] = {}
|
|
37
|
+
style_id = style.raw_id if style.raw_id is not None else style.id
|
|
38
|
+
if style_id is None:
|
|
39
|
+
continue
|
|
40
|
+
refs["style_id_ref"] = style_id
|
|
41
|
+
if style.para_pr_id_ref is not None:
|
|
42
|
+
refs["para_pr_id_ref"] = int(style.para_pr_id_ref)
|
|
43
|
+
return refs
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
|
|
28
47
|
@dataclass(frozen=True)
|
|
29
48
|
class _BuilderPreset:
|
|
30
49
|
name: str = "default"
|
|
@@ -72,8 +91,8 @@ class _BuilderPreset:
|
|
|
72
91
|
if self.is_government_report:
|
|
73
92
|
if run.bold and color is None:
|
|
74
93
|
color = "1F4E79"
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
if (run.bold or run.underline or run.highlight) and font is None:
|
|
95
|
+
font = "함초롬바탕"
|
|
77
96
|
return {
|
|
78
97
|
"bold": run.bold,
|
|
79
98
|
"italic": run.italic,
|
|
@@ -264,6 +283,7 @@ class Heading:
|
|
|
264
283
|
section_index=section_index,
|
|
265
284
|
char_pr_id_ref=char_pr_id,
|
|
266
285
|
inherit_style=False,
|
|
286
|
+
**_outline_style_refs(document, self.level),
|
|
267
287
|
)
|
|
268
288
|
|
|
269
289
|
|
|
@@ -362,6 +382,50 @@ class Table:
|
|
|
362
382
|
table.set_column_widths(self.column_widths)
|
|
363
383
|
|
|
364
384
|
|
|
385
|
+
def approval_box(
|
|
386
|
+
*,
|
|
387
|
+
labels: Sequence[str] | None = None,
|
|
388
|
+
approver_rows: int = 2,
|
|
389
|
+
delegated: str | None = None,
|
|
390
|
+
header_shading: str = "EAF1FB",
|
|
391
|
+
) -> Table:
|
|
392
|
+
"""Return a merged approval/sign-off table for official documents."""
|
|
393
|
+
|
|
394
|
+
normalized_labels = tuple(str(label).strip() for label in (labels or ("기안", "검토", "결재", "전결")) if str(label).strip())
|
|
395
|
+
if not normalized_labels:
|
|
396
|
+
normalized_labels = ("기안", "검토", "결재", "전결")
|
|
397
|
+
delegated_label = str(delegated or "").strip()
|
|
398
|
+
if delegated_label and delegated_label not in normalized_labels:
|
|
399
|
+
normalized_labels = (*normalized_labels, delegated_label)
|
|
400
|
+
row_count = max(int(approver_rows), 1)
|
|
401
|
+
rows = tuple(tuple("" for _ in normalized_labels) for _ in range(row_count))
|
|
402
|
+
if row_count < 2:
|
|
403
|
+
merges: tuple[str, ...] = ()
|
|
404
|
+
else:
|
|
405
|
+
merges = tuple(
|
|
406
|
+
f"{_spreadsheet_column_name(index)}2:{_spreadsheet_column_name(index)}{row_count + 1}"
|
|
407
|
+
for index in range(len(normalized_labels))
|
|
408
|
+
)
|
|
409
|
+
return Table(
|
|
410
|
+
header=normalized_labels,
|
|
411
|
+
rows=rows,
|
|
412
|
+
merges=merges,
|
|
413
|
+
header_shading=header_shading,
|
|
414
|
+
column_widths=tuple(1 for _ in normalized_labels),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _spreadsheet_column_name(index: int) -> str:
|
|
419
|
+
if index < 0:
|
|
420
|
+
raise ValueError("column index must be non-negative")
|
|
421
|
+
value = index + 1
|
|
422
|
+
letters: list[str] = []
|
|
423
|
+
while value:
|
|
424
|
+
value, remainder = divmod(value - 1, 26)
|
|
425
|
+
letters.append(chr(ord("A") + remainder))
|
|
426
|
+
return "".join(reversed(letters))
|
|
427
|
+
|
|
428
|
+
|
|
365
429
|
@dataclass(frozen=True)
|
|
366
430
|
class Image:
|
|
367
431
|
path: str | PathLike[str] | bytes
|
|
@@ -82,6 +82,16 @@ class BuilderSaveReport:
|
|
|
82
82
|
else {
|
|
83
83
|
"ok": self.id_integrity.ok,
|
|
84
84
|
"dangling": [str(item) for item in self.id_integrity.dangling],
|
|
85
|
+
"orphan_bin_data": [
|
|
86
|
+
{
|
|
87
|
+
"item_id": item.item_id,
|
|
88
|
+
"path": item.path,
|
|
89
|
+
"aliases": list(item.aliases),
|
|
90
|
+
"sources": list(item.sources),
|
|
91
|
+
"severity": item.severity,
|
|
92
|
+
}
|
|
93
|
+
for item in self.id_integrity.orphan_bin_data
|
|
94
|
+
],
|
|
85
95
|
"ignored": [
|
|
86
96
|
{
|
|
87
97
|
"part": item.part,
|