athena-python-docx 0.5.2__tar.gz → 0.5.3__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 (280) hide show
  1. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/CLAUDE.md +7 -0
  2. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/PKG-INFO +1 -1
  3. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/__init__.py +1 -1
  4. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/_buffer.py +213 -28
  5. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/_http_doc.py +92 -0
  6. athena_python_docx-0.5.3/docx/_ptc.py +173 -0
  7. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/commands.py +26 -0
  8. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/comments.py +91 -4
  9. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/document.py +332 -70
  10. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/errors.py +15 -0
  11. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/section.py +105 -18
  12. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/settings.py +29 -1
  13. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/shared.py +10 -7
  14. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/styles/style.py +252 -3
  15. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/styles/styles.py +64 -1
  16. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/table.py +161 -35
  17. athena_python_docx-0.5.3/docx/text/hyperlink.py +123 -0
  18. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/text/pagebreak.py +27 -1
  19. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/text/paragraph.py +52 -5
  20. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/text/run.py +185 -21
  21. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/pyproject.toml +1 -1
  22. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/scripts/release.sh +16 -9
  23. athena_python_docx-0.5.2/tests/fidelity/op_snapshots/25_table_merge_cells.json → athena_python_docx-0.5.3/tests/fidelity/op_snapshots/100_table_negative_indexing.json +1 -1
  24. athena_python_docx-0.5.2/tests/fidelity/op_snapshots/18_table_cell_text.json → athena_python_docx-0.5.3/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +3 -0
  25. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +1 -0
  26. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -15
  27. athena_python_docx-0.5.2/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json → athena_python_docx-0.5.3/tests/fidelity/op_snapshots/18_table_cell_text.json +5 -5
  28. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -6
  29. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +3 -0
  30. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +1 -0
  31. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/23_nested_table.json +2 -0
  32. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -4
  33. athena_python_docx-0.5.2/tests/fidelity/op_snapshots/100_table_negative_indexing.json → athena_python_docx-0.5.3/tests/fidelity/op_snapshots/25_table_merge_cells.json +1 -1
  34. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/35_full_report.json +6 -2
  35. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/40_large_table_10x10.json +100 -0
  36. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +2 -0
  37. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/49_resume_layout.json +3 -0
  38. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +3 -0
  39. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/56_everything_in_one.json +12 -3
  40. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +1 -0
  41. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +1 -0
  42. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +400 -0
  43. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/68_invoice.json +16 -7
  44. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/69_newsletter.json +1 -0
  45. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/72_legal_contract.json +6 -0
  46. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +20 -20
  47. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -4
  48. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -5
  49. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +10 -0
  50. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +1 -0
  51. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/91_many_small_tables.json +100 -0
  52. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +5 -0
  53. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +3 -0
  54. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex04_50x50_table.json +50 -0
  55. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +1 -0
  56. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex10_complex_bom.json +66 -0
  57. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +80 -0
  58. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +1 -0
  59. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +18 -0
  60. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +84 -0
  61. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega01_book_chapter.json +13 -0
  62. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega02_research_proposal.json +18 -0
  63. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega03_financial_statement.json +35 -0
  64. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega05_user_manual.json +28 -0
  65. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +40 -0
  66. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega09_signed_contract.json +12 -0
  67. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega10_api_documentation.json +44 -0
  68. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -8
  69. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -8
  70. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +8 -0
  71. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +16 -0
  72. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw14_nested_cell_table.json +2 -0
  73. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/intentional_deviations.json +31 -1
  74. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/reports/GAP_ANALYSIS.md +4 -3
  75. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/reports/gap_report.json +75 -3
  76. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/snapshots/athena_latest.json +1479 -105
  77. athena_python_docx-0.5.3/tests/test_batching_perf.py +261 -0
  78. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_iter_inner_content.py +37 -0
  79. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_pr19766_review_fixes.py +39 -0
  80. athena_python_docx-0.5.3/tests/test_ptc.py +346 -0
  81. athena_python_docx-0.5.3/tests/test_silent_stub_add_picture.py +82 -0
  82. athena_python_docx-0.5.3/tests/test_silent_stub_comments_get.py +177 -0
  83. athena_python_docx-0.5.3/tests/test_silent_stub_document_audit.py +219 -0
  84. athena_python_docx-0.5.3/tests/test_silent_stub_document_element.py +39 -0
  85. athena_python_docx-0.5.3/tests/test_silent_stub_font_audit.py +356 -0
  86. athena_python_docx-0.5.3/tests/test_silent_stub_header_footer.py +172 -0
  87. athena_python_docx-0.5.3/tests/test_silent_stub_hyperlink.py +263 -0
  88. athena_python_docx-0.5.3/tests/test_silent_stub_misc.py +268 -0
  89. athena_python_docx-0.5.3/tests/test_silent_stub_paragraph_style.py +191 -0
  90. athena_python_docx-0.5.3/tests/test_silent_stub_run_bool_setters.py +99 -0
  91. athena_python_docx-0.5.3/tests/test_silent_stub_section_audit.py +166 -0
  92. athena_python_docx-0.5.3/tests/test_silent_stub_settings.py +68 -0
  93. athena_python_docx-0.5.3/tests/test_silent_stub_shared_audit.py +105 -0
  94. athena_python_docx-0.5.3/tests/test_silent_stub_style.py +157 -0
  95. athena_python_docx-0.5.3/tests/test_silent_stub_styles.py +191 -0
  96. athena_python_docx-0.5.3/tests/test_silent_stub_table_audit.py +247 -0
  97. athena_python_docx-0.5.3/tests/test_silent_stub_table_cell.py +91 -0
  98. athena_python_docx-0.5.3/tests/test_silent_stub_table_dimensions.py +109 -0
  99. athena_python_docx-0.5.2/docx/text/hyperlink.py +0 -53
  100. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/.gitignore +0 -0
  101. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/README.md +0 -0
  102. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/_batching.py +0 -0
  103. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/_http.py +0 -0
  104. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/_image_utils.py +0 -0
  105. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/api.py +0 -0
  106. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/client.py +0 -0
  107. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/enum/__init__.py +0 -0
  108. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/enum/section.py +0 -0
  109. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/enum/style.py +0 -0
  110. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/enum/table.py +0 -0
  111. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/enum/text.py +0 -0
  112. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/exceptions.py +0 -0
  113. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/opc/__init__.py +0 -0
  114. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/opc/coreprops.py +0 -0
  115. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/revisions.py +0 -0
  116. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/shape.py +0 -0
  117. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/styles/__init__.py +0 -0
  118. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/text/__init__.py +0 -0
  119. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/text/font.py +0 -0
  120. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/text/parfmt.py +0 -0
  121. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/text/tabstops.py +0 -0
  122. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/docx/typing.py +0 -0
  123. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/scripts/publish.sh +0 -0
  124. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/scripts/round_trip_smoke.py +0 -0
  125. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/__init__.py +0 -0
  126. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/conftest.py +0 -0
  127. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/METHODOLOGY.md +0 -0
  128. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/README.md +0 -0
  129. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/__init__.py +0 -0
  130. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/ab_probe_cases.py +0 -0
  131. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/ab_probe_runner.py +0 -0
  132. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/auto_gen_cases.py +0 -0
  133. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/binary_round_trip.py +0 -0
  134. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/cases.py +0 -0
  135. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/complex_cases.py +0 -0
  136. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/coverage_report.py +0 -0
  137. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/extract.py +0 -0
  138. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/extreme_cases.py +0 -0
  139. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/fake_session.py +0 -0
  140. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/local_runner.py +0 -0
  141. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/mega_cases.py +0 -0
  142. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshot.py +0 -0
  143. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
  144. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
  145. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
  146. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
  147. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
  148. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
  149. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
  150. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
  151. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
  152. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
  153. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
  154. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
  155. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
  156. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
  157. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
  158. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
  159. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
  160. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
  161. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
  162. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
  163. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
  164. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
  165. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
  166. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
  167. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
  168. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
  169. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
  170. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
  171. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
  172. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
  173. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
  174. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
  175. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
  176. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
  177. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
  178. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
  179. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
  180. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
  181. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
  182. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
  183. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
  184. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
  185. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
  186. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
  187. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
  188. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
  189. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
  190. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
  191. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
  192. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
  193. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
  194. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
  195. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
  196. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
  197. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
  198. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
  199. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
  200. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
  201. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
  202. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
  203. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
  204. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
  205. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
  206. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
  207. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
  208. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
  209. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
  210. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
  211. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
  212. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
  213. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
  214. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
  215. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
  216. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
  217. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
  218. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
  219. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
  220. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
  221. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
  222. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
  223. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
  224. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
  225. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
  226. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
  227. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
  228. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
  229. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
  230. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
  231. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
  232. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
  233. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
  234. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
  235. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
  236. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
  237. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
  238. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
  239. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
  240. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
  241. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
  242. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
  243. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
  244. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
  245. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/ours_spec.json +0 -0
  246. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/parity_crawl.py +0 -0
  247. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/parity_diff.json +0 -0
  248. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/real_world_cases.py +0 -0
  249. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/round_trip_tests.py +0 -0
  250. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/runner.py +0 -0
  251. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/fidelity/stock_spec.json +0 -0
  252. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/README.md +0 -0
  253. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/__init__.py +0 -0
  254. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/baseline_gaps.json +0 -0
  255. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/compare.py +0 -0
  256. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/introspect.py +0 -0
  257. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/run_parity.py +0 -0
  258. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
  259. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/parity/test_parity_gap.py +0 -0
  260. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_buffer.py +0 -0
  261. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_command_dataclasses.py +0 -0
  262. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_commands.py +0 -0
  263. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_comments.py +0 -0
  264. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_document_create.py +0 -0
  265. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_http_transport.py +0 -0
  266. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_merged_cells.py +0 -0
  267. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_parity_misc.py +0 -0
  268. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_parity_round2.py +0 -0
  269. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_phase_a_behavior.py +0 -0
  270. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_phase_b_headers_footers.py +0 -0
  271. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_phase_c_tables.py +0 -0
  272. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_python_docx_api_parity.py +0 -0
  273. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_revisions.py +0 -0
  274. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_smoke_integration.py +0 -0
  275. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_style_acceptance.py +0 -0
  276. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_style_font.py +0 -0
  277. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_style_setters_contract.py +0 -0
  278. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_wire_contract.py +0 -0
  279. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/tests/test_zod_wire_contract.py +0 -0
  280. {athena_python_docx-0.5.2 → athena_python_docx-0.5.3}/uv.lock +0 -0
