athena-python-docx 0.22.0__tar.gz → 0.24.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.22.0 → athena_python_docx-0.24.0}/AGENTS.md +13 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/PKG-INFO +1 -1
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/__init__.py +1 -1
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_citations.py +122 -1
- athena_python_docx-0.24.0/docx/_run_order.py +217 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/commands.py +19 -2
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/shape.py +89 -12
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/text/paragraph.py +15 -1
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/text/run.py +106 -50
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/pyproject.toml +1 -1
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_citations.py +183 -0
- athena_python_docx-0.24.0/tests/test_inline_shape_alt_text.py +230 -0
- athena_python_docx-0.24.0/tests/test_run_identity.py +306 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_run_mark_inheritance.py +76 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_wire_contract.py +6 -4
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/.gitignore +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/CLAUDE.md +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/DOCX_EXEC_LAB.md +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/README.md +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_athena_extension.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_batching.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_buffer.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_execution.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_http.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_http_doc.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_image_utils.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_postproc.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_ptc.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_references.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_table_styles.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/_timeouts.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/api.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/bookmarks.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/charts.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/client.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/comments.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/document.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/enum/section.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/enum/shape.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/enum/style.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/enum/table.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/enum/text.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/errors.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/exceptions.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/fields.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/footnotes.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/math.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/opc/coreprops.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/oxml/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/revisions.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/sdt.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/section.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/session.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/settings.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/shared.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/styles/style.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/styles/styles.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/table.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/text/font.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/text/hyperlink.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/text/pagebreak.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/text/parfmt.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/text/tabstops.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/toc.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/docx/typing.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/docx_exec_lab.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/docx_exec_lab_examples/fast_table_fill.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/docx_exec_lab_examples/find_replace_literal.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/dump_wire_fixtures.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/release.sh +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/round_trip_smoke.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/smoke_test_block_not_found.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/scripts/validate_find_replace_asset.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/conftest.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/METHODOLOGY.md +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/ab_probe_cases.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/ab_probe_runner.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/binary_round_trip.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/fake_session.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/firm_templates/README.md +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/firm_templates/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/firm_templates/_runner.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/firm_templates/extractor.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/local_runner.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshot.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/ours_spec.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/parity_diff.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/real_world_cases.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/stock_spec.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/README.md +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/__init__.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/baseline_gaps.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/compare.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/intentional_deviations.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/introspect.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/reports/GAP_ANALYSIS.md +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/reports/gap_report.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/run_parity.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/snapshots/athena_latest.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/parity/test_parity_gap.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_add_section_extract_items.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_athena_extension_creates_session_open.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_athena_extensions_contract.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_athena_extensions_registry.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_batching_perf.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_block_not_found_error.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_buffer.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_add_table_not_supported.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_fastpath_runs.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_inner_add_hyperlink_stash.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_inner_add_run_via_cell_insert.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_inner_format_stash.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_inner_run_format_stash.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_inner_run_guard.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_text_plain_fastpath.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_cell_text_replace_semantics.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_collapsed_range_format.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_command_dataclasses.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_commands.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_comments.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_document_asset_id_property.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_document_clear.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_document_create.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_document_create_from_template.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_document_factory_validation.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_docx_exec_lab.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_e2e_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_execution_scope.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_find_replace_session_open.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_http_transport.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_hyperlink_coalescing.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_image_url_data_uri.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_insert_deferred.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_iter_inner_content.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_list_styles.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_merged_cell_secondary_slot.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_merged_cells.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_oxml_shim.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_paragraph_text_len_cache.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_parity_misc.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_parity_round2.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_phase_a_behavior.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_phase_b_headers_footers.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_phase_c_tables.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_positional_cell_id.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_postproc_cell_format_rewrite.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_postproc_cell_run_format_rewrite.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_postproc_ref_restore.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_pr19766_review_fixes.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_ptc.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_revisions.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_run_add_picture_anchor.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_add_picture.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_add_run.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_comments_get.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_document_audit.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_document_element.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_enum_section.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_font_audit.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_header_footer.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_hyperlink.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_inline_shape.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_misc.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_paragraph_style.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_parfmt.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_row_col_cell.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_run_add_break.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_run_style.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_run_style_strict.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_run_text.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_run_underline.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_section_audit.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_section_dimensions.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_section_onoff.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_settings.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_shared_audit.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_style.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_styles.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_table_audit.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_table_cell.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_table_dimensions.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_silent_stub_table_layout.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_smoke_integration.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_style_acceptance.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_style_font.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_style_setters_contract.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_table_set_cell_perf.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_table_style_id_resolution.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_temporarily_unavailable.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_validate_find_replace_asset_script.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_xml_attr_guard.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/tests/test_zod_wire_contract.py +0 -0
- {athena_python_docx-0.22.0 → athena_python_docx-0.24.0}/uv.lock +0 -0
|
@@ -481,6 +481,19 @@ Issue numbers reference `python-openxml/python-docx`.
|
|
|
481
481
|
an older API stores the record under the cell id, where nothing renders
|
|
482
482
|
it and the next edit retires it.
|
|
483
483
|
|
|
484
|
+
- **Image region citations**: cite a region of an image asset with
|
|
485
|
+
``AssetReference(id=<image>)`` + ``PixelRangeAnchor(x=, y=, width=,
|
|
486
|
+
height=)`` in source image pixels, or with a
|
|
487
|
+
``/dashboard/spaces/?asset_ids=<image>&type=image&anchor=pixel_range&x=…&y=…&width=…&height=…``
|
|
488
|
+
citation string. DOCX Studio stores ``type=image`` and keeps ``excerpt``
|
|
489
|
+
out of the URL (the record keeps it as the span of the cited paragraph to
|
|
490
|
+
highlight). Since 0.23.0, ``add_citation`` raises ``ValueError`` for the
|
|
491
|
+
image citations that chat and Sheets reject (agora
|
|
492
|
+
``validate_image_citation_url``): an image URL with ``excerpt`` or
|
|
493
|
+
``#:~:text=`` and no anchor, a non-region anchor, a region anchor without
|
|
494
|
+
x, y, width and height, and an ``AssetMeta(asset_type="image")`` source
|
|
495
|
+
with a non-region anchor or with an excerpt and no anchor.
|
|
496
|
+
|
|
484
497
|
- **`Document.export_docx(path=None, *, include_revisions=False)
|
|
485
498
|
-> bytes`** (0.11.4+) — export the document's current state as
|
|
486
499
|
``.docx`` bytes, optionally writing to ``path``. Closes the gap
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.0
|
|
4
4
|
Summary: Drop-in replacement for python-docx that connects to Athena's DOCX Studio collaborative document stack
|
|
5
5
|
Project-URL: Homepage, https://athenaintelligence.ai
|
|
6
6
|
Author-email: Athena Intelligence <engineering@athenaintelligence.ai>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import uuid
|
|
6
|
+
from collections.abc import Mapping
|
|
6
7
|
from typing import Any
|
|
7
8
|
from urllib.parse import parse_qs, urlparse
|
|
8
9
|
|
|
@@ -34,6 +35,87 @@ def source_ref_to_dict(source: AssetReference) -> dict[str, Any]:
|
|
|
34
35
|
return source.to_dict()
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
_IMAGE_REGION_ANCHOR_TYPES: tuple[str, ...] = ("pixel_range", "rect")
|
|
39
|
+
_IMAGE_REGION_FIELDS: tuple[str, ...] = ("x", "y", "width", "height")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_number(value: object) -> bool:
|
|
43
|
+
"""True for what agora's anchor parser reads as a number (``float(...)``)."""
|
|
44
|
+
if isinstance(value, bool):
|
|
45
|
+
return False
|
|
46
|
+
if isinstance(value, (int, float)):
|
|
47
|
+
return True
|
|
48
|
+
if isinstance(value, str):
|
|
49
|
+
try:
|
|
50
|
+
float(value)
|
|
51
|
+
except ValueError:
|
|
52
|
+
return False
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_image_citation(
|
|
58
|
+
*,
|
|
59
|
+
anchor_type: str | None,
|
|
60
|
+
has_excerpt: bool,
|
|
61
|
+
region: Mapping[str, object] | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Reject an image citation that chat and Sheets reject.
|
|
64
|
+
|
|
65
|
+
The same rules as agora's ``validate_citation_url`` for an image: an
|
|
66
|
+
image is cited by a region (``anchor=pixel_range`` with numeric x, y,
|
|
67
|
+
width and height in source image pixels), never by a page, a text anchor
|
|
68
|
+
or an excerpt. ``region`` holds the x, y, width and height values of a
|
|
69
|
+
region anchor (``None`` for a missing one).
|
|
70
|
+
"""
|
|
71
|
+
if anchor_type and anchor_type not in _IMAGE_REGION_ANCHOR_TYPES:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"An image citation cannot use anchor={anchor_type}. Cite a region "
|
|
74
|
+
"of the image: anchor=pixel_range with x, y, width and height in "
|
|
75
|
+
"source image pixels (PixelRangeAnchor)."
|
|
76
|
+
)
|
|
77
|
+
if not anchor_type and has_excerpt:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"An image citation cannot use an excerpt or a #:~:text= fragment: "
|
|
80
|
+
"an image has no text to find. Cite a region of the image: "
|
|
81
|
+
"anchor=pixel_range with x, y, width and height in source image "
|
|
82
|
+
"pixels (PixelRangeAnchor), and put the label in display_value."
|
|
83
|
+
)
|
|
84
|
+
if not anchor_type or region is None:
|
|
85
|
+
return
|
|
86
|
+
missing = [field for field in _IMAGE_REGION_FIELDS if region.get(field) is None]
|
|
87
|
+
if missing:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"An image citation with anchor={anchor_type} needs x, y, width "
|
|
90
|
+
f"and height. Missing: {', '.join(missing)}."
|
|
91
|
+
)
|
|
92
|
+
not_numbers = [field for field in _IMAGE_REGION_FIELDS if not _is_number(region[field])]
|
|
93
|
+
if not_numbers:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"An image citation with anchor={anchor_type} needs numbers for "
|
|
96
|
+
f"x, y, width and height. Not a number: {', '.join(not_numbers)}."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _check_image_citation_url(query: str, fragment: str) -> None:
|
|
101
|
+
# Blank values are dropped, as agora's validator parses the query.
|
|
102
|
+
params = parse_qs(query)
|
|
103
|
+
|
|
104
|
+
def first(key: str) -> str | None:
|
|
105
|
+
values = params.get(key)
|
|
106
|
+
return values[0] if values else None
|
|
107
|
+
|
|
108
|
+
asset_type = (first("asset_type") or first("type") or "").lower()
|
|
109
|
+
if asset_type != "image":
|
|
110
|
+
return
|
|
111
|
+
anchor_type = (first("anchor") or "").lower() or None
|
|
112
|
+
check_image_citation(
|
|
113
|
+
anchor_type=anchor_type,
|
|
114
|
+
has_excerpt=first("excerpt") is not None or fragment.startswith(":~:text="),
|
|
115
|
+
region={field: first(field) for field in _IMAGE_REGION_FIELDS},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
37
119
|
def normalize_citation_string(citation_string: str) -> str:
|
|
38
120
|
value = citation_string.strip()
|
|
39
121
|
if not value:
|
|
@@ -49,6 +131,7 @@ def normalize_citation_string(citation_string: str) -> str:
|
|
|
49
131
|
asset_ids = parse_qs(parsed.query).get("asset_ids")
|
|
50
132
|
if not asset_ids or not asset_ids[0]:
|
|
51
133
|
raise ValueError("Athena Spaces citation URLs must include asset_ids")
|
|
134
|
+
_check_image_citation_url(parsed.query, parsed.fragment)
|
|
52
135
|
return value
|
|
53
136
|
|
|
54
137
|
if not parsed.scheme and value.startswith("/"):
|
|
@@ -61,6 +144,7 @@ def normalize_citation_string(citation_string: str) -> str:
|
|
|
61
144
|
if relative.path.rstrip("/") == "/dashboard/spaces":
|
|
62
145
|
asset_ids = parse_qs(relative.query).get("asset_ids")
|
|
63
146
|
if asset_ids and asset_ids[0]:
|
|
147
|
+
_check_image_citation_url(relative.query, relative.fragment)
|
|
64
148
|
return value
|
|
65
149
|
raise ValueError("Athena Spaces citation URLs must include asset_ids")
|
|
66
150
|
|
|
@@ -106,6 +190,35 @@ def destination_anchor_to_dict(destination_anchor: object) -> dict[str, Any]:
|
|
|
106
190
|
)
|
|
107
191
|
|
|
108
192
|
|
|
193
|
+
def _check_typed_image_citation(
|
|
194
|
+
*,
|
|
195
|
+
source_ref: dict[str, Any],
|
|
196
|
+
source_anchor: dict[str, Any] | None,
|
|
197
|
+
excerpt: str | None,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Apply the image citation rules to an ``AssetReference`` source.
|
|
200
|
+
|
|
201
|
+
A ``PixelRangeAnchor`` / ``RectAnchor`` source is an image region:
|
|
202
|
+
DOCX Studio names it ``type=image`` and keeps the excerpt out of its URL
|
|
203
|
+
(the record keeps it as the span of the cited paragraph to highlight).
|
|
204
|
+
A source that ``AssetMeta(asset_type="image")`` names an image must use
|
|
205
|
+
one of those anchors.
|
|
206
|
+
"""
|
|
207
|
+
anchor = source_anchor or source_ref.get("anchor")
|
|
208
|
+
anchor_type = anchor.get("type") if isinstance(anchor, dict) else None
|
|
209
|
+
meta = source_ref.get("meta")
|
|
210
|
+
asset_type = meta.get("assetType") if isinstance(meta, dict) else None
|
|
211
|
+
is_region = anchor_type in _IMAGE_REGION_ANCHOR_TYPES
|
|
212
|
+
if not is_region and str(asset_type or "").lower() != "image":
|
|
213
|
+
return
|
|
214
|
+
meta_excerpt = meta.get("excerpt") if isinstance(meta, dict) else None
|
|
215
|
+
check_image_citation(
|
|
216
|
+
anchor_type=anchor_type,
|
|
217
|
+
has_excerpt=excerpt is not None or meta_excerpt is not None,
|
|
218
|
+
region=anchor if is_region and isinstance(anchor, dict) else None,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
109
222
|
def build_add_citation_command(
|
|
110
223
|
*,
|
|
111
224
|
destination_anchor: object,
|
|
@@ -142,11 +255,19 @@ def build_add_citation_command(
|
|
|
142
255
|
if source_ref is None and anchor is not None:
|
|
143
256
|
raise ValueError("anchor requires an AssetReference source; encode the anchor in citation_string instead")
|
|
144
257
|
|
|
258
|
+
source_anchor = anchor_to_dict(anchor)
|
|
259
|
+
if source_ref is not None:
|
|
260
|
+
_check_typed_image_citation(
|
|
261
|
+
source_ref=source_ref,
|
|
262
|
+
source_anchor=source_anchor,
|
|
263
|
+
excerpt=excerpt,
|
|
264
|
+
)
|
|
265
|
+
|
|
145
266
|
citation_id = new_citation_id()
|
|
146
267
|
return citation_id, AddCitation(
|
|
147
268
|
destination_anchor=destination_anchor_to_dict(destination_anchor),
|
|
148
269
|
source_ref=source_ref,
|
|
149
|
-
source_anchor=
|
|
270
|
+
source_anchor=source_anchor,
|
|
150
271
|
citation_string=normalized_citation_string,
|
|
151
272
|
citation_id=citation_id,
|
|
152
273
|
excerpt=excerpt,
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Run order of a paragraph, as ``Paragraph.add_run`` created its runs.
|
|
2
|
+
|
|
3
|
+
A picture takes no text offset, so two runs can end at the same offset:
|
|
4
|
+
``r1 = p.add_run("A"); r2 = p.add_run(); r2.add_picture(img)`` puts ``r2`` and
|
|
5
|
+
its picture at offset 1, where ``r1`` ends. The offset alone cannot tell the
|
|
6
|
+
server whether content for ``r1`` goes before or after that picture. The SDK
|
|
7
|
+
knows: ``add_run`` appends each run to the end of its paragraph.
|
|
8
|
+
|
|
9
|
+
This module keeps a record of each such run (its range and the pictures that
|
|
10
|
+
its ``add_picture`` added), in creation order, and gives:
|
|
11
|
+
|
|
12
|
+
- the run identity of a run-end insert (:func:`run_ref`): the first picture
|
|
13
|
+
that a later run holds at the same offset (for a read segment that ends
|
|
14
|
+
inside its run, also the run's own picture there). The server puts the
|
|
15
|
+
content in front of it; without it, after the pictures at the offset.
|
|
16
|
+
- current ranges: when a run grows or shrinks, its pictures and the runs after
|
|
17
|
+
it move (:func:`note_length_change`), and so do the live run proxies. A
|
|
18
|
+
replace cuts every record that it touches.
|
|
19
|
+
|
|
20
|
+
A record lives as long as its paragraph proxy, not as long as its run proxy,
|
|
21
|
+
so ``p.add_run().add_picture(img)`` keeps the picture's place. The records of
|
|
22
|
+
every paragraph proxy of one block (``doc.paragraphs`` makes new ones) are one
|
|
23
|
+
order. A read run (``paragraph.runs``) stands for the recorded run that holds
|
|
24
|
+
its last character; a read run over text that no recorded run holds counts as
|
|
25
|
+
a run before all of them. The order lives in the SDK process only; the
|
|
26
|
+
document stores none of it.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import itertools
|
|
32
|
+
import weakref
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import TYPE_CHECKING, Any
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from docx.text.run import Run
|
|
38
|
+
|
|
39
|
+
_SEQ = itertools.count()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(eq=False)
|
|
43
|
+
class RunRecord:
|
|
44
|
+
"""One run that ``add_run`` created."""
|
|
45
|
+
|
|
46
|
+
seq: int
|
|
47
|
+
range: tuple[int, int]
|
|
48
|
+
run: "weakref.ref[Run]"
|
|
49
|
+
# ``(picture id, offset)`` of each picture that the run's ``add_picture`` added.
|
|
50
|
+
pictures: list[tuple[str, int]] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Paragraph proxy -> the records of the runs its ``add_run`` created. A record
|
|
54
|
+
# holds no reference to the paragraph or session, so an entry goes with its
|
|
55
|
+
# paragraph proxy. Keyed by the proxy, not by its id: the first flush moves a
|
|
56
|
+
# new paragraph from its client id to the server id, and the runs created
|
|
57
|
+
# before that keep the client id.
|
|
58
|
+
_ORDERS: "weakref.WeakKeyDictionary[Any, list[RunRecord]]" = weakref.WeakKeyDictionary()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _records_of(run: "Run") -> list[RunRecord]:
|
|
62
|
+
paragraph = getattr(run, "_paragraph", None)
|
|
63
|
+
ids = {run._block_id}
|
|
64
|
+
if paragraph is not None:
|
|
65
|
+
ids.add(paragraph._node_id)
|
|
66
|
+
records: list[RunRecord] = []
|
|
67
|
+
for owner, owner_records in list(_ORDERS.items()):
|
|
68
|
+
if owner is paragraph or (owner._session is run._session and owner._node_id in ids):
|
|
69
|
+
records.extend(owner_records)
|
|
70
|
+
records.sort(key=lambda record: record.seq)
|
|
71
|
+
return records
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def register_run(run: "Run") -> None:
|
|
75
|
+
"""Record ``run`` as the last run of its paragraph (``Paragraph.add_run``)."""
|
|
76
|
+
record = RunRecord(seq=next(_SEQ), range=run._range, run=weakref.ref(run))
|
|
77
|
+
_ORDERS.setdefault(run._paragraph, []).append(record)
|
|
78
|
+
run._run_record = record
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def forget_paragraph(paragraph: Any) -> None:
|
|
82
|
+
"""Drop the order of a paragraph whose runs are gone (``Paragraph.text = ...``)."""
|
|
83
|
+
for owner in list(_ORDERS.keys()):
|
|
84
|
+
if owner is paragraph or (owner._session is paragraph._session and owner._node_id == paragraph._node_id):
|
|
85
|
+
_ORDERS.pop(owner, None)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _place(
|
|
89
|
+
run: "Run", span: tuple[int, int], records: list[RunRecord] | None = None
|
|
90
|
+
) -> tuple[RunRecord | None, list[RunRecord]]:
|
|
91
|
+
"""The record of ``run`` (over ``span``), if any, and the records after it."""
|
|
92
|
+
if records is None:
|
|
93
|
+
records = _records_of(run)
|
|
94
|
+
# The run itself, or the text that its ``add_text`` returned.
|
|
95
|
+
own = getattr(run, "_run_record", None) or getattr(run, "_owner_record", None)
|
|
96
|
+
for index, record in enumerate(records):
|
|
97
|
+
if record is own:
|
|
98
|
+
return record, records[index + 1 :]
|
|
99
|
+
# ``paragraph.runs`` makes a new proxy of a kept run: over the same range,
|
|
100
|
+
# or over a part of it (a read splits a run at a tab, a break or a change
|
|
101
|
+
# of format). The proxy stands for the run that holds its last character.
|
|
102
|
+
if span[0] != span[1]:
|
|
103
|
+
last = span[1] - 1
|
|
104
|
+
for index, record in enumerate(records):
|
|
105
|
+
if record.range[0] <= last < record.range[1]:
|
|
106
|
+
return record, records[index + 1 :]
|
|
107
|
+
return None, records
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _set_range(record: RunRecord, value: tuple[int, int]) -> None:
|
|
111
|
+
record.range = value
|
|
112
|
+
proxy = record.run()
|
|
113
|
+
if proxy is not None:
|
|
114
|
+
proxy._range = value
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def note_length_change(
|
|
118
|
+
run: "Run",
|
|
119
|
+
at: int,
|
|
120
|
+
delta: int,
|
|
121
|
+
*,
|
|
122
|
+
replaced: tuple[int, int] | None = None,
|
|
123
|
+
before: str | None = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""``run`` (already over its new range) changed its text by ``delta`` units.
|
|
126
|
+
|
|
127
|
+
Without ``replaced``: an insert of ``delta`` units at ``at`` (the end of
|
|
128
|
+
``run``), in front of the picture ``before`` when the command named one
|
|
129
|
+
(else after the pictures at ``at``). With ``replaced``: ``Run.text``
|
|
130
|
+
replaced the old range ``replaced``; the server keeps the pictures at its
|
|
131
|
+
start and end and removes the ones inside.
|
|
132
|
+
|
|
133
|
+
The text belongs to the record of ``run`` (see :func:`_place`). Every
|
|
134
|
+
record that the replaced range touches is cut; the records and pictures
|
|
135
|
+
after the change move.
|
|
136
|
+
"""
|
|
137
|
+
if delta == 0 and replaced is None:
|
|
138
|
+
return
|
|
139
|
+
records = _records_of(run)
|
|
140
|
+
if replaced is None:
|
|
141
|
+
start = end = at
|
|
142
|
+
inserted = delta
|
|
143
|
+
old_span = (run._range[0], run._range[1] - delta)
|
|
144
|
+
else:
|
|
145
|
+
start, end = replaced
|
|
146
|
+
inserted = (end - start) + delta
|
|
147
|
+
old_span = replaced
|
|
148
|
+
owner = _place(run, old_span, records)[0]
|
|
149
|
+
owner_index = records.index(owner) if owner is not None else -1
|
|
150
|
+
removed = end - start
|
|
151
|
+
|
|
152
|
+
def cut(x: int) -> int:
|
|
153
|
+
return x if x <= start else start if x < end else x - removed
|
|
154
|
+
|
|
155
|
+
# The pictures at ``at`` that an insert goes in front of: ``before`` and
|
|
156
|
+
# the ones after it, in document order (records in order, then the
|
|
157
|
+
# pictures of each record in order).
|
|
158
|
+
moved_at: set[str] = set()
|
|
159
|
+
if replaced is None and before is not None:
|
|
160
|
+
order = [pid for record in records for pid, off in record.pictures if off == at]
|
|
161
|
+
if before in order:
|
|
162
|
+
moved_at = set(order[order.index(before) :])
|
|
163
|
+
|
|
164
|
+
for index, record in enumerate(records):
|
|
165
|
+
rec_start, rec_end = cut(record.range[0]), cut(record.range[1])
|
|
166
|
+
if record is owner:
|
|
167
|
+
rec_end += inserted
|
|
168
|
+
elif index > owner_index and rec_start >= start:
|
|
169
|
+
rec_start, rec_end = rec_start + inserted, rec_end + inserted
|
|
170
|
+
_set_range(record, (rec_start, rec_end))
|
|
171
|
+
pictures: list[tuple[str, int]] = []
|
|
172
|
+
for pid, off in record.pictures:
|
|
173
|
+
if replaced is None:
|
|
174
|
+
pictures.append((pid, off + delta if off > at or pid in moved_at else off))
|
|
175
|
+
elif off <= start:
|
|
176
|
+
pictures.append((pid, off))
|
|
177
|
+
elif off >= end:
|
|
178
|
+
pictures.append((pid, off + delta))
|
|
179
|
+
# The moves keep document order; a stable sort by offset keeps it so.
|
|
180
|
+
pictures.sort(key=lambda item: item[1])
|
|
181
|
+
record.pictures = pictures
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def note_picture(run: "Run", picture_id: str, offset: int, *, before: str | None = None) -> None:
|
|
185
|
+
"""``run.add_picture`` put the picture ``picture_id`` at ``offset``, in front of ``before``.
|
|
186
|
+
|
|
187
|
+
A record keeps its pictures in document order: the new one goes in front
|
|
188
|
+
of ``before``, else after the pictures at ``offset`` and before the ones
|
|
189
|
+
after it (the server puts a picture after the pictures at its offset).
|
|
190
|
+
"""
|
|
191
|
+
record = _place(run, run._range)[0]
|
|
192
|
+
if record is None:
|
|
193
|
+
return
|
|
194
|
+
ids = [pid for pid, _ in record.pictures]
|
|
195
|
+
if before in ids:
|
|
196
|
+
index = ids.index(before)
|
|
197
|
+
else:
|
|
198
|
+
index = next((i for i, (_, off) in enumerate(record.pictures) if off > offset), len(ids))
|
|
199
|
+
record.pictures.insert(index, (picture_id, offset))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def run_ref(run: "Run") -> dict[str, str] | None:
|
|
203
|
+
"""``{"beforeImageId": id}``: the picture that an insert at the end of ``run`` goes in front of.
|
|
204
|
+
|
|
205
|
+
The first picture of a later run where ``run`` ends. A read segment that
|
|
206
|
+
ends inside its run (a picture of the run between two of its texts) also
|
|
207
|
+
puts the insert in front of the run's own picture there, right after the
|
|
208
|
+
segment's text.
|
|
209
|
+
"""
|
|
210
|
+
end = run._range[1]
|
|
211
|
+
record, later = _place(run, run._range)
|
|
212
|
+
candidates = ([record] if record is not None and end < record.range[1] else []) + later
|
|
213
|
+
for candidate in candidates:
|
|
214
|
+
for picture_id, offset in candidate.pictures:
|
|
215
|
+
if offset == end:
|
|
216
|
+
return {"beforeImageId": picture_id}
|
|
217
|
+
return None
|
|
@@ -109,12 +109,19 @@ class CreateTable(Command):
|
|
|
109
109
|
client_node_id: str | None = None
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
# Run identity of a run-end insert (``Run.add_text`` / ``add_tab`` /
|
|
113
|
+
# ``add_break`` / ``add_picture``, ``Run.text`` on an empty run):
|
|
114
|
+
# ``{"beforeImageId": <picture id>}``, the first picture that a later run
|
|
115
|
+
# holds at the same offset (see ``docx._run_order``). The server puts the
|
|
116
|
+
# content in front of it. ``None`` (not sent): after the pictures there.
|
|
117
|
+
# ``Insert``, ``InsertTab`` and ``InsertLineBreak`` carry it too.
|
|
112
118
|
@dataclass
|
|
113
119
|
class CreateImage(Command):
|
|
114
120
|
src: str
|
|
115
121
|
size: dict[str, Any]
|
|
116
122
|
at: dict[str, Any]
|
|
117
123
|
client_node_id: str | None = None
|
|
124
|
+
run: dict[str, Any] | None = None
|
|
118
125
|
|
|
119
126
|
|
|
120
127
|
@dataclass
|
|
@@ -142,6 +149,7 @@ class Insert(Command):
|
|
|
142
149
|
inherit_marks: bool | None = None
|
|
143
150
|
placement: str | None = None
|
|
144
151
|
content: dict[str, Any] | None = None
|
|
152
|
+
run: dict[str, Any] | None = None
|
|
145
153
|
|
|
146
154
|
|
|
147
155
|
@dataclass
|
|
@@ -181,12 +189,14 @@ class Replace(Command):
|
|
|
181
189
|
class InsertLineBreak(Command):
|
|
182
190
|
block_id: str
|
|
183
191
|
offset: int
|
|
192
|
+
run: dict[str, Any] | None = None
|
|
184
193
|
|
|
185
194
|
|
|
186
195
|
@dataclass
|
|
187
196
|
class InsertTab(Command):
|
|
188
197
|
block_id: str
|
|
189
198
|
offset: int
|
|
199
|
+
run: dict[str, Any] | None = None
|
|
190
200
|
|
|
191
201
|
|
|
192
202
|
@dataclass
|
|
@@ -490,10 +500,15 @@ class SetImageSize(Command):
|
|
|
490
500
|
class SetImageAltText(Command):
|
|
491
501
|
"""Set alt-text on an inline image. Mirrors DOCX Studio's
|
|
492
502
|
``doc.images.setAltText``. Used by accessibility flows that need
|
|
493
|
-
a text alternative for screen readers.
|
|
503
|
+
a text alternative for screen readers.
|
|
504
|
+
|
|
505
|
+
``alt_text`` is Word's description (``wp:docPr/@descr``). ``title`` is
|
|
506
|
+
Word's title (``wp:docPr/@title``); ``None`` leaves it unchanged, and an
|
|
507
|
+
older docx-studio ignores the field."""
|
|
494
508
|
|
|
495
509
|
image_id: str
|
|
496
510
|
alt_text: str
|
|
511
|
+
title: str | None = None
|
|
497
512
|
|
|
498
513
|
|
|
499
514
|
@dataclass
|
|
@@ -551,7 +566,9 @@ class GetNode(Command):
|
|
|
551
566
|
|
|
552
567
|
@dataclass
|
|
553
568
|
class ImagesList(Command):
|
|
554
|
-
|
|
569
|
+
# ``False`` leaves out each picture's ``src`` (a data URI can be MBs); an
|
|
570
|
+
# older docx-studio ignores the field and sends ``src``.
|
|
571
|
+
include_src: bool | None = None
|
|
555
572
|
|
|
556
573
|
|
|
557
574
|
@dataclass
|
|
@@ -65,6 +65,19 @@ def _strip_data_uri(src: str) -> tuple[bytes, str | None]:
|
|
|
65
65
|
return b"", mime
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
def _picture_id_parts(node_id: str) -> tuple[str, str, str | None]:
|
|
69
|
+
"""``(paraId, fingerprint, persisted id)`` of a picture id.
|
|
70
|
+
|
|
71
|
+
A picture id is ``<paraId>:<index>:<fingerprint>``, with a fourth part
|
|
72
|
+
``:<persisted id>`` when the room stores one; an older docx-studio gives
|
|
73
|
+
only ``<paraId>`` or ``<paraId>:<index>``.
|
|
74
|
+
"""
|
|
75
|
+
parts = node_id.split(":")
|
|
76
|
+
fingerprint = parts[2] if len(parts) > 2 else ""
|
|
77
|
+
persisted = parts[3] if len(parts) > 3 else None
|
|
78
|
+
return parts[0], fingerprint, persisted
|
|
79
|
+
|
|
80
|
+
|
|
68
81
|
class InlineShape:
|
|
69
82
|
"""An inline image/shape in the document."""
|
|
70
83
|
|
|
@@ -177,15 +190,75 @@ class InlineShape:
|
|
|
177
190
|
@alt_text.setter
|
|
178
191
|
@athena_extension(issue="PR#1530", description="InlineShape.alt_text setter.")
|
|
179
192
|
def alt_text(self, value: str | None) -> None:
|
|
193
|
+
text = "" if value is None else str(value)
|
|
194
|
+
if self._can_address(stacklevel=3):
|
|
195
|
+
self._send_alt_text(alt_text=text, title=None)
|
|
196
|
+
self._info["altText"] = text
|
|
197
|
+
|
|
198
|
+
def _can_address(self, *, stacklevel: int) -> bool:
|
|
199
|
+
"""``False`` (with a warning) for a shape that has no id.
|
|
200
|
+
|
|
201
|
+
A listed picture on an older docx-studio, whose ``ImagesList`` sends
|
|
202
|
+
no ``nodeId``, cannot be addressed, and an empty ``imageId`` fails the
|
|
203
|
+
whole batch there. The setter then sends nothing and keeps the old
|
|
204
|
+
value, so the shape does not report a change that the document does
|
|
205
|
+
not have.
|
|
206
|
+
"""
|
|
207
|
+
from docx.errors import DocxUnsupportedCommandWarning
|
|
208
|
+
|
|
209
|
+
if self._node_id:
|
|
210
|
+
return True
|
|
211
|
+
warnings.warn(
|
|
212
|
+
"InlineShape alt text / title / decorative was not set: this picture has "
|
|
213
|
+
"no id. Read the shape again from doc.inline_shapes after "
|
|
214
|
+
"docx-studio is updated.",
|
|
215
|
+
DocxUnsupportedCommandWarning,
|
|
216
|
+
stacklevel=stacklevel + 1,
|
|
217
|
+
)
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
def _send_alt_text(self, *, alt_text: str, title: str | None) -> None:
|
|
180
221
|
from docx.commands import SetImageAltText
|
|
181
222
|
|
|
182
|
-
text = "" if value is None else str(value)
|
|
183
223
|
run_sync(
|
|
184
224
|
self._session.send_command(
|
|
185
|
-
SetImageAltText(image_id=self._node_id, alt_text=
|
|
225
|
+
SetImageAltText(image_id=self._node_id, alt_text=alt_text, title=title),
|
|
186
226
|
),
|
|
187
227
|
)
|
|
188
|
-
|
|
228
|
+
|
|
229
|
+
def _stored_alt_text(self) -> str:
|
|
230
|
+
"""The description that the document stores for this picture now.
|
|
231
|
+
|
|
232
|
+
Another shape for the same picture can change the description after
|
|
233
|
+
this shape read it. ``ImagesList`` gives the current value, without
|
|
234
|
+
the image data; the cached value is the fallback when the list does
|
|
235
|
+
not name the picture (an older docx-studio). The read flushes pending
|
|
236
|
+
commands first, so a client id is the real id after it.
|
|
237
|
+
"""
|
|
238
|
+
rows = InlineShapes(session=self._session)._items(include_src=False)
|
|
239
|
+
node_id = self._node_id
|
|
240
|
+
para_id, fingerprint, persisted = _picture_id_parts(node_id)
|
|
241
|
+
same: list[dict] = []
|
|
242
|
+
for row in rows:
|
|
243
|
+
row_id = str(row.get("nodeId") or "")
|
|
244
|
+
if row_id == node_id:
|
|
245
|
+
same = [row]
|
|
246
|
+
break
|
|
247
|
+
row_para, row_fingerprint, row_persisted = _picture_id_parts(row_id)
|
|
248
|
+
# A persisted id names one picture, in any paragraph and position.
|
|
249
|
+
if persisted is not None and row_persisted is not None:
|
|
250
|
+
if row_persisted == persisted:
|
|
251
|
+
same = [row]
|
|
252
|
+
break
|
|
253
|
+
continue
|
|
254
|
+
# A room without persisted ids (an older editor removed them):
|
|
255
|
+
# the paragraph and the fingerprint, as the server does.
|
|
256
|
+
if row_para != para_id:
|
|
257
|
+
continue
|
|
258
|
+
if ":" not in node_id or (fingerprint and row_fingerprint == fingerprint):
|
|
259
|
+
same.append(row)
|
|
260
|
+
stored = same[0].get("altText") if len(same) == 1 else None
|
|
261
|
+
return stored if isinstance(stored, str) else self.alt_text
|
|
189
262
|
|
|
190
263
|
@property
|
|
191
264
|
@athena_extension(issue="PR#1530", description="InlineShape.title — image title.")
|
|
@@ -202,14 +275,15 @@ class InlineShape:
|
|
|
202
275
|
@title.setter
|
|
203
276
|
@athena_extension(issue="PR#1530", description="InlineShape.title setter.")
|
|
204
277
|
def title(self, value: str | None) -> None:
|
|
205
|
-
from docx.commands import SetImageAltText
|
|
206
|
-
|
|
207
278
|
text = "" if value is None else str(value)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
279
|
+
if not self._can_address(stacklevel=3):
|
|
280
|
+
return
|
|
281
|
+
# The command always carries the description: an older docx-studio
|
|
282
|
+
# requires ``altText`` and ignores ``title``. Sending the stored
|
|
283
|
+
# description keeps it; before, the title text replaced it.
|
|
284
|
+
alt_text = self._stored_alt_text()
|
|
285
|
+
self._send_alt_text(alt_text=alt_text, title=text)
|
|
286
|
+
self._info["altText"] = alt_text
|
|
213
287
|
self._info["title"] = text
|
|
214
288
|
|
|
215
289
|
@property
|
|
@@ -228,6 +302,8 @@ class InlineShape:
|
|
|
228
302
|
def decorative(self, value: bool) -> None:
|
|
229
303
|
from docx.commands import SetImageDecorative
|
|
230
304
|
|
|
305
|
+
if not self._can_address(stacklevel=3):
|
|
306
|
+
return
|
|
231
307
|
run_sync(
|
|
232
308
|
self._session.send_command(
|
|
233
309
|
SetImageDecorative(image_id=self._node_id, decorative=bool(value)),
|
|
@@ -283,8 +359,9 @@ class InlineShapes:
|
|
|
283
359
|
def __init__(self, *, session: "Session") -> None:
|
|
284
360
|
self._session: "Session" = session
|
|
285
361
|
|
|
286
|
-
def _items(self) -> list[dict]:
|
|
287
|
-
|
|
362
|
+
def _items(self, *, include_src: bool = True) -> list[dict]:
|
|
363
|
+
params: dict = {} if include_src else {"includeSrc": False}
|
|
364
|
+
info: object = run_sync(self._session.doc.images.list(params))
|
|
288
365
|
if isinstance(info, dict):
|
|
289
366
|
items: object = info.get("items") or info.get("images") or []
|
|
290
367
|
if isinstance(items, list):
|
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
7
7
|
|
|
8
8
|
from docx._athena_extension import athena_extension
|
|
9
9
|
from docx._batching import run_sync
|
|
10
|
+
from docx._run_order import forget_paragraph, register_run
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from docx._references import AssetReference
|
|
@@ -431,6 +432,8 @@ class Paragraph:
|
|
|
431
432
|
# We just replaced the entire paragraph contents with ``value``;
|
|
432
433
|
# the new text-address length is simply ``len(value)``.
|
|
433
434
|
self._text_len = len(value)
|
|
435
|
+
# The runs of the paragraph are gone, as in python-docx.
|
|
436
|
+
forget_paragraph(self)
|
|
434
437
|
# Content changed — drop the prefetched snapshot so a follow-up
|
|
435
438
|
# ``.text`` / ``.runs`` read reflects the replacement.
|
|
436
439
|
self._invalidate_node_info()
|
|
@@ -1232,6 +1235,12 @@ class Paragraph:
|
|
|
1232
1235
|
in_cell=self._in_cell,
|
|
1233
1236
|
cell_ooxml_address=self._cell_ooxml_address,
|
|
1234
1237
|
)
|
|
1238
|
+
# The paragraph's run order, so a run-end insert at an offset that two
|
|
1239
|
+
# runs share goes to the right run (see ``docx._run_order``).
|
|
1240
|
+
register_run(run_obj)
|
|
1241
|
+
# A new empty run has no formatting of its own yet: its first text
|
|
1242
|
+
# does not take the formatting of the run before it.
|
|
1243
|
+
run_obj._fresh = not text_str
|
|
1235
1244
|
if style is not None:
|
|
1236
1245
|
style_id: str = (
|
|
1237
1246
|
style.style_id if isinstance(style, CharacterStyle) else style
|
|
@@ -1501,7 +1510,12 @@ class Paragraph:
|
|
|
1501
1510
|
Pass either a serialized citation URL via ``citation_string`` /
|
|
1502
1511
|
positional ``source`` string, or an ``AssetReference`` from
|
|
1503
1512
|
``docx._references`` plus an optional source ``anchor`` such as a
|
|
1504
|
-
spreadsheet ``SheetRangeAnchor
|
|
1513
|
+
spreadsheet ``SheetRangeAnchor``, a media ``TimeRangeAnchor`` or an
|
|
1514
|
+
image ``PixelRangeAnchor`` (a region in source image pixels). An
|
|
1515
|
+
image citation that chat and Sheets reject raises ``ValueError``: an
|
|
1516
|
+
excerpt with no region anchor, a page or text anchor, or a region
|
|
1517
|
+
without numeric x, y, width and height. An excerpt beside a region
|
|
1518
|
+
anchor is allowed; it names the span of this paragraph to highlight.
|
|
1505
1519
|
|
|
1506
1520
|
A paragraph inside a table cell is citable too. ``cell.paragraphs[0]``
|
|
1507
1521
|
stands for the whole cell, so the citation covers the text of the
|