athena-python-docx 0.27.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.27.0 → athena_python_docx-0.28.0}/.gitignore +2 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/AGENTS.md +65 -13
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/PKG-INFO +1 -1
- {athena_python_docx-0.27.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.27.0 → athena_python_docx-0.28.0}/docx/_buffer.py +75 -2
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_http_doc.py +114 -2
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/client.py +7 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/text/hyperlink.py +4 -4
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/text/paragraph.py +12 -4
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/text/run.py +15 -3
- {athena_python_docx-0.27.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.27.0 → athena_python_docx-0.28.0}/tests/fidelity/METHODOLOGY.md +49 -1
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fixtures/field_reads.json +160 -1
- 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.27.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.27.0 → athena_python_docx-0.28.0}/tests/test_field_reads_python_docx_parity.py +6 -1
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_field_runs.py +16 -3
- 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.28.0/tests/test_round_trip_smoke_script.py +115 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/uv.lock +1 -1
- athena_python_docx-0.27.0/docx/_batching.py +0 -88
- athena_python_docx-0.27.0/scripts/round_trip_smoke.py +0 -231
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/CLAUDE.md +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/DOCX_EXEC_LAB.md +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/README.md +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_athena_extension.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_citations.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_execution.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_http.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_image_utils.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_postproc.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_ptc.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_references.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_run_order.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_table_styles.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/_timeouts.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/api.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/blkcntnr.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/bookmarks.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/charts.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/commands.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/comments.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/document.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/drawing/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/enum/base.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/enum/dml.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/enum/section.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/enum/shape.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/enum/style.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/enum/table.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/enum/text.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/errors.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/exceptions.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/fields.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/footnotes.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/image/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/image/exceptions.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/image/image.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/math.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/opc/coreprops.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/oxml/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/revisions.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/sdt.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/section.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/session.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/settings.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/shape.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/shared.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/styles/_table.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/styles/latent.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/styles/style.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/styles/styles.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/table.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/text/font.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/text/pagebreak.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/text/parfmt.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/text/tabstops.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/toc.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/docx/typing.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_examples/fast_table_fill.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_examples/find_replace_literal.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/scripts/dump_wire_fixtures.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/scripts/release.sh +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/scripts/smoke_test_block_not_found.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/scripts/validate_find_replace_asset.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/conftest.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/ab_probe_cases.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/ab_probe_runner.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/binary_round_trip.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/fake_session.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/README.md +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/_runner.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/extractor.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/local_runner.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshot.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.27.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.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/ours_spec.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/parity_diff.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/real_world_cases.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/stock_spec.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/README.md +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/__init__.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/baseline_gaps.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/compare.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/introspect.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/reports/GAP_ANALYSIS.md +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/reports/gap_report.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/run_parity.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/snapshots/athena_latest.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/parity/test_parity_gap.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_add_section.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_add_section_extract_items.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_athena_extension_creates_session_open.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_athena_extensions_contract.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_athena_extensions_registry.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_batching_perf.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_block_not_found_error.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_buffer.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_add_table_not_supported.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_borders_spec.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_fastpath_runs.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_add_hyperlink_stash.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_add_run_via_cell_insert.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_format_stash.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_run_format_stash.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_run_guard.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_merge.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_paragraphs_listed.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_text_plain_fastpath.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_cell_text_replace_semantics.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_citations.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_collapsed_range_format.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_color_format_theme.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_command_dataclasses.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_commands.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_comments.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_core_properties.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_document_asset_id_property.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_document_clear.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_document_create.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_document_create_from_template.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_document_factory_validation.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_docx_exec_lab.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_drawing_image.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_e2e_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_enum_parity.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_execution_scope.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_find_replace_session_open.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_http_transport.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_hyperlink_coalescing.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_image_url_data_uri.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_inline_shape_alt_text.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_inline_shape_size.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_insert_deferred.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_iter_inner_content.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_list_styles.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_merged_cell_secondary_slot.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_merged_cells.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_missing_modules.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_oxml_shim.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_page_break.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_paragraph_runs_hyperlinks.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_paragraph_text_len_cache.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_parity_misc.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_parity_round2.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_pdf_export_and_page_renders.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_phase_a_behavior.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_phase_b_headers_footers.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_phase_c_tables.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_positional_cell_id.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_postproc_cell_format_rewrite.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_postproc_cell_run_format_rewrite.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_postproc_ref_restore.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_pr19766_review_fixes.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_ptc.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_restart_numbering.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_revisions.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_run_add_picture_anchor.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_run_formatting_wire.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_run_identity.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_run_mark_inheritance.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_run_table_style_objects.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_run_text_python_docx.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_run_text_refreshes_paragraph.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_sdk_reads_served.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_section_read_lengths.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_picture.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_run.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_comments_get.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_document_audit.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_document_element.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_enum_section.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_font_audit.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_header_footer.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_hyperlink.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_inline_shape.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_misc.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_style.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_parfmt.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_row_col_cell.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_add_break.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_style.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_style_strict.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_text.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_underline.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_audit.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_dimensions.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_onoff.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_settings.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_shared_audit.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_style.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_styles.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_audit.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_cell.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_dimensions.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_layout.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_smoke_integration.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_style_acceptance.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_style_font.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_style_formatting_writes.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_style_metadata_writes.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_style_names.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_style_setters_contract.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_table_set_cell_perf.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_table_style_id_resolution.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_temporarily_unavailable.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_text_control_characters.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_uv_lock_version.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_validate_find_replace_asset_script.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_wire_contract.py +0 -0
- {athena_python_docx-0.27.0 → athena_python_docx-0.28.0}/tests/test_xml_attr_guard.py +0 -0
- {athena_python_docx-0.27.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:
|
|
@@ -291,7 +298,9 @@ Issue numbers reference `python-openxml/python-docx`.
|
|
|
291
298
|
`FootnotesGet` / `FootnotesPatch` / `FootnotesDelete`. The
|
|
292
299
|
parallel **`Document.endnotes: Endnotes`** + **`Run.add_endnote
|
|
293
300
|
(text, *, custom_mark=None)`** mirrors the same machinery for
|
|
294
|
-
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.
|
|
295
304
|
|
|
296
305
|
- **`Document.add_toc(*, levels=(1, 3), style_id=None,
|
|
297
306
|
include_hyperlinks=True, title=None, tab_leader="dot", at=None)
|
|
@@ -418,6 +427,8 @@ Issue numbers reference `python-openxml/python-docx`.
|
|
|
418
427
|
inspection. Use this when an agent needs to sample or inspect runs
|
|
419
428
|
without issuing one ``getNodeById`` query per paragraph through
|
|
420
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.
|
|
421
432
|
|
|
422
433
|
- **`Paragraph.list_id` / `Paragraph.list_level` /
|
|
423
434
|
`Paragraph.list_format` / `Paragraph.list_number`** — list-item
|
|
@@ -961,6 +972,13 @@ architecture or a DOCX Studio constraint:
|
|
|
961
972
|
sends a column break (`breakType: "column"`), which docx-studio writes after
|
|
962
973
|
the paragraph's content. `DOCX_STUDIO_LIMITATIONS.md` §26.
|
|
963
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
|
+
|
|
964
982
|
- **Tracked changes read as Word's final view.** `Paragraph.text`, `runs`,
|
|
965
983
|
`iter_inner_content()`, `Hyperlink.text` / `runs` and `_Cell.text` include
|
|
966
984
|
the runs of a tracked insertion (`w:ins`) and leave out a deletion's
|
|
@@ -973,23 +991,45 @@ architecture or a DOCX Studio constraint:
|
|
|
973
991
|
Markup) show insertions and hide deletions, as these reads do.
|
|
974
992
|
`doc.revisions` lists every change (`tests/test_run_text_python_docx.py`).
|
|
975
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
|
+
|
|
976
1010
|
- **A field's runs read as python-docx reads them; some writes to them
|
|
977
1011
|
differ.** `Paragraph.text` includes a complex field's displayed result and
|
|
978
1012
|
`Paragraph.runs` holds its begin, field code, separate, result and end runs
|
|
979
1013
|
(each with its own formatting), as python-docx reads the `w:r` children of
|
|
980
|
-
a `w:p`; a simple field (`w:fldSimple`) reads as nothing
|
|
981
|
-
|
|
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
|
|
982
1018
|
paragraphs (a table of contents) holds the field character runs it has
|
|
983
1019
|
(`tests/test_field_reads_python_docx_parity.py`, against stock python-docx
|
|
984
1020
|
on the same `.docx`; `DOCX_STUDIO_LIMITATIONS.md` § 27). A field takes no
|
|
985
1021
|
room in the text-address space, so its runs stand at one offset. Writes:
|
|
986
1022
|
- setting a result run's `.text` changes the field's displayed result,
|
|
987
|
-
as in python-docx (`Replace` with a `field` target)
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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;
|
|
993
1033
|
- formatting a result run formats the field's whole result (`FormatApply`
|
|
994
1034
|
with a `field` target), from its source formatting, where python-docx
|
|
995
1035
|
formats that run alone;
|
|
@@ -1006,10 +1046,22 @@ architecture or a DOCX Studio constraint:
|
|
|
1006
1046
|
`add_footnote`, `add_endnote`, `add_math` and `add_cross_reference` on
|
|
1007
1047
|
any of a field's runs raise `FieldRunWriteError`; python-docx appends to
|
|
1008
1048
|
that `w:r`.
|
|
1009
|
-
A field that `Run.add_field` adds is a
|
|
1010
|
-
|
|
1011
|
-
Against a docx-studio that predates `fieldRun` reads, a
|
|
1012
|
-
runs and no text, as before (`tests/test_field_runs.py`).
|
|
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`).
|
|
1013
1065
|
|
|
1014
1066
|
### If you need a deviation
|
|
1015
1067
|
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -165,6 +165,13 @@ def _deleted(run: dict) -> bool:
|
|
|
165
165
|
return run.get("trackedChange") == "deletion"
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def _note_reference(run: dict) -> bool:
|
|
169
|
+
"""True for a run of a footnote or endnote reference (``noteReference``),
|
|
170
|
+
which python-docx's ``Run.text`` reads as ``""``: the reference is no
|
|
171
|
+
``w:t``. Its number still takes its room in the text-address space."""
|
|
172
|
+
return run.get("noteReference") in ("footnote", "endnote")
|
|
173
|
+
|
|
174
|
+
|
|
168
175
|
def _node_text(session: "Session", node_id: str, *, final: bool = False) -> str:
|
|
169
176
|
"""Fetch a block's full text via doc.get_node_by_id (see
|
|
170
177
|
:func:`_extract_text`).
|
|
@@ -200,9 +207,10 @@ def _extract_text(info: object, *, final: bool = False) -> str:
|
|
|
200
207
|
runs left out and an insertion's read (python-docx leaves out both, as
|
|
201
208
|
it reads only a ``w:p``'s direct ``w:r`` and ``w:hyperlink`` children),
|
|
202
209
|
each run's text read as python-docx's ``Run.text`` reads it
|
|
203
|
-
(:func:`_word_text
|
|
204
|
-
|
|
205
|
-
text
|
|
210
|
+
(:func:`_word_text`; a note reference's run as none, though its number
|
|
211
|
+
takes room in the text-address space), and a complex field's displayed
|
|
212
|
+
result (the text of its result runs, never its field code), which takes no
|
|
213
|
+
room in the text-address space.
|
|
206
214
|
"""
|
|
207
215
|
if not isinstance(info, dict):
|
|
208
216
|
return ""
|
|
@@ -231,7 +239,7 @@ def _extract_text(info: object, *, final: bool = False) -> str:
|
|
|
231
239
|
if not isinstance(text, str):
|
|
232
240
|
continue
|
|
233
241
|
if final:
|
|
234
|
-
if _deleted(run):
|
|
242
|
+
if _deleted(run) or _note_reference(run):
|
|
235
243
|
continue
|
|
236
244
|
text = _word_text(text, _symbol_font(run))
|
|
237
245
|
parts.append(text)
|