athena-python-docx 0.2.3__tar.gz → 0.4.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 (212) hide show
  1. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/CLAUDE.md +12 -0
  2. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/PKG-INFO +1 -1
  3. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/__init__.py +1 -1
  4. athena_python_docx-0.4.0/docx/_http.py +189 -0
  5. athena_python_docx-0.4.0/docx/_http_doc.py +181 -0
  6. athena_python_docx-0.4.0/docx/api.py +84 -0
  7. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/client.py +95 -17
  8. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/document.py +88 -2
  9. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/pyproject.toml +1 -1
  10. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/README.md +1 -1
  11. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/fake_session.py +10 -1
  12. athena_python_docx-0.4.0/tests/test_document_create.py +187 -0
  13. athena_python_docx-0.4.0/tests/test_http_transport.py +172 -0
  14. athena_python_docx-0.2.3/docx/api.py +0 -41
  15. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/.gitignore +0 -0
  16. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/README.md +0 -0
  17. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/_batching.py +0 -0
  18. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/__init__.py +0 -0
  19. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/section.py +0 -0
  20. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/style.py +0 -0
  21. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/table.py +0 -0
  22. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/text.py +0 -0
  23. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/errors.py +0 -0
  24. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/opc/__init__.py +0 -0
  25. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/opc/coreprops.py +0 -0
  26. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/section.py +0 -0
  27. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/settings.py +0 -0
  28. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/shape.py +0 -0
  29. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/shared.py +0 -0
  30. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/styles/__init__.py +0 -0
  31. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/styles/style.py +0 -0
  32. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/styles/styles.py +0 -0
  33. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/table.py +0 -0
  34. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/__init__.py +0 -0
  35. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/hyperlink.py +0 -0
  36. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/paragraph.py +0 -0
  37. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/parfmt.py +0 -0
  38. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/run.py +0 -0
  39. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/typing.py +0 -0
  40. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/scripts/publish.sh +0 -0
  41. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/scripts/release.sh +0 -0
  42. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/__init__.py +0 -0
  43. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/conftest.py +0 -0
  44. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/METHODOLOGY.md +0 -0
  45. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/__init__.py +0 -0
  46. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/auto_gen_cases.py +0 -0
  47. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/binary_round_trip.py +0 -0
  48. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/cases.py +0 -0
  49. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/complex_cases.py +0 -0
  50. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/coverage_report.py +0 -0
  51. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/extract.py +0 -0
  52. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/extreme_cases.py +0 -0
  53. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/local_runner.py +0 -0
  54. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/mega_cases.py +0 -0
  55. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshot.py +0 -0
  56. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
  57. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
  58. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
  59. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
  60. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
  61. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
  62. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
  63. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
  64. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
  65. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
  66. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
  67. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
  68. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +0 -0
  69. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
  70. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
  71. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
  72. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
  73. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
  74. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
  75. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
  76. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
  77. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
  78. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
  79. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
  80. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
  81. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
  82. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
  83. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
  84. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
  85. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
  86. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/23_nested_table.json +0 -0
  87. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
  88. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
  89. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
  90. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
  91. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
  92. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
  93. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
  94. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
  95. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
  96. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
  97. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
  98. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
  99. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
  100. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
  101. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
  102. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
  103. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
  104. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
  105. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
  106. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
  107. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
  108. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
  109. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
  110. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
  111. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
  112. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
  113. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
  114. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +0 -0
  115. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
  116. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
  117. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
  118. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
  119. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
  120. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
  121. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
  122. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
  123. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
  124. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
  125. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
  126. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
  127. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
  128. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
  129. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
  130. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
  131. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
  132. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
  133. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
  134. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
  135. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
  136. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
  137. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
  138. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
  139. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
  140. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
  141. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
  142. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
  143. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
  144. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
  145. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
  146. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
  147. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
  148. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
  149. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
  150. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
  151. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
  152. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
  153. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
  154. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
  155. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
  156. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
  157. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
  158. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
  159. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
  160. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
  161. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
  162. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
  163. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +0 -0
  164. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
  165. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
  166. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
  167. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
  168. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
  169. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
  170. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
  171. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
  172. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
  173. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
  174. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
  175. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
  176. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
  177. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
  178. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
  179. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
  180. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
  181. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
  182. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
  183. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
  184. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
  185. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
  186. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
  187. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
  188. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
  189. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
  190. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
  191. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
  192. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
  193. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
  194. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
  195. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
  196. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
  197. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
  198. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
  199. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
  200. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
  201. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
  202. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
  203. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/ours_spec.json +0 -0
  204. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/parity_crawl.py +0 -0
  205. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/parity_diff.json +0 -0
  206. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/real_world_cases.py +0 -0
  207. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/round_trip_tests.py +0 -0
  208. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/runner.py +0 -0
  209. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/stock_spec.json +0 -0
  210. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/test_commands.py +0 -0
  211. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/test_python_docx_api_parity.py +0 -0
  212. {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/test_smoke_integration.py +0 -0
@@ -35,6 +35,18 @@ These standard python-docx members don't apply to a Superdoc-backed SDK:
35
35
  - `Document.settings` — Word app settings (not surfaced by Superdoc)
36
36
  - `InlineShape.chart` — charts (Phase 2+)
37
37
 
38
+ ### Intentional deviations (additions / different semantics)
39
+
40
+ These differ from stock python-docx because the SDK is asset-backed,
41
+ not file-backed. Each is documented in the relevant docstring.
42
+
43
+ - **`Document.create(name=, base_url=, api_key=, parent_folder_id=, workspace_id=)`**
44
+ classmethod. Stock python-docx returns a blank in-memory document
45
+ for `Document(None)`; we can't fabricate a SuperDocument asset
46
+ client-side, so net-new asset creation is a separate factory that
47
+ hits `POST {base_url}/docs/empty`. The constructor positional-arg
48
+ shape (`Document(asset_id)`) is preserved for parity.
49
+
38
50
  ### If you need a deviation
39
51
 
40
52
  If there is a genuine technical reason why a deviation from python-docx is necessary:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-docx
3
- Version: 0.2.3
3
+ Version: 0.4.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>
@@ -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.2.3"
9
+ __version__ = "0.4.0"
10
10
 
11
11
  from docx.api import Document
12
12
  # Re-exports python-docx ships at docx top-level for convenience.
@@ -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
+ )
@@ -0,0 +1,181 @@
1
+ """HTTP-backed Superdoc document handle.
2
+
3
+ This is the core of the ``transport="http"`` path. It mirrors the
4
+ shape of the bound doc API that ``superdoc-sdk`` exposes — every
5
+ existing SDK call site uses ``self._session.doc.<dotted.path>(params)``,
6
+ and ``HttpDocHandle`` matches that shape via a lazy attribute walker.
7
+
8
+ When the user calls ``doc.create.paragraph({...})`` the path proxy
9
+ accumulates the dotted name and the terminal ``__call__`` POSTs a
10
+ single-command request to docx-studio's ``POST /docs/:id/commands``,
11
+ unpacks the dispatcher's ``applied[0].result``, and returns it. The
12
+ existing SDK code expects a Python dict back, which the server returns
13
+ verbatim from Superdoc.
14
+
15
+ The HTTP path is a 1:1 substitute for the embedded Superdoc CLI; no
16
+ SDK call site has to change to opt in.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import urllib.error
23
+ import urllib.request
24
+ from typing import Any
25
+
26
+ from docx.errors import (
27
+ AuthenticationError,
28
+ DocxError,
29
+ SessionError,
30
+ )
31
+
32
+
33
+ def _user_agent() -> str:
34
+ # Lazy import — docx/__init__.py imports api.py at package load, but
35
+ # _http_doc is only imported lazily from client.py at session-open
36
+ # time, so the package is fully initialized by the time this runs.
37
+ from docx import __version__
38
+
39
+ return f"athena-python-docx/{__version__}"
40
+
41
+
42
+ def _http_post_json(
43
+ *,
44
+ url: str,
45
+ api_key: str,
46
+ body: dict,
47
+ timeout: float = 60.0,
48
+ ) -> dict:
49
+ """POST JSON, parse JSON, raise typed errors on non-2xx."""
50
+ payload: bytes = json.dumps(body).encode("utf-8")
51
+ req = urllib.request.Request( # noqa: S310
52
+ url,
53
+ data=payload,
54
+ method="POST",
55
+ headers={
56
+ "Content-Type": "application/json",
57
+ "Authorization": f"Bearer {api_key}",
58
+ "Accept": "application/json",
59
+ "User-Agent": _user_agent(),
60
+ },
61
+ )
62
+ try:
63
+ with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
64
+ raw: bytes = resp.read()
65
+ return json.loads(raw.decode("utf-8"))
66
+ except urllib.error.HTTPError as e:
67
+ err_body: str = ""
68
+ try:
69
+ err_body = e.read().decode("utf-8", errors="replace")
70
+ except Exception: # noqa: BLE001
71
+ pass
72
+ if e.code in (401, 403):
73
+ raise AuthenticationError(
74
+ f"docx-studio rejected the request (HTTP {e.code}): {err_body}",
75
+ ) from e
76
+ if e.code == 207:
77
+ # Partial-success — caller may want to inspect. Re-raise as
78
+ # DocxError but parse the body so they can see what failed.
79
+ try:
80
+ parsed = json.loads(err_body) if err_body else {}
81
+ except json.JSONDecodeError:
82
+ parsed = {"raw": err_body}
83
+ raise DocxError(
84
+ f"docx-studio batch reported a partial failure: {parsed!r}",
85
+ ) from e
86
+ raise DocxError(
87
+ f"docx-studio returned HTTP {e.code}: {err_body}",
88
+ ) from e
89
+ except urllib.error.URLError as e:
90
+ raise SessionError(
91
+ f"Unable to reach docx-studio at {url}: {e.reason}",
92
+ ) from e
93
+
94
+
95
+ class HttpClient:
96
+ """Owns the HTTP transport for a Document.
97
+
98
+ Stateless wrt batching today — every SDK primitive maps to one
99
+ POST /commands with a single-element commands array. Future:
100
+ add a batch buffer that flushes on `with doc.batch():` exit.
101
+ """
102
+
103
+ def __init__(self, *, base_url: str, api_key: str) -> None:
104
+ self._base_url: str = base_url.rstrip("/")
105
+ self._api_key: str = api_key
106
+
107
+ def execute(self, asset_id: str, op: str, params: dict) -> Any:
108
+ """Send one command to /docs/:id/commands and return its result."""
109
+ url: str = f"{self._base_url}/docs/{asset_id}/commands"
110
+ body: dict = {
111
+ "commands": [{"op": op, "params": params}],
112
+ "return": {"snapshot": False},
113
+ }
114
+ resp: dict = _http_post_json(url=url, api_key=self._api_key, body=body)
115
+ applied = resp.get("applied")
116
+ if not isinstance(applied, list) or not applied:
117
+ err = resp.get("error")
118
+ if err:
119
+ raise DocxError(
120
+ f"docx-studio command {op!r} failed: {err!r}",
121
+ )
122
+ raise DocxError(
123
+ f"docx-studio returned no applied entry for {op!r}: {resp!r}",
124
+ )
125
+ result = applied[0].get("result")
126
+ return result if isinstance(result, dict) else {}
127
+
128
+
129
+ class _PathProxy:
130
+ """Lazy attribute walker — turns ``doc.create.paragraph(p)`` into
131
+ ``client.execute(asset_id, "create.paragraph", p)``.
132
+
133
+ The Python SDK's call sites use snake_case dotted paths
134
+ (e.g. ``doc.format.paragraph.set_alignment``) which match the
135
+ Python ``superdoc-sdk`` Bound API. The server-side dispatcher
136
+ converts to camelCase before resolving on the npm SDK's bound API,
137
+ so the wire op-name stays in snake_case and clients across
138
+ languages can share it.
139
+ """
140
+
141
+ __slots__ = ("_client", "_asset_id", "_path")
142
+
143
+ def __init__(self, client: HttpClient, asset_id: str, path: str) -> None:
144
+ self._client: HttpClient = client
145
+ self._asset_id: str = asset_id
146
+ self._path: str = path
147
+
148
+ def __getattr__(self, name: str) -> "_PathProxy":
149
+ # Block Python special names so iteration / hash / etc. don't
150
+ # silently deepen the path.
151
+ if name.startswith("__"):
152
+ raise AttributeError(name)
153
+ new_path: str = f"{self._path}.{name}" if self._path else name
154
+ return _PathProxy(self._client, self._asset_id, new_path)
155
+
156
+ async def __call__(self, params: dict | None = None) -> Any:
157
+ # The existing SDK is async-style (calls go through run_sync);
158
+ # __call__ is async to match. _http_post_json is blocking under
159
+ # the hood, but run_sync runs us on a persistent event-loop
160
+ # thread, so blocking HTTP is fine — it doesn't starve the
161
+ # main thread.
162
+ return self._client.execute(self._asset_id, self._path, params or {})
163
+
164
+
165
+ class HttpDocHandle:
166
+ """Drop-in replacement for ``superdoc.AsyncSuperDocClient.open()``'s
167
+ return value. Existing SDK call sites use ``self._session.doc.<X>(p)``
168
+ where ``X`` is a dotted path on the bound doc API; this object
169
+ forwards every such access to the HTTP transport via _PathProxy.
170
+ """
171
+
172
+ __slots__ = ("_client", "_asset_id")
173
+
174
+ def __init__(self, client: HttpClient, asset_id: str) -> None:
175
+ self._client: HttpClient = client
176
+ self._asset_id: str = asset_id
177
+
178
+ def __getattr__(self, name: str) -> _PathProxy:
179
+ if name.startswith("__"):
180
+ raise AttributeError(name)
181
+ return _PathProxy(self._client, self._asset_id, name)
@@ -0,0 +1,84 @@
1
+ """The `Document` factory — matches python-docx's `docx.Document(path)`.
2
+
3
+ python-docx signature:
4
+ Document(docx=None) -> Document
5
+
6
+ Our signature deviates from the path-based one because we don't open
7
+ .docx files from disk — we open Y.Doc assets from Keryx. The parameter
8
+ is reused: you pass an asset_id string where python-docx would take a
9
+ file path.
10
+
11
+ For net-new asset creation, use the classmethod ``Document.create()``
12
+ (see ``docx.document``). Stock python-docx returns a blank document
13
+ when ``Document(None)`` is called, but our SDK can't fabricate a
14
+ SuperDocument asset out of thin air — there's an Athena-side asset
15
+ write that has to happen first. ``Document.create()`` handles that via
16
+ ``POST /docs/empty`` on docx-studio.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from docx.document import Document as _Document
22
+
23
+
24
+ def Document(
25
+ docx: str | None = None,
26
+ *,
27
+ transport: str = "http",
28
+ base_url: str | None = None,
29
+ api_key: str | None = None,
30
+ ) -> _Document:
31
+ """Open a Word document for editing.
32
+
33
+ Args:
34
+ docx: Athena asset_id of the SuperDoc document to open.
35
+ In python-docx this takes a file path; here it takes
36
+ an asset_id. To create a NEW document instead of opening
37
+ an existing one, call ``Document.create(...)`` (a
38
+ classmethod on the Document class).
39
+ transport: Either ``"http"`` (default since 0.4.0 — every
40
+ primitive routes through docx-studio's
41
+ ``POST /docs/:id/commands``; pure-Python client, no
42
+ embedded binary) or ``"direct"`` (legacy — embedded
43
+ Superdoc CLI talks to Keryx via y-websocket; requires
44
+ ``SUPERDOC_COLLAB_TOKEN`` env vars; emits a
45
+ ``DeprecationWarning`` and will be removed in 1.0).
46
+ The ``http`` transport requires ``base_url`` +
47
+ ``api_key`` (or the ``ATHENA_DOCX_*`` env vars).
48
+ base_url: docx-studio API base for ``transport="http"``. Falls
49
+ back to ``$ATHENA_DOCX_BASE_URL``.
50
+ api_key: Athena API key / PropelAuth bearer for
51
+ ``transport="http"``. Falls back to ``$ATHENA_DOCX_API_KEY``.
52
+
53
+ Returns:
54
+ A Document instance bound to the asset.
55
+
56
+ Raises:
57
+ ValueError: if ``docx`` is None or empty. Use ``Document.create()``
58
+ to make a fresh asset.
59
+ """
60
+ if not docx:
61
+ raise ValueError(
62
+ "athena-python-docx requires an asset_id. "
63
+ "Pass the SuperDoc asset ID as the first argument: "
64
+ "Document('asset_xxx...'). "
65
+ "To create a brand-new document, call "
66
+ "Document.create(name=..., base_url=..., api_key=...).",
67
+ )
68
+ return _Document(
69
+ asset_id=docx,
70
+ transport=transport,
71
+ http_base_url=base_url,
72
+ http_api_key=api_key,
73
+ )
74
+
75
+
76
+ # Re-export the classmethod factories at module level so callers can
77
+ # write either:
78
+ # from docx import Document
79
+ # Document.create(name=...)
80
+ # or:
81
+ # from docx.api import Document
82
+ # Document.create(name=...)
83
+ # (matching python-docx's flat API surface.)
84
+ Document.create = _Document.create # type: ignore[attr-defined]
@@ -64,12 +64,28 @@ class Session:
64
64
  *,