@@ -145,6 +145,13 @@ This is a **thin HTTP client** that mimics the sync python-docx API.
145
145
  resolve to `CommandBuffer.call(CreateParagraph(**p))`. Rewriting call
146
146
  sites to construct `Command` dataclasses directly is a possible
147
147
  follow-up — the wire format is typed end-to-end either way.
148
+ - **Programmatic Tool Calling (PTC):** every `CommandBuffer.call` and
149
+ `flush` emits begin/end events to agora via `docx/_ptc.py` when run
150
+ inside a Daytona sandbox with `ATHENA_PTC_URL` set. Each method call
151
+ surfaces as a nested sub-tool-card under the parent
152
+ `run_python_code` tool. See
153
+ [`docs/PROGRAMMATIC_TOOL_CALLING_GUIDE.md`](../../docs/PROGRAMMATIC_TOOL_CALLING_GUIDE.md)
154
+ at the monorepo root.
148
155
 
149
156
  ## Development
150
157
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-docx
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx 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.5.2"
9
+ __version__ = "0.5.3"
10
10
 
11
11
  from docx.api import Document
12
12
  from docx._buffer import flush_all
@@ -26,12 +26,36 @@ import weakref
26
26
  from contextlib import contextmanager
27
27
  from typing import TYPE_CHECKING, Any, Generator
