athena-python-docx 0.23.0__tar.gz → 0.25.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 (387) hide show
  1. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/AGENTS.md +44 -8
  2. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/PKG-INFO +1 -1
  3. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/__init__.py +1 -1
  4. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_buffer.py +17 -0
  5. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_http_doc.py +134 -8
  6. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_postproc.py +1 -1
  7. athena_python_docx-0.25.0/docx/_run_order.py +217 -0
  8. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_timeouts.py +2 -0
  9. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/client.py +16 -0
  10. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/commands.py +41 -6
  11. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/document.py +236 -43
  12. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/errors.py +38 -15
  13. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/section.py +172 -122
  14. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/shape.py +89 -12
  15. athena_python_docx-0.25.0/docx/styles/__init__.py +38 -0
  16. athena_python_docx-0.25.0/docx/styles/_table.py +303 -0
  17. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/styles/style.py +5 -1
  18. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/styles/styles.py +88 -34
  19. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/table.py +188 -60
  20. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/text/paragraph.py +98 -45
  21. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/text/run.py +249 -54
  22. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/pyproject.toml +1 -1
  23. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/fake_session.py +15 -1
  24. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +1 -0
  25. athena_python_docx-0.25.0/tests/fidelity/op_snapshots/15_run_add_break_page.json +6 -0
  26. athena_python_docx-0.25.0/tests/fidelity/op_snapshots/29_section_headers_linked.json +7 -0
  27. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +1 -0
  28. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/35_full_report.json +1 -0
  29. athena_python_docx-0.25.0/tests/fidelity/op_snapshots/48_font_color_round_trip.json +5 -0
  30. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/49_resume_layout.json +1 -0
  31. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +1 -0
  32. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +3 -1
  33. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/71_academic_paper.json +1 -0
  34. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/72_legal_contract.json +1 -0
  35. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +1 -0
  36. athena_python_docx-0.25.0/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +5 -0
  37. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +1 -0
  38. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +2 -2
  39. athena_python_docx-0.25.0/tests/fidelity/op_snapshots/85_header_footer_access.json +7 -0
  40. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +1 -1
  41. athena_python_docx-0.23.0/tests/fidelity/op_snapshots/47_font_size_round_trip.json → athena_python_docx-0.25.0/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +1 -1
  42. athena_python_docx-0.25.0/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +7 -0
  43. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +1 -0
  44. athena_python_docx-0.25.0/tests/fidelity/op_snapshots/98_style_contains_check.json +6 -0
  45. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +1 -0
  46. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +1 -0
  47. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +2 -1
  48. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +13 -6
  49. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +1 -0
  50. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +2 -2
  51. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +1 -0
  52. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +1 -0
  53. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +1 -0
  54. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +1 -0
  55. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +1 -0
  56. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +1 -0
  57. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +2 -1
  58. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +1 -0
  59. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +2 -1
  60. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +1 -0
  61. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw11_header_text.json +2 -0
  62. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +4 -0
  63. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +4 -0
  64. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +1 -0
  65. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/intentional_deviations.json +0 -3
  66. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_athena_extensions_registry.py +3 -0
  67. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_block_not_found_error.py +29 -0
  68. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_buffer.py +43 -6
  69. athena_python_docx-0.25.0/tests/test_cell_borders_spec.py +100 -0
  70. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_inner_format_stash.py +4 -2
  71. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_commands.py +19 -7
  72. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_http_transport.py +10 -10
  73. athena_python_docx-0.25.0/tests/test_inline_shape_alt_text.py +230 -0
  74. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_iter_inner_content.py +3 -3
  75. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_list_styles.py +8 -3
  76. athena_python_docx-0.25.0/tests/test_page_break.py +49 -0
  77. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_parity_misc.py +2 -2
  78. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_parity_round2.py +19 -5
  79. athena_python_docx-0.25.0/tests/test_pdf_export_and_page_renders.py +321 -0
  80. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_phase_a_behavior.py +2 -1
  81. athena_python_docx-0.25.0/tests/test_phase_b_headers_footers.py +471 -0
  82. athena_python_docx-0.25.0/tests/test_run_identity.py +306 -0
  83. athena_python_docx-0.25.0/tests/test_run_mark_inheritance.py +414 -0
  84. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_paragraph_style_strict.py +2 -1
  85. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_run_add_break.py +7 -7
  86. athena_python_docx-0.25.0/tests/test_style_names.py +272 -0
  87. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_wire_contract.py +23 -6
  88. athena_python_docx-0.23.0/docx/styles/__init__.py +0 -1
  89. athena_python_docx-0.23.0/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -5
  90. athena_python_docx-0.23.0/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -6
  91. athena_python_docx-0.23.0/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -3
  92. athena_python_docx-0.23.0/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -6
  93. athena_python_docx-0.23.0/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -5
  94. athena_python_docx-0.23.0/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -8
  95. athena_python_docx-0.23.0/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -6
  96. athena_python_docx-0.23.0/tests/test_phase_b_headers_footers.py +0 -281
  97. athena_python_docx-0.23.0/tests/test_run_mark_inheritance.py +0 -88
  98. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/.gitignore +0 -0
  99. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/CLAUDE.md +0 -0
  100. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/DOCX_EXEC_LAB.md +0 -0
  101. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/README.md +0 -0
  102. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_athena_extension.py +0 -0
  103. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_batching.py +0 -0
  104. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_citations.py +0 -0
  105. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_execution.py +0 -0
  106. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_http.py +0 -0
  107. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_image_utils.py +0 -0
  108. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_ptc.py +0 -0
  109. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_references.py +0 -0
  110. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/_table_styles.py +0 -0
  111. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/api.py +0 -0
  112. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/bookmarks.py +0 -0
  113. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/charts.py +0 -0
  114. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/comments.py +0 -0
  115. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/enum/__init__.py +0 -0
  116. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/enum/section.py +0 -0
  117. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/enum/shape.py +0 -0
  118. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/enum/style.py +0 -0
  119. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/enum/table.py +0 -0
  120. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/enum/text.py +0 -0
  121. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/exceptions.py +0 -0
  122. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/fields.py +0 -0
  123. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/footnotes.py +0 -0
  124. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/math.py +0 -0
  125. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/opc/__init__.py +0 -0
  126. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/opc/coreprops.py +0 -0
  127. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/oxml/__init__.py +0 -0
  128. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/revisions.py +0 -0
  129. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/sdt.py +0 -0
  130. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/session.py +0 -0
  131. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/settings.py +0 -0
  132. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/shared.py +0 -0
  133. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/text/__init__.py +0 -0
  134. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/text/font.py +0 -0
  135. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/text/hyperlink.py +0 -0
  136. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/text/pagebreak.py +0 -0
  137. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/text/parfmt.py +0 -0
  138. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/text/tabstops.py +0 -0
  139. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/toc.py +0 -0
  140. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/docx/typing.py +0 -0
  141. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/docx_exec_lab.py +0 -0
  142. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/docx_exec_lab_examples/fast_table_fill.py +0 -0
  143. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/docx_exec_lab_examples/find_replace_literal.py +0 -0
  144. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/docx_exec_lab_server.py +0 -0
  145. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/dump_wire_fixtures.py +0 -0
  146. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/release.sh +0 -0
  147. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/round_trip_smoke.py +0 -0
  148. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/smoke_test_block_not_found.py +0 -0
  149. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/scripts/validate_find_replace_asset.py +0 -0
  150. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/__init__.py +0 -0
  151. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/conftest.py +0 -0
  152. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/METHODOLOGY.md +0 -0
  153. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/README.md +0 -0
  154. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/__init__.py +0 -0
  155. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/ab_probe_cases.py +0 -0
  156. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/ab_probe_runner.py +0 -0
  157. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/auto_gen_cases.py +0 -0
  158. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/binary_round_trip.py +0 -0
  159. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/cases.py +0 -0
  160. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/complex_cases.py +0 -0
  161. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/coverage_report.py +0 -0
  162. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/extract.py +0 -0
  163. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/extreme_cases.py +0 -0
  164. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/firm_templates/README.md +0 -0
  165. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/firm_templates/__init__.py +0 -0
  166. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/firm_templates/_runner.py +0 -0
  167. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/firm_templates/extractor.py +0 -0
  168. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
  169. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
  170. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/local_runner.py +0 -0
  171. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/mega_cases.py +0 -0
  172. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshot.py +0 -0
  173. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
  174. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
  175. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
  176. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
  177. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
  178. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
  179. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
  180. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
  181. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
  182. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
  183. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
  184. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
  185. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
  186. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
  187. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
  188. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
  189. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
  190. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
  191. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
  192. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
  193. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
  194. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
  195. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
  196. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
  197. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
  198. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
  199. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
  200. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
  201. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
  202. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
  203. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
  204. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
  205. /athena_python_docx-0.23.0/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json → /athena_python_docx-0.25.0/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
  206. /athena_python_docx-0.23.0/tests/fidelity/op_snapshots/98_style_contains_check.json → /athena_python_docx-0.25.0/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
  207. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
  208. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
  209. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
  210. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
  211. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
  212. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
  213. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
  214. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
  215. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
  216. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
  217. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
  218. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
  219. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
  220. /athena_python_docx-0.23.0/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json → /athena_python_docx-0.25.0/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
  221. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
  222. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
  223. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
  224. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
  225. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
  226. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
  227. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
  228. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
  229. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
  230. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
  231. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
  232. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
  233. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
  234. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
  235. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
  236. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
  237. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
  238. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
  239. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
  240. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
  241. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
  242. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
  243. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
  244. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
  245. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
  246. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
  247. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
  248. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
  249. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
  250. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
  251. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
  252. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
  253. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
  254. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
  255. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
  256. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
  257. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
  258. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
  259. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
  260. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
  261. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
  262. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
  263. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
  264. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
  265. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
  266. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
  267. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
  268. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
  269. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
  270. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
  271. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
  272. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
  273. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
  274. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
  275. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
  276. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
  277. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
  278. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
  279. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/ours_spec.json +0 -0
  280. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/parity_crawl.py +0 -0
  281. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/parity_diff.json +0 -0
  282. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/real_world_cases.py +0 -0
  283. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/round_trip_tests.py +0 -0
  284. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/stock_spec.json +0 -0
  285. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
  286. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/README.md +0 -0
  287. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/__init__.py +0 -0
  288. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/baseline_gaps.json +0 -0
  289. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/compare.py +0 -0
  290. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/introspect.py +0 -0
  291. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/reports/GAP_ANALYSIS.md +0 -0
  292. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/reports/gap_report.json +0 -0
  293. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/run_parity.py +0 -0
  294. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/snapshots/athena_latest.json +0 -0
  295. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
  296. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/parity/test_parity_gap.py +0 -0
  297. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_add_section_extract_items.py +0 -0
  298. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_athena_extension_creates_session_open.py +0 -0
  299. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_athena_extensions_contract.py +0 -0
  300. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_batching_perf.py +0 -0
  301. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
  302. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_add_table_not_supported.py +0 -0
  303. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_fastpath_runs.py +0 -0
  304. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_inner_add_hyperlink_stash.py +0 -0
  305. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_inner_add_run_via_cell_insert.py +0 -0
  306. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_inner_run_format_stash.py +0 -0
  307. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_inner_run_guard.py +0 -0
  308. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_text_plain_fastpath.py +0 -0
  309. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_cell_text_replace_semantics.py +0 -0
  310. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_citations.py +0 -0
  311. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_collapsed_range_format.py +0 -0
  312. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_command_dataclasses.py +0 -0
  313. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_comments.py +0 -0
  314. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_document_asset_id_property.py +0 -0
  315. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_document_clear.py +0 -0
  316. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_document_create.py +0 -0
  317. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_document_create_from_template.py +0 -0
  318. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_document_factory_validation.py +0 -0
  319. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_docx_exec_lab.py +0 -0
  320. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_docx_exec_lab_server.py +0 -0
  321. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_e2e_partial_failure_cascade.py +0 -0
  322. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_execution_scope.py +0 -0
  323. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_find_replace_session_open.py +0 -0
  324. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_hyperlink_coalescing.py +0 -0
  325. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_image_url_data_uri.py +0 -0
  326. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_insert_deferred.py +0 -0
  327. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_merged_cell_secondary_slot.py +0 -0
  328. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_merged_cells.py +0 -0
  329. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_oxml_shim.py +0 -0
  330. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_paragraph_text_len_cache.py +0 -0
  331. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_partial_failure_cascade.py +0 -0
  332. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_phase_c_tables.py +0 -0
  333. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_positional_cell_id.py +0 -0
  334. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_postproc_cell_format_rewrite.py +0 -0
  335. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_postproc_cell_run_format_rewrite.py +0 -0
  336. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_postproc_ref_restore.py +0 -0
  337. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_pr19766_review_fixes.py +0 -0
  338. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_ptc.py +0 -0
  339. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_python_docx_api_parity.py +0 -0
  340. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_revisions.py +0 -0
  341. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_run_add_picture_anchor.py +0 -0
  342. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
  343. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_add_picture.py +0 -0
  344. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_add_run.py +0 -0
  345. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
  346. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
  347. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_comments_get.py +0 -0
  348. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_document_audit.py +0 -0
  349. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_document_element.py +0 -0
  350. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_enum_section.py +0 -0
  351. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_font_audit.py +0 -0
  352. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_header_footer.py +0 -0
  353. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_hyperlink.py +0 -0
  354. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_inline_shape.py +0 -0
  355. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
  356. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_misc.py +0 -0
  357. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
  358. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_paragraph_style.py +0 -0
  359. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_parfmt.py +0 -0
  360. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_row_col_cell.py +0 -0
  361. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
  362. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_run_style.py +0 -0
  363. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_run_style_strict.py +0 -0
  364. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_run_text.py +0 -0
  365. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_run_underline.py +0 -0
  366. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_section_audit.py +0 -0
  367. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_section_dimensions.py +0 -0
  368. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_section_onoff.py +0 -0
  369. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_settings.py +0 -0
  370. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_shared_audit.py +0 -0
  371. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_style.py +0 -0
  372. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_styles.py +0 -0
  373. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_table_audit.py +0 -0
  374. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_table_cell.py +0 -0
  375. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_table_dimensions.py +0 -0
  376. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_silent_stub_table_layout.py +0 -0
  377. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_smoke_integration.py +0 -0
  378. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_style_acceptance.py +0 -0
  379. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_style_font.py +0 -0
  380. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_style_setters_contract.py +0 -0
  381. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_table_set_cell_perf.py +0 -0
  382. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_table_style_id_resolution.py +0 -0
  383. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_temporarily_unavailable.py +0 -0
  384. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_validate_find_replace_asset_script.py +0 -0
  385. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_xml_attr_guard.py +0 -0
  386. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/tests/test_zod_wire_contract.py +0 -0
  387. {athena_python_docx-0.23.0 → athena_python_docx-0.25.0}/uv.lock +0 -0
