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