28
28
 
29
+ from docx import _ptc
29
30
  from docx.commands import Command, must_flush_immediately
30
31
 
31
32
  if TYPE_CHECKING:
32
33
  from docx._http_doc import HttpClient
33
34
 
34
35
 
36
+ def _ptc_emit_end_batch(cmds: list[Command], *, is_error: bool) -> None:
37
+ """Emit a PTC ``end`` event for every cmd that received a ``begin``.
38
+
39
+ Tolerant: missing call_ids (cmd was added before PTC env was set) and
40
+ PTC client exceptions both silently no-op.
41
+ """
42
+ for c in cmds:
43
+ call_id = getattr(c, "_ptc_call_id", None)
44
+ if not call_id:
45
+ continue
46
+ try:
47
+ _ptc.emit_end(
48
+ call_id=call_id,
49
+ tool_name=type(c).__name__,
50
+ result={"ok": not is_error}
51
+ if not is_error
52
+ else {"ok": False, "error": "command batch failed"},
53
+ is_error=is_error,
54
+ )
55
+ except Exception: # noqa: BLE001
56
+ pass
57
+
58
+
35
59
  # ---------------------------------------------------------------------------
36
60
  # Process-wide registry for flush_all()
37
61
  # ---------------------------------------------------------------------------
@@ -130,8 +154,45 @@ class CommandBuffer:
130
154
  # original semantics.
131
155
  self._change_mode: str | None = None
132
156
  self._user: dict[str, str] | None = None
157
+ # Batch mode flag — when True, Create* commands buffer instead
158
+ # of flushing immediately, and proxies bind to a client-side
159
+ # UUID instead of waiting for the server response. See
160
+ # :meth:`Document.batch` and ``PERFORMANCE_BATCHING_ANALYSIS.md``.
161
+ self._batch_depth: int = 0
162
+ # client_node_id → list of (proxy, attr) pairs to update with the
163
+ # real nodeId after flush. Populated by ``add_paragraph`` /
164
+ # ``add_heading`` / etc. when they queue a Create with a
165
+ # client-assigned id. Drained on every flush.
166
+ self._proxy_id_refs: dict[
167
+ str, list[tuple[object, str]]
168
+ ] = {}
133
169
  _register(self)
