athena-python-docx 0.2.3__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. athena_python_docx-0.5.0/CLAUDE.md +118 -0
  2. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/PKG-INFO +7 -3
  3. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/__init__.py +3 -1
  4. athena_python_docx-0.5.0/docx/_buffer.py +248 -0
  5. athena_python_docx-0.5.0/docx/_http.py +189 -0
  6. athena_python_docx-0.5.0/docx/_http_doc.py +518 -0
  7. athena_python_docx-0.5.0/docx/_image_utils.py +63 -0
  8. athena_python_docx-0.5.0/docx/api.py +73 -0
  9. athena_python_docx-0.5.0/docx/client.py +208 -0
  10. athena_python_docx-0.5.0/docx/commands.py +752 -0
  11. athena_python_docx-0.5.0/docx/comments.py +319 -0
  12. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/document.py +260 -38
  13. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/enum/section.py +17 -0
  14. athena_python_docx-0.5.0/docx/enum/style.py +227 -0
  15. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/enum/text.py +13 -3
  16. athena_python_docx-0.5.0/docx/exceptions.py +42 -0
  17. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/opc/coreprops.py +62 -3
  18. athena_python_docx-0.5.0/docx/section.py +689 -0
  19. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/shape.py +97 -10
  20. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/shared.py +12 -12
  21. athena_python_docx-0.5.0/docx/styles/style.py +360 -0
  22. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/styles/styles.py +90 -0
  23. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/table.py +328 -22
  24. athena_python_docx-0.5.0/docx/text/font.py +15 -0
  25. athena_python_docx-0.5.0/docx/text/pagebreak.py +96 -0
  26. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/paragraph.py +262 -44
  27. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/parfmt.py +4 -57
  28. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/run.py +248 -48
  29. athena_python_docx-0.5.0/docx/text/tabstops.py +238 -0
  30. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/pyproject.toml +14 -3
  31. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/README.md +1 -1
  32. athena_python_docx-0.5.0/tests/fidelity/ab_probe_cases.py +520 -0
  33. athena_python_docx-0.5.0/tests/fidelity/ab_probe_runner.py +537 -0
  34. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/binary_round_trip.py +19 -6
  35. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/fake_session.py +286 -51
  36. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/local_runner.py +5 -2
  37. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshot.py +2 -1
  38. athena_python_docx-0.5.0/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +12 -0
  39. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +2 -2
  40. athena_python_docx-0.5.0/tests/fidelity/op_snapshots/29_section_headers_linked.json +6 -0
  41. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/35_full_report.json +1 -1
  42. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +2 -0
  43. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +1 -1
  44. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +2 -0
  45. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +5 -0
  46. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +3 -3
  47. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +10 -0
  48. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +5 -5
  49. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +4 -0
  50. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +7 -0
  51. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +1 -1
  52. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +3 -0
  53. athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +12 -0
  54. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +4 -0
  55. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +20 -0
  56. athena_python_docx-0.2.3/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json → athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw11_header_text.json +1 -0
  57. athena_python_docx-0.2.3/tests/fidelity/op_snapshots/29_section_headers_linked.json → athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw12_first_page_footer.json +2 -1
  58. athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw13_even_page_header.json +5 -0
  59. athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw14_nested_cell_table.json +13 -0
  60. athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +5 -0
  61. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/ours_spec.json +17 -142
  62. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/parity_diff.json +3 -2
  63. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/real_world_cases.py +69 -0
  64. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/runner.py +4 -1
  65. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/stock_spec.json +16 -16
  66. athena_python_docx-0.5.0/tests/parity/README.md +170 -0
  67. athena_python_docx-0.5.0/tests/parity/__init__.py +5 -0
  68. athena_python_docx-0.5.0/tests/parity/baseline_gaps.json +7 -0
  69. athena_python_docx-0.5.0/tests/parity/compare.py +768 -0
  70. athena_python_docx-0.5.0/tests/parity/intentional_deviations.json +189 -0
  71. athena_python_docx-0.5.0/tests/parity/introspect.py +362 -0
  72. athena_python_docx-0.5.0/tests/parity/reports/GAP_ANALYSIS.md +396 -0
  73. athena_python_docx-0.5.0/tests/parity/reports/gap_report.json +5524 -0
  74. athena_python_docx-0.5.0/tests/parity/run_parity.py +212 -0
  75. athena_python_docx-0.5.0/tests/parity/snapshots/athena_latest.json +12717 -0
  76. athena_python_docx-0.5.0/tests/parity/snapshots/upstream_python_docx_1.2.0.json +68840 -0
  77. athena_python_docx-0.5.0/tests/parity/test_parity_gap.py +183 -0
  78. athena_python_docx-0.5.0/tests/test_buffer.py +174 -0
  79. athena_python_docx-0.5.0/tests/test_command_dataclasses.py +144 -0
  80. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/test_commands.py +12 -2
  81. athena_python_docx-0.5.0/tests/test_comments.py +238 -0
  82. athena_python_docx-0.5.0/tests/test_document_create.py +165 -0
  83. athena_python_docx-0.5.0/tests/test_http_transport.py +177 -0
  84. athena_python_docx-0.5.0/tests/test_iter_inner_content.py +75 -0
  85. athena_python_docx-0.5.0/tests/test_merged_cells.py +76 -0
  86. athena_python_docx-0.5.0/tests/test_parity_misc.py +227 -0
  87. athena_python_docx-0.5.0/tests/test_parity_round2.py +566 -0
  88. athena_python_docx-0.5.0/tests/test_phase_a_behavior.py +162 -0
  89. athena_python_docx-0.5.0/tests/test_phase_b_headers_footers.py +193 -0
  90. athena_python_docx-0.5.0/tests/test_phase_c_tables.py +138 -0
  91. athena_python_docx-0.5.0/tests/test_pr19766_review_fixes.py +307 -0
  92. athena_python_docx-0.5.0/tests/test_style_acceptance.py +113 -0
  93. athena_python_docx-0.5.0/tests/test_style_font.py +168 -0
  94. athena_python_docx-0.5.0/tests/test_style_setters_contract.py +125 -0
  95. athena_python_docx-0.5.0/tests/test_wire_contract.py +426 -0
  96. athena_python_docx-0.5.0/tests/test_zod_wire_contract.py +109 -0
  97. athena_python_docx-0.5.0/uv.lock +715 -0
  98. athena_python_docx-0.2.3/CLAUDE.md +0 -63
  99. athena_python_docx-0.2.3/docx/api.py +0 -41
  100. athena_python_docx-0.2.3/docx/client.py +0 -238
  101. athena_python_docx-0.2.3/docx/enum/style.py +0 -64
  102. athena_python_docx-0.2.3/docx/section.py +0 -358
  103. athena_python_docx-0.2.3/docx/styles/style.py +0 -77
  104. athena_python_docx-0.2.3/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -7
  105. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/.gitignore +0 -0
  106. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/README.md +0 -0
  107. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/_batching.py +0 -0
  108. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/enum/__init__.py +0 -0
  109. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/enum/table.py +0 -0
  110. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/errors.py +0 -0
  111. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/opc/__init__.py +0 -0
  112. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/settings.py +0 -0
  113. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/styles/__init__.py +0 -0
  114. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/__init__.py +0 -0
  115. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/hyperlink.py +0 -0
  116. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/typing.py +0 -0
  117. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/scripts/publish.sh +0 -0
  118. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/scripts/release.sh +0 -0
  119. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/__init__.py +0 -0
  120. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/conftest.py +0 -0
  121. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/METHODOLOGY.md +0 -0
  122. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/__init__.py +0 -0
  123. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/auto_gen_cases.py +0 -0
  124. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/cases.py +0 -0
  125. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/complex_cases.py +0 -0
  126. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/coverage_report.py +0 -0
  127. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/extract.py +0 -0
  128. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/extreme_cases.py +0 -0
  129. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/mega_cases.py +0 -0
  130. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
  131. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
  132. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
  133. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
  134. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
  135. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
  136. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
  137. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
  138. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
  139. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
  140. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
  141. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +0 -0
  142. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
  143. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
  144. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
  145. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
  146. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
  147. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
  148. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
  149. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
  150. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
  151. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
  152. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
  153. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
  154. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
  155. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
  156. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
  157. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
  158. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/23_nested_table.json +0 -0
  159. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
  160. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
  161. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
  162. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
  163. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
  164. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
  165. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
  166. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
  167. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
  168. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
  169. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
  170. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
  171. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
  172. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
  173. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
  174. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
  175. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
  176. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
  177. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
  178. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
  179. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
  180. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
  181. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
  182. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
  183. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +0 -0
  184. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
  185. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
  186. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
  187. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
  188. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
  189. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
  190. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
  191. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
  192. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
  193. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
  194. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
  195. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
  196. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
  197. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
  198. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
  199. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
  200. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
  201. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
  202. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
  203. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
  204. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
  205. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
  206. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
  207. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
  208. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
  209. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
  210. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
  211. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
  212. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
  213. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
  214. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
  215. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
  216. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
  217. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
  218. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
  219. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
  220. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
  221. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
  222. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
  223. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
  224. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
  225. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
  226. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
  227. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
  228. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
  229. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +0 -0
  230. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
  231. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
  232. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
  233. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
  234. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
  235. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
  236. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
  237. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
  238. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
  239. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
  240. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
  241. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
  242. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
  243. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
  244. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
  245. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
  246. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
  247. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
  248. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
  249. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
  250. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
  251. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
  252. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
  253. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
  254. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
  255. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
  256. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
  257. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
  258. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
  259. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/parity_crawl.py +0 -0
  260. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/round_trip_tests.py +0 -0
  261. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/test_python_docx_api_parity.py +0 -0
  262. {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/test_smoke_integration.py +0 -0
@@ -0,0 +1,118 @@
1
+ # athena-python-docx SDK — Claude Instructions
2
+
3
+ ## API Parity Rule (MANDATORY)
4
+
5
+ **This SDK MUST be a 100% exact replica of the standard [python-docx](https://python-docx.readthedocs.io/) API.**
6
+
7
+ Every class, method, property, and parameter name must match python-docx exactly. The goal is that any code written for python-docx works identically with this SDK — no surprises, no differences.
8
+
9
+ ### What this means in practice
10
+
11
+ - **Do NOT add new methods** that don't exist in python-docx
12
+ - **Do NOT add new properties** that don't exist in python-docx
13
+ - **Do NOT rename parameters** — use the exact same parameter names as python-docx
14
+ - **Do NOT change method signatures** — if python-docx's `add_heading()` takes `(text, level=1)`, ours must too
15
+ - **Do NOT change return types** — if python-docx's `paragraph.runs` returns `list[Run]`, ours must too
16
+
17
+ ### How to verify parity
18
+
19
+ Before adding or modifying any API surface:
20
+
21
+ 1. Check the [python-docx documentation](https://python-docx.readthedocs.io/)
22
+ 2. Check the [python-docx source code](https://github.com/python-openxml/python-docx)
23
+ 3. Confirm the method/property/parameter exists with the same name and signature
24
+ 4. If it doesn't exist in python-docx, **do not add it** without explicit user approval
25
+
26
+ Run `uv run pytest tests/test_python_docx_api_parity.py -v -s` to verify parity.
27
+
28
+ ### Intentionally omitted (Superdoc SDK limitations)
29
+
30
+ These standard python-docx members don't apply to a Superdoc-backed SDK:
31
+
32
+ - `Paragraph._p`, `Run._r` — XML element access (no local XML)
33
+ - `Document.part`, `Paragraph.part`, `Run.part` — package part access
34
+ - `Document.core_properties.last_modified_by` — we use Keryx attribution instead
35
+ - `Document.settings` — Word app settings (not surfaced by Superdoc)
36
+ - `InlineShape.chart` — charts (Phase 2+)
37
+ - `Comment.add_paragraph` / `Comment.add_table` /
38
+ `Comment.iter_inner_content` / `Comment.paragraphs` /
39
+ `Comment.tables` — SuperDoc models a comment as a single text body,
40
+ not a `BlockItemContainer`. Use `Comment.text` instead. The
41
+ paragraph/table-bearing surface raises `CommentsNotImplementedError`.
42
+
43
+ ### Phase-3b upstream-blocked surface
44
+
45
+ These are *shipped at the surface* but partially or fully no-op at
46
+ runtime, pending SuperDoc SDK changes outside this repo. The
47
+ upstream constraints have been verified against
48
+ [SuperDoc's published docs](https://docs.superdoc.dev/document-api/reference/styles/apply)
49
+ and the bundled TypeScript types in
50
+ `node_modules/@superdoc-dev/sdk/dist/generated/client.d.ts`. See
51
+ `docx-studio/PYTHON_DOCX_PARITY_GAPS.md` § *What unblocks the
52
+ remaining work* for the exact wire-op shape needed to unblock each.
53
+
54
+ - **24 setters on `BaseStyle` / `CharacterStyle` / `ParagraphStyle`**
55
+ (`name`, `style_id`, `hidden`, `locked`, `priority`, `quick_style`,
56
+ `unhide_when_used`, `base_style`, `next_paragraph_style`) emit
57
+ `PendingStyleMutationWarning` and stash on `_overrides`.
58
+ SuperDoc 1.7's `doc.styles.apply` is constrained to
59
+ `target.scope: "docDefaults"` — no per-style metadata variant.
60
+ Behavior pinned by `tests/test_style_setters_contract.py` (11
61
+ tests); when SuperDoc lands a metadata op, those tests will need
62
+ to flip to assert real bus calls.
63
+ - **`Styles.latent_styles`** returns an empty `_LatentStyles` —
64
+ `DocInfoResult.styles` only surfaces `paragraphStyles[]` (a flat
65
+ usage-count list), not the OOXML `<w:latentStyles>` block.
66
+
67
+ ### Intentional deviations (additions / different semantics)
68
+
69
+ These differ from stock python-docx because the SDK is asset-backed,
70
+ not file-backed. Each is documented in the relevant docstring.
71
+
72
+ - **`Document.create(name=, base_url=, api_key=, parent_folder_id=, workspace_id=)`**
73
+ classmethod. Stock python-docx returns a blank in-memory document
74
+ for `Document(None)`; we can't fabricate a SuperDocument asset
75
+ client-side, so net-new asset creation is a separate factory that
76
+ hits `POST {base_url}/docs/empty`. The constructor positional-arg
77
+ shape (`Document(asset_id)`) is preserved for parity.
78
+
79
+ ### If you need a deviation
80
+
81
+ If there is a genuine technical reason why a deviation from python-docx is necessary:
82
+
83
+ 1. **Stop and ask the user** before implementing
84
+ 2. Explain what the deviation is and why it's needed
85
+ 3. Get explicit confirmation that the deviation is acceptable
86
+ 4. Document the deviation in the "Intentionally omitted" list above
87
+
88
+ ## Architecture (0.5.0+)
89
+
90
+ This is a **thin HTTP client** that mimics the sync python-docx API.
91
+
92
+ - Sync façade (matches python-docx) — `doc.save()`, `paragraph.add_run()`
93
+ - Every internal call constructs a typed `Command` dataclass
94
+ (`docx.commands`) and ships it through a `CommandBuffer`
95
+ (`docx._buffer`) which POSTs to docx-studio's `/docs/:id/commands`.
96
+ - HTTP transport: `requests.Session` with a Retry (3 attempts, 0.5s
97
+ backoff, 429/502/503/504). The legacy `transport="direct"` (embedded
98
+ Superdoc CLI + y-websocket from Python) was removed in 0.5.0; agent
99
+ pods no longer embed Superdoc.
100
+ - Batching: queries and response-bearing creates flush eagerly (and
101
+ drain pending pure mutations in the same batch); pure mutations
102
+ buffer with a 100 ms idle timer. `Document.save()` and the context
103
+ manager exit drain explicitly. `docx.flush_all()` is the Daytona
104
+ prelude hook — flushes every live buffer in the process.
105
+ - The path-proxy in `_http_doc.py` is an internal translation layer:
106
+ call sites that look like `await self._session.doc.create.paragraph(p)`
107
+ resolve to `CommandBuffer.call(CreateParagraph(**p))`. Rewriting call
108
+ sites to construct `Command` dataclasses directly is a possible
109
+ follow-up — the wire format is typed end-to-end either way.
110
+
111
+ ## Development
112
+
113
+ ```bash
114
+ uv venv
115
+ uv pip install -e ".[dev]"
116
+ uv run pytest tests/ -x -q
117
+ uv run pytest tests/test_python_docx_api_parity.py -v
118
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-docx
3
- Version: 0.2.3
3
+ Version: 0.5.0
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>
@@ -11,14 +11,18 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Python: >=3.11
14
- Requires-Dist: httpx>=0.27
15
- Requires-Dist: superdoc-sdk>=1.6.0.dev6
14
+ Requires-Dist: requests>=2.28
15
+ Requires-Dist: urllib3>=1.26.6
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: mypy>=1.8; extra == 'dev'
18
18
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
19
19
  Requires-Dist: pytest>=8.0; extra == 'dev'
20
20
  Requires-Dist: python-docx>=1.1; extra == 'dev'
21
+ Requires-Dist: responses>=0.24; extra == 'dev'
21
22
  Requires-Dist: ruff>=0.3; extra == 'dev'
23
+ Provides-Extra: fidelity
24
+ Requires-Dist: httpx>=0.27; extra == 'fidelity'
25
+ Requires-Dist: superdoc-sdk>=1.8.0; extra == 'fidelity'
22
26
  Description-Content-Type: text/markdown
23
27
 
24
28
  # athena-python-docx
@@ -6,9 +6,10 @@ See CLAUDE.md for the API parity contract.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- __version__ = "0.2.3"
9
+ __version__ = "0.5.0"
10
10
 
11
11
  from docx.api import Document
12
+ from docx._buffer import flush_all
12
13
  # Re-exports python-docx ships at docx top-level for convenience.
13
14
  from docx.shared import Emu, Inches, Pt, Cm, Mm, Twips, Length, RGBColor
14
15
 
@@ -22,5 +23,6 @@ __all__ = [
22
23
  "Twips",
23
24
  "Length",
24
25
  "RGBColor",
26
+ "flush_all",
25
27
  "__version__",
26
28
  ]
@@ -0,0 +1,248 @@
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.commands import Command, must_flush_immediately
30
+
31
+ if TYPE_CHECKING:
32
+ from docx._http_doc import HttpClient
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Process-wide registry for flush_all()
37
+ # ---------------------------------------------------------------------------
38
+
39
+ _active_buffers: list[weakref.ref["CommandBuffer"]] = []
40
+ _registry_lock = threading.Lock()
41
+
42
+
43
+ def _register(buffer: "CommandBuffer") -> None:
44
+ with _registry_lock:
45
+ _active_buffers.append(weakref.ref(buffer))
46
+
47
+
48
+ def _unregister(buffer: "CommandBuffer") -> None:
49
+ """Drop ``buffer`` from the active-registry list.
50
+
51
+ Called from :meth:`CommandBuffer.close` so a long-lived process that
52
+ opens and closes many Documents doesn't accumulate dead weak-refs in
53
+ the registry between ``flush_all`` calls.
54
+ """
55
+ with _registry_lock:
56
+ _active_buffers[:] = [
57
+ ref
58
+ for ref in _active_buffers
59
+ if (b := ref()) is not None and b is not buffer
60
+ ]
61
+
62
+
63
+ def flush_all() -> None:
64
+ """Flush every live CommandBuffer in this process.
65
+
66
+ Used by the Daytona sandbox prelude after user code returns, so
67
+ buffered mutations make it to Keryx before the sandbox is suspended.
68
+ Safe to call when no Buffers exist (no-op). Failures are logged and
69
+ swallowed — we don't want one stuck buffer to mask the rest.
70
+ """
71
+ with _registry_lock:
72
+ snapshot = list(_active_buffers)
73
+ # Compact dead refs while we hold the lock.
74
+ live = [ref for ref in snapshot if ref() is not None]
75
+ _active_buffers[:] = live
76
+
77
+ for ref in live:
78
+ buf = ref()
79
+ if buf is None:
80
+ continue
81
+ try:
82
+ buf.flush()
83
+ except Exception as e: # noqa: BLE001
84
+ sys.stderr.write(
85
+ f"[docx-sdk] flush_all: buffer {buf.asset_id} flush failed: {e}\n",
86
+ )
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # CommandBuffer
91
+ # ---------------------------------------------------------------------------
92
+
93
+ # Idle window before a pure-mutation buffer auto-flushes. Short by design —
94
+ # the SDK's main consumer is agent code that fires sequential property
95
+ # setters within milliseconds of each other; we just need to coalesce those
96
+ # without holding writes back from Keryx for human-perceptible time.
97
+ DEFAULT_AUTO_FLUSH_SECONDS: float = 0.1
98
+
99
+
100
+ class CommandBuffer:
101
+ """Buffers commands for one Document/asset.
102
+
103
+ Behaviour:
104
+ - Queries and response-bearing mutations (creates, inserts that return
105
+ ids) flush immediately. Pending pure mutations are flushed in the
106
+ same batch so ordering is preserved.
107
+ - Pure mutations (formatters, setters) are queued. An idle timer flushes
108
+ them after :data:`DEFAULT_AUTO_FLUSH_SECONDS` of inactivity, OR on
109
+ the next eager call, OR on explicit ``flush()``.
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ client: "HttpClient",
115
+ asset_id: str,
116
+ *,
117
+ auto_flush_seconds: float = DEFAULT_AUTO_FLUSH_SECONDS,
118
+ ) -> None:
119
+ self._client: "HttpClient" = client
120
+ self._asset_id: str = asset_id
121
+ self._pending: list[Command] = []
122
+ self._lock: threading.RLock = threading.RLock()
123
+ self._timer: threading.Timer | None = None
124
+ self._auto_flush_seconds: float = auto_flush_seconds
125
+ self._closed: bool = False
126
+ _register(self)
127
+
128
+ @property
129
+ def asset_id(self) -> str:
130
+ return self._asset_id
131
+
132
+ @property
133
+ def pending_count(self) -> int:
134
+ with self._lock:
135
+ return len(self._pending)
136
+
137
+ def call(self, cmd: Command) -> Any:
138
+ """Execute or buffer ``cmd``.
139
+
140
+ For eager commands (queries, creates), drains the pending queue and
141
+ runs ``cmd`` in the same batch; returns the per-cmd result dict.
142
+
143
+ For pure mutations, appends to the queue, resets the idle timer,
144
+ and returns ``None``. The caller MUST NOT rely on the return value
145
+ of a buffered mutation — it's not available until flush.
146
+ """
147
+ if self._closed:
148
+ raise RuntimeError(
149
+ f"CommandBuffer for {self._asset_id} is closed",
150
+ )
151
+
152
+ if must_flush_immediately(cmd):
153
+ return self._eager_flush_with(cmd)
154
+
155
+ with self._lock:
156
+ self._pending.append(cmd)
157
+ self._reset_timer_locked()
158
+ return None
159
+
160
+ def flush(self) -> list[Any]:
161
+ """Flush pending commands as one HTTP batch.
162
+
163
+ Returns the list of per-command result dicts. Empty list if nothing
164
+ was pending. Cancels any active idle timer.
165
+ """
166
+ with self._lock:
167
+ self._cancel_timer_locked()
168
+ pending = self._pending
169
+ self._pending = []
170
+ if not pending:
171
+ return []
172
+ return self._client.execute_batch(self._asset_id, pending)
173
+
174
+ @contextmanager
175
+ def batch(self) -> Generator[None, None, None]:
176
+ """Group calls into one HTTP batch.
177
+
178
+ Inside the ``with`` block, eager commands still flush immediately
179
+ (we can't buffer reads — the caller is awaiting their result), but
180
+ pure mutations accumulate without their idle timer firing.
181
+ On exit, drains anything left.
182
+ """
183
+ # Cancel the idle timer for the duration of the block; we'll flush
184
+ # explicitly on exit. New adds inside the block go straight onto
185
+ # _pending without rescheduling the timer.
186
+ with self._lock:
187
+ self._cancel_timer_locked()
188
+ old_window = self._auto_flush_seconds
189
+ self._auto_flush_seconds = 0.0 # disable scheduling
190
+ try:
191
+ yield
192
+ finally:
193
+ with self._lock:
194
+ self._auto_flush_seconds = old_window
195
+ self.flush()
196
+
197
+ def close(self) -> None:
198
+ """Flush and disable. Idempotent."""
199
+ if self._closed:
200
+ return
201
+ try:
202
+ self.flush()
203
+ finally:
204
+ self._closed = True
205
+ _unregister(self)
206
+
207
+ # ----- internals -----
208
+
209
+ def _eager_flush_with(self, cmd: Command) -> Any:
210
+ """Drain pending + run ``cmd`` in one batch; return cmd's result."""
211
+ with self._lock:
212
+ self._cancel_timer_locked()
213
+ pending = self._pending
214
+ self._pending = []
215
+ all_cmds: list[Command] = [*pending, cmd]
216
+ results: list[Any] = self._client.execute_batch(self._asset_id, all_cmds)
217
+ if not results:
218
+ return {}
219
+ return results[-1]
220
+
221
+ def _reset_timer_locked(self) -> None:
222
+ # Caller must hold self._lock.
223
+ self._cancel_timer_locked()
224
+ if self._auto_flush_seconds <= 0:
225
+ return # disabled (e.g. inside batch())
226
+ self._timer = threading.Timer(
227
+ self._auto_flush_seconds,
228
+ self._auto_flush,
229
+ )
230
+ self._timer.daemon = True
231
+ self._timer.start()
232
+
233
+ def _cancel_timer_locked(self) -> None:
234
+ # Caller must hold self._lock.
235
+ if self._timer is not None:
236
+ self._timer.cancel()
237
+ self._timer = None
238
+
239
+ def _auto_flush(self) -> None:
240
+ try:
241
+ self.flush()
242
+ except Exception as e: # noqa: BLE001
243
+ sys.stderr.write(
244
+ f"[docx-sdk] auto-flush failed for {self._asset_id}: {e}\n",
245
+ )
246
+
247
+
248
+ __all__ = ["CommandBuffer", "flush_all", "DEFAULT_AUTO_FLUSH_SECONDS"]
@@ -0,0 +1,189 @@
1
+ """HTTP bootstrap for Document.create().
2
+
3
+ The SDK's primary transport is y-websocket via Superdoc; this module is
4
+ the *only* HTTP client in the package. It exists solely so that
5
+ ``Document.create()`` can hit ``POST /docs/empty`` on docx-studio to
6
+ provision a new SuperDocument asset and receive a collab bundle to open
7
+ the document with.
8
+
9
+ We use ``urllib`` from stdlib to avoid pulling ``httpx`` / ``requests``
10
+ into the runtime — this code runs in Daytona sandboxes where every MB
11
+ matters, and the existing SDK already keeps its dep tree tight.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import urllib.error
19
+ import urllib.request
20
+ from typing import TypedDict
21
+
22
+ from docx.errors import (
23
+ AuthenticationError,
24
+ DocxError,
25
+ SessionError,
26
+ )
27
+
28
+
29
+ class _CollabBundle(TypedDict):
30
+ """The collab credentials returned alongside a freshly-created asset.
31
+
32
+ Note: env-var-shape (matches what ``Session`` reads), even though
33
+ the wire format from docx-studio uses different key names. The
34
+ keys are renamed at parse time.
35
+ """
36
+
37
+ SUPERDOC_COLLAB_TOKEN: str
38
+ KERYX_WS_URL: str
39
+ ATHENA_WORKSPACE_ID: str
40
+
41
+
42
+ class CreateAssetResult(TypedDict):
43
+ """Parsed response from ``POST /docs/empty``."""
44
+
45
+ asset_id: str
46
+ name: str
47
+ workspace_id: str
48
+ collab: _CollabBundle
49
+
50
+
51
+ _BASE_URL_ENV = "ATHENA_DOCX_BASE_URL"
52
+ _API_KEY_ENV = "ATHENA_DOCX_API_KEY" # noqa: S105
53
+
54
+
55
+ def create_empty_document(
56
+ *,
57
+ base_url: str | None = None,
58
+ api_key: str | None = None,
59
+ name: str | None = None,
60
+ parent_folder_id: str | None = None,
61
+ workspace_id: str | None = None,
62
+ timeout: float = 30.0,
63
+ ) -> CreateAssetResult:
64
+ """Call ``POST {base_url}/docs/empty`` and parse the response.
65
+
66
+ Args:
67
+ base_url: docx-studio API base, e.g. ``https://docx-studio.stg.athenaintel.com``.
68
+ Falls back to ``$ATHENA_DOCX_BASE_URL``.
69
+ api_key: Athena API key (PropelAuth user API key) or PropelAuth
70
+ access token. Sent as ``Authorization: Bearer <api_key>``.
71
+ Falls back to ``$ATHENA_DOCX_API_KEY``.
72
+ name: Optional display title.
73
+ parent_folder_id: Optional parent folder; defaults to workspace root.
74
+ workspace_id: Optional workspace UUID; defaults to caller's
75
+ current workspace.
76
+ timeout: Request timeout in seconds.
77
+
78
+ Returns:
79
+ Parsed response dict with the new asset_id and a collab bundle
80
+ ready to feed into ``Session(..., bundle=...)``.
81
+
82
+ Raises:
83
+ SessionError: missing base_url or unparseable response.
84
+ AuthenticationError: missing api_key, or the server returned 401/403.
85
+ DocxError: any other 4xx/5xx from the server.
86
+ """
87
+ resolved_base: str | None = base_url or os.environ.get(_BASE_URL_ENV)
88
+ resolved_key: str | None = api_key or os.environ.get(_API_KEY_ENV)
89
+
90
+ if not resolved_base:
91
+ raise SessionError(
92
+ f"Missing base_url and {_BASE_URL_ENV} env var. "
93
+ "Pass base_url= to Document.create() or set the env var.",
94
+ )
95
+ if not resolved_key:
96
+ raise AuthenticationError(
97
+ f"Missing api_key and {_API_KEY_ENV} env var. "
98
+ "Pass api_key= to Document.create() or set the env var.",
99
+ )
100
+
101
+ url: str = resolved_base.rstrip("/") + "/docs/empty"
102
+ body: dict[str, str] = {}
103
+ if name is not None:
104
+ body["name"] = name
105
+ if parent_folder_id is not None:
106
+ body["parentFolderId"] = parent_folder_id
107
+ if workspace_id is not None:
108
+ body["workspaceId"] = workspace_id
109
+
110
+ # Lazy import — _http is loaded lazily by Document.create(), so the
111
+ # package is fully initialized by the time we reach this code.
112
+ from docx import __version__
113
+
114
+ payload: bytes = json.dumps(body).encode("utf-8")
115
+ req = urllib.request.Request( # noqa: S310
116
+ url,
117
+ data=payload,
118
+ method="POST",
119
+ headers={
120
+ "Content-Type": "application/json",
121
+ "Authorization": f"Bearer {resolved_key}",
122
+ "Accept": "application/json",
123
+ "User-Agent": f"athena-python-docx/{__version__}",
124
+ },
125
+ )
126
+
127
+ try:
128
+ with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
129
+ raw: bytes = resp.read()
130
+ except urllib.error.HTTPError as e:
131
+ err_body: str = ""
132
+ try:
133
+ err_body = e.read().decode("utf-8", errors="replace")
134
+ except Exception: # noqa: BLE001
135
+ pass
136
+ if e.code in (401, 403):
137
+ raise AuthenticationError(
138
+ f"docx-studio rejected the API key (HTTP {e.code}): {err_body}",
139
+ ) from e
140
+ raise DocxError(
141
+ f"docx-studio /docs/empty returned HTTP {e.code}: {err_body}",
142
+ ) from e
143
+ except urllib.error.URLError as e:
144
+ raise SessionError(
145
+ f"Unable to reach docx-studio at {url}: {e.reason}",
146
+ ) from e
147
+
148
+ try:
149
+ parsed: dict = json.loads(raw.decode("utf-8"))
150
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
151
+ raise SessionError(
152
+ f"docx-studio returned non-JSON response: {raw[:200]!r}",
153
+ ) from e
154
+
155
+ asset_id_obj = parsed.get("assetId")
156
+ name_obj = parsed.get("name")
157
+ ws_obj = parsed.get("workspaceId")
158
+ collab_obj = parsed.get("collab")
159
+ if not (
160
+ isinstance(asset_id_obj, str)
161
+ and isinstance(name_obj, str)
162
+ and isinstance(ws_obj, str)
163
+ and isinstance(collab_obj, dict)
164
+ ):
165
+ raise SessionError(
166
+ f"docx-studio response is missing required fields: {parsed!r}",
167
+ )
168
+ token_obj = collab_obj.get("token")
169
+ ws_url_obj = collab_obj.get("wsUrl")
170
+ bundle_ws_obj = collab_obj.get("workspaceId")
171
+ if not (
172
+ isinstance(token_obj, str)
173
+ and isinstance(ws_url_obj, str)
174
+ and isinstance(bundle_ws_obj, str)
175
+ ):
176
+ raise SessionError(
177
+ f"docx-studio collab bundle is malformed: {collab_obj!r}",
178
+ )
179
+
180
+ return CreateAssetResult(
181
+ asset_id=asset_id_obj,
182
+ name=name_obj,
183
+ workspace_id=ws_obj,
184
+ collab=_CollabBundle(
185
+ SUPERDOC_COLLAB_TOKEN=token_obj,
186
+ KERYX_WS_URL=ws_url_obj,
187
+ ATHENA_WORKSPACE_ID=bundle_ws_obj,
188
+ ),
189
+ )