@@ -268,7 +268,14 @@ Issue numbers reference `python-openxml/python-docx`.
268
268
  inside_horizontal=, inside_vertical=)`** + **`_Cell.borders`**
269
269
  property + **`Table.set_borders(...)`** — per-edge border control.
270
270
  python-docx issue #201 (5 👍), #1238 demand a cell-borders API.
271
- Routes through `SetCellBorders` / `SetTableBorders`.
271
+ Routes through `SetCellBorders` / `SetTableBorders`. Each edge is
272
+ `{"style", "size_pt", "color", "space"}` (points; `sizePt` also
273
+ accepted) or `"single 0.5pt #D9D9D9"`; the SDK validates it and sends
274
+ `{style, sizePt, color: "#RRGGBB", space}`. Any other key (`val`, `sz`,
275
+ `size`, `width`) raises `ValueError` naming the accepted form, and
276
+ docx-studio refuses it too (`InvalidBorderSpec`): such edges were once
277
+ stored without their size and colour, and one with no style broke every
278
+ later export of the document.
272
279
 
273
280
  - **`_Cell.set_shading(*, fill=, pattern=, color=)`** +
274
281
  **`_Cell.shading`** property — cell fill / shading. python-docx
@@ -370,7 +377,29 @@ Issue numbers reference `python-openxml/python-docx`.
370
377
  include_revisions=False) -> bytes`** — render to PDF. python-docx