134
170
 
171
+ @property
172
+ def is_batching(self) -> bool:
173
+ """``True`` if a ``with doc.batch():`` block is currently open.
174
+
175
+ SDK call sites read this to decide whether to defer Create*
176
+ commands or eager-flush them. Reentrant — nested ``batch()``
177
+ blocks increment the counter and decrement on exit; the outer
178
+ block is what actually flushes.
179
+ """
180
+ return self._batch_depth > 0
181
+
182
+ def register_proxy_id_ref(
183
+ self, client_id: str, proxy: object, attr: str = "_node_id"
184
+ ) -> None:
185
+ """Register that ``proxy.<attr>`` should be rewritten from
186
+ ``client_id`` to the real node id once the current batch flushes.
187
+
188
+ Called by ``Document.add_paragraph`` (and equivalents) when they
189
+ queue a Create with a ``client_node_id``. The flush loop walks
190
+ the per-cmd result for ``real_node_id`` / ``real_entity_id``
191
+ echoes and applies setattr to every registered ref.
192
+ """
193
+ with self._lock:
194
+ self._proxy_id_refs.setdefault(client_id, []).append((proxy, attr))
195
+
135
196
  @property
136
197
  def asset_id(self) -> str:
137
198
  return self._asset_id
@@ -193,19 +254,35 @@ class CommandBuffer:
193
254
  def call(self, cmd: Command) -> Any:
194
255
  """Execute or buffer ``cmd``.
195
256
 
196
- For eager commands (queries, creates), drains the pending queue and
197
- runs ``cmd`` in the same batch; returns the per-cmd result dict.
257
+ For eager commands (queries, response-bearing creates), drains
258
+ the pending queue and runs ``cmd`` in the same batch; returns
259
+ the per-cmd result dict.
198
260
 
199
261
  For pure mutations, appends to the queue, resets the idle timer,
200
- and returns ``None``. The caller MUST NOT rely on the return value
201
- of a buffered mutation — it's not available until flush.
262
+ and returns ``None``. The caller MUST NOT rely on the return
263
+ value of a buffered mutation — it's not available until flush.
264
+
265
+ **Batch mode:** when ``self.is_batching`` is True AND the command
266
+ carries a non-None ``client_node_id`` / ``client_entity_id``
267
+ (Create*, CommentsCreate), the command is buffered rather than
268
+ eager-flushed. The caller is expected to have pre-stamped a
269
+ proxy with the client-side id; after the batch flushes, the
270
+ applier's ``real_node_id`` / ``real_entity_id`` echo lets the
271
+ SDK rewrite the proxy in-place.
202
272
  """
203
273
  if self._closed:
204
274
  raise RuntimeError(
205
275
  f"CommandBuffer for {self._asset_id} is closed",
206
276
  )
207
277
 
208
- if must_flush_immediately(cmd):
278
+ # PTC begin: one event per user-facing method call, before any
279
+ # batching. Failures here can't crash the user's code path.
280
+ try:
281
+ cmd._ptc_call_id = _ptc.emit_begin(type(cmd).__name__, cmd.to_dict()) # type: ignore[attr-defined]
282
+ except Exception: # noqa: BLE001
283
+ pass
284
+
285
+ if must_flush_immediately(cmd) and not self._is_batched_create(cmd):
209
286
  return self._eager_flush_with(cmd)
210
287
 
211
288
  with self._lock:
@@ -213,11 +290,36 @@ class CommandBuffer:
213
290
  self._reset_timer_locked()
214
291
  return None
215
292
 
293
+ def _is_batched_create(self, cmd: Command) -> bool:
294
+ """Return True iff this command can be safely deferred because
295
+ the caller pre-assigned a client-side id and the buffer is in
296
+ batch mode.
297
+
298
+ We only defer Create-shape commands (those with
299
+ ``client_node_id`` / ``client_entity_id`` set). Pure queries
300
+ like ``BlocksList`` must still eager-flush — the caller is
301
+ awaiting their data. Read-bearing creates without a client id
302
+ (legacy callers / non-batch path) also stay eager so we don't
303
+ change their semantics.
304
+ """
305
+ if not self.is_batching:
306
+ return False
307
+ cli_node = getattr(cmd, "client_node_id", None)
308
+ cli_ent = getattr(cmd, "client_entity_id", None)
309
+ return cli_node is not None or cli_ent is not None
310
+
216
311
  def flush(self) -> list[Any]:
