athena-python-docx 0.18.3__tar.gz → 0.20.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.
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/CLAUDE.md +74 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/PKG-INFO +1 -1
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/__init__.py +1 -1
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_buffer.py +40 -6
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_http_doc.py +88 -17
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_image_utils.py +160 -1
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/comments.py +6 -2
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/document.py +73 -16
- athena_python_docx-0.20.0/docx/enum/shape.py +30 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/style.py +17 -68
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/errors.py +34 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/opc/coreprops.py +45 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/section.py +75 -33
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/shape.py +23 -6
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/shared.py +10 -8
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/styles/styles.py +45 -20
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/table.py +43 -31
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/hyperlink.py +13 -11
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/paragraph.py +4 -1
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/parfmt.py +25 -22
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/run.py +50 -11
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/pyproject.toml +1 -1
- athena_python_docx-0.20.0/scripts/dump_wire_fixtures.py +59 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/release.sh +9 -12
- athena_python_docx-0.20.0/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +3 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/intentional_deviations.json +0 -2
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/reports/GAP_ANALYSIS.md +5 -33
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/reports/gap_report.json +9 -1688
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/snapshots/athena_latest.json +2812 -580
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_batching_perf.py +4 -8
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_buffer.py +119 -4
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_commands.py +4 -3
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_http_transport.py +159 -6
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_parity_misc.py +35 -6
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_ptc.py +2 -2
- athena_python_docx-0.20.0/tests/test_python_docx_api_parity.py +357 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_revisions.py +18 -47
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_hyperlink.py +6 -4
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_parfmt.py +37 -17
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_section_dimensions.py +36 -17
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/uv.lock +1 -1
- athena_python_docx-0.18.3/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -1
- athena_python_docx-0.18.3/tests/test_python_docx_api_parity.py +0 -161
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/.gitignore +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/DOCX_EXEC_LAB.md +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/README.md +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_athena_extension.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_batching.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_execution.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_http.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_postproc.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_ptc.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_table_styles.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/_timeouts.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/api.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/bookmarks.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/charts.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/client.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/commands.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/section.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/table.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/enum/text.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/exceptions.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/fields.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/footnotes.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/math.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/oxml/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/revisions.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/sdt.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/session.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/settings.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/styles/style.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/font.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/pagebreak.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/text/tabstops.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/toc.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/docx/typing.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/docx_exec_lab.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/docx_exec_lab_examples/fast_table_fill.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/docx_exec_lab_examples/find_replace_literal.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/publish.sh +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/round_trip_smoke.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/smoke_test_block_not_found.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/scripts/validate_find_replace_asset.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/conftest.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/METHODOLOGY.md +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/ab_probe_cases.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/ab_probe_runner.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/binary_round_trip.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/fake_session.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/README.md +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/_runner.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/extractor.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/local_runner.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshot.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/ours_spec.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/parity_diff.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/real_world_cases.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/runner.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/stock_spec.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/README.md +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/__init__.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/baseline_gaps.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/compare.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/introspect.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/run_parity.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/parity/test_parity_gap.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_add_section_extract_items.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_athena_extensions_contract.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_athena_extensions_registry.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_block_not_found_error.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_add_table_not_supported.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_add_hyperlink_stash.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_add_run_via_cell_insert.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_format_stash.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_run_format_stash.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_inner_run_guard.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_text_plain_fastpath.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_cell_text_replace_semantics.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_collapsed_range_format.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_command_dataclasses.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_comments.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_asset_id_property.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_clear.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_create.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_create_from_template.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_document_factory_validation.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_docx_exec_lab.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_e2e_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_execution_scope.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_find_replace_session_open.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_hyperlink_coalescing.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_image_url_data_uri.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_insert_deferred.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_iter_inner_content.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_list_styles.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_merged_cell_secondary_slot.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_merged_cells.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_oxml_shim.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_paragraph_text_len_cache.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_parity_round2.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_phase_a_behavior.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_phase_b_headers_footers.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_phase_c_tables.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_positional_cell_id.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_postproc_cell_format_rewrite.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_postproc_cell_run_format_rewrite.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_postproc_ref_restore.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_pr19766_review_fixes.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_add_picture.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_add_run.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_comments_get.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_document_audit.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_document_element.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_enum_section.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_font_audit.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_header_footer.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_inline_shape.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_misc.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_paragraph_style.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_row_col_cell.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_add_break.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_style.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_style_strict.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_text.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_run_underline.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_section_audit.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_section_onoff.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_settings.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_shared_audit.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_style.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_styles.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_table_audit.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_table_cell.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_table_dimensions.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_silent_stub_table_layout.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_smoke_integration.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_style_acceptance.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_style_font.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_style_setters_contract.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_table_set_cell_perf.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_table_style_id_resolution.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_temporarily_unavailable.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_validate_find_replace_asset_script.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_wire_contract.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_xml_attr_guard.py +0 -0
- {athena_python_docx-0.18.3 → athena_python_docx-0.20.0}/tests/test_zod_wire_contract.py +0 -0
|
@@ -601,6 +601,80 @@ natively, the SDK should swap to the upstream signature in place;
|
|
|
601
601
|
this section is the canonical reference for what each Athena
|
|
602
602
|
extension wires up.
|
|
603
603
|
|
|
604
|
+
### Behavioral-parity deviations (2026 deep-dive, v0.19.0)
|
|
605
|
+
|
|
606
|
+
A deep behavioral audit against python-docx 1.2.0 (the structural
|
|
607
|
+
parity ratchet only sees shape, not runtime behavior) fixed ~40
|
|
608
|
+
divergences so they now match upstream exactly — `Pt`/`RGBColor`
|
|
609
|
+
coercion, `Font.size` bare-int-is-EMU, `Font.underline`
|
|
610
|
+
SINGLE→`True`/NONE→`False`, `line_spacing` integer multipliers,
|
|
611
|
+
`line_spacing_rule` rule-only members, `Hyperlink.url`/`.address`,
|
|
612
|
+
`Table.autofit`/`_Row.cells`/`_Column.cells`, `Section`
|
|
613
|
+
orientation/start-type defaults + `Sections` slicing,
|
|
614
|
+
`Styles.get_by_id`/`get_style_id`/`default(LIST)`/`add_style`,
|
|
615
|
+
`Document.add_heading` (`ValueError`), `add_paragraph` strict heading
|
|
616
|
+
routing, `add_picture` native size + aspect ratio, `WD_BUILTIN_STYLE`
|
|
617
|
+
(now the 132-member int enum aliased to `WD_STYLE`),
|
|
618
|
+
`docx.enum.shape.WD_INLINE_SHAPE`, `InlineShape.type`/zero-dimensions,
|
|
619
|
+
and `CoreProperties` validation. These are not deviations — they close
|
|
620
|
+
gaps. The items below are the **residual intentional deviations** that
|
|
621
|
+
remain because matching upstream would break the asset-backed/HTTP
|
|
622
|
+
architecture or a SuperDoc constraint:
|
|
623
|
+
|
|
624
|
+
- **`WD_*` enum `.value` is the SuperDoc wire string, not the MS Word
|
|
625
|
+
integer constant.** `WD_ALIGN_PARAGRAPH.CENTER.value == "center"`
|
|
626
|
+
(upstream: `1`); `int(member)` / `member == 1` / `WD_*(1)` therefore
|
|
627
|
+
differ. The string value is load-bearing — every command serializes
|
|
628
|
+
`enum.value` directly onto the wire (`underline=value.value`,
|
|
629
|
+
`WD_COLOR_INDEX(v).value`, …), so int-valuing the enums would require
|
|
630
|
+
re-plumbing the entire command layer. Members are accessed by name
|
|
631
|
+
(`WD_ALIGN_PARAGRAPH.CENTER`), so the drift is academic for normal
|
|
632
|
+
use. (`WD_STYLE`/`WD_BUILTIN_STYLE` and `WD_INLINE_SHAPE_TYPE` are NOT
|
|
633
|
+
wire-load-bearing and DO carry the upstream int values.)
|
|
634
|
+
|
|
635
|
+
- **`RGBColor.from_string` additionally accepts a leading `#`.** Upstream
|
|
636
|
+
rejects `"#FF0000"`; agents routinely pass it, so a single `#` is
|
|
637
|
+
stripped before the (otherwise byte-identical) upstream slice parse.
|
|
638
|
+
A strict superset — every value upstream accepts parses identically.
|
|
639
|
+
|
|
640
|
+
- **`_Cell.vertical_alignment` setter accepts the four SuperDoc wire
|
|
641
|
+
tokens** (`"top"`/`"center"`/`"bottom"`/`"both"`) in addition to
|
|
642
|
+
`WD_ALIGN_VERTICAL` members. Upstream rejects all plain strings; this
|
|
643
|
+
is an ergonomic superset (typos like `"middle"` still raise
|
|
644
|
+
`ValueError`).
|
|
645
|
+
|
|
646
|
+
- **`Table.cell(row, col)` bounds-checks each axis independently** and
|
|
647
|
+
raises `IndexError` for an out-of-range row or column. Upstream
|
|
648
|
+
indexes a single flat row-major `_cells` list, so a column overflow
|
|
649
|
+
spills into the next row and `cell(-1, -1)` resolves against the flat
|
|
650
|
+
list (NOT the bottom-right cell). Athena's per-axis contract is the
|
|
651
|
+
more intuitive, stricter behavior for an HTTP-backed proxy that can't
|
|
652
|
+
cheaply enumerate every cell. `(0,0)` and in-range negative indices
|
|
653
|
+
behave identically to upstream.
|
|
654
|
+
|
|
655
|
+
- **`BaseStyle.style_id` returns the display name for built-ins** (so
|
|
656
|
+
`style_id == name`). The SDK is HTTP-only and can't read `styles.xml`
|
|
657
|
+
to recover the OOXML styleId (`"Heading 1"` → `"Heading1"`); the wire
|
|
658
|
+
format also uses the display name as the style id. Consequently
|
|
659
|
+
`get_by_id("Heading1", …)` / `"Heading1" in styles` won't resolve the
|
|
660
|
+
OOXML-id form (use the display name).
|
|
661
|
+
|
|
662
|
+
- **`Document.add_table(rows, cols)` requires `rows >= 1` and
|
|
663
|
+
`cols >= 1`.** Upstream allows a 0-row/0-col grid you later populate,
|
|
664
|
+
but SuperDoc's ProseMirror table node can't represent one; the SDK
|
|
665
|
+
raises eagerly rather than failing opaquely on the wire.
|
|
666
|
+
|
|
667
|
+
- **`Comment.comment_id` returns `str`, not `int`.** SuperDoc uses
|
|
668
|
+
non-numeric entity ids (`"c-42"`), so the upstream `int` contract
|
|
669
|
+
can't be honored. Tracked in `SUPERDOC_UPSTREAM_REQUESTS.md`.
|
|
670
|
+
|
|
671
|
+
- **`Section` dimension setters and `ParagraphFormat.line_spacing_rule
|
|
672
|
+
= None` warn-and-preserve on a clear** rather than raising (upstream
|
|
673
|
+
accepts `None` to clear, but SuperDoc's merging mutation ops have no
|
|
674
|
+
single-attribute clear). They emit a `Pending…ClearWarning` and leave
|
|
675
|
+
the value intact instead of destructively clearing the whole block.
|
|
676
|
+
Tracked in `SUPERDOC_UPSTREAM_REQUESTS.md`.
|
|
677
|
+
|
|
604
678
|
### If you need a deviation
|
|
605
679
|
|
|
606
680
|
If there is a genuine technical reason why a deviation from python-docx is necessary:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx collaborative document stack
|
|
5
5
|
Project-URL: Homepage, https://athenaintelligence.ai
|
|
6
6
|
Author-email: Athena Intelligence <engineering@athenaintelligence.ai>
|
|
@@ -41,7 +41,7 @@ from docx._execution import (
|
|
|
41
41
|
)
|
|
42
42
|
from docx._timeouts import timeout_for_commands
|
|
43
43
|
from docx.commands import Command, is_response_bearing, must_flush_immediately
|
|
44
|
-
from docx.errors import DocxError, FlushAllError
|
|
44
|
+
from docx.errors import DocxError, DocxUnsupportedCommandError, FlushAllError
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def _apply_proxy_id_rewrites(
|
|
@@ -467,8 +467,17 @@ class CommandBuffer:
|
|
|
467
467
|
def flush(self) -> list[Any]:
|
|
468
468
|
"""Flush pending commands as one HTTP batch.
|
|
469
469
|
|
|
470
|
-
Returns the
|
|
471
|
-
|
|
470
|
+
Returns the per-command result list, positionally aligned with the
|
|
471
|
+
flushed commands (skipped commands occupy their slot as ``None``).
|
|
472
|
+
Empty list if nothing was pending. Cancels any active idle timer.
|
|
473
|
+
|
|
474
|
+
This is the FIRE-AND-FORGET path: callers here don't await an
|
|
475
|
+
individual command's result, so an unsupported command that the
|
|
476
|
+
server skipped is simply warned (inside ``execute_batch``) and its
|
|
477
|
+
slot left ``None`` — the supported commands still applied. (A caller
|
|
478
|
+
that DOES need a single command's result goes through
|
|
479
|
+
:meth:`_eager_flush_with`, which raises on a skipped trailing command
|
|
480
|
+
rather than returning the wrong slot.)
|
|
472
481
|
|
|
473
482
|
After the batch returns, walks per-cmd results for
|
|
474
483
|
``real_node_id`` / ``real_entity_id`` echoes and updates any
|
|
@@ -490,7 +499,7 @@ class CommandBuffer:
|
|
|
490
499
|
if not pending:
|
|
491
500
|
return []
|
|
492
501
|
try:
|
|
493
|
-
results = self._client.execute_batch(
|
|
502
|
+
results, _skipped_by_index = self._client.execute_batch(
|
|
494
503
|
self._asset_id,
|
|
495
504
|
pending,
|
|
496
505
|
change_mode=change_mode,
|
|
@@ -554,8 +563,11 @@ class CommandBuffer:
|
|
|
554
563
|
proxy_id_refs = self._proxy_id_refs
|
|
555
564
|
self._proxy_id_refs = {}
|
|
556
565
|
all_cmds: list[Command] = [*pending, cmd]
|
|
566
|
+
trailing_index = len(all_cmds) - 1
|
|
557
567
|
try:
|
|
558
|
-
results: list[Any]
|
|
568
|
+
results: list[Any]
|
|
569
|
+
skipped_by_index: dict[int, dict[str, Any]]
|
|
570
|
+
results, skipped_by_index = self._client.execute_batch(
|
|
559
571
|
self._asset_id,
|
|
560
572
|
all_cmds,
|
|
561
573
|
change_mode=change_mode,
|
|
@@ -576,10 +588,32 @@ class CommandBuffer:
|
|
|
576
588
|
_ptc_emit_end_batch(all_cmds, is_error=True)
|
|
577
589
|
raise
|
|
578
590
|
_ptc_emit_end_batch(all_cmds, is_error=False)
|
|
591
|
+
# Proxy-id rewrites still run for the buffered prefix that DID apply —
|
|
592
|
+
# the trailing skip (if any) doesn't strand the supported Creates that
|
|
593
|
+
# flushed alongside it.
|
|
579
594
|
_apply_proxy_id_rewrites(results, proxy_id_refs)
|
|
595
|
+
# This is the AWAITED-result path: the caller is about to read the
|
|
596
|
+
# trailing command's result. If the server skipped that command
|
|
597
|
+
# (capability gap), there is no result to return — returning some
|
|
598
|
+
# earlier applied mutation's slot would be silent WRONG DATA. Fail
|
|
599
|
+
# loud instead. (Fire-and-forget mutations never reach here; they go
|
|
600
|
+
# through :meth:`flush`, which keeps the skip+warn behavior.)
|
|
601
|
+
if trailing_index in skipped_by_index:
|
|
602
|
+
skip = skipped_by_index[trailing_index]
|
|
603
|
+
reason = skip.get("reason") if isinstance(skip, dict) else None
|
|
604
|
+
raise DocxUnsupportedCommandError(
|
|
605
|
+
f"docx-studio skipped command {type(cmd).__name__!r} but the "
|
|
606
|
+
f"SDK was awaiting its result" + (f": {reason}" if reason else ".")
|
|
607
|
+
)
|
|
580
608
|
if not results:
|
|
581
609
|
return {}
|
|
582
|
-
|
|
610
|
+
# Trailing command applied: its slot holds the result dict (or ``{}``
|
|
611
|
+
# for a non-dict result). A ``None`` here would only arise if the
|
|
612
|
+
# server omitted the trailing command's index from ``applied[]``
|
|
613
|
+
# without listing it in ``skipped[]`` — fall back to ``{}`` to match
|
|
614
|
+
# the legacy "no result" behavior rather than handing back ``None``.
|
|
615
|
+
trailing = results[-1]
|
|
616
|
+
return trailing if trailing is not None else {}
|
|
583
617
|
|
|
584
618
|
def _reset_timer_locked(self) -> None:
|
|
585
619
|
# Caller must hold self._lock.
|
|
@@ -31,6 +31,7 @@ from __future__ import annotations
|
|
|
31
31
|
|
|
32
32
|
import json
|
|
33
33
|
import os
|
|
34
|
+
import warnings
|
|
34
35
|
from typing import Any
|
|
35
36
|
|
|
36
37
|
import requests
|
|
@@ -172,6 +173,7 @@ from docx.errors import (
|
|
|
172
173
|
BlockNotFoundError,
|
|
173
174
|
DocxError,
|
|
174
175
|
DocxStudioTemporarilyUnavailable,
|
|
176
|
+
DocxUnsupportedCommandWarning,
|
|
175
177
|
NotFoundError,
|
|
176
178
|
SessionError,
|
|
177
179
|
)
|
|
@@ -662,6 +664,29 @@ def _http_post_json(
|
|
|
662
664
|
)
|
|
663
665
|
|
|
664
666
|
|
|
667
|
+
def _warn_skipped_commands(skipped: list[Any]) -> None:
|
|
668
|
+
"""Emit one :class:`DocxUnsupportedCommandWarning` per server-reported
|
|
669
|
+
skipped command.
|
|
670
|
+
|
|
671
|
+
Each entry is the wire-shape ``{index, type, reason}`` dict the applier
|
|
672
|
+
records when a command raises ``UnsupportedDocxEditorCommandError``. The
|
|
673
|
+
batch still succeeded for every supported command; warning (rather than
|
|
674
|
+
raising) is the graceful-degradation contract so a script that ends with
|
|
675
|
+
an unsupported op doesn't abort into an empty document.
|
|
676
|
+
|
|
677
|
+
Tolerant of malformed entries: anything that isn't a dict is summarized
|
|
678
|
+
generically rather than crashing the (otherwise successful) batch.
|
|
679
|
+
"""
|
|
680
|
+
for entry in skipped:
|
|
681
|
+
if isinstance(entry, dict):
|
|
682
|
+
cmd_type = entry.get("type", "<unknown>")
|
|
683
|
+
reason = entry.get("reason") or "not supported by the docx-editor engine yet"
|
|
684
|
+
message = f"Skipped unsupported command {cmd_type!r}: {reason}"
|
|
685
|
+
else:
|
|
686
|
+
message = f"Skipped unsupported command: {entry!r}"
|
|
687
|
+
warnings.warn(message, DocxUnsupportedCommandWarning, stacklevel=2)
|
|
688
|
+
|
|
689
|
+
|
|
665
690
|
# ---------------------------------------------------------------------------
|
|
666
691
|
# HttpClient — the bare HTTP transport. Stateless wrt batching.
|
|
667
692
|
# ---------------------------------------------------------------------------
|
|
@@ -706,8 +731,22 @@ class HttpClient:
|
|
|
706
731
|
*,
|
|
707
732
|
change_mode: str | None = None,
|
|
708
733
|
user: dict[str, str] | None = None,
|
|
709
|
-
) -> list[Any]:
|
|
710
|
-
"""POST a batch of typed commands
|
|
734
|
+
) -> tuple[list[Any], dict[int, dict[str, Any]]]:
|
|
735
|
+
"""POST a batch of typed commands.
|
|
736
|
+
|
|
737
|
+
Returns ``(results, skipped_by_index)``:
|
|
738
|
+
|
|
739
|
+
- ``results`` is **positionally aligned** with ``commands``: one slot
|
|
740
|
+
per input command, in order. An applied command's slot holds its
|
|
741
|
+
``result`` dict (or ``{}`` for a non-dict result); a SKIPPED
|
|
742
|
+
command's slot holds ``None``. Positional alignment is what lets a
|
|
743
|
+
caller awaiting a single command's result (``_eager_flush_with``)
|
|
744
|
+
map ``results[-1]`` back to *its* command and detect that the
|
|
745
|
+
command was skipped (``None``) rather than silently returning some
|
|
746
|
+
earlier applied mutation's result.
|
|
747
|
+
- ``skipped_by_index`` maps the input index of each skipped command to
|
|
748
|
+
its ``{index, type, reason}`` entry, so callers awaiting a result a
|
|
749
|
+
skipped command can't provide can raise a precise error.
|
|
711
750
|
|
|
712
751
|
Envelope fields:
|
|
713
752
|
change_mode: ``"direct"`` | ``"tracked"`` | ``None``. When
|
|
@@ -720,14 +759,22 @@ class HttpClient:
|
|
|
720
759
|
opens the per-asset session for the first time; ignored
|
|
721
760
|
on subsequent batches against the same pooled session.
|
|
722
761
|
|
|
723
|
-
On success,
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
762
|
+
On success, ``len(applied) + len(skipped) == len(commands)``. The
|
|
763
|
+
server's ``skipped[]`` array carries commands the docx-editor engine
|
|
764
|
+
doesn't implement yet; each is surfaced as a
|
|
765
|
+
:class:`DocxUnsupportedCommandWarning` (graceful degradation — one
|
|
766
|
+
unsupported FIRE-AND-FORGET mutation no longer aborts the whole batch
|
|
767
|
+
into an empty document). Fire-and-forget mutations don't read their
|
|
768
|
+
result, so a warning + skip is correct for them. A skipped command
|
|
769
|
+
that a caller IS awaiting (a query / response-bearing op routed
|
|
770
|
+
through ``_eager_flush_with``) is detected positionally and raised by
|
|
771
|
+
that caller. On 207 partial-failure, the server returns 207 with
|
|
772
|
+
`applied` (the successful prefix) plus an `error` for the failing
|
|
773
|
+
command — :func:`_http_post_json` translates that to a
|
|
774
|
+
:class:`DocxError`.
|
|
728
775
|
"""
|
|
729
776
|
if not commands:
|
|
730
|
-
return []
|
|
777
|
+
return [], {}
|
|
731
778
|
|
|
732
779
|
url: str = f"{self._base_url}/docs/{asset_id}/commands"
|
|
733
780
|
body: dict[str, Any] = {
|
|
@@ -747,6 +794,12 @@ class HttpClient:
|
|
|
747
794
|
timeout=timeout_for_commands(commands=commands),
|
|
748
795
|
)
|
|
749
796
|
applied = resp.get("applied")
|
|
797
|
+
# Commands the engine doesn't implement yet are reported in
|
|
798
|
+
# ``skipped[]`` (non-fatal). Warn per skip and count them toward the
|
|
799
|
+
# batch total — they're a success, not a failure.
|
|
800
|
+
skipped_raw = resp.get("skipped")
|
|
801
|
+
skipped = skipped_raw if isinstance(skipped_raw, list) else []
|
|
802
|
+
_warn_skipped_commands(skipped)
|
|
750
803
|
# An empty applied list paired with an `error` field is the
|
|
751
804
|
# 'all-commands-failed' shape — surface it rather than returning
|
|
752
805
|
# an empty result list to a caller that's about to index [-1].
|
|
@@ -758,19 +811,37 @@ class HttpClient:
|
|
|
758
811
|
f"docx-studio response missing applied[]: {resp!r}",
|
|
759
812
|
)
|
|
760
813
|
|
|
761
|
-
|
|
814
|
+
# Applied + skipped together must account for every command. A short
|
|
815
|
+
# ``applied`` is only legitimate when the remainder were skipped.
|
|
816
|
+
if len(applied) + len(skipped) != len(commands):
|
|
762
817
|
raise DocxError(
|
|
763
|
-
f"docx-studio applied {len(applied)}
|
|
818
|
+
f"docx-studio applied {len(applied)} (+{len(skipped)} skipped) "
|
|
819
|
+
f"of {len(commands)} commands",
|
|
764
820
|
)
|
|
765
821
|
|
|
766
|
-
|
|
822
|
+
# Build a positionally-aligned result list (one slot per input
|
|
823
|
+
# command). Applied slots carry the result dict; skipped slots stay
|
|
824
|
+
# ``None``. Each entry's ``index`` field places it at the right slot —
|
|
825
|
+
# we don't assume ``applied[]`` is a contiguous prefix once skips can
|
|
826
|
+
# appear anywhere in the batch.
|
|
827
|
+
results: list[Any] = [None] * len(commands)
|
|
767
828
|
for entry in applied:
|
|
768
|
-
if isinstance(entry, dict):
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
829
|
+
if not isinstance(entry, dict):
|
|
830
|
+
continue
|
|
831
|
+
idx = entry.get("index")
|
|
832
|
+
if not isinstance(idx, int) or not (0 <= idx < len(results)):
|
|
833
|
+
continue
|
|
834
|
+
result = entry.get("result")
|
|
835
|
+
results[idx] = result if isinstance(result, dict) else {}
|
|
836
|
+
|
|
837
|
+
skipped_by_index: dict[int, dict[str, Any]] = {}
|
|
838
|
+
for entry in skipped:
|
|
839
|
+
if not isinstance(entry, dict):
|
|
840
|
+
continue
|
|
841
|
+
idx = entry.get("index")
|
|
842
|
+
if isinstance(idx, int) and 0 <= idx < len(commands):
|
|
843
|
+
skipped_by_index[idx] = entry
|
|
844
|
+
return results, skipped_by_index
|
|
774
845
|
|
|
775
846
|
|
|
776
847
|
# ---------------------------------------------------------------------------
|
|
@@ -211,4 +211,163 @@ def _decode_image_data_uri(uri: str) -> bytes:
|
|
|
211
211
|
raise ValueError(f"add_picture could not decode data: URI: {exc}") from exc
|
|
212
212
|
|
|
213
213
|
|
|
214
|
-
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# Native dimension / DPI sniffing — parity with python-docx's add_picture
|
|
216
|
+
# native-size + aspect-ratio behavior (no Pillow dependency).
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
_EMU_PER_INCH: int = 914400
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _png_dimensions(b: bytes) -> "tuple[int, int, float, float] | None":
|
|
223
|
+
# IHDR (width/height) sits right after the 8-byte signature in the
|
|
224
|
+
# first chunk: [4-byte len][b"IHDR"][4-byte width][4-byte height]...
|
|
225
|
+
if len(b) < 24 or b[12:16] != b"IHDR":
|
|
226
|
+
return None
|
|
227
|
+
px_w = int.from_bytes(b[16:20], "big")
|
|
228
|
+
px_h = int.from_bytes(b[20:24], "big")
|
|
229
|
+
if px_w <= 0 or px_h <= 0:
|
|
230
|
+
return None
|
|
231
|
+
horz_dpi = vert_dpi = 72.0 # python-docx's PNG default
|
|
232
|
+
# Optional pHYs chunk carries pixels-per-unit; unit 1 == metre.
|
|
233
|
+
idx = b.find(b"pHYs")
|
|
234
|
+
if idx != -1 and idx + 13 <= len(b):
|
|
235
|
+
ppu_x = int.from_bytes(b[idx + 4 : idx + 8], "big")
|
|
236
|
+
ppu_y = int.from_bytes(b[idx + 8 : idx + 12], "big")
|
|
237
|
+
unit = b[idx + 12]
|
|
238
|
+
if unit == 1 and ppu_x > 0 and ppu_y > 0:
|
|
239
|
+
# python-docx rounds DPI to an int (``int(round(ppu*0.0254))``),
|
|
240
|
+
# so match that to produce identical native EMU dimensions.
|
|
241
|
+
horz_dpi = float(round(ppu_x * 0.0254))
|
|
242
|
+
vert_dpi = float(round(ppu_y * 0.0254))
|
|
243
|
+
return px_w, px_h, horz_dpi, vert_dpi
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _gif_dimensions(b: bytes) -> "tuple[int, int, float, float] | None":
|
|
247
|
+
if len(b) < 10:
|
|
248
|
+
return None
|
|
249
|
+
px_w = int.from_bytes(b[6:8], "little")
|
|
250
|
+
px_h = int.from_bytes(b[8:10], "little")
|
|
251
|
+
if px_w <= 0 or px_h <= 0:
|
|
252
|
+
return None
|
|
253
|
+
# GIF carries no resolution; python-docx defaults to 72 DPI.
|
|
254
|
+
return px_w, px_h, 72.0, 72.0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _bmp_dimensions(b: bytes) -> "tuple[int, int, float, float] | None":
|
|
258
|
+
if len(b) < 26:
|
|
259
|
+
return None
|
|
260
|
+
px_w = int.from_bytes(b[18:22], "little", signed=True)
|
|
261
|
+
px_h = abs(int.from_bytes(b[22:26], "little", signed=True))
|
|
262
|
+
if px_w <= 0 or px_h <= 0:
|
|
263
|
+
return None
|
|
264
|
+
horz_dpi = vert_dpi = 96.0 # python-docx's BMP default
|
|
265
|
+
if len(b) >= 46:
|
|
266
|
+
ppm_x = int.from_bytes(b[38:42], "little", signed=True)
|
|
267
|
+
ppm_y = int.from_bytes(b[42:46], "little", signed=True)
|
|
268
|
+
if ppm_x > 0 and ppm_y > 0:
|
|
269
|
+
horz_dpi = float(round(ppm_x * 0.0254))
|
|
270
|
+
vert_dpi = float(round(ppm_y * 0.0254))
|
|
271
|
+
return px_w, px_h, horz_dpi, vert_dpi
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _jpeg_dimensions(b: bytes) -> "tuple[int, int, float, float] | None":
|
|
275
|
+
# Default density per python-docx's JFIF handling.
|
|
276
|
+
horz_dpi = vert_dpi = 72.0
|
|
277
|
+
px_w = px_h = 0
|
|
278
|
+
n = len(b)
|
|
279
|
+
pos = 2 # skip SOI (FF D8)
|
|
280
|
+
while pos + 4 <= n:
|
|
281
|
+
if b[pos] != 0xFF:
|
|
282
|
+
pos += 1
|
|
283
|
+
continue
|
|
284
|
+
marker = b[pos + 1]
|
|
285
|
+
if marker in (0xD8, 0xD9) or 0xD0 <= marker <= 0xD7:
|
|
286
|
+
pos += 2
|
|
287
|
+
continue
|
|
288
|
+
seg_len = int.from_bytes(b[pos + 2 : pos + 4], "big")
|
|
289
|
+
if seg_len < 2:
|
|
290
|
+
break
|
|
291
|
+
seg = b[pos + 4 : pos + 2 + seg_len]
|
|
292
|
+
if marker == 0xE0 and seg[:5] == b"JFIF\x00" and len(seg) >= 12:
|
|
293
|
+
units = seg[7]
|
|
294
|
+
dx = int.from_bytes(seg[8:10], "big")
|
|
295
|
+
dy = int.from_bytes(seg[10:12], "big")
|
|
296
|
+
if units == 1 and dx and dy: # dots per inch
|
|
297
|
+
horz_dpi, vert_dpi = float(dx), float(dy)
|
|
298
|
+
elif units == 2 and dx and dy: # dots per cm
|
|
299
|
+
horz_dpi, vert_dpi = dx * 2.54, dy * 2.54
|
|
300
|
+
# SOFn frame markers carry the actual dimensions.
|
|
301
|
+
elif marker in (
|
|
302
|
+
0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7,
|
|
303
|
+
0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF,
|
|
304
|
+
) and len(seg) >= 5:
|
|
305
|
+
px_h = int.from_bytes(seg[1:3], "big")
|
|
306
|
+
px_w = int.from_bytes(seg[3:5], "big")
|
|
307
|
+
break
|
|
308
|
+
pos += 2 + seg_len
|
|
309
|
+
if px_w <= 0 or px_h <= 0:
|
|
310
|
+
return None
|
|
311
|
+
return px_w, px_h, horz_dpi, vert_dpi
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def sniff_image_dimensions(image_bytes: bytes) -> "tuple[int, int, float, float] | None":
|
|
315
|
+
"""Return ``(px_width, px_height, horz_dpi, vert_dpi)`` for raster
|
|
316
|
+
image bytes, or ``None`` if the format/dimensions can't be parsed.
|
|
317
|
+
|
|
318
|
+
Mirrors the header parsers in python-docx's ``docx.image`` (PNG,
|
|
319
|
+
JPEG, GIF, BMP) without requiring Pillow. DPI defaults match
|
|
320
|
+
python-docx (72 for PNG/JPEG/GIF, 96 for BMP) when the image carries
|
|
321
|
+
no embedded resolution.
|
|
322
|
+
"""
|
|
323
|
+
if image_bytes.startswith(_PNG_MAGIC):
|
|
324
|
+
return _png_dimensions(image_bytes)
|
|
325
|
+
if image_bytes.startswith(_JPEG_MAGIC):
|
|
326
|
+
return _jpeg_dimensions(image_bytes)
|
|
327
|
+
if image_bytes.startswith(_GIF87_MAGIC) or image_bytes.startswith(_GIF89_MAGIC):
|
|
328
|
+
return _gif_dimensions(image_bytes)
|
|
329
|
+
if image_bytes.startswith(_BMP_MAGIC):
|
|
330
|
+
return _bmp_dimensions(image_bytes)
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def scaled_dimensions_emu(
|
|
335
|
+
image_bytes: bytes,
|
|
336
|
+
width_emu: "int | None",
|
|
337
|
+
height_emu: "int | None",
|
|
338
|
+
) -> "tuple[int, int] | None":
|
|
339
|
+
"""Resolve the render size (in EMU) for ``add_picture``, mirroring
|
|
340
|
+
python-docx's ``Image.scaled_dimensions``.
|
|
341
|
+
|
|
342
|
+
- both width and height given -> use them verbatim.
|
|
343
|
+
- neither given -> the image's native size (from px + DPI).
|
|
344
|
+
- exactly one given -> compute the other to preserve aspect ratio.
|
|
345
|
+
|
|
346
|
+
Returns ``None`` only when a dimension must be derived but native
|
|
347
|
+
dimensions can't be sniffed — the caller then falls back to its
|
|
348
|
+
legacy fixed default.
|
|
349
|
+
"""
|
|
350
|
+
if width_emu is not None and height_emu is not None:
|
|
351
|
+
return width_emu, height_emu
|
|
352
|
+
dims = sniff_image_dimensions(image_bytes)
|
|
353
|
+
if dims is None:
|
|
354
|
+
return None
|
|
355
|
+
px_w, px_h, dpi_x, dpi_y = dims
|
|
356
|
+
nat_w = int(round(px_w / dpi_x * _EMU_PER_INCH))
|
|
357
|
+
nat_h = int(round(px_h / dpi_y * _EMU_PER_INCH))
|
|
358
|
+
if width_emu is None and height_emu is None:
|
|
359
|
+
return nat_w, nat_h
|
|
360
|
+
if height_emu is None: # width supplied, scale height
|
|
361
|
+
scale = (float(width_emu) / float(nat_w)) if nat_w else 1.0
|
|
362
|
+
return width_emu, int(round(nat_h * scale))
|
|
363
|
+
# height supplied, scale width
|
|
364
|
+
scale = (float(height_emu) / float(nat_h)) if nat_h else 1.0
|
|
365
|
+
return int(round(nat_w * scale)), height_emu
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
__all__ = [
|
|
369
|
+
"load_image_bytes",
|
|
370
|
+
"sniff_content_type",
|
|
371
|
+
"sniff_image_dimensions",
|
|
372
|
+
"scaled_dimensions_emu",
|
|
373
|
+
]
|
|
@@ -217,8 +217,12 @@ class Comment:
|
|
|
217
217
|
self._info["creatorName"] = value
|
|
218
218
|
|
|
219
219
|
@property
|
|
220
|
-
def initials(self) -> str:
|
|
221
|
-
|
|
220
|
+
def initials(self) -> str | None:
|
|
221
|
+
# python-docx's CT_Comment.initials is OptionalAttribute(str), so
|
|
222
|
+
# an absent/None initials reads back as None (not ""), letting
|
|
223
|
+
# callers distinguish "no initials set" from "empty initials".
|
|
224
|
+
val = self._info.get("initials")
|
|
225
|
+
return None if val is None else str(val)
|
|
222
226
|
|
|
223
227
|
@initials.setter
|
|
224
228
|
def initials(self, value: str) -> None:
|
|
@@ -1525,14 +1525,18 @@ class Document:
|
|
|
1525
1525
|
#
|
|
1526
1526
|
# For heading styles we still dispatch to add_heading, which uses
|
|
1527
1527
|
# `doc.create.heading` (also canonical, takes `level`).
|
|
1528
|
-
if style_str
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1528
|
+
if style_str is not None:
|
|
1529
|
+
low: str = style_str.lower()
|
|
1530
|
+
# Only the canonical built-in heading ids (``Heading 1`` ..
|
|
1531
|
+
# ``Heading 9``) route to add_heading. A custom style whose
|
|
1532
|
+
# name merely starts with "heading" (e.g. "Heading Box",
|
|
1533
|
+
# "Heading Note") must be applied verbatim — the previous
|
|
1534
|
+
# loose ``startswith("heading")`` + parse-or-default-to-1
|
|
1535
|
+
# silently relabeled every such style as Heading 1.
|
|
1536
|
+
if low in {f"heading {n}" for n in range(1, 10)}:
|
|
1537
|
+
return self.add_heading(text=text, level=int(low.rsplit(" ", 1)[-1]))
|
|
1538
|
+
if low == "title":
|
|
1539
|
+
return self.add_heading(text=text, level=0)
|
|
1536
1540
|
|
|
1537
1541
|
# python-docx parity: tabs and line breaks in `text` must
|
|
1538
1542
|
# round-trip as Word ``<w:tab/>`` / ``<w:br/>`` elements, not
|
|
@@ -1725,9 +1729,9 @@ class Document:
|
|
|
1725
1729
|
self._ensure_open()
|
|
1726
1730
|
self._reset_list_chain()
|
|
1727
1731
|
if not 0 <= level <= 9:
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
)
|
|
1732
|
+
# python-docx raises a builtin ``ValueError`` here (not an
|
|
1733
|
+
# SDK ValidationError) with this exact message.
|
|
1734
|
+
raise ValueError("level must be in range 0-9, got %d" % level)
|
|
1731
1735
|
|
|
1732
1736
|
# python-docx parity: tabs/newlines in `text` must round-trip
|
|
1733
1737
|
# as Word inlines, not literal characters. SuperDoc's
|
|
@@ -1797,9 +1801,18 @@ class Document:
|
|
|
1797
1801
|
|
|
1798
1802
|
self._ensure_open()
|
|
1799
1803
|
self._reset_list_chain()
|
|
1804
|
+
# Intentional deviation from python-docx (documented in CLAUDE.md):
|
|
1805
|
+
# upstream allows ``add_table(0, cols)`` to create an empty grid
|
|
1806
|
+
# you then populate with ``add_row()``, but SuperDoc's ProseMirror
|
|
1807
|
+
# table node requires at least one row and one column — a 0-row
|
|
1808
|
+
# table can't be represented — so we reject eagerly with a clear
|
|
1809
|
+
# error rather than failing opaquely on the wire.
|
|
1800
1810
|
if rows < 1 or cols < 1:
|
|
1801
1811
|
raise ValidationError(
|
|
1802
|
-
f"rows and cols must be >= 1; got rows={rows} cols={cols}"
|
|
1812
|
+
f"rows and cols must be >= 1; got rows={rows} cols={cols}. "
|
|
1813
|
+
f"SuperDoc cannot represent a 0-row/0-col table — create "
|
|
1814
|
+
f"with the final dimensions (or 1x1, then add_row/"
|
|
1815
|
+
f"add_column).",
|
|
1803
1816
|
)
|
|
1804
1817
|
|
|
1805
1818
|
# The real param is `columns` (not `cols`). `at` is required to
|
|
@@ -1901,7 +1914,11 @@ class Document:
|
|
|
1901
1914
|
server-side), or a ``data:`` URI. The URL and ``data:`` forms are
|
|
1902
1915
|
Athena extensions beyond python-docx, which is path/stream-only.
|
|
1903
1916
|
"""
|
|
1904
|
-
from docx._image_utils import
|
|
1917
|
+
from docx._image_utils import (
|
|
1918
|
+
load_image_bytes,
|
|
1919
|
+
scaled_dimensions_emu,
|
|
1920
|
+
sniff_content_type,
|
|
1921
|
+
)
|
|
1905
1922
|
from docx.shape import InlineShape
|
|
1906
1923
|
from docx.text.run import _build_inline_shape_info
|
|
1907
1924
|
|
|
@@ -1931,8 +1948,32 @@ class Document:
|
|
|
1931
1948
|
b64: str = base64.b64encode(image_bytes).decode("ascii")
|
|
1932
1949
|
data_uri: str = f"data:{content_type};base64,{b64}"
|
|
1933
1950
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1951
|
+
# python-docx parity: with no width/height the picture appears at
|
|
1952
|
+
# its native size (px / DPI); with exactly one dimension the other
|
|
1953
|
+
# is computed to preserve the aspect ratio. Only when the native
|
|
1954
|
+
# dimensions can't be sniffed (unknown/SVG/corrupt) do we keep the
|
|
1955
|
+
# legacy fixed 6" x 4.5" fallback.
|
|
1956
|
+
dims_emu = scaled_dimensions_emu(
|
|
1957
|
+
image_bytes,
|
|
1958
|
+
int(width) if width is not None else None,
|
|
1959
|
+
int(height) if height is not None else None,
|
|
1960
|
+
)
|
|
1961
|
+
if dims_emu is not None:
|
|
1962
|
+
w_emu, h_emu = dims_emu
|
|
1963
|
+
else:
|
|
1964
|
+
# Undecodable native size — legacy 6" x 4.5" fixed fallback
|
|
1965
|
+
# (576px / 432px @ 96 DPI) or the explicit dimension.
|
|
1966
|
+
w_emu = int(width) if width is not None else 5486400
|
|
1967
|
+
h_emu = int(height) if height is not None else 4114800
|
|
1968
|
+
# The ``create.image`` wire wants pixels (96 DPI); the returned
|
|
1969
|
+
# InlineShape's ``size`` is read back as POINTS (the getter wraps
|
|
1970
|
+
# it in ``Pt(...)``, matching the resize setter's ``unit: "pt"``),
|
|
1971
|
+
# so store the two units separately — otherwise ``shape.width``
|
|
1972
|
+
# reads back ~1.33x inflated (px interpreted as pt).
|
|
1973
|
+
w_px: float = _emu_to_px(w_emu)
|
|
1974
|
+
h_px: float = _emu_to_px(h_emu)
|
|
1975
|
+
w_pt: float = w_emu / 12700.0
|
|
1976
|
+
h_pt: float = h_emu / 12700.0
|
|
1936
1977
|
|
|
1937
1978
|
# Pre-mint the client-side UUID so the create can buffer and
|
|
1938
1979
|
# the InlineShape proxy hands the caller a stable id without
|
|
@@ -1948,7 +1989,7 @@ class Document:
|
|
|
1948
1989
|
# InlineShape stores its id under ``_info["nodeId"]`` rather than
|
|
1949
1990
|
# a top-level ``_node_id`` attribute, so route the flush-time
|
|
1950
1991
|
# rewrite through a small setattr-adapter wrapper.
|
|
1951
|
-
info = _build_inline_shape_info(result, width=
|
|
1992
|
+
info = _build_inline_shape_info(result, width=w_pt, height=h_pt)
|
|
1952
1993
|
info["nodeId"] = client_node_id
|
|
1953
1994
|
shape = InlineShape(session=self._session, info=info)
|
|
1954
1995
|
buffer = self._buffer()
|
|
@@ -1981,6 +2022,22 @@ class Document:
|
|
|
1981
2022
|
if start_type is not None:
|
|
1982
2023
|
if isinstance(start_type, WD_SECTION_START):
|
|
1983
2024
|
break_type = start_type.to_superdoc()
|
|
2025
|
+
elif isinstance(start_type, int):
|
|
2026
|
+
# python-docx's WD_SECTION_START members ARE ints
|
|
2027
|
+
# (NEW_PAGE == 2), so ``add_section(2)`` is valid upstream.
|
|
2028
|
+
# Athena's members carry SuperDoc string values, so map the
|
|
2029
|
+
# upstream integer constants to the matching member.
|
|
2030
|
+
_int_to_member = {
|
|
2031
|
+
0: WD_SECTION_START.CONTINUOUS,
|
|
2032
|
+
1: WD_SECTION_START.NEW_COLUMN,
|
|
2033
|
+
2: WD_SECTION_START.NEW_PAGE,
|
|
2034
|
+
3: WD_SECTION_START.EVEN_PAGE,
|
|
2035
|
+
4: WD_SECTION_START.ODD_PAGE,
|
|
2036
|
+
}
|
|
2037
|
+
member = _int_to_member.get(int(start_type))
|
|
2038
|
+
if member is None:
|
|
2039
|
+
raise ValueError(f"{start_type!r} is not a valid WD_SECTION_START")
|
|
2040
|
+
break_type = member.to_superdoc()
|
|
1984
2041
|
elif isinstance(start_type, str):
|
|
1985
2042
|
# python-docx coerces strings through the enum, raising
|
|
1986
2043
|
# ``ValueError`` for non-member values. Mirror that here
|