athena-python-docx 0.8.0__tar.gz → 0.9.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 (309) hide show
  1. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/CLAUDE.md +21 -0
  2. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/PKG-INFO +1 -1
  3. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/__init__.py +1 -1
  4. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/_buffer.py +127 -75
  5. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/_http_doc.py +138 -22
  6. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/_ptc.py +32 -49
  7. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/document.py +41 -39
  8. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/errors.py +46 -1
  9. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/pyproject.toml +1 -1
  10. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/baseline_gaps.json +1 -1
  11. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/reports/GAP_ANALYSIS.md +1 -1
  12. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/reports/gap_report.json +2 -2
  13. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/snapshots/athena_latest.json +251 -1
  14. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_block_not_found_error.py +52 -11
  15. athena_python_docx-0.9.0/tests/test_partial_failure_cascade.py +346 -0
  16. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_ptc.py +246 -10
  17. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/uv.lock +1 -1
  18. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/.gitignore +0 -0
  19. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/README.md +0 -0
  20. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/_batching.py +0 -0
  21. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/_http.py +0 -0
  22. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/_image_utils.py +0 -0
  23. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/api.py +0 -0
  24. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/client.py +0 -0
  25. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/commands.py +0 -0
  26. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/comments.py +0 -0
  27. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/enum/__init__.py +0 -0
  28. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/enum/section.py +0 -0
  29. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/enum/style.py +0 -0
  30. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/enum/table.py +0 -0
  31. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/enum/text.py +0 -0
  32. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/exceptions.py +0 -0
  33. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/opc/__init__.py +0 -0
  34. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/opc/coreprops.py +0 -0
  35. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/oxml/__init__.py +0 -0
  36. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/revisions.py +0 -0
  37. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/section.py +0 -0
  38. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/settings.py +0 -0
  39. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/shape.py +0 -0
  40. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/shared.py +0 -0
  41. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/styles/__init__.py +0 -0
  42. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/styles/style.py +0 -0
  43. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/styles/styles.py +0 -0
  44. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/table.py +0 -0
  45. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/text/__init__.py +0 -0
  46. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/text/font.py +0 -0
  47. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/text/hyperlink.py +0 -0
  48. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/text/pagebreak.py +0 -0
  49. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/text/paragraph.py +0 -0
  50. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/text/parfmt.py +0 -0
  51. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/text/run.py +0 -0
  52. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/text/tabstops.py +0 -0
  53. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/docx/typing.py +0 -0
  54. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/scripts/publish.sh +0 -0
  55. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/scripts/release.sh +0 -0
  56. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/scripts/round_trip_smoke.py +0 -0
  57. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/__init__.py +0 -0
  58. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/conftest.py +0 -0
  59. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/METHODOLOGY.md +0 -0
  60. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/README.md +0 -0
  61. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/__init__.py +0 -0
  62. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/ab_probe_cases.py +0 -0
  63. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/ab_probe_runner.py +0 -0
  64. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/auto_gen_cases.py +0 -0
  65. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/binary_round_trip.py +0 -0
  66. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/cases.py +0 -0
  67. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/complex_cases.py +0 -0
  68. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/coverage_report.py +0 -0
  69. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/extract.py +0 -0
  70. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/extreme_cases.py +0 -0
  71. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/fake_session.py +0 -0
  72. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/local_runner.py +0 -0
  73. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/mega_cases.py +0 -0
  74. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshot.py +0 -0
  75. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
  76. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
  77. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
  78. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
  79. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
  80. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
  81. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
  82. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
  83. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
  84. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
  85. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
  86. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
  87. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +0 -0
  88. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
  89. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
  90. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
  91. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
  92. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
  93. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
  94. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
  95. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
  96. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
  97. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
  98. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
  99. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
  100. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
  101. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
  102. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
  103. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
  104. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
  105. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/23_nested_table.json +0 -0
  106. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
  107. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
  108. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
  109. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
  110. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
  111. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
  112. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
  113. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
  114. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
  115. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
  116. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
  117. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
  118. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
  119. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
  120. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
  121. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
  122. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
  123. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
  124. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
  125. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
  126. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
  127. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
  128. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
  129. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
  130. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
  131. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
  132. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
  133. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +0 -0
  134. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
  135. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
  136. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
  137. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
  138. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
  139. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
  140. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
  141. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
  142. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
  143. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
  144. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
  145. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
  146. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
  147. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
  148. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
  149. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
  150. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
  151. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
  152. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
  153. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
  154. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
  155. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
  156. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
  157. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
  158. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
  159. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
  160. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
  161. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
  162. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
  163. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
  164. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
  165. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
  166. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
  167. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
  168. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
  169. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
  170. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
  171. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
  172. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
  173. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
  174. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
  175. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
  176. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
  177. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
  178. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
  179. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
  180. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
  181. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
  182. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +0 -0
  183. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
  184. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
  185. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
  186. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
  187. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
  188. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
  189. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
  190. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
  191. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
  192. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
  193. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
  194. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
  195. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
  196. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
  197. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
  198. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
  199. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
  200. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
  201. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
  202. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
  203. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
  204. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
  205. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
  206. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
  207. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
  208. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
  209. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
  210. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
  211. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
  212. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
  213. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
  214. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
  215. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
  216. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
  217. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
  218. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
  219. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
  220. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
  221. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
  222. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
  223. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
  224. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
  225. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw14_nested_cell_table.json +0 -0
  226. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
  227. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/ours_spec.json +0 -0
  228. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/parity_crawl.py +0 -0
  229. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/parity_diff.json +0 -0
  230. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/real_world_cases.py +0 -0
  231. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/round_trip_tests.py +0 -0
  232. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/runner.py +0 -0
  233. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/stock_spec.json +0 -0
  234. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
  235. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/README.md +0 -0
  236. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/__init__.py +0 -0
  237. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/compare.py +0 -0
  238. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/intentional_deviations.json +0 -0
  239. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/introspect.py +0 -0
  240. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/run_parity.py +0 -0
  241. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
  242. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/parity/test_parity_gap.py +0 -0
  243. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_batching_perf.py +0 -0
  244. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_buffer.py +0 -0
  245. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_cell_text_plain_fastpath.py +0 -0
  246. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_collapsed_range_format.py +0 -0
  247. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_command_dataclasses.py +0 -0
  248. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_commands.py +0 -0
  249. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_comments.py +0 -0
  250. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_document_create.py +0 -0
  251. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_http_transport.py +0 -0
  252. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_hyperlink_coalescing.py +0 -0
  253. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_insert_deferred.py +0 -0
  254. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_iter_inner_content.py +0 -0
  255. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_list_styles.py +0 -0
  256. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_merged_cells.py +0 -0
  257. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_oxml_shim.py +0 -0
  258. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_paragraph_text_len_cache.py +0 -0
  259. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_parity_misc.py +0 -0
  260. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_parity_round2.py +0 -0
  261. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_phase_a_behavior.py +0 -0
  262. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_phase_b_headers_footers.py +0 -0
  263. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_phase_c_tables.py +0 -0
  264. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_pr19766_review_fixes.py +0 -0
  265. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_python_docx_api_parity.py +0 -0
  266. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_revisions.py +0 -0
  267. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
  268. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_add_picture.py +0 -0
  269. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_add_run.py +0 -0
  270. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
  271. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
  272. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_comments_get.py +0 -0
  273. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_document_audit.py +0 -0
  274. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_document_element.py +0 -0
  275. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_enum_section.py +0 -0
  276. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_font_audit.py +0 -0
  277. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_header_footer.py +0 -0
  278. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_hyperlink.py +0 -0
  279. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_inline_shape.py +0 -0
  280. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
  281. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_misc.py +0 -0
  282. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
  283. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_paragraph_style.py +0 -0
  284. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
  285. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_parfmt.py +0 -0
  286. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_row_col_cell.py +0 -0
  287. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_run_add_break.py +0 -0
  288. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
  289. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_run_style.py +0 -0
  290. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_run_style_strict.py +0 -0
  291. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_run_text.py +0 -0
  292. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_run_underline.py +0 -0
  293. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_section_audit.py +0 -0
  294. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_section_dimensions.py +0 -0
  295. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_section_onoff.py +0 -0
  296. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_settings.py +0 -0
  297. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_shared_audit.py +0 -0
  298. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_style.py +0 -0
  299. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_styles.py +0 -0
  300. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_table_audit.py +0 -0
  301. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_table_cell.py +0 -0
  302. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_table_dimensions.py +0 -0
  303. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_silent_stub_table_layout.py +0 -0
  304. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_smoke_integration.py +0 -0
  305. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_style_acceptance.py +0 -0
  306. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_style_font.py +0 -0
  307. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_style_setters_contract.py +0 -0
  308. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_wire_contract.py +0 -0
  309. {athena_python_docx-0.8.0 → athena_python_docx-0.9.0}/tests/test_zod_wire_contract.py +0 -0