217
312
  """Flush pending commands as one HTTP batch.
218
313
 
219
314
  Returns the list of per-command result dicts. Empty list if nothing
220
315
  was pending. Cancels any active idle timer.
316
+
317
+ After the batch returns, walks per-cmd results for
318
+ ``real_node_id`` / ``real_entity_id`` echoes and updates any
319
+ proxies registered via :meth:`register_proxy_id_ref` with the
320
+ resolved server ids. This is what completes the round-trip for
321
+ ``with doc.batch():`` — the SDK's caller-facing proxies pick up
322
+ real ids transparently after the block exits.
221
323
  """
222
324
  with self._lock:
223
325
  self._cancel_timer_locked()
@@ -225,37 +327,86 @@ class CommandBuffer:
225
327
  self._pending = []
226
328
  change_mode = self._change_mode
227
329
  user = None if self._user is None else dict(self._user)
330
+ proxy_id_refs = self._proxy_id_refs
331
+ self._proxy_id_refs = {}
228
332
  if not pending:
229
333
  return []
230
- return self._client.execute_batch(
231
- self._asset_id,
232
- pending,
233
- change_mode=change_mode,
234
- user=user,
235
- )
334
+ try:
335
+ results = self._client.execute_batch(
336
+ self._asset_id,
337
+ pending,
338
+ change_mode=change_mode,
339
+ user=user,
340
+ )
341
+ except Exception:
342
+ _ptc_emit_end_batch(pending, is_error=True)
343
+ raise
344
+ _ptc_emit_end_batch(pending, is_error=False)
345
+ # Rewrite registered proxies from client-side UUIDs to the real
346
+ # SuperDoc ids the applier echoed back. Defensive: tolerate
347
+ # missing fields (legacy or transitional applier without the
348
+ # client-id support).
349
+ if proxy_id_refs:
350
+ for result in results:
351
+ if not isinstance(result, dict):
352
+ continue
353
+ for cli_key, real_key in (
354
+ ("client_node_id", "real_node_id"),
355
+ ("client_entity_id", "real_entity_id"),
356
+ ):
357
+ cli = result.get(cli_key)
358
+ real = result.get(real_key)
359
+ if not (isinstance(cli, str) and isinstance(real, str)):
360
+ continue
361
+ refs = proxy_id_refs.pop(cli, [])
362
+ for proxy, attr in refs:
363
+ try:
364
+ setattr(proxy, attr, real)
365
+ except Exception: # noqa: BLE001
366
+ # A proxy that rejected setattr (slots without
367
+ # the attr, frozen dataclass, etc.) silently
368
+ # keeps its client_id — the rewriter in the
369
+ # applier will still resolve it correctly
370
+ # for the next batch.
371
+ pass
372
+ return results
236
373
 
237
374
  @contextmanager
238
375
  def batch(self) -> Generator[None, None, None]:
239
376
  """Group calls into one HTTP batch.
240
377
 
241
- Inside the ``with`` block, eager commands still flush immediately
242
- (we can't buffer reads — the caller is awaiting their result), but
243
- pure mutations accumulate without their idle timer firing.
244
- On exit, drains anything left.
378
+ Inside the ``with`` block:
379
+
380
+ - Queries (``BlocksList``, ``Find``, ``GetNodeById``, …) still
381
+ eager-flush — the caller is awaiting their result, so we have
382
+ no choice.
383
+ - Pure mutations (formatters, setters) accumulate without their
384
+ idle timer firing.
385
+ - Create*-shape commands that carry a ``client_node_id`` /
386
+ ``client_entity_id`` (set by ``Document.add_paragraph`` and
387
+ friends when they detect batch mode) also accumulate — the
388
+ server resolves the client UUIDs to real SuperDoc ids in a
389
+ single batch via the per-request ``clientIdMap``.
390
+
391
+ On exit, drains anything left. Reentrant: nested ``batch()``
392
+ blocks share the same accumulating queue; only the outermost
393
+ block flushes.
245
394
  """
246
- # Cancel the idle timer for the duration of the block; we'll flush
247
- # explicitly on exit. New adds inside the block go straight onto
248
- # _pending without rescheduling the timer.
249
395
  with self._lock:
250
396
  self._cancel_timer_locked()
251
397
  old_window = self._auto_flush_seconds
252
- self._auto_flush_seconds = 0.0 # disable scheduling
398
+ self._auto_flush_seconds = 0.0 # disable timer-scheduled flush
399
+ self._batch_depth += 1
253
400
  try:
254
401
  yield
255
402
  finally:
256
403
  with self._lock:
