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.
Files changed (423) hide show
  1. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/.gitignore +2 -0
  2. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/AGENTS.md +96 -1
  3. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/PKG-INFO +1 -1
  4. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/__init__.py +1 -1
  5. athena_python_docx-0.28.0/docx/_batching.py +137 -0
  6. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_buffer.py +75 -2
  7. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_http_doc.py +114 -2
  8. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/client.py +7 -0
  9. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/errors.py +10 -0
  10. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/hyperlink.py +4 -4
  11. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/paragraph.py +103 -11
  12. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/run.py +157 -4
  13. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/pyproject.toml +1 -1
  14. athena_python_docx-0.28.0/scripts/round_trip_smoke.py +426 -0
  15. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/METHODOLOGY.md +49 -1
  16. athena_python_docx-0.28.0/tests/fixtures/field_reads.json +1022 -0
  17. athena_python_docx-0.28.0/tests/fixtures/note_reference_reads.json +193 -0
  18. athena_python_docx-0.28.0/tests/live/__init__.py +0 -0
  19. athena_python_docx-0.28.0/tests/live/baseline.json +1925 -0
  20. athena_python_docx-0.28.0/tests/live/reader.py +231 -0
  21. athena_python_docx-0.28.0/tests/live/runner.py +700 -0
  22. athena_python_docx-0.28.0/tests/live/sdk_driver.py +56 -0
  23. athena_python_docx-0.28.0/tests/live/stock_driver.py +65 -0
  24. athena_python_docx-0.28.0/tests/live/test_live_runner.py +411 -0
  25. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/intentional_deviations.json +39 -0
  26. athena_python_docx-0.28.0/tests/test_batch_chunking.py +213 -0
  27. athena_python_docx-0.28.0/tests/test_field_reads_python_docx_parity.py +110 -0
  28. athena_python_docx-0.28.0/tests/test_field_runs.py +399 -0
  29. athena_python_docx-0.28.0/tests/test_note_reference_reads_python_docx_parity.py +236 -0
  30. athena_python_docx-0.28.0/tests/test_repeated_node_read.py +119 -0
  31. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_revisions.py +38 -0
  32. athena_python_docx-0.28.0/tests/test_round_trip_smoke_script.py +115 -0
  33. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/uv.lock +1 -1
  34. athena_python_docx-0.26.0/docx/_batching.py +0 -88
  35. athena_python_docx-0.26.0/scripts/round_trip_smoke.py +0 -231
  36. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/CLAUDE.md +0 -0
  37. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/DOCX_EXEC_LAB.md +0 -0
  38. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/README.md +0 -0
  39. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_athena_extension.py +0 -0
  40. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_citations.py +0 -0
  41. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_execution.py +0 -0
  42. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_http.py +0 -0
  43. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_image_utils.py +0 -0
  44. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_postproc.py +0 -0
  45. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_ptc.py +0 -0
  46. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_references.py +0 -0
  47. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_run_order.py +0 -0
  48. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_table_styles.py +0 -0
  49. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/_timeouts.py +0 -0
  50. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/api.py +0 -0
  51. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/blkcntnr.py +0 -0
  52. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/bookmarks.py +0 -0
  53. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/charts.py +0 -0
  54. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/commands.py +0 -0
  55. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/comments.py +0 -0
  56. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/document.py +0 -0
  57. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/drawing/__init__.py +0 -0
  58. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/__init__.py +0 -0
  59. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/base.py +0 -0
  60. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/dml.py +0 -0
  61. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/section.py +0 -0
  62. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/shape.py +0 -0
  63. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/style.py +0 -0
  64. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/table.py +0 -0
  65. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/enum/text.py +0 -0
  66. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/exceptions.py +0 -0
  67. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/fields.py +0 -0
  68. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/footnotes.py +0 -0
  69. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/image/__init__.py +0 -0
  70. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/image/exceptions.py +0 -0
  71. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/image/image.py +0 -0
  72. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/math.py +0 -0
  73. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/opc/__init__.py +0 -0
  74. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/opc/coreprops.py +0 -0
  75. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/oxml/__init__.py +0 -0
  76. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/revisions.py +0 -0
  77. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/sdt.py +0 -0
  78. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/section.py +0 -0
  79. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/session.py +0 -0
  80. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/settings.py +0 -0
  81. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/shape.py +0 -0
  82. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/shared.py +0 -0
  83. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/__init__.py +0 -0
  84. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/_table.py +0 -0
  85. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/latent.py +0 -0
  86. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/style.py +0 -0
  87. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/styles/styles.py +0 -0
  88. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/table.py +0 -0
  89. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/__init__.py +0 -0
  90. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/font.py +0 -0
  91. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/pagebreak.py +0 -0
  92. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/parfmt.py +0 -0
  93. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/text/tabstops.py +0 -0
  94. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/toc.py +0 -0
  95. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/docx/typing.py +0 -0
  96. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab.py +0 -0
  97. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_examples/fast_table_fill.py +0 -0
  98. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_examples/find_replace_literal.py +0 -0
  99. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/docx_exec_lab_server.py +0 -0
  100. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/dump_wire_fixtures.py +0 -0
  101. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/release.sh +0 -0
  102. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/smoke_test_block_not_found.py +0 -0
  103. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/scripts/validate_find_replace_asset.py +0 -0
  104. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/__init__.py +0 -0
  105. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/conftest.py +0 -0
  106. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/README.md +0 -0
  107. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/__init__.py +0 -0
  108. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/ab_probe_cases.py +0 -0
  109. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/ab_probe_runner.py +0 -0
  110. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/auto_gen_cases.py +0 -0
  111. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/binary_round_trip.py +0 -0
  112. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/cases.py +0 -0
  113. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/complex_cases.py +0 -0
  114. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/coverage_report.py +0 -0
  115. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/extract.py +0 -0
  116. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/extreme_cases.py +0 -0
  117. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/fake_session.py +0 -0
  118. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/README.md +0 -0
  119. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/__init__.py +0 -0
  120. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/_runner.py +0 -0
  121. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/extractor.py +0 -0
  122. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
  123. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
  124. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/local_runner.py +0 -0
  125. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/mega_cases.py +0 -0
  126. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshot.py +0 -0
  127. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
  128. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
  129. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
  130. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
  131. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
  132. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
  133. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
  134. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
  135. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
  136. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
  137. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
  138. {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
  139. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
  140. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
  141. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
  142. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
  143. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
  144. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
  145. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
  146. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
  147. {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
  148. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
  149. {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
  150. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
  151. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
  152. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
  153. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
  154. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
  155. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
  156. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
  157. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
  158. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
  159. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
  160. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
  161. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
  162. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
  163. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
  164. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
  165. {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
  166. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
  167. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
  168. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
  169. {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
  170. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
  171. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
  172. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
  173. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
  174. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
  175. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
  176. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
  177. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
  178. {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
  179. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
  180. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
  181. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
  182. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
  183. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
  184. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
  185. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
  186. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
  187. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
  188. {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
  189. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
  190. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
  191. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
  192. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
  193. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
  194. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
  195. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
  196. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
  197. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
  198. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
  199. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
  200. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
  201. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
  202. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
  203. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
  204. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
  205. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
  206. {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
  207. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
  208. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
  209. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
  210. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
  211. {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
  212. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
  213. {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
  214. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
  215. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
  216. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
  217. {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
  218. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
  219. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
  220. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
  221. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
  222. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
  223. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
  224. {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
  225. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
  226. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
  227. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
  228. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
  229. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
  230. {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
  231. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
  232. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
  233. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
  234. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
  235. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
  236. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
  237. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
  238. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
  239. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
  240. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
  241. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
  242. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
  243. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
  244. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
  245. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
  246. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
  247. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
  248. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
  249. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
  250. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
  251. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
  252. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
  253. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
  254. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
  255. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
  256. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
  257. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
  258. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
  259. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
  260. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
  261. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
  262. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
  263. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
  264. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
  265. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
  266. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
  267. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
  268. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
  269. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
  270. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
  271. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
  272. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
  273. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
  274. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/ours_spec.json +0 -0
  275. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/parity_crawl.py +0 -0
  276. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/parity_diff.json +0 -0
  277. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/real_world_cases.py +0 -0
  278. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/round_trip_tests.py +0 -0
  279. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/stock_spec.json +0 -0
  280. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
  281. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/README.md +0 -0
  282. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/__init__.py +0 -0
  283. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/baseline_gaps.json +0 -0
  284. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/compare.py +0 -0
  285. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/introspect.py +0 -0
  286. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/reports/GAP_ANALYSIS.md +0 -0
  287. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/reports/gap_report.json +0 -0
  288. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/run_parity.py +0 -0
  289. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/snapshots/athena_latest.json +0 -0
  290. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
  291. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/parity/test_parity_gap.py +0 -0
  292. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_add_section.py +0 -0
  293. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_add_section_extract_items.py +0 -0
  294. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_athena_extension_creates_session_open.py +0 -0
  295. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_athena_extensions_contract.py +0 -0
  296. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_athena_extensions_registry.py +0 -0
  297. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_batching_perf.py +0 -0
  298. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_block_not_found_error.py +0 -0
  299. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_buffer.py +0 -0
  300. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
  301. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_add_table_not_supported.py +0 -0
  302. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_borders_spec.py +0 -0
  303. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_fastpath_runs.py +0 -0
  304. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_add_hyperlink_stash.py +0 -0
  305. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_add_run_via_cell_insert.py +0 -0
  306. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_format_stash.py +0 -0
  307. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_run_format_stash.py +0 -0
  308. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_inner_run_guard.py +0 -0
  309. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_merge.py +0 -0
  310. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_paragraphs_listed.py +0 -0
  311. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_text_plain_fastpath.py +0 -0
  312. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_cell_text_replace_semantics.py +0 -0
  313. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_citations.py +0 -0
  314. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_collapsed_range_format.py +0 -0
  315. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_color_format_theme.py +0 -0
  316. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_command_dataclasses.py +0 -0
  317. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_commands.py +0 -0
  318. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_comments.py +0 -0
  319. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_core_properties.py +0 -0
  320. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_asset_id_property.py +0 -0
  321. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_clear.py +0 -0
  322. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_create.py +0 -0
  323. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_create_from_template.py +0 -0
  324. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_document_factory_validation.py +0 -0
  325. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_docx_exec_lab.py +0 -0
  326. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_docx_exec_lab_server.py +0 -0
  327. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_drawing_image.py +0 -0
  328. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_e2e_partial_failure_cascade.py +0 -0
  329. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_enum_parity.py +0 -0
  330. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_execution_scope.py +0 -0
  331. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_find_replace_session_open.py +0 -0
  332. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_http_transport.py +0 -0
  333. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_hyperlink_coalescing.py +0 -0
  334. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_image_url_data_uri.py +0 -0
  335. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_inline_shape_alt_text.py +0 -0
  336. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_inline_shape_size.py +0 -0
  337. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_insert_deferred.py +0 -0
  338. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_iter_inner_content.py +0 -0
  339. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_list_styles.py +0 -0
  340. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_merged_cell_secondary_slot.py +0 -0
  341. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_merged_cells.py +0 -0
  342. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_missing_modules.py +0 -0
  343. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_oxml_shim.py +0 -0
  344. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_page_break.py +0 -0
  345. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_paragraph_runs_hyperlinks.py +0 -0
  346. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_paragraph_text_len_cache.py +0 -0
  347. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_parity_misc.py +0 -0
  348. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_parity_round2.py +0 -0
  349. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_partial_failure_cascade.py +0 -0
  350. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_pdf_export_and_page_renders.py +0 -0
  351. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_phase_a_behavior.py +0 -0
  352. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_phase_b_headers_footers.py +0 -0
  353. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_phase_c_tables.py +0 -0
  354. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_positional_cell_id.py +0 -0
  355. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_postproc_cell_format_rewrite.py +0 -0
  356. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_postproc_cell_run_format_rewrite.py +0 -0
  357. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_postproc_ref_restore.py +0 -0
  358. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_pr19766_review_fixes.py +0 -0
  359. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_ptc.py +0 -0
  360. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_python_docx_api_parity.py +0 -0
  361. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_restart_numbering.py +0 -0
  362. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_add_picture_anchor.py +0 -0
  363. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_formatting_wire.py +0 -0
  364. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_identity.py +0 -0
  365. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_mark_inheritance.py +0 -0
  366. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_table_style_objects.py +0 -0
  367. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_text_python_docx.py +0 -0
  368. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_run_text_refreshes_paragraph.py +0 -0
  369. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_sdk_reads_served.py +0 -0
  370. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_section_read_lengths.py +0 -0
  371. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
  372. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_picture.py +0 -0
  373. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_add_run.py +0 -0
  374. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
  375. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
  376. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_comments_get.py +0 -0
  377. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_document_audit.py +0 -0
  378. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_document_element.py +0 -0
  379. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_enum_section.py +0 -0
  380. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_font_audit.py +0 -0
  381. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_header_footer.py +0 -0
  382. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_hyperlink.py +0 -0
  383. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_inline_shape.py +0 -0
  384. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
  385. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_misc.py +0 -0
  386. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
  387. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_style.py +0 -0
  388. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
  389. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_parfmt.py +0 -0
  390. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_row_col_cell.py +0 -0
  391. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_add_break.py +0 -0
  392. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
  393. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_style.py +0 -0
  394. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_style_strict.py +0 -0
  395. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_text.py +0 -0
  396. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_run_underline.py +0 -0
  397. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_audit.py +0 -0
  398. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_dimensions.py +0 -0
  399. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_section_onoff.py +0 -0
  400. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_settings.py +0 -0
  401. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_shared_audit.py +0 -0
  402. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_style.py +0 -0
  403. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_styles.py +0 -0
  404. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_audit.py +0 -0
  405. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_cell.py +0 -0
  406. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_dimensions.py +0 -0
  407. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_silent_stub_table_layout.py +0 -0
  408. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_smoke_integration.py +0 -0
  409. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_acceptance.py +0 -0
  410. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_font.py +0 -0
  411. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_formatting_writes.py +0 -0
  412. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_metadata_writes.py +0 -0
  413. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_names.py +0 -0
  414. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_style_setters_contract.py +0 -0
  415. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_table_set_cell_perf.py +0 -0
  416. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_table_style_id_resolution.py +0 -0
  417. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_temporarily_unavailable.py +0 -0
  418. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_text_control_characters.py +0 -0
  419. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_uv_lock_version.py +0 -0
  420. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_validate_find_replace_asset_script.py +0 -0
  421. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_wire_contract.py +0 -0
  422. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_xml_attr_guard.py +0 -0
  423. {athena_python_docx-0.26.0 → athena_python_docx-0.28.0}/tests/test_zod_wire_contract.py +0 -0
@@ -18,6 +18,8 @@ dist/
18
18
  .pytest_cache/
19
19
  .mypy_cache/
20
20
  .ruff_cache/
21
+ # The live parity runner's scoreboard and exports (python-sdk/tests/live/runner.py).
22
+ /python-sdk/out/
21
23
 
22
24
  # Editors
23
25
  .vscode/
@@ -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.26.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>
@@ -6,7 +6,7 @@ See CLAUDE.md for the API parity contract.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- __version__ = "0.26.0"
9
+ __version__ = "0.28.0"
10
10
 
11
11
  from docx.api import Document
12
12
  from docx._buffer import discard_all, flush_all
@@ -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
- return self._eager_flush_with(cmd)
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 a batch of typed commands.
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": [c.to_dict() for c in 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; a tracked deletion's run is left out, as
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