@@ -181,6 +181,27 @@ This is a **thin HTTP client** that mimics the sync python-docx API.
181
181
  and the follow-up `doc.insert` is buffered, so plain cell
182
182
  assignments are 0 HTTP requests until the next flush. See
183
183
  ``docx.table._is_plain_text`` for the trigger predicate.
184
+ - **Partial-failure cascade fix (0.9.0+):** when an HTTP batch raises
185
+ on a 207 partial-failure (e.g. one cell-paragraph mistake mid-script),
186
+ the SDK now rewrites ``proxy_id_refs`` for the ``applied`` prefix
187
+ the server reports as successful — BEFORE re-raising. Pre-0.9.0 the
188
+ rewrite was inside the success-path-only branch, so every Create that
189
+ committed server-side left its Python proxy stuck on the client UUID;
190
+ the next batch shipped dead client UUIDs and the cascade restarted.
191
+ Preview-session ``thread_bafba02b`` turned one cell-paragraph error
192
+ into thirteen downstream "Block not found" failures + a
193
+ ``DOCUMENT_IDENTITY_CONFLICT`` before this fix. The path goes
194
+ ``_http_post_json`` (attaches ``applied[]`` via
195
+ ``DocxError.with_partial_applied``) → ``CommandBuffer.flush`` /
196
+ ``_eager_flush_with`` (catches ``DocxError``, drains the prefix
197
+ through ``_apply_proxy_id_rewrites``, re-raises). See
198
+ ``tests/test_partial_failure_cascade.py`` for the contract.
199
+ - **Table-query hint (0.9.0+):** ``BlockNotFoundError`` on
200
+ ``TablesGet`` / ``TablesGetCells`` / etc. used to fall through with
201
+ no hint (the cell-paragraph workaround doesn't apply). Now carries
202
+ a dedicated "stale client-table-UUID" hint pointing at the actual
203
+ recovery (``doc.save()`` to drain pending + re-anchor proxies). See
204
+ ``_TABLE_CLIENT_ID_HINT`` in ``docx/_http_doc.py``.
184
205
  - The path-proxy in `_http_doc.py` is an internal translation layer:
185
206
  call sites that look like `await self._session.doc.create.paragraph(p)`
186
207
  resolve to `CommandBuffer.call(CreateParagraph(**p))`. Rewriting call
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-docx
3
- Version: 0.8.0
3
+ Version: 0.9.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.8.0"
9
+ __version__ = "0.9.0"
10
10
 
11
11
  from docx.api import Document
12
12
  from docx._buffer import flush_all
@@ -35,11 +35,94 @@ from typing import TYPE_CHECKING, Any
35
35
 
36
36
  from docx import _ptc
37
37
  from docx.commands import Command, is_response_bearing, must_flush_immediately
38
+ from docx.errors import DocxError
39
+
40
+
41
+ def _apply_proxy_id_rewrites(
42
+ results: list[Any],
43
+ proxy_id_refs: "dict[str, list[tuple[object, str]]]",
44
+ ) -> None:
45
+ """Walk ``results`` and rewrite registered proxy ids from client UUIDs
46
+ to the real SuperDoc ids the applier echoed back.
47
+
48
+ Shared by the success path in :meth:`CommandBuffer.flush` /
49
+ :meth:`_eager_flush_with` AND by the **partial-failure recovery
50
+ path** (the SDK now reads ``DocxError.partial_applied`` on raise
51
+ and applies the same rewrite for the prefix that committed before
52
+ the server hit its first failure). Without this, every Create that
53
+ succeeded server-side would leave its Python proxy stuck on the
54
+ client UUID and the next batch would ship the dead id straight
55
+ back to the server — that cascade is what turned one cell-
56
+ paragraph mistake in preview-session ``thread_bafba02b`` into
57
+ thirteen downstream "Block not found" errors.
58
+
59
+ Tolerant of legacy / transitional applier results that don't
60
+ include ``client_node_id`` / ``real_node_id`` echoes — we simply
61
+ leave those proxies on their client id; the applier's per-batch
62
+ ``clientIdMap`` rewrite still resolves them server-side on the
63
+ *next* call.
64
+ """
65
+ if not proxy_id_refs:
66
+ return
67
+ for result in results:
68
+ if not isinstance(result, dict):
69
+ continue
70
+ for cli_key, real_key in (
71
+ ("client_node_id", "real_node_id"),
72
+ ("client_entity_id", "real_entity_id"),
73
+ ):
74
+ cli = result.get(cli_key)
75
+ real = result.get(real_key)
76
+ if not (isinstance(cli, str) and isinstance(real, str)):
77
+ continue
78
+ refs = proxy_id_refs.pop(cli, [])
79
+ for proxy, attr in refs:
80
+ try:
81
+ setattr(proxy, attr, real)
82
+ except Exception: # noqa: BLE001
83
+ # A proxy that rejected setattr (slots without the
84
+ # attr, frozen dataclass, etc.) silently keeps its
85
+ # client_id — the applier's rewriter will still
86
+ # resolve it correctly on the next batch.
87
+ pass
88
+
89
+
90
+ def _extract_partial_results(exc: BaseException) -> list[Any]:
91
+ """Pull per-command ``result`` dicts out of a partial-failure error.
92
+
93
+ The HTTP layer attaches the server's ``applied`` array to
94
+ :class:`DocxError` (and subclasses) as ``partial_applied``. Each
95
+ entry is the wire-format ``{index, type, result}`` triple. We
96
+ return just the ``result`` dicts so the rewrite path can share its
97
+ success-path code without caring whether it's running on a happy
98
+ batch or a salvaged prefix.
99
+ """
100
+ applied = getattr(exc, "partial_applied", None)
101
+ if not isinstance(applied, list):
102
+ return []
103
+ out: list[Any] = []
104
+ for entry in applied:
105
+ if isinstance(entry, dict):
106
+ result = entry.get("result")
107
+ if isinstance(result, dict):
108
+ out.append(result)
109
+ return out
38
110
 
39
111
  if TYPE_CHECKING:
40
112
  from docx._http_doc import HttpClient
41
113
 
42
114
 
115
+ # Only commands whose class name is in this set produce PTC sub-tool-cards.
116
+ # Every Command subclass is a candidate, but each emit is now a synchronous
117
+ # HTTP POST to agora (see ``_ptc._send``), so emitting one card per low-level
118
+ # mutation produces hundreds of sub-cards per script and pays full network
119
+ # RTT on every one. The allow-list keeps the per-action signal — one card
120
+ # per logical paragraph creation — without the spam or the cumulative
121
+ # latency. Asset-level creation events (``CreateDocument``) are emitted
122
+ # from ``Document.create`` directly, not through this buffer path.
123
+ _PTC_EMIT_TOOLS: frozenset[str] = frozenset({"CreateParagraph"})
124
+
125
+
43
126
  def _ptc_emit_end_batch(cmds: list[Command], *, is_error: bool) -> None:
44
127
  """Emit a PTC ``end`` event for every cmd that received a ``begin``.
45
128
 
@@ -85,9 +168,7 @@ def _unregister(buffer: "CommandBuffer") -> None:
85
168
  """
86
169
  with _registry_lock:
87
170
  _active_buffers[:] = [
88
- ref
89
- for ref in _active_buffers
90
- if (b := ref()) is not None and b is not buffer
171
+ ref for ref in _active_buffers if (b := ref()) is not None and b is not buffer
91
172
  ]
92
173
 
93
174
 
@@ -170,14 +251,10 @@ class CommandBuffer:
170
251
  # real nodeId after flush. Populated by ``add_paragraph`` /
171
252
  # ``add_heading`` / etc. when they queue a Create with a
172
253
  # client-assigned id. Drained on every flush.
173
- self._proxy_id_refs: dict[
174
- str, list[tuple[object, str]]
175
- ] = {}
254
+ self._proxy_id_refs: dict[str, list[tuple[object, str]]] = {}
176
255
  _register(self)
177
256
 
178
- def register_proxy_id_ref(
179
- self, client_id: str, proxy: object, attr: str = "_node_id"
180
- ) -> None:
257
+ def register_proxy_id_ref(self, client_id: str, proxy: object, attr: str = "_node_id") -> None:
181
258
  """Register that ``proxy.<attr>`` should be rewritten from
182
259
  ``client_id`` to the real node id once the queue flushes.
183
260
 
@@ -215,9 +292,7 @@ class CommandBuffer:
215
292
  ``"direct"``, ``"tracked"``, or ``None`` to clear.
216
293
  """
217
294
  if mode is not None and mode not in ("direct", "tracked"):
218
- raise ValueError(
219
- f"change_mode must be 'direct', 'tracked', or None; got {mode!r}"
220
- )
295
+ raise ValueError(f"change_mode must be 'direct', 'tracked', or None; got {mode!r}")
221
296
  with self._lock:
222
297
  current = self._change_mode
223
298
  if current == mode:
@@ -269,16 +344,19 @@ class CommandBuffer:
269
344
  f"CommandBuffer for {self._asset_id} is closed",
270
345
  )
