athena-python-docx 0.26.0__tar.gz → 0.28.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.26.0 → athena_python_docx-0.28.0}/.gitignore +2 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/AGENTS.md +96 -1
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/PKG-INFO +1 -1
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/__init__.py +1 -1
- athena_python_docx-0.28.0/docx/_batching.py +137 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_buffer.py +75 -2
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_http_doc.py +114 -2
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/client.py +7 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/errors.py +10 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/hyperlink.py +4 -4
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/paragraph.py +103 -11
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/run.py +157 -4
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/pyproject.toml +1 -1
- athena_python_docx-0.28.0/scripts/round_trip_smoke.py +426 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/METHODOLOGY.md +49 -1
- athena_python_docx-0.28.0/tests/fixtures/field_reads.json +1022 -0
- athena_python_docx-0.28.0/tests/fixtures/note_reference_reads.json +193 -0
- athena_python_docx-0.28.0/tests/live/__init__.py +0 -0
- athena_python_docx-0.28.0/tests/live/baseline.json +1925 -0
- athena_python_docx-0.28.0/tests/live/reader.py +231 -0
- athena_python_docx-0.28.0/tests/live/runner.py +700 -0
- athena_python_docx-0.28.0/tests/live/sdk_driver.py +56 -0
- athena_python_docx-0.28.0/tests/live/stock_driver.py +65 -0
- athena_python_docx-0.28.0/tests/live/test_live_runner.py +411 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/intentional_deviations.json +39 -0
- athena_python_docx-0.28.0/tests/test_batch_chunking.py +213 -0
- athena_python_docx-0.28.0/tests/test_field_reads_python_docx_parity.py +110 -0
- athena_python_docx-0.28.0/tests/test_field_runs.py +399 -0
- athena_python_docx-0.28.0/tests/test_note_reference_reads_python_docx_parity.py +236 -0
- athena_python_docx-0.28.0/tests/test_repeated_node_read.py +119 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_revisions.py +38 -0
- athena_python_docx-0.28.0/tests/test_round_trip_smoke_script.py +115 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/uv.lock +1 -1
- athena_python_docx-0.26.0/docx/_batching.py +0 -88
- athena_python_docx-0.26.0/scripts/round_trip_smoke.py +0 -231
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/CLAUDE.md +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/DOCX_EXEC_LAB.md +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/README.md +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_athena_extension.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_citations.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_execution.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_http.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_image_utils.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_postproc.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_ptc.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_references.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_run_order.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_table_styles.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_timeouts.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/api.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/blkcntnr.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/bookmarks.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/charts.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/commands.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/comments.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/document.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/drawing/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/base.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/dml.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/section.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/shape.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/style.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/table.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/text.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/exceptions.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/fields.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/footnotes.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/image/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/image/exceptions.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/image/image.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/math.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/opc/coreprops.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/oxml/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/revisions.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/sdt.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/section.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/session.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/settings.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/shape.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/shared.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/_table.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/latent.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/style.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/styles.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/table.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/font.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/pagebreak.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/parfmt.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/tabstops.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/toc.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/typing.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_examples/fast_table_fill.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_examples/find_replace_literal.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/dump_wire_fixtures.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/release.sh +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/smoke_test_block_not_found.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/validate_find_replace_asset.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/conftest.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/ab_probe_cases.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/ab_probe_runner.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/binary_round_trip.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/fake_session.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/README.md +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/_runner.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/extractor.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/local_runner.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshot.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/ours_spec.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/parity_diff.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/real_world_cases.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/stock_spec.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/README.md +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/__init__.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/baseline_gaps.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/compare.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/introspect.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/reports/GAP_ANALYSIS.md +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/reports/gap_report.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/run_parity.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/snapshots/athena_latest.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/test_parity_gap.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_add_section.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_add_section_extract_items.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_athena_extension_creates_session_open.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_athena_extensions_contract.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_athena_extensions_registry.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_batching_perf.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_block_not_found_error.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_buffer.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_add_table_not_supported.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_borders_spec.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_fastpath_runs.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_add_hyperlink_stash.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_add_run_via_cell_insert.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_format_stash.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_run_format_stash.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_run_guard.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_merge.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_paragraphs_listed.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_text_plain_fastpath.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_text_replace_semantics.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_citations.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_collapsed_range_format.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_color_format_theme.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_command_dataclasses.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_commands.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_comments.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_core_properties.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_asset_id_property.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_clear.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_create.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_create_from_template.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_factory_validation.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_docx_exec_lab.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_drawing_image.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_e2e_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_enum_parity.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_execution_scope.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_find_replace_session_open.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_http_transport.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_hyperlink_coalescing.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_image_url_data_uri.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_inline_shape_alt_text.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_inline_shape_size.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_insert_deferred.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_iter_inner_content.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_list_styles.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_merged_cell_secondary_slot.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_merged_cells.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_missing_modules.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_oxml_shim.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_page_break.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_paragraph_runs_hyperlinks.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_paragraph_text_len_cache.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_parity_misc.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_parity_round2.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_pdf_export_and_page_renders.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_phase_a_behavior.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_phase_b_headers_footers.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_phase_c_tables.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_positional_cell_id.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_postproc_cell_format_rewrite.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_postproc_cell_run_format_rewrite.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_postproc_ref_restore.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_pr19766_review_fixes.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_ptc.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_restart_numbering.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_add_picture_anchor.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_formatting_wire.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_identity.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_mark_inheritance.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_table_style_objects.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_text_python_docx.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_text_refreshes_paragraph.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_sdk_reads_served.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_section_read_lengths.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_picture.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_run.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_comments_get.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_document_audit.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_document_element.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_enum_section.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_font_audit.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_header_footer.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_hyperlink.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_inline_shape.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_misc.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_style.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_parfmt.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_row_col_cell.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_add_break.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_style.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_style_strict.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_text.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_underline.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_audit.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_dimensions.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_onoff.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_settings.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_shared_audit.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_style.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_styles.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_audit.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_cell.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_dimensions.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_layout.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_smoke_integration.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_acceptance.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_font.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_formatting_writes.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_metadata_writes.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_names.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_setters_contract.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_table_set_cell_perf.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_table_style_id_resolution.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_temporarily_unavailable.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_text_control_characters.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_uv_lock_version.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_validate_find_replace_asset_script.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_wire_contract.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_xml_attr_guard.py +0 -0
- {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_zod_wire_contract.py +0 -0
|
@@ -25,6 +25,13 @@ Before adding or modifying any API surface:
|
|
|
25
25
|
|
|
26
26
|
Run `uv run pytest tests/test_python_docx_api_parity.py -v -s` to verify parity.
|
|
27
27
|
|
|
28
|
+
The API ratchet sees shape only. For behaviour, `uv run --extra dev python -m
|
|
29
|
+
tests.live.runner --cases <name>` runs a battery case through stock
|
|
30
|
+
python-docx 1.2.0 and through the SDK against a live in-memory DOCX Studio
|
|
31
|
+
and compares what each reads, field by field (`tests/fidelity/METHODOLOGY.md`
|
|
32
|
+
§ L9). CI fails on a live gap `tests/live/baseline.json` does not hold, and
|
|
33
|
+
on a baseline gap that is gone.
|
|
34
|
+
|
|
28
35
|
### Intentionally omitted (current SDK limitations)
|
|
29
36
|
|
|
30
37
|
These standard python-docx members don't apply to a DOCX Studio-backed SDK:
|
|
@@ -224,6 +231,11 @@ not file-backed. Each is documented in the relevant docstring.
|
|
|
224
231
|
`Revision.reject()` resolve a single change. python-docx upstream
|
|
225
232
|
has zero track-changes API, so this entire surface is an Athena
|
|
226
233
|
extension wired to DOCX Studio's `doc.trackChanges.{list,get,decide}`.
|
|
234
|
+
A run's tracked formatting change (`w:rPrChange`) and a paragraph's
|
|
235
|
+
(`w:pPrChange`) are `"format"` revisions; `reject()` gives the run or
|
|
236
|
+
paragraph exactly the earlier properties the change recorded
|
|
237
|
+
(`tests/test_revisions.py`, docx-studio's
|
|
238
|
+
`tracked-run-property-changes.test.ts`).
|
|
227
239
|
|
|
228
240
|
- **Style writes refuse what would write a broken document.** python-docx
|
|
229
241
|
writes these into `styles.xml`; the SDK raises before anything is sent,
|
|
@@ -286,7 +298,9 @@ Issue numbers reference `python-openxml/python-docx`.
|
|
|
286
298
|
`FootnotesGet` / `FootnotesPatch` / `FootnotesDelete`. The
|
|
287
299
|
parallel **`Document.endnotes: Endnotes`** + **`Run.add_endnote
|
|
288
300
|
(text, *, custom_mark=None)`** mirrors the same machinery for
|
|
289
|
-
end-of-section notes.
|
|
301
|
+
end-of-section notes. Both list the notes a reference reaches, as
|
|
302
|
+
Word and the export do: a note whose reference was deleted in the
|
|
303
|
+
editor stays in the package for undo, and is not listed.
|
|
290
304
|
|
|
291
305
|
- **`Document.add_toc(*, levels=(1, 3), style_id=None,
|
|
292
306
|
include_hyperlinks=True, title=None, tab_leader="dot", at=None)
|
|
@@ -413,6 +427,8 @@ Issue numbers reference `python-openxml/python-docx`.
|
|
|
413
427
|
inspection. Use this when an agent needs to sample or inspect runs
|
|
414
428
|
without issuing one ``getNodeById`` query per paragraph through
|
|
415
429
|
``doc.paragraphs`` + ``paragraph.runs``. Routes through `IterRuns`.
|
|
430
|
+
These three see a footnote or endnote only while a reference reaches
|
|
431
|
+
it, as `Document.footnotes` / `Document.endnotes` list them.
|
|
416
432
|
|
|
417
433
|
- **`Paragraph.list_id` / `Paragraph.list_level` /
|
|
418
434
|
`Paragraph.list_format` / `Paragraph.list_number`** — list-item
|
|
@@ -956,6 +972,13 @@ architecture or a DOCX Studio constraint:
|
|
|
956
972
|
sends a column break (`breakType: "column"`), which docx-studio writes after
|
|
957
973
|
the paragraph's content. `DOCX_STUDIO_LIMITATIONS.md` §26.
|
|
958
974
|
|
|
975
|
+
- **`CoreProperties.modified` is the time of the last save.** The asset is a
|
|
976
|
+
live Word document, and docx-studio stamps `dcterms:modified` when it saves
|
|
977
|
+
a change to the package: a section, style or property write, and the export
|
|
978
|
+
of a changed document. Word stamps it on every save; upstream leaves the
|
|
979
|
+
value it read until the script sets it. Owner decision, 2026-10-12. Recorded
|
|
980
|
+
in `tests/parity/intentional_deviations.json` (`live_deviations`).
|
|
981
|
+
|
|
959
982
|
- **Tracked changes read as Word's final view.** `Paragraph.text`, `runs`,
|
|
960
983
|
`iter_inner_content()`, `Hyperlink.text` / `runs` and `_Cell.text` include
|
|
961
984
|
the runs of a tracked insertion (`w:ins`) and leave out a deletion's
|
|
@@ -968,6 +991,78 @@ architecture or a DOCX Studio constraint:
|
|
|
968
991
|
Markup) show insertions and hide deletions, as these reads do.
|
|
969
992
|
`doc.revisions` lists every change (`tests/test_run_text_python_docx.py`).
|
|
970
993
|
|
|
994
|
+
- **A citation link Athena added reads as its text, as the export writes it**
|
|
995
|
+
(owner decision #40122). Athena's citation flow stamps the links it writes
|
|
996
|
+
with the `Athena citation` tooltip. These links only navigate inside Athena,
|
|
997
|
+
so the export leaves them out and keeps their text: no `w:hyperlink`, and no
|
|
998
|
+
citation highlight on that text.
|
|
999
|
+
- **The reads match the export.** The run reads with no `hyperlink` and no
|
|
1000
|
+
highlight in `Paragraph.runs`, `Paragraph.hyperlinks`, a cell's paragraphs
|
|
1001
|
+
and docx-studio's markdown read.
|
|
1002
|
+
- **Where upstream differs.** Upstream reads the room's `w:hyperlink` as a
|
|
1003
|
+
link.
|
|
1004
|
+
- **Which links are Athena's** is decided by the room's provenance, not by
|
|
1005
|
+
the tooltip alone. A link with the tooltip that came in the uploaded file
|
|
1006
|
+
(the room's source package holds it) is a link everywhere, as Word keeps
|
|
1007
|
+
it.
|
|
1008
|
+
- **Tests:** `applier.test.ts` › `applier — citations`.
|
|
1009
|
+
|
|
1010
|
+
- **A field's runs read as python-docx reads them; some writes to them
|
|
1011
|
+
differ.** `Paragraph.text` includes a complex field's displayed result and
|
|
1012
|
+
`Paragraph.runs` holds its begin, field code, separate, result and end runs
|
|
1013
|
+
(each with its own formatting), as python-docx reads the `w:r` children of
|
|
1014
|
+
a `w:p`; a simple field (`w:fldSimple`) reads as nothing (one in a tracked
|
|
1015
|
+
change, which the export writes as a complex field, reads that field's
|
|
1016
|
+
runs), a result run of a note reference reads `""`, and a field in a link
|
|
1017
|
+
is the link's, as in python-docx. Each paragraph of a field that spans
|
|
1018
|
+
paragraphs (a table of contents) holds the field character runs it has
|
|
1019
|
+
(`tests/test_field_reads_python_docx_parity.py`, against stock python-docx
|
|
1020
|
+
on the same `.docx`; `DOCX_STUDIO_LIMITATIONS.md` § 27). A field takes no
|
|
1021
|
+
room in the text-address space, so its runs stand at one offset. Writes:
|
|
1022
|
+
- setting a result run's `.text` changes the field's displayed result,
|
|
1023
|
+
as in python-docx (`Replace` with a `field` target): the export keeps
|
|
1024
|
+
the result's runs where the new text allows (text still there stays in
|
|
1025
|
+
its run, new text goes in the run whose text it replaced; another result
|
|
1026
|
+
run's tab or Word character stays as the source wrote it, and a note
|
|
1027
|
+
reference stays where it is, where python-docx's write of its run would
|
|
1028
|
+
delete it), but writes no result run left with no text, where
|
|
1029
|
+
python-docx keeps an empty `w:r`, and a result run read before the
|
|
1030
|
+
field's result changed (through another read's run, or by a
|
|
1031
|
+
collaborator) raises `FieldRunWriteError` rather than write over that
|
|
1032
|
+
change, where python-docx's run is live;
|
|
1033
|
+
- formatting a result run formats the field's whole result (`FormatApply`
|
|
1034
|
+
with a `field` target), from its source formatting, where python-docx
|
|
1035
|
+
formats that run alone;
|
|
1036
|
+
- `.text` (and `clear()`) on a field character or field code run raises
|
|
1037
|
+
`docx.errors.FieldRunWriteError` naming the field: python-docx replaces
|
|
1038
|
+
the run's `w:fldChar` or `w:instrText` with the text, which breaks the
|
|
1039
|
+
field;
|
|
1040
|
+
- formatting set on a field character or field code run is not applied and
|
|
1041
|
+
emits `PendingFieldRunFormatWarning` (those runs show nothing; docx-studio
|
|
1042
|
+
has no command that writes their own `w:rPr`), where python-docx writes
|
|
1043
|
+
it, so `for run in p.runs: run.bold = True` formats the text and the
|
|
1044
|
+
field's result and warns for the rest;
|
|
1045
|
+
- `add_text`, `add_tab`, `add_break`, `add_picture`, `add_field`,
|
|
1046
|
+
`add_footnote`, `add_endnote`, `add_math` and `add_cross_reference` on
|
|
1047
|
+
any of a field's runs raise `FieldRunWriteError`; python-docx appends to
|
|
1048
|
+
that `w:r`.
|
|
1049
|
+
A field that `Run.add_field` adds is a complex field, as Word's Insert
|
|
1050
|
+
Field writes one, so its paragraph reads its runs, as python-docx reads the
|
|
1051
|
+
exported file. Against a docx-studio that predates `fieldRun` reads, a
|
|
1052
|
+
field reads as no runs and no text, as before (`tests/test_field_runs.py`).
|
|
1053
|
+
|
|
1054
|
+
- **`Font.name = "…"` also clears the theme fonts of the two slots it
|
|
1055
|
+
sets.** python-docx writes the name to `w:rFonts`' `w:ascii` and `w:hAnsi`
|
|
1056
|
+
and leaves a `w:asciiTheme` / `w:hAnsiTheme` beside them; Word reads a
|
|
1057
|
+
theme font over the name in its slot (ISO/IEC 29500-1 §17.3.2.26), so
|
|
1058
|
+
after python-docx's write Word still shows the theme font. The SDK sets the
|
|
1059
|
+
same two slots and docx-studio drops their theme fonts with them, so Word
|
|
1060
|
+
shows the font that was set. The run's other slots (`w:eastAsia`, `w:cs`
|
|
1061
|
+
and their theme fonts) and its `w:hint` stay as they were, as in
|
|
1062
|
+
python-docx. The export writes exactly the slots the run's fonts hold
|
|
1063
|
+
(#42248). Recorded in `tests/parity/intentional_deviations.json`
|
|
1064
|
+
(`_comment_behavioral_deviations`).
|
|
1065
|
+
|
|
971
1066
|
### If you need a deviation
|
|
972
1067
|
|
|
973
1068
|
If there is a genuine technical reason why a deviation from python-docx is necessary:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.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>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Sync→async bridge. Runs a single event loop in a background thread
|
|
2
|
+
and dispatches coroutines from sync callers.
|
|
3
|
+
|
|
4
|
+
This is the only place in the SDK that uses threading.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import concurrent.futures
|
|
11
|
+
import contextvars
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Any, Coroutine, TypeVar
|
|
14
|
+
|
|
15
|
+
from docx._timeouts import DEFAULT_OP_TIMEOUT_SECONDS
|
|
16
|
+
from docx.errors import OperationTimeoutError
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
_loop: asyncio.AbstractEventLoop | None = None
|
|
21
|
+
_thread: threading.Thread | None = None
|
|
22
|
+
_lock = threading.Lock()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Progress:
|
|
26
|
+
"""What the thread waiting on a call (:func:`run_sync`) and the call share:
|
|
27
|
+
the steps the call has finished, and whether the waiter gave up."""
|
|
28
|
+
|
|
29
|
+
__slots__ = ("abandoned", "steps")
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self.steps = 0
|
|
33
|
+
self.abandoned = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_progress: contextvars.ContextVar[_Progress | None] = contextvars.ContextVar(
|
|
37
|
+
"docx_sdk_call_progress", default=None
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def note_progress() -> None:
|
|
42
|
+
"""A step of the running call finished: one batch of a flush that goes
|
|
43
|
+
out in several (``HttpClient.execute_batch``). :func:`run_sync` gives the
|
|
44
|
+
call its ``timeout`` again, so each batch has the budget its request has."""
|
|
45
|
+
progress = _progress.get()
|
|
46
|
+
if progress is not None:
|
|
47
|
+
progress.steps += 1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def call_abandoned() -> bool:
|
|
51
|
+
"""True once the caller of the running call timed out
|
|
52
|
+
(:class:`OperationTimeoutError`): a call of several steps starts no more."""
|
|
53
|
+
progress = _progress.get()
|
|
54
|
+
return progress is not None and progress.abandoned
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ensure_loop() -> asyncio.AbstractEventLoop:
|
|
58
|
+
"""Return the persistent background event loop. Starts it lazily."""
|
|
59
|
+
global _loop, _thread
|
|
60
|
+
|
|
61
|
+
with _lock:
|
|
62
|
+
if _loop is not None and _loop.is_running():
|
|
63
|
+
return _loop
|
|
64
|
+
|
|
65
|
+
loop = asyncio.new_event_loop()
|
|
66
|
+
loop_ready = threading.Event()
|
|
67
|
+
|
|
68
|
+
def _run() -> None:
|
|
69
|
+
asyncio.set_event_loop(loop)
|
|
70
|
+
loop_ready.set()
|
|
71
|
+
loop.run_forever()
|
|
72
|
+
|
|
73
|
+
thread = threading.Thread(
|
|
74
|
+
target=_run,
|
|
75
|
+
name="docx-sdk-loop",
|
|
76
|
+
daemon=True,
|
|
77
|
+
)
|
|
78
|
+
thread.start()
|
|
79
|
+
loop_ready.wait(timeout=5)
|
|
80
|
+
_loop = loop
|
|
81
|
+
_thread = thread
|
|
82
|
+
return loop
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def run_sync(
|
|
86
|
+
coro: Coroutine[Any, Any, T],
|
|
87
|
+
*,
|
|
88
|
+
timeout: float | None = DEFAULT_OP_TIMEOUT_SECONDS,
|
|
89
|
+
) -> T:
|
|
90
|
+
"""Run a coroutine on the background loop and block until it returns.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
coro: The coroutine to execute.
|
|
94
|
+
timeout: Maximum seconds to wait, counted again whenever the call
|
|
95
|
+
finishes a step (:func:`note_progress`). ``None`` blocks
|
|
96
|
+
indefinitely (use sparingly — prefer the default so stalls
|
|
97
|
+
surface early).
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
OperationTimeoutError: if the coroutine neither completes nor
|
|
101
|
+
finishes a step within `timeout` seconds. The call then starts
|
|
102
|
+
no further step (:func:`call_abandoned`).
|
|
103
|
+
The coroutine's exception, re-raised in the calling thread.
|
|
104
|
+
"""
|
|
105
|
+
loop = _ensure_loop()
|
|
106
|
+
progress = _Progress()
|
|
107
|
+
# The call runs in a copy of this thread's context (call_soon_threadsafe
|
|
108
|
+
# and the task each copy the current one), so it finds `progress`.
|
|
109
|
+
token = _progress.set(progress)
|
|
110
|
+
try:
|
|
111
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
112
|
+
finally:
|
|
113
|
+
_progress.reset(token)
|
|
114
|
+
seen = 0
|
|
115
|
+
while True:
|
|
116
|
+
try:
|
|
117
|
+
return future.result(timeout=timeout)
|
|
118
|
+
except concurrent.futures.TimeoutError as exc:
|
|
119
|
+
if progress.steps > seen:
|
|
120
|
+
seen = progress.steps
|
|
121
|
+
continue
|
|
122
|
+
progress.abandoned = True
|
|
123
|
+
future.cancel()
|
|
124
|
+
raise OperationTimeoutError(timeout_seconds=timeout) from exc
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def shutdown() -> None:
|
|
128
|
+
"""Stop the background event loop. For test teardown."""
|
|
129
|
+
global _loop, _thread
|
|
130
|
+
with _lock:
|
|
131
|
+
if _loop is None:
|
|
132
|
+
return
|
|
133
|
+
_loop.call_soon_threadsafe(_loop.stop)
|
|
134
|
+
if _thread is not None:
|
|
135
|
+
_thread.join(timeout=5)
|
|
136
|
+
_loop = None
|
|
137
|
+
_thread = None
|
|
@@ -28,8 +28,11 @@ alive.
|
|
|
28
28
|
|
|
29
29
|
from __future__ import annotations
|
|
30
30
|
|
|
31
|
+
import copy
|
|
32
|
+
import json
|
|
31
33
|
import sys
|
|
32
34
|
import threading
|
|
35
|
+
import time
|
|
33
36
|
import weakref
|
|
34
37
|
from typing import TYPE_CHECKING, Any
|
|
35
38
|
|
|
@@ -40,7 +43,7 @@ from docx._execution import (
|
|
|
40
43
|
is_stale_execution,
|
|
41
44
|
)
|
|
42
45
|
from docx._timeouts import timeout_for_commands
|
|
43
|
-
from docx.commands import Command, is_response_bearing, must_flush_immediately
|
|
46
|
+
from docx.commands import Command, GetNodeById, is_response_bearing, must_flush_immediately
|
|
44
47
|
from docx.errors import DocxError, DocxUnsupportedCommandError, FlushAllError
|
|
45
48
|
|
|
46
49
|
|
|
@@ -93,6 +96,24 @@ def _apply_proxy_id_rewrites(
|
|
|
93
96
|
pass
|
|
94
97
|
|
|
95
98
|
|
|
99
|
+
def _repeatable_read_key(cmd: Command) -> str | None:
|
|
100
|
+
"""The key a repeat of ``cmd`` answers from (:meth:`CommandBuffer.call`):
|
|
101
|
+
a ``GetNodeById``'s wire form; ``None`` for any other command."""
|
|
102
|
+
if isinstance(cmd, GetNodeById):
|
|
103
|
+
return json.dumps(cmd.to_dict(), sort_keys=True)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# How long a node read answers a repeat of it (:meth:`CommandBuffer.call`):
|
|
108
|
+
# long enough for one script statement's property reads of a paragraph or
|
|
109
|
+
# run, short enough that a collaborator's edit since reads.
|
|
110
|
+
READ_REUSE_SECONDS: float = 1.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _now() -> float:
|
|
114
|
+
return time.monotonic()
|
|
115
|
+
|
|
116
|
+
|
|
96
117
|
def _extract_partial_results(exc: BaseException) -> list[Any]:
|
|
97
118
|
"""Pull per-command ``result`` dicts out of a partial-failure error.
|
|
98
119
|
|
|
@@ -196,6 +217,17 @@ def _unregister(buffer: "CommandBuffer") -> None:
|
|
|
196
217
|
]
|
|
197
218
|
|
|
198
219
|
|
|
220
|
+
def _forget_reads_of_others(sender: "CommandBuffer") -> None:
|
|
221
|
+
"""Every other buffer of ``sender``'s document in this process sends its
|
|
222
|
+
next node read to the server: ``sender`` sent the document a batch, which
|
|
223
|
+
a read those buffers reuse (:meth:`CommandBuffer.call`) would not show."""
|
|
224
|
+
with _registry_lock:
|
|
225
|
+
buffers = [buffer for ref in _active_buffers if (buffer := ref()) is not None]
|
|
226
|
+
for buffer in buffers:
|
|
227
|
+
if buffer is not sender and buffer.asset_id == sender.asset_id:
|
|
228
|
+
buffer.forget_reads()
|
|
229
|
+
|
|
230
|
+
|
|
199
231
|
def flush_all(*, strict: bool = False) -> None:
|
|
200
232
|
"""Flush every live CommandBuffer in this process.
|
|
201
233
|
|
|
@@ -367,6 +399,9 @@ class CommandBuffer:
|
|
|
367
399
|
# ``add_heading`` / etc. when they queue a Create with a
|
|
368
400
|
# client-assigned id. Drained on every flush.
|
|
369
401
|
self._proxy_id_refs: dict[str, list[tuple[object, str]]] = {}
|
|
402
|
+
# The last command's ``GetNodeById`` key, result and time, while
|
|
403
|
+
# nothing has been sent or buffered after it (see :meth:`call`).
|
|
404
|
+
self._last_read: tuple[str, Any, float] | None = None
|
|
370
405
|
_register(self)
|
|
371
406
|
|
|
372
407
|
def register_proxy_id_ref(self, client_id: str, proxy: object, attr: str = "_node_id") -> None:
|
|
@@ -450,6 +485,12 @@ class CommandBuffer:
|
|
|
450
485
|
if email:
|
|
451
486
|
self._user["email"] = email
|
|
452
487
|
|
|
488
|
+
def forget_reads(self) -> None:
|
|
489
|
+
"""The next node read goes to the server, not to the read before it
|
|
490
|
+
(:meth:`call`): for a read that checks the document before a write."""
|
|
491
|
+
with self._lock:
|
|
492
|
+
self._last_read = None
|
|
493
|
+
|
|
453
494
|
def call(self, cmd: Command) -> Any:
|
|
454
495
|
"""Execute or buffer ``cmd``.
|
|
455
496
|
|
|
@@ -474,6 +515,24 @@ class CommandBuffer:
|
|
|
474
515
|
f"CommandBuffer for {self._asset_id} is closed",
|
|
475
516
|
)
|
|
476
517
|
|
|
518
|
+
# A read of the node the last command read, with nothing sent or
|
|
519
|
+
# buffered since and within READ_REUSE_SECONDS of it, answers from
|
|
520
|
+
# that read: python-docx reads several properties of one paragraph or
|
|
521
|
+
# run in a row, and each of the SDK's property getters reads the
|
|
522
|
+
# whole node. A read that guards a write calls forget_reads() first,
|
|
523
|
+
# and a batch another handle of the document sends clears the read
|
|
524
|
+
# (_forget_reads_of_others).
|
|
525
|
+
read_key = _repeatable_read_key(cmd)
|
|
526
|
+
with self._lock:
|
|
527
|
+
last_read = self._last_read if not self._pending else None
|
|
528
|
+
if (
|
|
529
|
+
read_key is not None
|
|
530
|
+
and last_read is not None
|
|
531
|
+
and last_read[0] == read_key
|
|
532
|
+
and _now() - last_read[2] <= READ_REUSE_SECONDS
|
|
533
|
+
):
|
|
534
|
+
return copy.deepcopy(last_read[1])
|
|
535
|
+
|
|
477
536
|
# PTC begin: emit only for commands in the allow-list. Other
|
|
478
537
|
# commands still flow through the buffer (and through the batched
|
|
479
538
|
# HTTP POST), they just don't produce a sub-tool-card. ``emit_end``
|
|
@@ -489,9 +548,17 @@ class CommandBuffer:
|
|
|
489
548
|
pass
|
|
490
549
|
|
|
491
550
|
if must_flush_immediately(cmd) and not self._has_client_id(cmd):
|
|
492
|
-
|
|
551
|
+
with self._lock:
|
|
552
|
+
self._last_read = None
|
|
553
|
+
result = self._eager_flush_with(cmd)
|
|
554
|
+
if read_key is not None:
|
|
555
|
+
with self._lock:
|
|
556
|
+
if not self._pending:
|
|
557
|
+
self._last_read = (read_key, copy.deepcopy(result), _now())
|
|
558
|
+
return result
|
|
493
559
|
|
|
494
560
|
with self._lock:
|
|
561
|
+
self._last_read = None
|
|
495
562
|
self._pending.append(cmd)
|
|
496
563
|
self._reset_timer_locked()
|
|
497
564
|
return None
|
|
@@ -577,6 +644,8 @@ class CommandBuffer:
|
|
|
577
644
|
except Exception:
|
|
578
645
|
_ptc_emit_end_batch(pending, is_error=True)
|
|
579
646
|
raise
|
|
647
|
+
finally:
|
|
648
|
+
_forget_reads_of_others(self)
|
|
580
649
|
_ptc_emit_end_batch(pending, is_error=False)
|
|
581
650
|
_apply_proxy_id_rewrites(results, proxy_id_refs)
|
|
582
651
|
return results
|
|
@@ -650,6 +719,10 @@ class CommandBuffer:
|
|
|
650
719
|
except Exception:
|
|
651
720
|
_ptc_emit_end_batch(all_cmds, is_error=True)
|
|
652
721
|
raise
|
|
722
|
+
finally:
|
|
723
|
+
# A batch of one node read changes nothing another buffer read.
|
|
724
|
+
if pending or _repeatable_read_key(cmd) is None:
|
|
725
|
+
_forget_reads_of_others(self)
|
|
653
726
|
_ptc_emit_end_batch(all_cmds, is_error=False)
|
|
654
727
|
# Proxy-id rewrites still run for the buffered prefix that DID apply —
|
|
655
728
|
# the trailing skip (if any) doesn't strand the supported Creates that
|
|
@@ -39,6 +39,7 @@ import requests
|
|
|
39
39
|
from requests.adapters import HTTPAdapter
|
|
40
40
|
from urllib3.util.retry import Retry
|
|
41
41
|
|
|
42
|
+
from docx._batching import call_abandoned, note_progress
|
|
42
43
|
from docx._buffer import CommandBuffer
|
|
43
44
|
from docx._timeouts import DEFAULT_OP_TIMEOUT_SECONDS, timeout_for_commands
|
|
44
45
|
from docx.commands import (
|
|
@@ -187,6 +188,39 @@ from docx.errors import (
|
|
|
187
188
|
)
|
|
188
189
|
from docx.exceptions import InvalidSpanError
|
|
189
190
|
|
|
191
|
+
# The most commands docx-studio takes in one batch request
|
|
192
|
+
# (``CommandsRequestSchema`` in ``apps/api/src/commands/types.ts``).
|
|
193
|
+
MAX_BATCH_COMMANDS: int = 500
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _minted_ids(results: list[Any]) -> dict[str, str]:
|
|
197
|
+
"""The client → real ids that the Creates among a batch's ``results`` echo."""
|
|
198
|
+
ids: dict[str, str] = {}
|
|
199
|
+
for result in results:
|
|
200
|
+
if not isinstance(result, dict):
|
|
201
|
+
continue
|
|
202
|
+
for client_key, real_key in (
|
|
203
|
+
("client_node_id", "real_node_id"),
|
|
204
|
+
("client_entity_id", "real_entity_id"),
|
|
205
|
+
):
|
|
206
|
+
client, real = result.get(client_key), result.get(real_key)
|
|
207
|
+
if isinstance(client, str) and isinstance(real, str) and client and real:
|
|
208
|
+
ids[client] = real
|
|
209
|
+
return ids
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _with_real_ids(value: Any, ids: dict[str, str]) -> Any:
|
|
213
|
+
"""A command's wire form with each value that is a client id in ``ids``
|
|
214
|
+
replaced by its real id. A client id is a random id the SDK mints, so no
|
|
215
|
+
other value equals one."""
|
|
216
|
+
if isinstance(value, str):
|
|
217
|
+
return ids.get(value, value)
|
|
218
|
+
if isinstance(value, list):
|
|
219
|
+
return [_with_real_ids(item, ids) for item in value]
|
|
220
|
+
if isinstance(value, dict):
|
|
221
|
+
return {key: _with_real_ids(item, ids) for key, item in value.items()}
|
|
222
|
+
return value
|
|
223
|
+
|
|
190
224
|
|
|
191
225
|
_NOT_FOUND_ERROR_NAMES: frozenset[str] = frozenset(
|
|
192
226
|
{
|
|
@@ -850,7 +884,83 @@ class HttpClient:
|
|
|
850
884
|
change_mode: str | None = None,
|
|
851
885
|
user: dict[str, str] | None = None,
|
|
852
886
|
) -> tuple[list[Any], dict[int, dict[str, Any]]]:
|
|
853
|
-
"""POST
|
|
887
|
+
"""POST ``commands`` in batches of at most :data:`MAX_BATCH_COMMANDS`,
|
|
888
|
+
the most docx-studio takes in one request (it answers HTTP 400 to
|
|
889
|
+
more), and return their results as one batch's
|
|
890
|
+
(:meth:`_execute_one_batch`): positionally aligned, skips by index.
|
|
891
|
+
|
|
892
|
+
A buffer that a loop filled with more commands than that (``for i in
|
|
893
|
+
range(1000): doc.add_paragraph(...)``) flushes as consecutive
|
|
894
|
+
batches. Each batch is atomic on the server; a batch that fails
|
|
895
|
+
leaves the batches before it applied, and its ``DocxError`` carries
|
|
896
|
+
their results in ``partial_applied`` (input indexes), so the proxies
|
|
897
|
+
of the Creates that landed still get their real ids.
|
|
898
|
+
|
|
899
|
+
A later batch addresses the nodes and comments an earlier one created
|
|
900
|
+
by the real ids that batch returned, not by the client ids the
|
|
901
|
+
commands were built with: docx-studio keeps an asset's client ids for
|
|
902
|
+
a while and evicts some past 4,096, and a flush can create more.
|
|
903
|
+
|
|
904
|
+
Each batch that lands gives the waiting caller its timeout again
|
|
905
|
+
(``run_sync``), so every batch has the budget its request has. Once
|
|
906
|
+
the caller has timed out, no further batch is sent: the
|
|
907
|
+
``DocxError`` names the commands that were not, and carries the
|
|
908
|
+
results of those that were.
|
|
909
|
+
"""
|
|
910
|
+
if len(commands) <= MAX_BATCH_COMMANDS:
|
|
911
|
+
return self._execute_one_batch(asset_id, commands, change_mode=change_mode, user=user)
|
|
912
|
+
results: list[Any] = []
|
|
913
|
+
skipped_by_index: dict[int, dict[str, Any]] = {}
|
|
914
|
+
applied: list[dict[str, Any]] = []
|
|
915
|
+
minted: dict[str, str] = {}
|
|
916
|
+
for start in range(0, len(commands), MAX_BATCH_COMMANDS):
|
|
917
|
+
chunk = commands[start : start + MAX_BATCH_COMMANDS]
|
|
918
|
+
if start and call_abandoned():
|
|
919
|
+
raise DocxError(
|
|
920
|
+
f"docx-studio batch not sent: the call timed out after the first {start} "
|
|
921
|
+
f"of {len(commands)} commands went out; the other "
|
|
922
|
+
f"{len(commands) - start} were not sent",
|
|
923
|
+
).with_partial_applied(applied)
|
|
924
|
+
try:
|
|
925
|
+
chunk_results, chunk_skipped = self._execute_one_batch(
|
|
926
|
+
asset_id, chunk, change_mode=change_mode, user=user, ids=minted
|
|
927
|
+
)
|
|
928
|
+
except DocxError as exc:
|
|
929
|
+
failed: object = getattr(exc, "partial_applied", None)
|
|
930
|
+
failed_applied = [
|
|
931
|
+
{**entry, "index": entry["index"] + start}
|
|
932
|
+
for entry in (failed if isinstance(failed, list) else [])
|
|
933
|
+
if isinstance(entry, dict) and isinstance(entry.get("index"), int)
|
|
934
|
+
]
|
|
935
|
+
raise exc.with_partial_applied(applied + failed_applied)
|
|
936
|
+
for offset, result in enumerate(chunk_results):
|
|
937
|
+
if result is not None:
|
|
938
|
+
command = chunk[offset]
|
|
939
|
+
applied.append(
|
|
940
|
+
{
|
|
941
|
+
"index": start + offset,
|
|
942
|
+
"type": getattr(command, "type", type(command).__name__),
|
|
943
|
+
"result": result,
|
|
944
|
+
}
|
|
945
|
+
)
|
|
946
|
+
results.extend(chunk_results)
|
|
947
|
+
for index, entry in chunk_skipped.items():
|
|
948
|
+
skipped_by_index[start + index] = {**entry, "index": start + index}
|
|
949
|
+
minted.update(_minted_ids(chunk_results))
|
|
950
|
+
note_progress()
|
|
951
|
+
return results, skipped_by_index
|
|
952
|
+
|
|
953
|
+
def _execute_one_batch(
|
|
954
|
+
self,
|
|
955
|
+
asset_id: str,
|
|
956
|
+
commands: list[Command],
|
|
957
|
+
*,
|
|
958
|
+
change_mode: str | None = None,
|
|
959
|
+
user: dict[str, str] | None = None,
|
|
960
|
+
ids: dict[str, str] | None = None,
|
|
961
|
+
) -> tuple[list[Any], dict[int, dict[str, Any]]]:
|
|
962
|
+
"""POST a batch of typed commands, each client id in ``ids`` sent as
|
|
963
|
+
the real id it maps to (:meth:`execute_batch`).
|
|
854
964
|
|
|
855
965
|
Returns ``(results, skipped_by_index)``:
|
|
856
966
|
|
|
@@ -897,7 +1007,9 @@ class HttpClient:
|
|
|
897
1007
|
url: str = f"{self._base_url}/docs/{asset_id}/commands"
|
|
898
1008
|
body: dict[str, Any] = {
|
|
899
1009
|
"client": {"type": "python-sdk", "version": _sdk_version()},
|
|
900
|
-
"commands": [
|
|
1010
|
+
"commands": [
|
|
1011
|
+
_with_real_ids(c.to_dict(), ids) if ids else c.to_dict() for c in commands
|
|
1012
|
+
],
|
|
901
1013
|
"return": {"snapshot": False},
|
|
902
1014
|
}
|
|
903
1015
|
if change_mode is not None:
|
|
@@ -115,6 +115,13 @@ class Session:
|
|
|
115
115
|
return float(timeout)
|
|
116
116
|
return DEFAULT_OP_TIMEOUT_SECONDS
|
|
117
117
|
|
|
118
|
+
def forget_reads(self) -> None:
|
|
119
|
+
"""The next node read goes to the server (``CommandBuffer.forget_reads``)."""
|
|
120
|
+
handle = self._doc_handle
|
|
121
|
+
forget = getattr(getattr(handle, "buffer", None), "forget_reads", None)
|
|
122
|
+
if callable(forget):
|
|
123
|
+
forget()
|
|
124
|
+
|
|
118
125
|
async def open(self) -> None:
|
|
119
126
|
"""Construct the HTTP client + buffered doc handle.
|
|
120
127
|
|
|
@@ -129,6 +129,16 @@ class NotNumberedListItemError(DocxError, ValueError):
|
|
|
129
129
|
"""
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
class FieldRunWriteError(DocxError, ValueError):
|
|
133
|
+
"""Raised for a write to a run of a Word field that the SDK refuses:
|
|
134
|
+
text written to a field character or field code run (python-docx's write
|
|
135
|
+
replaces the run's ``w:fldChar`` or ``w:instrText`` and breaks the field),
|
|
136
|
+
and content appended to any of a field's runs. The message names the
|
|
137
|
+
field. Subclasses :class:`ValueError` so ``except ValueError`` also
|
|
138
|
+
catches it.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
|
|
132
142
|
class SessionError(DocxError):
|
|
133
143
|
"""Raised when the DOCX Studio SDK session cannot be established or used."""
|
|
134
144
|
|
|
@@ -122,15 +122,15 @@ class Hyperlink:
|
|
|
122
122
|
Mirrors python-docx 1.x: a hyperlink's ``text`` is the visible
|
|
123
123
|
anchor text, which for a multi-run hyperlink is the
|
|
124
124
|
concatenation of every child run's text, each read as
|
|
125
|
-
:attr:`Run.text` reads it
|
|
126
|
-
Word's final view shows the link.
|
|
125
|
+
:attr:`Run.text` reads it (a note reference as none); a tracked
|
|
126
|
+
deletion's run is left out, as Word's final view shows the link.
|
|
127
127
|
"""
|
|
128
|
-
from docx.text.paragraph import _deleted, _symbol_font, _word_text
|
|
128
|
+
from docx.text.paragraph import _deleted, _note_reference, _symbol_font, _word_text
|
|
129
129
|
|
|
130
130
|
parts: list[str] = []
|
|
131
131
|
for run_data, _ in self._segments:
|
|
132
132
|
t = run_data.get("text")
|
|
133
|
-
if isinstance(t, str) and not _deleted(run_data):
|
|
133
|
+
if isinstance(t, str) and not _deleted(run_data) and not _note_reference(run_data):
|
|
134
134
|
parts.append(_word_text(t, _symbol_font(run_data)))
|
|
135
135
|
return "".join(parts)
|
|
136
136
|
|