257
- self._auto_flush_seconds = old_window
258
- self.flush()
404
+ self._batch_depth -= 1
405
+ outermost = self._batch_depth == 0
406
+ if outermost:
407
+ self._auto_flush_seconds = old_window
408
+ if outermost:
409
+ self.flush()
259
410
 
260
411
  def close(self) -> None:
261
412
  """Flush and disable. Idempotent."""
@@ -270,20 +421,54 @@ class CommandBuffer:
270
421
  # ----- internals -----
271
422
 
272
423
  def _eager_flush_with(self, cmd: Command) -> Any:
273
- """Drain pending + run ``cmd`` in one batch; return cmd's result."""
424
+ """Drain pending + run ``cmd`` in one batch; return cmd's result.
425
+
426
+ When pending commands include batched Creates with registered
427
+ proxy refs (e.g. a query mid-batch triggers an eager flush
428
+ before the ``with doc.batch():`` block ends), the proxies still
429
+ get their real ids written back via the same flush-time rewrite
430
+ that :meth:`flush` does.
431
+ """
274
432
  with self._lock:
275
433
  self._cancel_timer_locked()
276
434
  pending = self._pending
277
435
  self._pending = []
278
436
  change_mode = self._change_mode
279
437
  user = None if self._user is None else dict(self._user)
438
+ proxy_id_refs = self._proxy_id_refs
439
+ self._proxy_id_refs = {}
280
440
  all_cmds: list[Command] = [*pending, cmd]
281
- results: list[Any] = self._client.execute_batch(
282
- self._asset_id,
283
- all_cmds,
284
- change_mode=change_mode,
285
- user=user,
286
- )
441
+ try:
442
+ results: list[Any] = self._client.execute_batch(
443
+ self._asset_id,
444
+ all_cmds,
445
+ change_mode=change_mode,
446
+ user=user,
447
+ )
448
+ except Exception:
449
+ _ptc_emit_end_batch(all_cmds, is_error=True)
450
+ raise
451
+ _ptc_emit_end_batch(all_cmds, is_error=False)
452
+ # Apply the same proxy-rewrite pass as ``flush()`` — see comment
453
+ # there for the contract.
454
+ if proxy_id_refs:
455
+ for result in results:
456
+ if not isinstance(result, dict):
457
+ continue
458
+ for cli_key, real_key in (
459
+ ("client_node_id", "real_node_id"),
460
+ ("client_entity_id", "real_entity_id"),
461
+ ):
462
+ cli = result.get(cli_key)
463
+ real = result.get(real_key)
464
+ if not (isinstance(cli, str) and isinstance(real, str)):
465
+ continue
466
+ refs = proxy_id_refs.pop(cli, [])
467
+ for proxy, attr in refs:
468
+ try:
469
+ setattr(proxy, attr, real)
470
+ except Exception: # noqa: BLE001
471
+ pass
287
472
  if not results:
288
473
  return {}
289
474
  return results[-1]