371
378
  issue #113 was closed as
372
379
  wontfix upstream but the request keeps recurring. Routes through
373
- `ExportPDF` (DOCX Studio handles the actual conversion).
380
+ `ExportPDF` (0.25.0+ opens the session first and sends buffered edits in
381
+ the same request). DOCX Studio lays the document out with the engine the
382
+ editor paginates with and paints every page onto Skia's PDF backend: a
383
+ vector PDF with selectable text in embedded fonts that matches
384
+ `render_page` — no LibreOffice anywhere, so never install it in a sandbox.
385
+ `quality` is `"draft"` (pictures as JPEG), `"standard"` (default) or
386
+ `"high"`; `include_revisions=True` shows tracked changes as markup (the
387
+ default renders them accepted); `destination` writes a local file and
388
+ refuses asset ids and URLs with `ValueError`. A deployment that cannot
389
+ produce the PDF raises `DocxUnsupportedCommandError` naming the reason.
390
+ `GET /docs/:id/export.pdf` serves the same PDF read-only (VIEW gate).
391
+
392
+ - **`Document.render_page(page_number=1, scale=2, as_pil=False)`** +
393
+ **`Document.screenshot_to_path(path, page=1, scale=2) -> Path`** +
394
+ **`Document.page_count`** (0.25.0+) — look at the document as the editor
395
+ lays it out: one page (numbered from 1) as PNG bytes or a Pillow image,
396
+ written to disk, and the page count. Names follow
397
+ athena-python-pptx's `slide.render(scale=2, as_pil=True)` and
398
+ athena-openpyxl's `Worksheet.screenshot_to_path`. Buffered edits flush
399
+ first. Backed by docx-studio's read-only `GET /docs/:id/pages/:n.png` and
400
+ `GET /docs/:id/layout.json` (VIEW gate, the SDK's API key); a page past
401
+ the end raises `PageOutOfRangeError` (an `IndexError`) carrying
402
+ `page_count`.
374
403
 
375
404
  - **`Document.clear()`** (0.14.0+) — empty the document body so subsequent
376
405
  writes start from a clean slate. python-docx has no equivalent (it
@@ -754,12 +783,19 @@ architecture or a DOCX Studio constraint:
754
783
  cheaply enumerate every cell. `(0,0)` and in-range negative indices
755
784
  behave identically to upstream.
756
785
 
757
- - **`BaseStyle.style_id` returns the display name for built-ins** (so
758
- `style_id == name`). The SDK is HTTP-only and can't read `styles.xml`
759
- to recover the OOXML styleId (`"Heading 1"` → `"Heading1"`); the wire
760
- format also uses the display name as the style id. Consequently
761
- `get_by_id("Heading1", …)` / `"Heading1" in styles` won't resolve the
762
- OOXML-id form (use the display name).
786
+ - **Unknown paragraph style names raise only when Word has no such
787
+ built-in.** Style names and ids follow python-docx: `.name` is the UI
788
+ name (`"Heading 1"`), `.style_id` the id (`"Heading1"`), read from the
789
+ document's style table (docx-studio `Info` with `includeStyles`, fetched
790
+ once per session in `docx/styles/_table.py`). A style set by name or id
791
+ is sent as the document's id, and an unstyled paragraph reads as the
792
+ default paragraph style. `paragraph.style = …` raises `KeyError` for a
793
+ name the document defines no style for, like python-docx — except Word's
794
+ built-in styles (`"Heading 4"`, `"List Bullet"`, `"Title"`…), which many
795
+ templates never write into `styles.xml` until used. Those are sent by
796
+ their Word id; Word and docx-studio apply the built-in definition. Against
797
+ an older docx-studio (no style table) a built-in name is sent as its id
798
+ and anything else as given.
763
799
 
764
800
  - **`Document.add_table(rows, cols)` requires `rows >= 1` and
765
801
  `cols >= 1`.** Upstream allows a 0-row/0-col grid you later populate,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: athena-python-docx
3
- Version: 0.23.0
3
+ Version: 0.25.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.23.0"
9
+ __version__ = "0.25.0"
10
10
 
11
11
  from docx.api import Document
12
12
  from docx._buffer import discard_all, flush_all
@@ -341,6 +341,10 @@ class CommandBuffer:
341
341
  self._asset_id: str = asset_id
342
342
  self._pending: list[Command] = []
343
343
  self._lock: threading.RLock = threading.RLock()
344
+ # Held for the whole of a flush (drain + POST), so batches reach the
345
+ # server one at a time and in order, and a flush returns only once
346
+ # every batch taken before it has been delivered.
347
+ self._post_lock: threading.RLock = threading.RLock()
344
348
  self._timer: threading.Timer | None = None
345
349
  self._auto_flush_seconds: float = auto_flush_seconds
346
350
  self._closed: bool = False
@@ -532,6 +536,14 @@ class CommandBuffer:
532
536
  proxies pick up real ids without the caller ever seeing the
533
537
  UUID swap.
534
538
  """
539
+ # One batch in flight at a time: a flush that finds the queue empty
540
+ # still waits for a batch the idle timer already took and is posting,
541
+ # so a read that follows (a query, a page render) sees those edits and
542
+ # a failure of that batch is raised here.
543
+ with self._post_lock:
544
+ return self._flush_in_order()
545
+
546
+ def _flush_in_order(self) -> list[Any]:
535
547
  self._assert_current_execution()
536
548
  self._raise_deferred_flush_error()
537
549
  with self._lock:
@@ -599,7 +611,12 @@ class CommandBuffer:
599
611
  still get their real ids written back via the same flush-time
600
612
  rewrite that :meth:`flush` does.
601
613
  """
614
+ with self._post_lock:
615
+ return self._eager_flush_in_order(cmd)
616
+
617
+ def _eager_flush_in_order(self, cmd: Command) -> Any:
602
618
  self._assert_current_execution()
619
+ self._raise_deferred_flush_error()
603
620
  with self._lock:
604
621
  self._cancel_timer_locked()
605
622
  pending = self._pending
@@ -31,15 +31,16 @@ from __future__ import annotations
31
31
 
32
32
  import json
33
33
  import os
34
+ import urllib.parse
34
35
  import warnings
35
- from typing import Any
36
+ from typing import Any, TypedDict
36
37
 
37
38
  import requests
38
39
  from requests.adapters import HTTPAdapter
39
40
  from urllib3.util.retry import Retry
40
41
 
41
42
  from docx._buffer import CommandBuffer
42
- from docx._timeouts import timeout_for_commands
43
+ from docx._timeouts import DEFAULT_OP_TIMEOUT_SECONDS, timeout_for_commands
43
44
  from docx.commands import (
44
45
  BlocksList,
45
46
  BookmarksGet,
@@ -177,6 +178,7 @@ from docx.errors import (
177
178
  DocxUnsupportedCommandWarning,
178
179
  NotFoundError,
179
180
  SessionError,
181
+ _missing_block_id,
180
182
  )
181
183
 
182
184
 
@@ -338,6 +340,19 @@ _TABLE_CLIENT_ID_HINT: str = (
338
340
  )
339
341
 
340
342
 
343
+ _HEADER_FOOTER_PARAGRAPH_HINT: str = (
344
+ "\n\nHint: this paragraph is in a header or footer (``section.header`` / "
345
+ "``section.footer``), not in a table cell. docx-studio found no "
346
+ "paragraph with this id: the header/footer part that held it was "
347
+ "deleted, or the docx-studio server predates run and paragraph edits "
348
+ "in headers and footers. Add the paragraph again with "
349
+ "``section.footer.add_paragraph(text)`` (or ``section.header``) and edit "
350
+ "the paragraph it returns; until the server supports those edits, pass "
351
+ "the paragraph's full text to ``add_paragraph`` and set only its style "
352
+ "and alignment."
353
+ )
354
+
355
+
341
356
  def _looks_like_not_found(parsed: dict) -> bool:
342
357
  """Inspect a docx-studio partial-failure dict and decide whether it
343
358
  describes a "no such entity" miss (vs. a real transport / validation
@@ -446,7 +461,9 @@ def _build_retry_session() -> requests.Session:
446
461
  "other": 0,
447
462
  "backoff_factor": 2.0,
448
463
  "status_forcelist": (429, 502, 503, 504),
449
- "allowed_methods": frozenset(["POST"]),
464
+ # GET serves only the read-only render routes (page PNGs, layout),
465
+ # which are always safe to retry.
466
+ "allowed_methods": frozenset(["POST", "GET"]),
450
467
  "raise_on_status": False,
451
468
  "respect_retry_after_header": True,
452
469
  }
@@ -482,6 +499,16 @@ def _parse_retry_after(value: str | None) -> float | None:
482
499
  return None
483
500
 
484
501
 
502
+ def _delegation_headers() -> dict[str, str]:
503
+ """The user a service-key caller acts for, as ``X-Custom-Attributions``.
504
+
505
+ docx-studio's ownership gate reads it (``delegatedUserId``) and agora's
506
+ ``/access`` refuses a service-key call that delegates no one, so every
507
+ request carries it: commands and the read-only GETs alike."""
508
+ custom_attr = os.environ.get("ATHENA_DOCX_CUSTOM_ATTRIBUTIONS")
509
+ return {"X-Custom-Attributions": custom_attr} if custom_attr else {}
510
+
511
+
485
512
  def _http_post_json(
486
513
  *,
487
514
  session: requests.Session,
@@ -489,17 +516,21 @@ def _http_post_json(
489
516
  api_key: str,
490
517
  body: dict,
491
518
  timeout: float = 60.0,
519
+ header_footer_block_ids: "set[str] | frozenset[str]" = frozenset(),
492
520
  ) -> dict:
493
- """POST JSON, parse JSON, raise typed errors on non-2xx."""
521
+ """POST JSON, parse JSON, raise typed errors on non-2xx.
522
+
523
+ ``header_footer_block_ids`` are the header/footer paragraph ids this
524
+ document's ``_HeaderFooter.add_paragraph`` returned: a ``block not
525
+ found`` on one of them gets the header/footer hint.
526
+ """
494
527
  headers = {
495
528
  "Content-Type": "application/json",
496
529
  "Authorization": f"Bearer {api_key}",
497
530
  "Accept": "application/json",
498
531
  "User-Agent": _user_agent(),
532
+ **_delegation_headers(),
499
533
  }
500
- custom_attr = os.environ.get("ATHENA_DOCX_CUSTOM_ATTRIBUTIONS")
501
- if custom_attr:
502
- headers["X-Custom-Attributions"] = custom_attr
503
534
  try:
504
535
  resp = session.post(
505
536
  url,
@@ -621,7 +652,9 @@ def _http_post_json(
621
652
  isinstance(cmd_type, str) and cmd_type in _PARAGRAPH_TARGETING_COMMANDS
622
653
  )
623
654
  table_query = isinstance(cmd_type, str) and cmd_type in _TABLE_QUERY_COMMANDS
624
- if paragraph_targeting:
655
+ if _missing_block_id(err_obj.get("message")) in header_footer_block_ids:
656
+ hint = _HEADER_FOOTER_PARAGRAPH_HINT
657
+ elif paragraph_targeting:
625
658
  hint = _CELL_PARAGRAPH_HINT
626
659
  elif table_query:
627
660
  hint = _TABLE_CLIENT_ID_HINT
@@ -696,6 +729,63 @@ def _warn_skipped_commands(skipped: list[Any]) -> None:
696
729
  warnings.warn(message, DocxUnsupportedCommandWarning, stacklevel=2)
697
730
 
698
731
 
732
+ def _http_get(
733
+ *,
734
+ session: requests.Session,
735
+ url: str,
736
+ api_key: str,
737
+ params: dict[str, str] | None = None,
738
+ timeout: float = DEFAULT_OP_TIMEOUT_SECONDS,
739
+ ) -> requests.Response:
740
+ """GET one of docx-studio's read-only routes; raise typed errors on non-2xx.
741
+
742
+ A 404 keeps the server's JSON body as the error payload, so a caller can
743
+ tell a page past the end (``error: "PageNotFound"``, ``pageCount``) from an
744
+ asset the caller cannot read.
745
+ """
746
+ headers = {
747
+ "Authorization": f"Bearer {api_key}",
748
+ "User-Agent": _user_agent(),
749
+ **_delegation_headers(),
750
+ }
751
+ try:
752
+ resp = session.get(url, params=params, headers=headers, timeout=timeout)
753
+ except requests.Timeout as e:
754
+ raise SessionError(f"docx-studio timed out at {url}: {e}") from e
755
+ except requests.ConnectionError as e:
756
+ raise SessionError(f"Unable to reach docx-studio at {url}: {e}") from e
757
+ if 200 <= resp.status_code < 300:
758
+ return resp
759
+ err_body: str = resp.text
760
+ if resp.status_code in (401, 403):
761
+ raise AuthenticationError(
762
+ f"docx-studio rejected the request (HTTP {resp.status_code}): {err_body}",
763
+ )
764
+ if resp.status_code == 404:
765
+ try:
766
+ parsed: object = resp.json()
767
+ except ValueError:
768
+ parsed = None
769
+ payload: dict[str, Any] = {"status": 404, "body": err_body}
770
+ if isinstance(parsed, dict):
771
+ payload.update(parsed)
772
+ raise NotFoundError(f"docx-studio returned HTTP 404: {err_body}", payload=payload)
773
+ if resp.status_code in (502, 503, 504):
774
+ raise DocxStudioTemporarilyUnavailable(
775
+ f"docx-studio returned HTTP {resp.status_code}: {err_body}",
776
+ http_status=resp.status_code,
777
+ retry_after_seconds=_parse_retry_after(resp.headers.get("Retry-After")),
778
+ )
779
+ raise DocxError(f"docx-studio returned HTTP {resp.status_code}: {err_body}")
780
+
781
+
782
+ class RenderedPagePng(TypedDict):
783
+ """One page from ``GET /docs/:id/pages/:n.png``."""
784
+
785
+ png: bytes
786
+ page_count: int
787
+
788
+
699
789
  # ---------------------------------------------------------------------------
700
790
  # HttpClient — the bare HTTP transport. Stateless wrt batching.
701
791
  # ---------------------------------------------------------------------------
@@ -720,6 +810,8 @@ class HttpClient:
720
810
  self._base_url: str = base_url.rstrip("/")
721
811
  self._api_key: str = api_key
722
812
  self._session: requests.Session = session or _build_retry_session()
813
+ # Header/footer paragraph ids, for the ``block not found`` hint.
814
+ self.header_footer_block_ids: set[str] = set()
723
815
 
724
816
  def close(self) -> None:
725
817
  """Release the pooled HTTP connections held by ``requests.Session``.
@@ -801,6 +893,7 @@ class HttpClient:
801
893
  api_key=self._api_key,
802
894
  body=body,
803
895
  timeout=timeout_for_commands(commands=commands),
896
+ header_footer_block_ids=self.header_footer_block_ids,
804
897
  )
805
898
  applied = resp.get("applied")
806
899
  # Commands the engine doesn't implement yet are reported in
@@ -852,6 +945,38 @@ class HttpClient:
852
945
  skipped_by_index[idx] = entry
853
946
  return results, skipped_by_index
854
947
 
948
+ def _doc_url(self, asset_id: str, path: str) -> str:
949
+ return f"{self._base_url}/docs/{urllib.parse.quote(asset_id, safe='')}/{path}"
950
+
951
+ def get_layout(self, asset_id: str) -> dict[str, Any]:
952
+ """``GET /docs/:id/layout.json`` — the page count and page sizes as
953
+ DOCX Studio's layout engine paginates the document (VIEW gate)."""
954
+ resp = _http_get(
955
+ session=self._session,
956
+ url=self._doc_url(asset_id, "layout.json"),
957
+ api_key=self._api_key,
958
+ )
959
+ try:
960
+ data: object = resp.json()
961
+ except ValueError as e:
962
+ raise DocxError(f"docx-studio returned a non-JSON layout: {resp.text[:200]!r}") from e
963
+ if not isinstance(data, dict):
964
+ raise DocxError(f"docx-studio returned an unexpected layout: {data!r}")
965
+ return data
966
+
967
+ def render_page_png(self, asset_id: str, page_number: int, *, scale: float) -> RenderedPagePng:
968
+ """``GET /docs/:id/pages/:n.png?scale=`` — one page (1-indexed) as PNG,
969
+ from the same layout engine the editor paginates with (VIEW gate)."""
970
+ resp = _http_get(
971
+ session=self._session,
972
+ url=self._doc_url(asset_id, f"pages/{page_number}.png"),
973
+ api_key=self._api_key,
974
+ params={"scale": f"{scale:g}"},
975
+ )
976
+ page_count_header = resp.headers.get("X-Page-Count", "")
977
+ page_count = int(page_count_header) if page_count_header.isdigit() else 0
978
+ return RenderedPagePng(png=resp.content, page_count=page_count)
979
+
855
980
 
856
981
  # ---------------------------------------------------------------------------
857
982
  # Path proxy — translates dotted paths + camelCase param dicts into typed
@@ -1045,6 +1170,7 @@ _FIELD_RENAMES: dict[str, dict[str, str]] = {
1045
1170
  # path-proxy snake-case transform leaves `in` as a Python keyword
1046
1171
  # collision; rename to `in_` here so the dataclass accepts it.
1047
1172
  "find_replace": {"in": "in_"},
1173
+ "blocks.list": {"in": "in_"},
1048
1174
  "find_text": {"in": "in_"},
1049
1175
  "iter_runs": {"in": "in_"},
1050
1176
  "numbering.list": {"in": "in_"},
@@ -2055,7 +2055,7 @@ def _border_spec_to_ooxml_attrs(spec: dict[str, Any]) -> dict[str, str]:
2055
2055
  out["sz"] = str(int(round(float(size_pt) * 8)))
2056
2056
  except (TypeError, ValueError):
2057
2057
  pass
2058
- space = spec.get("space_pt")
2058
+ space = spec.get("space", spec.get("space_pt"))
2059
2059
  if space is not None:
2060
2060
  try:
2061
2061
  out["space"] = str(int(round(float(space))))
@@ -0,0 +1,217 @@
1
+ """Run order of a paragraph, as ``Paragraph.add_run`` created its runs.
2
+
3
+ A picture takes no text offset, so two runs can end at the same offset:
4
+ ``r1 = p.add_run("A"); r2 = p.add_run(); r2.add_picture(img)`` puts ``r2`` and
5
+ its picture at offset 1, where ``r1`` ends. The offset alone cannot tell the
6
+ server whether content for ``r1`` goes before or after that picture. The SDK
7
+ knows: ``add_run`` appends each run to the end of its paragraph.
8
+
9
+ This module keeps a record of each such run (its range and the pictures that
10
+ its ``add_picture`` added), in creation order, and gives:
11
+
12
+ - the run identity of a run-end insert (:func:`run_ref`): the first picture
13
+ that a later run holds at the same offset (for a read segment that ends
14
+ inside its run, also the run's own picture there). The server puts the
15
+ content in front of it; without it, after the pictures at the offset.
16
+ - current ranges: when a run grows or shrinks, its pictures and the runs after
17
+ it move (:func:`note_length_change`), and so do the live run proxies. A
18
+ replace cuts every record that it touches.
19
+
20
+ A record lives as long as its paragraph proxy, not as long as its run proxy,
21
+ so ``p.add_run().add_picture(img)`` keeps the picture's place. The records of
22
+ every paragraph proxy of one block (``doc.paragraphs`` makes new ones) are one
23
+ order. A read run (``paragraph.runs``) stands for the recorded run that holds
24
+ its last character; a read run over text that no recorded run holds counts as
25
+ a run before all of them. The order lives in the SDK process only; the
26
+ document stores none of it.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import itertools
32
+ import weakref
33
+ from dataclasses import dataclass, field
34
+ from typing import TYPE_CHECKING, Any
35
+
36
+ if TYPE_CHECKING:
37
+ from docx.text.run import Run
38
+
39
+ _SEQ = itertools.count()
40
+
41
+
42
+ @dataclass(eq=False)
43
+ class RunRecord:
44
+ """One run that ``add_run`` created."""
45
+
46
+ seq: int
47
+ range: tuple[int, int]
48
+ run: "weakref.ref[Run]"
49
+ # ``(picture id, offset)`` of each picture that the run's ``add_picture`` added.
50
+ pictures: list[tuple[str, int]] = field(default_factory=list)
51
+
52
+
53
+ # Paragraph proxy -> the records of the runs its ``add_run`` created. A record
54
+ # holds no reference to the paragraph or session, so an entry goes with its
55
+ # paragraph proxy. Keyed by the proxy, not by its id: the first flush moves a
56
+ # new paragraph from its client id to the server id, and the runs created
57
+ # before that keep the client id.
58
+ _ORDERS: "weakref.WeakKeyDictionary[Any, list[RunRecord]]" = weakref.WeakKeyDictionary()
59
+
60
+
61
+ def _records_of(run: "Run") -> list[RunRecord]:
62
+ paragraph = getattr(run, "_paragraph", None)
63
+ ids = {run._block_id}
64
+ if paragraph is not None:
65
+ ids.add(paragraph._node_id)
66
+ records: list[RunRecord] = []
67
+ for owner, owner_records in list(_ORDERS.items()):
68
+ if owner is paragraph or (owner._session is run._session and owner._node_id in ids):
69
+ records.extend(owner_records)
70
+ records.sort(key=lambda record: record.seq)
71
+ return records
72
+
73
+
74
+ def register_run(run: "Run") -> None:
75
+ """Record ``run`` as the last run of its paragraph (``Paragraph.add_run``)."""
76
+ record = RunRecord(seq=next(_SEQ), range=run._range, run=weakref.ref(run))
77
+ _ORDERS.setdefault(run._paragraph, []).append(record)
78
+ run._run_record = record
79
+
80
+
81
+ def forget_paragraph(paragraph: Any) -> None:
82
+ """Drop the order of a paragraph whose runs are gone (``Paragraph.text = ...``)."""
83
+ for owner in list(_ORDERS.keys()):
84
+ if owner is paragraph or (owner._session is paragraph._session and owner._node_id == paragraph._node_id):
85
+ _ORDERS.pop(owner, None)
86
+
87
+
88
+ def _place(
89
+ run: "Run", span: tuple[int, int], records: list[RunRecord] | None = None
90
+ ) -> tuple[RunRecord | None, list[RunRecord]]:
91
+ """The record of ``run`` (over ``span``), if any, and the records after it."""
92
+ if records is None:
93
+ records = _records_of(run)
94
+ # The run itself, or the text that its ``add_text`` returned.
95
+ own = getattr(run, "_run_record", None) or getattr(run, "_owner_record", None)
96
+ for index, record in enumerate(records):
97
+ if record is own:
98
+ return record, records[index + 1 :]
99
+ # ``paragraph.runs`` makes a new proxy of a kept run: over the same range,
100
+ # or over a part of it (a read splits a run at a tab, a break or a change
101
+ # of format). The proxy stands for the run that holds its last character.
102
+ if span[0] != span[1]:
103
+ last = span[1] - 1
104
+ for index, record in enumerate(records):
105
+ if record.range[0] <= last < record.range[1]:
106
+ return record, records[index + 1 :]
107
+ return None, records
108
+
109
+
110
+ def _set_range(record: RunRecord, value: tuple[int, int]) -> None:
111
+ record.range = value
112
+ proxy = record.run()
113
+ if proxy is not None:
114
+ proxy._range = value
115
+
116
+
117
+ def note_length_change(
118
+ run: "Run",
119
+ at: int,
120
+ delta: int,
121
+ *,
122
+ replaced: tuple[int, int] | None = None,
123
+ before: str | None = None,
124
+ ) -> None:
125
+ """``run`` (already over its new range) changed its text by ``delta`` units.
126
+
127
+ Without ``replaced``: an insert of ``delta`` units at ``at`` (the end of
128
+ ``run``), in front of the picture ``before`` when the command named one
129
+ (else after the pictures at ``at``). With ``replaced``: ``Run.text``
130
+ replaced the old range ``replaced``; the server keeps the pictures at its
131
+ start and end and removes the ones inside.
132
+
133
+ The text belongs to the record of ``run`` (see :func:`_place`). Every
134
+ record that the replaced range touches is cut; the records and pictures
135
+ after the change move.
136
+ """
137
+ if delta == 0 and replaced is None:
138
+ return
139
+ records = _records_of(run)
140
+ if replaced is None:
141
+ start = end = at
142
+ inserted = delta
143
+ old_span = (run._range[0], run._range[1] - delta)
144
+ else:
145
+ start, end = replaced
146
+ inserted = (end - start) + delta
147
+ old_span = replaced
148
+ owner = _place(run, old_span, records)[0]
149
+ owner_index = records.index(owner) if owner is not None else -1
150
+ removed = end - start
151
+
152
+ def cut(x: int) -> int:
153
+ return x if x <= start else start if x < end else x - removed
154
+
155
+ # The pictures at ``at`` that an insert goes in front of: ``before`` and
156
+ # the ones after it, in document order (records in order, then the
157
+ # pictures of each record in order).
158
+ moved_at: set[str] = set()
159
+ if replaced is None and before is not None:
160
+ order = [pid for record in records for pid, off in record.pictures if off == at]
161
+ if before in order:
162
+ moved_at = set(order[order.index(before) :])
163
+
164
+ for index, record in enumerate(records):
165
+ rec_start, rec_end = cut(record.range[0]), cut(record.range[1])
166
+ if record is owner:
167
+ rec_end += inserted
168
+ elif index > owner_index and rec_start >= start:
169
+ rec_start, rec_end = rec_start + inserted, rec_end + inserted
170
+ _set_range(record, (rec_start, rec_end))
171
+ pictures: list[tuple[str, int]] = []
172
+ for pid, off in record.pictures:
173
+ if replaced is None:
174
+ pictures.append((pid, off + delta if off > at or pid in moved_at else off))
175
+ elif off <= start:
176
+ pictures.append((pid, off))
177
+ elif off >= end:
178
+ pictures.append((pid, off + delta))
179
+ # The moves keep document order; a stable sort by offset keeps it so.
180
+ pictures.sort(key=lambda item: item[1])
181
+ record.pictures = pictures
182
+
183
+
184
+ def note_picture(run: "Run", picture_id: str, offset: int, *, before: str | None = None) -> None:
185
+ """``run.add_picture`` put the picture ``picture_id`` at ``offset``, in front of ``before``.
186
+
187
+ A record keeps its pictures in document order: the new one goes in front
188
+ of ``before``, else after the pictures at ``offset`` and before the ones
189
+ after it (the server puts a picture after the pictures at its offset).
190
+ """
191
+ record = _place(run, run._range)[0]
192
+ if record is None:
193
+ return
194
+ ids = [pid for pid, _ in record.pictures]
195
+ if before in ids:
196
+ index = ids.index(before)
197
+ else:
198
+ index = next((i for i, (_, off) in enumerate(record.pictures) if off > offset), len(ids))
199
+ record.pictures.insert(index, (picture_id, offset))
200
+
201
+
202
+ def run_ref(run: "Run") -> dict[str, str] | None:
203
+ """``{"beforeImageId": id}``: the picture that an insert at the end of ``run`` goes in front of.
204
+
205
+ The first picture of a later run where ``run`` ends. A read segment that
206
+ ends inside its run (a picture of the run between two of its texts) also
207
+ puts the insert in front of the run's own picture there, right after the
208
+ segment's text.
209
+ """
210
+ end = run._range[1]
211
+ record, later = _place(run, run._range)
212
+ candidates = ([record] if record is not None and end < record.range[1] else []) + later
213
+ for candidate in candidates:
214
+ for picture_id, offset in candidate.pictures:
215
+ if offset == end:
216
+ return {"beforeImageId": picture_id}
217
+ return None
@@ -22,6 +22,8 @@ _LONG_RUNNING_COMMAND_TYPES: frozenset[str] = frozenset(
22
22
  # ``Document.clear()`` replaces the whole body story in one write;
23
23
  # on a large document that single command can outlast the 60s budget.
24
24
  "ClearBody",
25
+ # ``Document.to_pdf()`` lays out and paints every page of the document.
26
+ "ExportPDF",
25
27
  }
26
28
  )
27
29
 
@@ -178,6 +178,22 @@ class Session:
178
178
  )
179
179
  return self._doc_handle
180
180
 
181
+ @property
182
+ def http_client(self) -> Any:
183
+ """The opened :class:`HttpClient`, for docx-studio's read-only routes
184
+ outside the command bus (page renders, layout)."""
185
+ self._assert_current_execution()
186
+ if not self._opened:
187
+ raise SessionError(
188
+ "Session not yet opened; call await session.open() first "
189
+ "or use the sync facade via Document().",
190
+ )
191
+ if self._closed:
192
+ raise DocumentClosedError(
193
+ f"Session for asset {self._asset_id} is closed.",
194
+ )
195
+ return self._client
196
+
181
197
  async def send_command(self, cmd: Any) -> Any:
182
198
  """Route a pre-built :class:`docx.commands.Command` through the
183
199
  buffer.