athena-python-docx 0.5.1__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 (282) hide show
  1. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/CLAUDE.md +45 -0
  2. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/PKG-INFO +1 -1
  3. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/__init__.py +1 -1
  4. athena_python_docx-0.5.3/docx/_buffer.py +503 -0
  5. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/_http_doc.py +124 -0
  6. athena_python_docx-0.5.3/docx/_ptc.py +173 -0
  7. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/api.py +15 -0
  8. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/commands.py +101 -0
  9. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/comments.py +91 -4
  10. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/document.py +517 -82
  11. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/errors.py +15 -0
  12. athena_python_docx-0.5.3/docx/revisions.py +377 -0
  13. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/section.py +105 -18
  14. athena_python_docx-0.5.3/docx/settings.py +99 -0
  15. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/shared.py +10 -7
  16. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/styles/style.py +252 -3
  17. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/styles/styles.py +64 -1
  18. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/table.py +161 -35
  19. athena_python_docx-0.5.3/docx/text/hyperlink.py +123 -0
  20. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/pagebreak.py +27 -1
  21. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/paragraph.py +52 -5
  22. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/run.py +185 -21
  23. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/pyproject.toml +1 -1
  24. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/scripts/release.sh +16 -9
  25. athena_python_docx-0.5.1/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
  26. athena_python_docx-0.5.1/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
  27. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +1 -0
  28. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -15
  29. athena_python_docx-0.5.1/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
  30. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -6
  31. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +3 -0
  32. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +1 -0
  33. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/23_nested_table.json +2 -0
  34. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -4
  35. athena_python_docx-0.5.1/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
  36. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/35_full_report.json +6 -2
  37. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/40_large_table_10x10.json +100 -0
  38. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +2 -0
  39. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/49_resume_layout.json +3 -0
  40. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +3 -0
  41. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/56_everything_in_one.json +12 -3
  42. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +1 -0
  43. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +1 -0
  44. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +400 -0
  45. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/68_invoice.json +16 -7
  46. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/69_newsletter.json +1 -0
  47. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/72_legal_contract.json +6 -0
  48. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +20 -20
  49. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -4
  50. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -5
  51. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +10 -0
  52. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +1 -0
  53. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/91_many_small_tables.json +100 -0
  54. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +5 -0
  55. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +3 -0
  56. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex04_50x50_table.json +50 -0
  57. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +1 -0
  58. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex10_complex_bom.json +66 -0
  59. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +80 -0
  60. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +1 -0
  61. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +18 -0
  62. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +84 -0
  63. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega01_book_chapter.json +13 -0
  64. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega02_research_proposal.json +18 -0
  65. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega03_financial_statement.json +35 -0
  66. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega05_user_manual.json +28 -0
  67. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +40 -0
  68. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega09_signed_contract.json +12 -0
  69. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega10_api_documentation.json +44 -0
  70. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -8
  71. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -8
  72. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +8 -0
  73. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +16 -0
  74. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw14_nested_cell_table.json +2 -0
  75. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/intentional_deviations.json +31 -1
  76. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/reports/GAP_ANALYSIS.md +4 -3
  77. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/reports/gap_report.json +75 -3
  78. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/snapshots/athena_latest.json +1479 -105
  79. athena_python_docx-0.5.3/tests/test_batching_perf.py +261 -0
  80. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_buffer.py +9 -0
  81. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_http_transport.py +90 -0
  82. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_iter_inner_content.py +37 -0
  83. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_parity_misc.py +19 -0
  84. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_pr19766_review_fixes.py +39 -0
  85. athena_python_docx-0.5.3/tests/test_ptc.py +346 -0
  86. athena_python_docx-0.5.3/tests/test_revisions.py +966 -0
  87. athena_python_docx-0.5.3/tests/test_silent_stub_add_picture.py +82 -0
  88. athena_python_docx-0.5.3/tests/test_silent_stub_comments_get.py +177 -0
  89. athena_python_docx-0.5.3/tests/test_silent_stub_document_audit.py +219 -0
  90. athena_python_docx-0.5.3/tests/test_silent_stub_document_element.py +39 -0
  91. athena_python_docx-0.5.3/tests/test_silent_stub_font_audit.py +356 -0
  92. athena_python_docx-0.5.3/tests/test_silent_stub_header_footer.py +172 -0
  93. athena_python_docx-0.5.3/tests/test_silent_stub_hyperlink.py +263 -0
  94. athena_python_docx-0.5.3/tests/test_silent_stub_misc.py +268 -0
  95. athena_python_docx-0.5.3/tests/test_silent_stub_paragraph_style.py +191 -0
  96. athena_python_docx-0.5.3/tests/test_silent_stub_run_bool_setters.py +99 -0
  97. athena_python_docx-0.5.3/tests/test_silent_stub_section_audit.py +166 -0
  98. athena_python_docx-0.5.3/tests/test_silent_stub_settings.py +68 -0
  99. athena_python_docx-0.5.3/tests/test_silent_stub_shared_audit.py +105 -0
  100. athena_python_docx-0.5.3/tests/test_silent_stub_style.py +157 -0
  101. athena_python_docx-0.5.3/tests/test_silent_stub_styles.py +191 -0
  102. athena_python_docx-0.5.3/tests/test_silent_stub_table_audit.py +247 -0
  103. athena_python_docx-0.5.3/tests/test_silent_stub_table_cell.py +91 -0
  104. athena_python_docx-0.5.3/tests/test_silent_stub_table_dimensions.py +109 -0
  105. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_wire_contract.py +21 -0
  106. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/uv.lock +1 -1
  107. athena_python_docx-0.5.1/docx/_buffer.py +0 -248
  108. athena_python_docx-0.5.1/docx/settings.py +0 -30
  109. athena_python_docx-0.5.1/docx/text/hyperlink.py +0 -53
  110. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/.gitignore +0 -0
  111. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/README.md +0 -0
  112. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/_batching.py +0 -0
  113. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/_http.py +0 -0
  114. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/_image_utils.py +0 -0
  115. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/client.py +0 -0
  116. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/__init__.py +0 -0
  117. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/section.py +0 -0
  118. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/style.py +0 -0
  119. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/table.py +0 -0
  120. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/text.py +0 -0
  121. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/exceptions.py +0 -0
  122. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/opc/__init__.py +0 -0
  123. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/opc/coreprops.py +0 -0
  124. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/shape.py +0 -0
  125. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/styles/__init__.py +0 -0
  126. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/__init__.py +0 -0
  127. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/font.py +0 -0
  128. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/parfmt.py +0 -0
  129. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/tabstops.py +0 -0
  130. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/typing.py +0 -0
  131. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/scripts/publish.sh +0 -0
  132. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/scripts/round_trip_smoke.py +0 -0
  133. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/__init__.py +0 -0
  134. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/conftest.py +0 -0
  135. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/METHODOLOGY.md +0 -0
  136. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/README.md +0 -0
  137. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/__init__.py +0 -0
  138. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/ab_probe_cases.py +0 -0
  139. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/ab_probe_runner.py +0 -0
  140. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/auto_gen_cases.py +0 -0
  141. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/binary_round_trip.py +0 -0
  142. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/cases.py +0 -0
  143. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/complex_cases.py +0 -0
  144. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/coverage_report.py +0 -0
  145. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/extract.py +0 -0
  146. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/extreme_cases.py +0 -0
  147. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/fake_session.py +0 -0
  148. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/local_runner.py +0 -0
  149. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/mega_cases.py +0 -0
  150. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshot.py +0 -0
  151. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
  152. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
  153. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
  154. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
  155. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
  156. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
  157. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
  158. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
  159. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
  160. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
  161. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
  162. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
  163. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
  164. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
  165. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
  166. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
  167. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
  168. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
  169. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
  170. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
  171. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
  172. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
  173. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
  174. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
  175. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
  176. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
  177. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
  178. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
  179. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
  180. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
  181. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
  182. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
  183. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
  184. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
  185. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
  186. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
  187. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
  188. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
  189. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
  190. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
  191. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
  192. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
  193. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
  194. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
  195. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
  196. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
  197. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
  198. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
  199. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
  200. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
  201. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
  202. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
  203. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
  204. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
  205. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
  206. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
  207. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
  208. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
  209. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
  210. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
  211. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
  212. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
  213. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
  214. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
  215. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
  216. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
  217. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
  218. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
  219. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
  220. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
  221. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
  222. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
  223. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
  224. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
  225. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
  226. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
  227. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
  228. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
  229. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
  230. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
  231. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
  232. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
  233. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
  234. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
  235. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
  236. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
  237. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
  238. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
  239. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
  240. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
  241. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
  242. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
  243. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
  244. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
  245. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
  246. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
  247. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
  248. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
  249. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
  250. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
  251. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
  252. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
  253. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/ours_spec.json +0 -0
  254. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/parity_crawl.py +0 -0
  255. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/parity_diff.json +0 -0
  256. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/real_world_cases.py +0 -0
  257. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/round_trip_tests.py +0 -0
  258. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/runner.py +0 -0
  259. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/stock_spec.json +0 -0
  260. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/README.md +0 -0
  261. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/__init__.py +0 -0
  262. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/baseline_gaps.json +0 -0
  263. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/compare.py +0 -0
  264. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/introspect.py +0 -0
  265. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/run_parity.py +0 -0
  266. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
  267. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/test_parity_gap.py +0 -0
  268. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_command_dataclasses.py +0 -0
  269. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_commands.py +0 -0
  270. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_comments.py +0 -0
  271. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_document_create.py +0 -0
  272. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_merged_cells.py +0 -0
  273. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_parity_round2.py +0 -0
  274. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_phase_a_behavior.py +0 -0
  275. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_phase_b_headers_footers.py +0 -0
  276. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_phase_c_tables.py +0 -0
  277. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_python_docx_api_parity.py +0 -0
  278. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_smoke_integration.py +0 -0
  279. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_style_acceptance.py +0 -0
  280. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_style_font.py +0 -0
  281. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_style_setters_contract.py +0 -0
  282. {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_zod_wire_contract.py +0 -0
@@ -76,6 +76,44 @@ not file-backed. Each is documented in the relevant docstring.
76
76
  hits `POST {base_url}/docs/empty`. The constructor positional-arg
77
77
  shape (`Document(asset_id)`) is preserved for parity.
78
78
 
79
+ - **`Document.save(path_or_stream=None)`** — argument is optional.
80
+ Stock python-docx requires it and `TypeError`s on no-arg, but in an
81
+ asset-backed SDK there is no local file to write — writes are always
82
+ in-place against the Y.Doc. Forcing the upstream signature broke
83
+ every agent invocation that reflexively called `doc.save()` (a
84
+ near-universal Python pattern), so we accept it for parity-friendly
85
+ call sites and ignore the value at runtime.
86
+
87
+ - **`Document.track_revisions: bool`** — when `True`, all subsequent
88
+ mutations are recorded as tracked revisions instead of direct edits.
89
+ python-docx upstream has no top-level toggle (the closest is
90
+ hand-edited `<w:trackChanges/>` on `Document.settings.element`).
91
+ We expose this as a Document-level property because every SuperDoc
92
+ mutation accepts a per-call `changeMode` envelope field. The
93
+ python-docx-parity alias `Document.settings.track_revisions` reads
94
+ and writes the same flag so call sites that walk `doc.settings`
95
+ see consistent state.
96
+
97
+ - **`Document.tracked_revisions()` context manager** — scope
98
+ `track_revisions=True` to a `with` block; restores the previous
99
+ value on exit (even on exception). Block-scoped track-changes is a
100
+ Pythonic ergonomic that doesn't exist in upstream python-docx
101
+ because the upstream package has no track-changes API at all.
102
+
103
+ - **`Document.user_name: str | None` / `Document.user_email: str |
104
+ None`** — author identity threaded through to SuperDoc on session-
105
+ open for tracked-change attribution. python-docx's
106
+ `Document.core_properties.last_modified_by` is the closest upstream
107
+ surface; we use the SuperDoc-native fields directly because SuperDoc
108
+ bakes them into the change records.
109
+
110
+ - **`Document.revisions: Revisions`** — collection of tracked changes
111
+ with iteration, `get(id)`, `accept_all()`, `reject_all()`,
112
+ `accept(id)`, `reject(id)`. Per-revision `Revision.accept()` /
113
+ `Revision.reject()` resolve a single change. python-docx upstream
114
+ has zero track-changes API, so this entire surface is an Athena
115
+ extension wired to SuperDoc's `doc.trackChanges.{list,get,decide}`.
116
+
79
117
  ### If you need a deviation
80
118
 
81
119
  If there is a genuine technical reason why a deviation from python-docx is necessary:
@@ -107,6 +145,13 @@ This is a **thin HTTP client** that mimics the sync python-docx API.
107
145
  resolve to `CommandBuffer.call(CreateParagraph(**p))`. Rewriting call
108
146
  sites to construct `Command` dataclasses directly is a possible
109
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.
110
155
 
111
156
  ## Development
112
157
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-docx
3
- Version: 0.5.1
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.1"
9
+ __version__ = "0.5.3"
10
10
 
11
11
  from docx.api import Document
12
12
  from docx._buffer import flush_all
@@ -0,0 +1,503 @@
1
+ """HTTP command buffer for the docx-studio SDK.
2
+
3
+ The buffer sits between SDK call sites and the HTTP transport. Every SDK
4
+ mutation routes through it; we flush eagerly for queries and response-bearing
5
+ ops (creates), and idle-batch pure mutations so a burst of setters becomes
6
+ one HTTP request instead of N.
7
+
8
+ Concurrency model: the SDK serializes calls through ``_batching.run_sync``
9
+ (one persistent event-loop thread), so the buffer's primary thread is the
10
+ loop thread. The auto-flush timer fires on a *different* thread, so we
11
+ guard the pending list with an RLock and treat the timer flush as a
12
+ best-effort coalesce — if the loop thread races us we just flush twice
13
+ (or zero times; the next loop-thread call drains it).
14
+
15
+ `flush_all` is a process-wide hook used by the Daytona sandbox prelude to
16
+ make sure pending writes hit Keryx before the sandbox is suspended. It walks
17
+ a weak-ref registry so dead Workbook instances don't keep their buffers
18
+ alive.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import sys
24
+ import threading
25
+ import weakref
26
+ from contextlib import contextmanager
27
+ from typing import TYPE_CHECKING, Any, Generator
28
+
29
+ from docx import _ptc
30
+ from docx.commands import Command, must_flush_immediately
31
+
32
+ if TYPE_CHECKING:
33
+ from docx._http_doc import HttpClient
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
+
59
+ # ---------------------------------------------------------------------------
60
+ # Process-wide registry for flush_all()
61
+ # ---------------------------------------------------------------------------
62
+
63
+ _active_buffers: list[weakref.ref["CommandBuffer"]] = []
64
+ _registry_lock = threading.Lock()
65
+
66
+
67
+ def _register(buffer: "CommandBuffer") -> None:
68
+ with _registry_lock:
69
+ _active_buffers.append(weakref.ref(buffer))
70
+
71
+
72
+ def _unregister(buffer: "CommandBuffer") -> None:
73
+ """Drop ``buffer`` from the active-registry list.
74
+
75
+ Called from :meth:`CommandBuffer.close` so a long-lived process that
76
+ opens and closes many Documents doesn't accumulate dead weak-refs in
77
+ the registry between ``flush_all`` calls.
78
+ """
79
+ with _registry_lock:
80
+ _active_buffers[:] = [
81
+ ref
82
+ for ref in _active_buffers
83
+ if (b := ref()) is not None and b is not buffer
84
+ ]
85
+
86
+
87
+ def flush_all() -> None:
88
+ """Flush every live CommandBuffer in this process.
89
+
90
+ Used by the Daytona sandbox prelude after user code returns, so
91
+ buffered mutations make it to Keryx before the sandbox is suspended.
92
+ Safe to call when no Buffers exist (no-op). Failures are logged and
93
+ swallowed — we don't want one stuck buffer to mask the rest.
94
+ """
95
+ with _registry_lock:
96
+ snapshot = list(_active_buffers)
97
+ # Compact dead refs while we hold the lock.
98
+ live = [ref for ref in snapshot if ref() is not None]
99
+ _active_buffers[:] = live
100
+
101
+ for ref in live:
102
+ buf = ref()
103
+ if buf is None:
104
+ continue
105
+ try:
106
+ buf.flush()
107
+ except Exception as e: # noqa: BLE001
108
+ sys.stderr.write(
109
+ f"[docx-sdk] flush_all: buffer {buf.asset_id} flush failed: {e}\n",
110
+ )
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # CommandBuffer
115
+ # ---------------------------------------------------------------------------
116
+
117
+ # Idle window before a pure-mutation buffer auto-flushes. Short by design —
118
+ # the SDK's main consumer is agent code that fires sequential property
119
+ # setters within milliseconds of each other; we just need to coalesce those
120
+ # without holding writes back from Keryx for human-perceptible time.
121
+ DEFAULT_AUTO_FLUSH_SECONDS: float = 0.1
122
+
123
+
124
+ class CommandBuffer:
125
+ """Buffers commands for one Document/asset.
126
+
127
+ Behaviour:
128
+ - Queries and response-bearing mutations (creates, inserts that return
129
+ ids) flush immediately. Pending pure mutations are flushed in the
130
+ same batch so ordering is preserved.
131
+ - Pure mutations (formatters, setters) are queued. An idle timer flushes
132
+ them after :data:`DEFAULT_AUTO_FLUSH_SECONDS` of inactivity, OR on
133
+ the next eager call, OR on explicit ``flush()``.
134
+ """
135
+
136
+ def __init__(
137
+ self,
138
+ client: "HttpClient",
139
+ asset_id: str,
140
+ *,
141
+ auto_flush_seconds: float = DEFAULT_AUTO_FLUSH_SECONDS,
142
+ ) -> None:
143
+ self._client: "HttpClient" = client
144
+ self._asset_id: str = asset_id
145
+ self._pending: list[Command] = []
146
+ self._lock: threading.RLock = threading.RLock()
147
+ self._timer: threading.Timer | None = None
148
+ self._auto_flush_seconds: float = auto_flush_seconds
149
+ self._closed: bool = False
150
+ # Track-changes envelope state — propagated as request-level
151
+ # ``changeMode`` and ``user`` on every batch. ``Document``
152
+ # mutates these via :meth:`set_change_mode` / :meth:`set_user`,
153
+ # which flush before changing so prior buffered ops keep their
154
+ # original semantics.
155
+ self._change_mode: str | None = None
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
+ ] = {}
169
+ _register(self)
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
+
196
+ @property
197
+ def asset_id(self) -> str:
198
+ return self._asset_id
199
+
200
+ @property
201
+ def pending_count(self) -> int:
202
+ with self._lock:
203
+ return len(self._pending)
204
+
205
+ @property
206
+ def change_mode(self) -> str | None:
207
+ return self._change_mode
208
+
209
+ @property
210
+ def user(self) -> "dict[str, str] | None":
211
+ return None if self._user is None else dict(self._user)
212
+
213
+ def set_change_mode(self, mode: str | None) -> None:
214
+ """Set the batch-level change mode and flush prior pending ops.
215
+
216
+ Flushing first means commands queued under the previous mode
217
+ retain those semantics — switching modes mid-stream doesn't
218
+ retro-actively re-tag earlier mutations. ``mode`` is one of
219
+ ``"direct"``, ``"tracked"``, or ``None`` to clear.
220
+ """
221
+ if mode is not None and mode not in ("direct", "tracked"):
222
+ raise ValueError(
223
+ f"change_mode must be 'direct', 'tracked', or None; got {mode!r}"
224
+ )
225
+ with self._lock:
226
+ current = self._change_mode
227
+ if current == mode:
228
+ return
229
+ # Drain any queued commands with the *previous* mode before
230
+ # adopting the new one.
231
+ self.flush()
232
+ with self._lock:
233
+ self._change_mode = mode
234
+
235
+ def set_user(self, name: str | None, email: str | None = None) -> None:
236
+ """Set the batch-level user identity.
237
+
238
+ Sent on every commands request envelope. SuperDoc honors the
239
+ identity at session-open time only — subsequent batches against
240
+ a pooled session ignore the value, but the payload is still
241
+ included so a fresh session picks it up.
242
+ """
243
+ if name is None:
244
+ with self._lock:
245
+ self._user = None
246
+ return
247
+ if not name:
248
+ raise ValueError("user name must be a non-empty string")
249
+ with self._lock:
250
+ self._user = {"name": name}
251
+ if email:
252
+ self._user["email"] = email
253
+
254
+ def call(self, cmd: Command) -> Any:
255
+ """Execute or buffer ``cmd``.
256
+
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.
260
+
261
+ For pure mutations, appends to the queue, resets the idle timer,
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.
272
+ """
273
+ if self._closed:
274
+ raise RuntimeError(
275
+ f"CommandBuffer for {self._asset_id} is closed",
276
+ )
277
+
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):
286
+ return self._eager_flush_with(cmd)
287
+
288
+ with self._lock:
289
+ self._pending.append(cmd)
290
+ self._reset_timer_locked()
291
+ return None
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
+
311
+ def flush(self) -> list[Any]:
312
+ """Flush pending commands as one HTTP batch.
313
+
314
+ Returns the list of per-command result dicts. Empty list if nothing
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.
323
+ """
324
+ with self._lock:
325
+ self._cancel_timer_locked()
326
+ pending = self._pending
327
+ self._pending = []
328
+ change_mode = self._change_mode
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 = {}
332
+ if not pending:
333
+ return []
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
373
+
374
+ @contextmanager
375
+ def batch(self) -> Generator[None, None, None]:
376
+ """Group calls into one HTTP batch.
377
+
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.
394
+ """
395
+ with self._lock:
396
+ self._cancel_timer_locked()
397
+ old_window = self._auto_flush_seconds
398
+ self._auto_flush_seconds = 0.0 # disable timer-scheduled flush
399
+ self._batch_depth += 1
400
+ try:
401
+ yield
402
+ finally:
403
+ with self._lock:
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()
410
+
411
+ def close(self) -> None:
412
+ """Flush and disable. Idempotent."""
413
+ if self._closed:
414
+ return
415
+ try:
416
+ self.flush()
417
+ finally:
418
+ self._closed = True
419
+ _unregister(self)
420
+
421
+ # ----- internals -----
422
+
423
+ def _eager_flush_with(self, cmd: Command) -> Any:
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
+ """
432
+ with self._lock:
433
+ self._cancel_timer_locked()
434
+ pending = self._pending
435
+ self._pending = []
436
+ change_mode = self._change_mode
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 = {}
440
+ all_cmds: list[Command] = [*pending, cmd]
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
472
+ if not results:
473
+ return {}
474
+ return results[-1]
475
+
476
+ def _reset_timer_locked(self) -> None:
477
+ # Caller must hold self._lock.
478
+ self._cancel_timer_locked()
479
+ if self._auto_flush_seconds <= 0:
480
+ return # disabled (e.g. inside batch())
481
+ self._timer = threading.Timer(
482
+ self._auto_flush_seconds,
483
+ self._auto_flush,
484
+ )
485
+ self._timer.daemon = True
486
+ self._timer.start()
487
+
488
+ def _cancel_timer_locked(self) -> None:
489
+ # Caller must hold self._lock.
490
+ if self._timer is not None:
491
+ self._timer.cancel()
492
+ self._timer = None
493
+
494
+ def _auto_flush(self) -> None:
495
+ try:
496
+ self.flush()
497
+ except Exception as e: # noqa: BLE001
498
+ sys.stderr.write(
499
+ f"[docx-sdk] auto-flush failed for {self._asset_id}: {e}\n",
500
+ )
501
+
502
+
503
+ __all__ = ["CommandBuffer", "flush_all", "DEFAULT_AUTO_FLUSH_SECONDS"]