271
346
 
272
- # PTC begin: one event per user-facing method call, before any
273
- # batching. Failures here can't crash the user's code path.
274
- try:
275
- cmd._ptc_call_id = _ptc.emit_begin( # type: ignore[attr-defined]
276
- type(cmd).__name__,
277
- cmd.to_dict(),
278
- asset_id=self._asset_id,
279
- )
280
- except Exception: # noqa: BLE001
281
- pass
347
+ # PTC begin: emit only for commands in the allow-list. Other
348
+ # commands still flow through the buffer (and through the batched
349
+ # HTTP POST), they just don't produce a sub-tool-card. ``emit_end``
350
+ # is automatically skipped because no ``_ptc_call_id`` was set.
351
+ if type(cmd).__name__ in _PTC_EMIT_TOOLS:
352
+ try:
353
+ cmd._ptc_call_id = _ptc.emit_begin( # type: ignore[attr-defined]
354
+ type(cmd).__name__,
355
+ cmd.to_dict(),
356
+ asset_id=self._asset_id,
357
+ )
358
+ except Exception: # noqa: BLE001
359
+ pass
282
360
 
283
361
  if must_flush_immediately(cmd) and not self._has_client_id(cmd):
284
362
  return self._eager_flush_with(cmd)
@@ -300,12 +378,9 @@ class CommandBuffer:
300
378
  legacy callers) keep their eager-flush semantics so callers