@@ -110,10 +110,87 @@ from docx.commands import (
110
110
  from docx.errors import (
111
111
  AuthenticationError,
112
112
  DocxError,
113
+ NotFoundError,
113
114
  SessionError,
114
115
  )
115
116
 
116
117
 
118
+ _NOT_FOUND_ERROR_NAMES: frozenset[str] = frozenset(
119
+ {
120
+ "notfound",
121
+ "notfounderror",
122
+ "entitynotfound",
123
+ "entitynotfounderror",
124
+ "commentnotfound",
125
+ "commentnotfounderror",
126
+ "nodenotfound",
127
+ "nodenotfounderror",
128
+ "missingnode",
129
+ "missingnodeerror",
130
+ }
131
+ )
132
+
133
+
134
+ def _looks_like_not_found(parsed: dict) -> bool:
135
+ """Inspect a docx-studio partial-failure dict and decide whether it
136
+ describes a "no such entity" miss (vs. a real transport / validation
137
+ failure).
138
+
139
+ Prefers a structured signal (the server-side error class name, or a
140
+ typed ``code`` field) and falls back to a broadened set of message
141
+ substrings so phrasing changes upstream don't silently re-route a
142
+ miss into a transport error.
143
+ """
144
+ if not isinstance(parsed, dict):
145
+ return False
146
+ err_obj = parsed.get("error")
147
+ code = parsed.get("code")
148
+ name_candidates: list[str] = []
149
+ if isinstance(err_obj, str):
150
+ name_candidates.append(err_obj)
151
+ elif isinstance(err_obj, dict):
152
+ for key in ("name", "code", "type", "kind"):
153
+ v = err_obj.get(key)
154
+ if isinstance(v, str):
155
+ name_candidates.append(v)
156
+ if isinstance(code, str):
157
+ name_candidates.append(code)
158
+ for name in name_candidates:
159
+ if name.replace("_", "").replace("-", "").lower() in _NOT_FOUND_ERROR_NAMES:
160
+ return True
161
+ if parsed.get("status") == 404 or parsed.get("statusCode") == 404:
162
+ return True
163
+ # Broad substring fallback (case-insensitive) — covers phrasing
164
+ # variants the server (or SuperDoc) may use before a typed error
165
+ # code lands. Includes the original 3 patterns plus additional
166
+ # common phrasings.
167
+ msg_parts: list[str] = []
168
+ for key in ("message", "detail", "error"):
169
+ v = parsed.get(key)
170
+ if isinstance(v, str):
171
+ msg_parts.append(v)
172
+ elif isinstance(v, dict):
173
+ inner = v.get("message")
174
+ if isinstance(inner, str):
175
+ msg_parts.append(inner)
176
+ haystack = " ".join(msg_parts).lower()
177
+ if not haystack:
178
+ return False
179
+ return any(
180
+ needle in haystack
181
+ for needle in (
182
+ "not found",
183
+ "no such",
184
+ "does not exist",
185
+ "doesn't exist",
186
+ "doesnt exist",
187
+ "404",
188
+ "unknown id",
189
+ "missing entity",
190
+ )
191
+ )
192
+
193
+
117
194
  def _user_agent() -> str:
118
195
  # Lazy import — docx/__init__.py imports api.py at package load, but
119
196
  # _http_doc is only imported lazily from client.py at session-open
@@ -200,6 +277,16 @@ def _http_post_json(
200
277
  parsed = json.loads(body) if body else {}
201
278
  except json.JSONDecodeError:
202
279
  parsed = {"raw": body}
280
+ # If the failing command's error looks like "no such entity",
281
+ # raise a typed :class:`NotFoundError` so speculative-read call
282
+ # sites (``Comments.get``) can coerce it to ``None`` without
283
+ # swallowing real transport / validation failures.
284
+ err_obj = parsed.get("error") if isinstance(parsed, dict) else None
285
+ if isinstance(err_obj, dict) and _looks_like_not_found(err_obj):
286
+ raise NotFoundError(
287
+ f"docx-studio batch reported a partial failure: {parsed!r}",
288
+ payload=err_obj,
289
+ )
203
290
  raise DocxError(
204
291
  f"docx-studio batch reported a partial failure: {parsed!r}",
205
292
  )
@@ -217,6 +304,11 @@ def _http_post_json(
217
304
  raise AuthenticationError(
218
305
  f"docx-studio rejected the request (HTTP {resp.status_code}): {err_body}",
219
306
  )
307
+ if resp.status_code == 404:
308
+ raise NotFoundError(
309
+ f"docx-studio returned HTTP 404: {err_body}",
310
+ payload={"status": 404, "body": err_body},
311
+ )
220
312
  raise DocxError(
221
313
  f"docx-studio returned HTTP {resp.status_code}: {err_body}",
222
314
  )
@@ -0,0 +1,173 @@
1
+ """Programmatic Tool Calling (PTC) — client side, docx SDK.
2
+
3
+ Activated only when ``ATHENA_PTC_URL`` is set. The URL is a presigned
4
+ endpoint URL with an HMAC-signed token embedded as ``?t=…``; the token
5
+ carries the (thread, parent_tool_call_id, run) triple and an expiry.
6
+ The SDK doesn't need to know that triple — it just POSTs to the URL
7
+ verbatim, and the server derives identity from the token.
8
+
9
+ Without ``ATHENA_PTC_URL`` set, every call here is a no-op.
10
+
11
+ Emits are fire-and-forget on a background daemon thread:
12
+
13
+ - Never raise into user code.
14
+ - Never block the calling thread.
15
+ - Re-read ``ATHENA_PTC_URL`` on every emit so updates between sandbox
16
+ executions take effect immediately (the SDK module itself is cached
17
+ across runs).
18
+ - Snapshot the URL at ``emit_begin`` time and carry it to ``emit_end``
19
+ so late end events can't be misattributed if a new sandbox run
20
+ swapped in a different URL between begin and end.
21
+
22
+ This module is *intentionally* duplicated into the pptx and xlsx SDKs
23
+ — the three SDKs have no shared base. Keeping it byte-identical
24
+ (modulo ``_LIB_NAME``) makes diffs trivial.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import os
31
+ import queue
32
+ import threading
33
+ import time
34
+ import urllib.error
35
+ import urllib.request
36
+ import uuid
37
+ from typing import Any
38
+
39
+ _LIB_NAME = "docx"
40
+
41
+ _MAX_QUEUE = 4096
42
+ _MAX_BODY = 64 * 1024
43
+ _HTTP_TIMEOUT = 2.0
44
+
45
+ _PTC_URL_ENV = "ATHENA_PTC_URL"
46
+
47
+
48
+ def _read_url() -> str | None:
49
+ url = os.environ.get(_PTC_URL_ENV)
50
+ return url if url else None
51
+
52
+
53
+ def _utcnow_iso() -> str:
54
+ return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())
55
+
56
+
57
+ class _PTCClient:
58
+ def __init__(self) -> None:
59
+ # Lazy: don't snapshot URL at import time; runs change between
60
+ # sandbox executions and the module is cached in sys.modules.
61
+ self._q: queue.Queue[tuple[str, dict[str, Any]] | None] = queue.Queue(
62
+ maxsize=_MAX_QUEUE,
63
+ )
64
+ self._started = False
65
+ self._lock = threading.Lock()
66
+ # call_id -> URL snapshot at begin time. Lets emit_end target
67
+ # the original run's endpoint even if env changed since.
68
+ self._call_url: dict[str, str] = {}
69
+
70
+ def _ensure_started(self) -> None:
71
+ if self._started:
72
+ return
73
+ with self._lock:
74
+ if self._started:
75
+ return
76
+ t = threading.Thread(
77
+ target=self._drain,
78
+ name=f"athena-{_LIB_NAME}-ptc",
79
+ daemon=True,
80
+ )
81
+ t.start()
82
+ self._started = True
83
+
84
+ def emit_begin(self, tool_name: str, args: dict[str, Any]) -> str:
85
+ call_id = uuid.uuid4().hex
86
+ url = _read_url()
87
+ if url is None:
88
+ return call_id
89
+ self._call_url[call_id] = url
90
+ self._ensure_started()
91
+ body = {
92
+ "callId": call_id,
93
+ "toolName": f"{_LIB_NAME}.{tool_name}",
94
+ "phase": "begin",
95
+ "args": args,
96
+ "ts": _utcnow_iso(),
97
+ }
98
+ self._enqueue(url, body)
99
+ return call_id
100
+
101
+ def emit_end(
102
+ self,
103
+ *,
104
+ call_id: str,
105
+ tool_name: str,
106
+ result: dict[str, Any] | None,
107
+ is_error: bool,
108
+ ) -> None:
109
+ url = self._call_url.pop(call_id, None)
110
+ if url is None:
111
+ return # PTC was disabled at begin, or begin wasn't emitted
112
+ body = {
113
+ "callId": call_id,
114
+ "toolName": f"{_LIB_NAME}.{tool_name}",
115
+ "phase": "end",
116
+ "result": result,
117
+ "isError": is_error,
118
+ "ts": _utcnow_iso(),
119
+ }
120
+ self._enqueue(url, body)
121
+
122
+ def _enqueue(self, url: str, body: dict[str, Any]) -> None:
123
+ try:
124
+ self._q.put_nowait((url, body))
125
+ except queue.Full:
126
+ pass # drop on backpressure; never block user code
127
+
128
+ def _drain(self) -> None:
129
+ while True:
130
+ item = self._q.get()
131
+ if item is None:
132
+ return
133
+ url, body = item
134
+ try:
135
+ raw = json.dumps(body).encode("utf-8")
136
+ if len(raw) > _MAX_BODY:
137
+ truncated_key = "args" if body.get("phase") == "begin" else "result"
138
+ body[truncated_key] = {"__truncated__": True}
139
+ raw = json.dumps(body).encode("utf-8")
140
+ req = urllib.request.Request(
141
+ url,
142
+ data=raw,
143
+ method="POST",
144
+ headers={"Content-Type": "application/json"},
145
+ )
146
+ urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT).close()
147
+ except (urllib.error.URLError, OSError, ValueError):
148
+ pass # never propagate
149
+
150
+
151
+ _CLIENT = _PTCClient()
152
+
153
+
154
+ def emit_begin(tool_name: str, args: dict[str, Any]) -> str:
155
+ return _CLIENT.emit_begin(tool_name, args)
156
+
157
+
158
+ def emit_end(
159
+ *,
160
+ call_id: str,
161
+ tool_name: str,
162
+ result: dict[str, Any] | None,
163
+ is_error: bool,
164
+ ) -> None:
165
+ _CLIENT.emit_end(
166
+ call_id=call_id,
167
+ tool_name=tool_name,
168
+ result=result,
169
+ is_error=is_error,
170
+ )
171
+
172
+
173
+ __all__ = ["emit_begin", "emit_end"]