65
65
  asset_id: str,
66
66
  user_info: dict[str, str] | None = None,
67
+ bundle: dict[str, str] | None = None,
68
+ transport: str = "direct",
69
+ http_base_url: str | None = None,
70
+ http_api_key: str | None = None,
67
71
  ) -> None:
68
72
  self._asset_id: str = asset_id
69
73
  self._user_info: dict[str, str] = user_info or {
70
74
  "name": "Athena Agent",
71
75
  "email": "agent@athenaintel.com",
72
76
  }
77
+ # Optional pre-minted collab bundle. When set, open() uses these
78
+ # values instead of reading from os.environ. Used by Document.create()
79
+ # so the freshly-minted Keryx JWT returned from the create API is
80
+ # consumed directly without a round-trip through env vars.
81
+ self._bundle: dict[str, str] | None = bundle
82
+ # Transport: "direct" → embedded superdoc-sdk talks Keryx directly
83
+ # (legacy path, requires SUPERDOC_COLLAB_TOKEN env vars).
84
+ # "http" → all primitives go through docx-studio's
85
+ # POST /docs/:id/commands; no embedded Superdoc, no Keryx WS in-process.
86
+ self._transport: str = transport
87
+ self._http_base_url: str | None = http_base_url
88
+ self._http_api_key: str | None = http_api_key
73
89
  self._client: Any | None = None