301
379
  that read the response still see real data.
302
380
  """
303
- return (
304
- is_response_bearing(cmd)
305
- and (
306
- getattr(cmd, "client_node_id", None) is not None
307
- or getattr(cmd, "client_entity_id", None) is not None
308
- )
381
+ return is_response_bearing(cmd) and (
382
+ getattr(cmd, "client_node_id", None) is not None
383
+ or getattr(cmd, "client_entity_id", None) is not None
309
384
  )
310
385
 
311
386
  def flush(self) -> list[Any]:
@@ -339,37 +414,22 @@ class CommandBuffer:
339
414
  change_mode=change_mode,
340
415
  user=user,
341
416
  )
417
+ except DocxError as exc:
418
+ # Partial-failure cascade protection: even though the batch
419
+ # raised, the server's ``applied`` prefix tells us which
420
+ # commands DID commit before the failure. Rewrite the
421
+ # corresponding proxy ids before re-raising so the next
422
+ # batch ships REAL ids for the survivors (vs. dead client
423
+ # UUIDs that would cascade into another "Block not found").
424
+ # See ``_apply_proxy_id_rewrites`` for the full rationale.
425
+ _apply_proxy_id_rewrites(_extract_partial_results(exc), proxy_id_refs)
426
+ _ptc_emit_end_batch(pending, is_error=True)
427
+ raise
342
428
  except Exception:
343
429
  _ptc_emit_end_batch(pending, is_error=True)
344
430
  raise
345
431
  _ptc_emit_end_batch(pending, is_error=False)
346
- # Rewrite registered proxies from client-side UUIDs to the real
347
- # SuperDoc ids the applier echoed back. Defensive: tolerate
348
- # missing fields (legacy or transitional applier without the
349
- # client-id support).
350
- if proxy_id_refs:
351
- for result in results:
352
- if not isinstance(result, dict):
353
- continue
354
- for cli_key, real_key in (
355
- ("client_node_id", "real_node_id"),
356
- ("client_entity_id", "real_entity_id"),
357
- ):
358
- cli = result.get(cli_key)
359
- real = result.get(real_key)
360
- if not (isinstance(cli, str) and isinstance(real, str)):
361
- continue
362
- refs = proxy_id_refs.pop(cli, [])
363
- for proxy, attr in refs:
364
- try:
365
- setattr(proxy, attr, real)
366
- except Exception: # noqa: BLE001
367
- # A proxy that rejected setattr (slots without
368
- # the attr, frozen dataclass, etc.) silently
369
- # keeps its client_id — the rewriter in the
370
- # applier will still resolve it correctly
371
- # for the next batch.
372
- pass
432
+ _apply_proxy_id_rewrites(results, proxy_id_refs)
373
433
  return results
374
434
 
375
435
  def close(self) -> None:
@@ -409,30 +469,22 @@ class CommandBuffer:
409
469
  change_mode=change_mode,
410
470
  user=user,
411
471
  )
472
+ except DocxError as exc:
473
+ # Same partial-failure cascade protection as :meth:`flush` —
474
+ # rewrite proxy ids for the prefix the server reports as
475
+ # ``applied`` before re-raising. Critical here because the
476
+ # eager-flush path is hit by every query in user code (e.g.
477
+ # ``table.cell(0, 0)`` → ``tables.get``), so a failure on
478
+ # the trailing ``cmd`` would otherwise abandon every
479
+ # buffered Create's rewrite at once.
480
+ _apply_proxy_id_rewrites(_extract_partial_results(exc), proxy_id_refs)
481
+ _ptc_emit_end_batch(all_cmds, is_error=True)
482
+ raise
412
483
  except Exception:
413
484
  _ptc_emit_end_batch(all_cmds, is_error=True)
414
485
  raise
415
486
  _ptc_emit_end_batch(all_cmds, is_error=False)
416
- # Apply the same proxy-rewrite pass as ``flush()`` — see comment
417
- # there for the contract.
418
- if proxy_id_refs:
419
- for result in results:
420
- if not isinstance(result, dict):
421
- continue
422
- for cli_key, real_key in (
423
- ("client_node_id", "real_node_id"),
424
- ("client_entity_id", "real_entity_id"),
425
- ):
426
- cli = result.get(cli_key)
427
- real = result.get(real_key)
428
- if not (isinstance(cli, str) and isinstance(real, str)):
429
- continue
430
- refs = proxy_id_refs.pop(cli, [])
431
- for proxy, attr in refs:
432
- try:
433
- setattr(proxy, attr, real)
434
- except Exception: # noqa: BLE001
435
- pass
487
+ _apply_proxy_id_rewrites(results, proxy_id_refs)
436
488
  if not results:
437
489
  return {}
438
490
  return results[-1]
@@ -154,17 +154,102 @@ def _looks_like_block_not_found(err_obj: dict) -> bool:
154
154
  return 'block "' in lower or ('block ' in lower and ' was not found' in lower)
155
155
 
156
156
 
157
+ # Commands that target a paragraph block (or its inline range) and
158
+ # therefore trip the SuperDoc 1.8.1 cell-paragraph addressing gap when
159
+ # the target paragraph is nested inside a ``tableCell``. Used by the
160
+ # ``BlockNotFoundError`` hint logic to decide whether to surface the
161
+ # materialization workaround. Membership is checked against the
162
+ # ``err_obj["type"]`` field that ``apps/api`` echoes back on partial
163
+ # failure, so the gate covers both error-message shapes the SuperDoc
164
+ # CLI emits:
165
+ #
166
+ # * ``Block "paragraph:<uuid>" was not found.`` (SetParagraph*, etc.)
167
+ # * ``Block "<uuid>" not found.`` (Insert)
168
+ #
169
+ # Without this gate, the hint either fired for stale-id misses on
170
+ # unrelated command types (Greptile #20555) or skipped the Insert form
171
+ # entirely (preview session
172
+ # thread_b952794f, where 3 of 4 failures were bare-UUID Inserts and
173
+ # the agent burned 4 retries without ever seeing the workaround).
174
+ _PARAGRAPH_TARGETING_COMMANDS: frozenset[str] = frozenset(
175
+ {
176
+ "Insert",
177
+ "FormatApply",
178
+ "SetParagraphAlignment",
179
+ "ClearParagraphAlignment",
180
+ "SetParagraphStyle",
181
+ "SetParagraphIndentation",
182
+ "ClearParagraphIndentation",
183
+ "SetParagraphSpacing",
184
+ "ClearParagraphSpacing",
185
+ "SetParagraphKeepOptions",
186
+ "SetParagraphFlowOptions",
187
+ "SetParagraphTabStop",
188
+ "ClearParagraphTabStops",
189
+ "Replace",
190
+ "InsertLineBreak",
191
+ "InsertTab",
192
+ }
193
+ )
194
+
195
+
157
196
  _CELL_PARAGRAPH_HINT: str = (
158
197
  "\n\nHint: SuperDoc 1.8.1 cannot format paragraphs nested inside "
159
198
  "table cells via SetParagraphAlignment / SetParagraphStyle / "
160
- "SetParagraphIndentation / SetParagraphSpacing or doc.insert with "
161
- "a paragraph-block target. The cell's inner paragraph id is "
162
- "returned by cell.getNodeById but isn't a top-level addressable "
163
- "block. Materialize the cell's paragraph first via "
164
- '``cell.text = "value"``, then re-read ``cell.paragraphs[0]`` and '
165
- "apply format ops to that post-materialization Paragraph proxy. "
166
- "Tracked upstream at docx-studio/SUPERDOC_UPSTREAM_REQUESTS.md "
167
- "§ 'cell-inner paragraph addressing'."
199
+ "SetParagraphIndentation / SetParagraphSpacing / doc.insert with "
200
+ "a paragraph-block target, or any FormatApply on a cell-inner run. "
201
+ "The cell's inner paragraph id is returned by cell.getNodeById but "
202
+ "isn't a top-level addressable block. Materialize the cell's "
203
+ 'paragraph first via ``cell.text = "value"``, then re-read '
204
+ "``cell.paragraphs[0]`` and apply format ops to that post-"
205
+ "materialization Paragraph proxy. Tracked upstream at "
206
+ "docx-studio/SUPERDOC_UPSTREAM_REQUESTS.md § 'cell-inner paragraph "
207
+ "addressing'."
208
+ )
209
+
210
+
211
+ # ``tables.get`` / ``tables.getCells`` / ``tables.getProperties`` carry
212
+ # the table id at the top-level ``nodeId`` field — and the agent code
213
+ # that triggers them (``table.cell(0, 0)``, ``table.rows``, etc.) runs
214
+ # *immediately* after ``doc.add_table(...)``. The first such query
215
+ # triggers an eager flush that drains the buffered CreateTable in the
216
+ # same HTTP batch, so the applier's per-batch ``clientIdMap`` rewrite
217
+ # normally catches it. But when an EARLIER batch (e.g. a doomed cell-
218
+ # paragraph mutation a few lines up in the user script) raised before
219
+ # the SDK could rewrite ``proxy_id_refs``, the Table proxy still holds
220
+ # the client UUID. The next batch ships the dead id straight to SuperDoc
221
+ # and we land here. The hint points the agent at the canonical recovery
222
+ # pattern (force a save() to reseat the proxy refs, or split the
223
+ # offending mutation into its own execution).
224
+ _TABLE_QUERY_COMMANDS: frozenset[str] = frozenset(
225
+ {
226
+ "TablesGet",
227
+ "TablesGetCells",
228
+ "TablesGetProperties",
229
+ "TablesSetStyle",
230
+ "TablesSetLayout",
231
+ "TablesSetTableOptions",
232
+ "TablesSetCellProperties",
233
+ "TablesSetColumnWidth",
234
+ "TablesSetRowHeight",
235
+ "TablesInsertRow",
236
+ "TablesInsertColumn",
237
+ "TablesMergeCells",
238
+ }
239
+ )
240
+
241
+
242
+ _TABLE_CLIENT_ID_HINT: str = (
243
+ "\n\nHint: this looks like a stale client-side table UUID "
244
+ '(``t_xxxxxxxxxxxx``). Either an earlier batch in this execution '
245
+ "raised before the SDK could rewrite the Table proxy's id from the "
246
+ "client UUID to the real SuperDoc id, OR the table belongs to a "
247
+ "prior execution whose state has been discarded. Recover by calling "
248
+ "``doc.save()`` to drain pending mutations + re-anchor live "
249
+ "proxies, then re-query via ``doc.tables`` to get fresh proxies "
250
+ "with real ids. If you saw a cell-paragraph error in the same "
251
+ "execution, address that first — its partial-failure is what "
252
+ "abandoned the rewrite."
168
253
  )
169
254
 
170
255
 
@@ -314,6 +399,20 @@ def _http_post_json(
314
399
  parsed = json.loads(body) if body else {}
315
400
  except json.JSONDecodeError:
316
401
  parsed = {"raw": body}
402
+ # Extract the ``applied`` prefix so callers can rewrite
403
+ # ``proxy_id_refs`` for commands that DID succeed before the
404
+ # batch hit its first failure. Without this, the SDK throws
405
+ # away every successful Create's client-UUID → real-id mapping
406
+ # the moment ONE command fails, and the next batch keeps
407
+ # shipping dead client UUIDs. That cascade turned the preview-
408
+ # session thread_bafba02b's first cell-paragraph mistake into
409
+ # thirteen downstream "Block not found" errors and finally a
410
+ # ``DOCUMENT_IDENTITY_CONFLICT``. The structure is wire-shape:
411
+ # ``[{index, type, result: {client_node_id, real_node_id, …}}]``.
412
+ applied_raw = parsed.get("applied") if isinstance(parsed, dict) else None
413
+ partial_applied: list[dict] = []
414
+ if isinstance(applied_raw, list):
415
+ partial_applied = [a for a in applied_raw if isinstance(a, dict)]
317
416
  # If the failing command's error looks like "no such entity",
318
417
  # raise a typed :class:`NotFoundError` so speculative-read call
319
418
  # sites (``Comments.get``) can coerce it to ``None`` without
@@ -325,27 +424,44 @@ def _http_post_json(
325
424
  # likely targeting a cell-inner paragraph or stale-session
326
425
  # block id. Surface the typed exception plus an agent-readable
327
426
  # workaround so the next attempt doesn't repeat the same
328
- # mistake. The cell-paragraph hint only applies when the
329
- # missing id has the ``paragraph:`` prefix (SuperDoc's quoted-
330
- # id error format) stale list-item / table-row / image ids
331
- # would benefit from a different hint or none at all, and a
332
- # red-herring "use cell.text" pointer would just waste the
333
- # next retry.
427
+ # mistake.
428
+ #
429
+ # The cell-paragraph hint applies when the failing command is
430
+ # a paragraph-targeting op (Insert, FormatApply, SetParagraph*,
431
+ # …). We can't gate on the ``paragraph:`` prefix alone — the
432
+ # bare-UUID ``Block "<uuid>" not found.`` shape that SuperDoc's
433
+ # CLI emits for ``Insert`` failures is the dominant form of
434
+ # this bug in practice (preview-session thread_b952794f hit
435
+ # it 3 of 4 times without ever seeing the workaround under the
436
+ # prefix-only gate).
334
437
  if _looks_like_block_not_found(err_obj):
335
- msg_str = err_obj.get("message")
336
- paragraph_block = (
337
- isinstance(msg_str, str)
338
- and "paragraph:" in msg_str.lower()
438
+ cmd_type = err_obj.get("type")
439
+ paragraph_targeting = (
440
+ isinstance(cmd_type, str)
441
+ and cmd_type in _PARAGRAPH_TARGETING_COMMANDS
339
442
  )
443
+ table_query = (
444
+ isinstance(cmd_type, str)
445
+ and cmd_type in _TABLE_QUERY_COMMANDS
446
+ )
447
+ if paragraph_targeting:
448
+ hint = _CELL_PARAGRAPH_HINT
449
+ elif table_query:
450
+ hint = _TABLE_CLIENT_ID_HINT
451
+ else:
452
+ hint = ""
340
453
  raise BlockNotFoundError(
341
- base_msg + (_CELL_PARAGRAPH_HINT if paragraph_block else ""),
454
+ base_msg + hint,
342
455
  payload=err_obj,
343
- )
456
+ ).with_partial_applied(partial_applied)
344
457
  if _looks_like_not_found(err_obj):
345
- raise NotFoundError(base_msg, payload=err_obj)
458
+ raise NotFoundError(
459
+ base_msg,
460
+ payload=err_obj,
461
+ ).with_partial_applied(partial_applied)
346
462
  raise DocxError(
347
463
  f"docx-studio batch reported a partial failure: {parsed!r}",
348
- )
464
+ ).with_partial_applied(partial_applied)
349
465
 
350
466
  if 200 <= resp.status_code < 300:
351
467
  try:
@@ -8,10 +8,15 @@ verbatim, and the server derives identity from the token.
8
8
 
9
9
  Without ``ATHENA_PTC_URL`` set, every call here is a no-op.
10
10
 
11
- Emits are fire-and-forget on a background daemon thread:
12
-
13
- - Never raise into user code.
14
- - Never block the calling thread.
11
+ Emits are **synchronous, best-effort** HTTP POSTs:
12
+
13
+ - Never raise into user code (all transport errors are swallowed).
14
+ - Block the caller for the duration of one POST (sub-millisecond on
15
+ localhost, tens of milliseconds against agora over the network).
16
+ This is intentional: the agora-side ``PTCRunRegistry`` tears down
17
+ its in-flight call set as soon as the parent tool's sandbox returns,
18
+ so an async outbox would let ``end`` events arrive after teardown
19
+ and the sub-tool-card would force-fail with "ended before end event".
15
20
  - Re-read ``ATHENA_PTC_URL`` on every emit so updates between sandbox
16
21
  executions take effect immediately (the SDK module itself is cached
17
22
  across runs).
@@ -28,67 +33,45 @@ from __future__ import annotations
28
33
 
29
34
  import json
30
35
  import os
31
- import queue
32
- import threading
33
36
  import time
34
37
  import urllib.error
35
38
  import urllib.request
36
39
  import uuid
37
40
  from typing import Any
38
41
 
39
- _MAX_QUEUE = 4096
40
42
  _MAX_BODY = 64 * 1024
41
43
  _HTTP_TIMEOUT = 2.0
42
44
 
43
- # Singleton state. PTC has exactly one outbox per process.
44
- _outbox: queue.Queue[tuple[str, dict[str, Any]]] = queue.Queue(maxsize=_MAX_QUEUE)
45
- _thread_lock = threading.Lock()
46
- _thread_started = False
47
45
  # call_id -> URL snapshot at begin time; lets emit_end target the
48
46
  # original run's endpoint even if ATHENA_PTC_URL changed since.
49
47
  _call_url: dict[str, str] = {}
50
48
 
51
49
 
52
- def _drain() -> None:
53
- while True:
54
- item = _outbox.get()
55
- if item is None:
56
- return
57
- url, body = item
58
- try:
59
- raw = json.dumps(body).encode("utf-8")
60
- if len(raw) > _MAX_BODY:
61
- key = "args" if body.get("phase") == "begin" else "result"
62
- body[key] = {"__truncated__": True}
63
- raw = json.dumps(body).encode("utf-8")
64
- req = urllib.request.Request(
65
- url,
66
- data=raw,
67
- method="POST",
68
- headers={"Content-Type": "application/json"},
69
- )
70
- urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT).close()
71
- except (urllib.error.URLError, OSError, ValueError):
72
- pass # never propagate
73
-
74
-
75
- def _ensure_thread() -> None:
76
- global _thread_started
77
- if _thread_started:
78
- return
79
- with _thread_lock:
80
- if _thread_started:
81
- return
82
- threading.Thread(target=_drain, name="athena-ptc", daemon=True).start()
83
- _thread_started = True
84
-
85
-
86
50
  def _send(url: str, body: dict[str, Any]) -> None:
87
- _ensure_thread()
51
+ """Synchronously POST one PTC event. Best-effort; never raises.
52
+
53
+ Was previously an enqueue onto a background daemon thread. Made
54
+ synchronous so the agora-side ``PTCRunRegistry`` can rely on
55
+ "``emit_begin``/``emit_end`` returned" ⇒ "the registry has received
56
+ the event" — eliminating a teardown race that surfaced as every
57
+ sub-tool-card showing "ended before end event". See
58
+ ``docs/plans/2026-05-14-ptc-flush-race.md`` for the rationale.
59
+ """
88
60
  try:
89
- _outbox.put_nowait((url, body))
90
- except queue.Full:
91
- pass # drop on backpressure; never block user code
61
+ raw = json.dumps(body).encode("utf-8")
62
+ if len(raw) > _MAX_BODY:
63
+ key = "args" if body.get("phase") == "begin" else "result"
64
+ body[key] = {"__truncated__": True}
65
+ raw = json.dumps(body).encode("utf-8")
66
+ req = urllib.request.Request(
67
+ url,
68
+ data=raw,
69
+ method="POST",
70
+ headers={"Content-Type": "application/json"},
71
+ )
72
+ urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT).close()
73
+ except (urllib.error.URLError, OSError, ValueError):
74
+ pass # never propagate — PTC is best-effort
92
75
 
93
76
 
94
77
  def emit_begin(