74
90
  self._doc_handle: Any | None = None
75
91
  self._opened: bool = False
@@ -84,10 +100,19 @@ class Session:
84
100
  return self._opened and not self._closed
85
101
 
86
102
  async def open(self) -> None:
87
- """Open the Superdoc SDK session against Keryx.
103
+ """Open a session against the configured transport.
104
+
105
+ For ``transport="http"`` this is a near no-op — we just construct
106
+ the HTTP doc handle. The first command-level call hits docx-studio,
107
+ which performs the ownership check and acquires the Superdoc
108
+ session server-side.
109
+
110
+ For ``transport="direct"`` (legacy) this opens an embedded
111
+ Superdoc session and y-websocket to Keryx. Requires the
112
+ ``SUPERDOC_COLLAB_TOKEN`` env var (or a pre-minted bundle).
88
113
 
89
114
  Raises:
90
- AuthenticationError: if SUPERDOC_COLLAB_TOKEN is missing/invalid.
115
+ AuthenticationError: if creds are missing/invalid.
91
116
  SessionError: on any other open-time failure.
92
117
  """
93
118
  if self._closed:
@@ -97,9 +122,57 @@ class Session:
97
122
  if self._opened:
98
123
  return
99
124
 
100
- token: str | None = os.environ.get(_TOKEN_ENV)
101
- ws_url: str | None = os.environ.get(_WS_URL_ENV)
102
- workspace_id: str | None = os.environ.get(_WORKSPACE_ENV)
125
+ if self._transport == "direct":
126
+ import warnings
127
+
128
+ warnings.warn(
129
+ "transport='direct' is deprecated and will be removed in "
130
+ "athena-python-docx 1.0. Pass transport='http' (the new "
131
+ "default) or remove the argument. The HTTP transport is "
132
+ "pure-Python, has no embedded Superdoc binary, and works "
133
+ "without the SUPERDOC_COLLAB_TOKEN env-var dance.",
134
+ DeprecationWarning,
135
+ stacklevel=3,
136
+ )
137
+ elif self._transport == "http":
138
+ from docx._http_doc import HttpClient, HttpDocHandle
139
+
140
+ base_url: str | None = self._http_base_url or os.environ.get(
141
+ "ATHENA_DOCX_BASE_URL",
142
+ )
143
+ api_key: str | None = self._http_api_key or os.environ.get(
144
+ "ATHENA_DOCX_API_KEY",
145
+ )
146
+ if not base_url:
147
+ raise SessionError(
148
+ "Missing base_url for HTTP transport. Pass base_url= "
149
+ "to Document(...) or set ATHENA_DOCX_BASE_URL.",
150
+ )
151
+ if not api_key:
152
+ raise AuthenticationError(
153
+ "Missing api_key for HTTP transport. Pass api_key= "
154
+ "to Document(...) or set ATHENA_DOCX_API_KEY.",
155
+ )
156
+ client = HttpClient(base_url=base_url, api_key=api_key)
157
+ self._client = client
158
+ self._doc_handle = HttpDocHandle(client, self._asset_id)
159
+ self._opened = True
160
+ _log_info(f"Opened {self._asset_id} (http)")
161
+ return
162
+
163
+ # ----- legacy direct transport -----
164
+
165
+ # Prefer the in-memory bundle over env vars when present — that
166
+ # path is taken by Document.create(), which receives a fresh
167
+ # bundle in the create-asset HTTP response.
168
+ if self._bundle is not None:
169
+ token = self._bundle.get(_TOKEN_ENV)
170
+ ws_url = self._bundle.get(_WS_URL_ENV)
171
+ workspace_id = self._bundle.get(_WORKSPACE_ENV)
172
+ else:
173
+ token = os.environ.get(_TOKEN_ENV)
174
+ ws_url = os.environ.get(_WS_URL_ENV)
175
+ workspace_id = os.environ.get(_WORKSPACE_ENV)
103
176
 
104
177
  if not token:
105
178
  raise AuthenticationError(
@@ -173,33 +246,38 @@ class Session:
173
246
  return self._doc_handle
174
247
 
175
248
  async def save(self, *, in_place: bool = True) -> None: # noqa: ARG002
176
- """Ensure pending mutations have flushed to Keryx.
249
+ """Ensure pending mutations have flushed.
177
250
 
178
- In collab mode (our only mode), the Y-Websocket provider streams
179
- updates to Keryx as they happen there's no explicit save RPC
180
- to call. The underlying Superdoc SDK's ``save({"inPlace": True})``
181
- targets file-backed docs and errors with "this session has no
182
- source path" for collab sessions. So save() is a thin flush:
183
- sleep briefly so in-flight WebSocket frames land, then return.
251
+ For ``transport="http"`` this is a true no-op every primitive
252
+ was a synchronous HTTP round-trip that the server already
253
+ committed to Keryx before responding.
184
254
 
185
- The ``in_place`` kwarg is accepted for python-docx parity but
186
- has no effect in collab mode (there is no alternate target).
255
+ For ``transport="direct"`` we sleep briefly to let in-flight
256
+ y-websocket frames land before close, matching the legacy
257
+ superdoc_write_utils.py 1-second pre-close delay.
187
258
  """
188
259
  if not self._opened:
189
260
  raise SessionError("Cannot save a session that was never opened.")
190
261
  if self._closed:
191
262
  raise DocumentClosedError(f"Session {self._asset_id} is closed.")
192
263
 
193
- # Short flush — matches superdoc_write_utils.py's 1-second pre-close
194
- # delay. Keeps a single in-flight frame from being dropped on close.
264
+ if self._transport == "http":
265
+ _log_info(f"Saved (no-op for http transport) {self._asset_id}")
266
+ return
267
+
195
268
  await asyncio.sleep(1)
196
269
  _log_info(f"Saved (flushed) {self._asset_id}")
197
270
 
198
271
  async def close(self) -> None:
199
- """Close the Superdoc session. Idempotent."""
272
+ """Close the session. Idempotent."""
200
273
  if self._closed:
201
274
  return
202
275
 
276
+ if self._transport == "http":
277
+ self._closed = True
278
+ _log_info(f"Closed {self._asset_id} (http)")
279
+ return
280
+
203
281
  if self._opened and self._doc_handle is not None:
204
282
  try:
205
283
  # Match the 1-second flush delay from superdoc_write_utils