athena-python-docx 0.5.1__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/CLAUDE.md +45 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/PKG-INFO +1 -1
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/__init__.py +1 -1
- athena_python_docx-0.5.3/docx/_buffer.py +503 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/_http_doc.py +124 -0
- athena_python_docx-0.5.3/docx/_ptc.py +173 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/api.py +15 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/commands.py +101 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/comments.py +91 -4
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/document.py +517 -82
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/errors.py +15 -0
- athena_python_docx-0.5.3/docx/revisions.py +377 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/section.py +105 -18
- athena_python_docx-0.5.3/docx/settings.py +99 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/shared.py +10 -7
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/styles/style.py +252 -3
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/styles/styles.py +64 -1
- {athena_python_docx-0.5.1 → 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.1 → athena_python_docx-0.5.3}/docx/text/pagebreak.py +27 -1
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/paragraph.py +52 -5
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/run.py +185 -21
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/pyproject.toml +1 -1
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/scripts/release.sh +16 -9
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/25_table_merge_cells.json → athena_python_docx-0.5.3/tests/fidelity/op_snapshots/100_table_negative_indexing.json +1 -1
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/18_table_cell_text.json → athena_python_docx-0.5.3/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +3 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +1 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -15
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json → athena_python_docx-0.5.3/tests/fidelity/op_snapshots/18_table_cell_text.json +5 -5
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -6
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +3 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +1 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/23_nested_table.json +2 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -4
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/100_table_negative_indexing.json → athena_python_docx-0.5.3/tests/fidelity/op_snapshots/25_table_merge_cells.json +1 -1
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/35_full_report.json +6 -2
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/40_large_table_10x10.json +100 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +2 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/49_resume_layout.json +3 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +3 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/56_everything_in_one.json +12 -3
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +1 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +1 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +400 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/68_invoice.json +16 -7
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/69_newsletter.json +1 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/72_legal_contract.json +6 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +20 -20
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -4
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -5
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +10 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +1 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/91_many_small_tables.json +100 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +5 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +3 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex04_50x50_table.json +50 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +1 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex10_complex_bom.json +66 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +80 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +1 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +18 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +84 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega01_book_chapter.json +13 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega02_research_proposal.json +18 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega03_financial_statement.json +35 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega05_user_manual.json +28 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +40 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega09_signed_contract.json +12 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega10_api_documentation.json +44 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -8
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -8
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +8 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +16 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw14_nested_cell_table.json +2 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/intentional_deviations.json +31 -1
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/reports/GAP_ANALYSIS.md +4 -3
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/reports/gap_report.json +75 -3
- {athena_python_docx-0.5.1 → 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.1 → athena_python_docx-0.5.3}/tests/test_buffer.py +9 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_http_transport.py +90 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_iter_inner_content.py +37 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_parity_misc.py +19 -0
- {athena_python_docx-0.5.1 → 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_revisions.py +966 -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.1 → athena_python_docx-0.5.3}/tests/test_wire_contract.py +21 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/uv.lock +1 -1
- athena_python_docx-0.5.1/docx/_buffer.py +0 -248
- athena_python_docx-0.5.1/docx/settings.py +0 -30
- athena_python_docx-0.5.1/docx/text/hyperlink.py +0 -53
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/.gitignore +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/README.md +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/_batching.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/_http.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/_image_utils.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/client.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/section.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/style.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/table.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/enum/text.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/exceptions.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/opc/coreprops.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/shape.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/font.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/parfmt.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/text/tabstops.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/docx/typing.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/scripts/publish.sh +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/scripts/round_trip_smoke.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/__init__.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/conftest.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/METHODOLOGY.md +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/ab_probe_cases.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/ab_probe_runner.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/binary_round_trip.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/fake_session.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/local_runner.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshot.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/ours_spec.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/parity_diff.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/real_world_cases.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/runner.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/fidelity/stock_spec.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/README.md +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/__init__.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/baseline_gaps.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/compare.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/introspect.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/run_parity.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/parity/test_parity_gap.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_command_dataclasses.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_commands.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_comments.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_document_create.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_merged_cells.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_parity_round2.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_phase_a_behavior.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_phase_b_headers_footers.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_phase_c_tables.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_smoke_integration.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_style_acceptance.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_style_font.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_style_setters_contract.py +0 -0
- {athena_python_docx-0.5.1 → athena_python_docx-0.5.3}/tests/test_zod_wire_contract.py +0 -0
|
@@ -76,6 +76,44 @@ not file-backed. Each is documented in the relevant docstring.
|
|
|
76
76
|
hits `POST {base_url}/docs/empty`. The constructor positional-arg
|
|
77
77
|
shape (`Document(asset_id)`) is preserved for parity.
|
|
78
78
|
|
|
79
|
+
- **`Document.save(path_or_stream=None)`** — argument is optional.
|
|
80
|
+
Stock python-docx requires it and `TypeError`s on no-arg, but in an
|
|
81
|
+
asset-backed SDK there is no local file to write — writes are always
|
|
82
|
+
in-place against the Y.Doc. Forcing the upstream signature broke
|
|
83
|
+
every agent invocation that reflexively called `doc.save()` (a
|
|
84
|
+
near-universal Python pattern), so we accept it for parity-friendly
|
|
85
|
+
call sites and ignore the value at runtime.
|
|
86
|
+
|
|
87
|
+
- **`Document.track_revisions: bool`** — when `True`, all subsequent
|
|
88
|
+
mutations are recorded as tracked revisions instead of direct edits.
|
|
89
|
+
python-docx upstream has no top-level toggle (the closest is
|
|
90
|
+
hand-edited `<w:trackChanges/>` on `Document.settings.element`).
|
|
91
|
+
We expose this as a Document-level property because every SuperDoc
|
|
92
|
+
mutation accepts a per-call `changeMode` envelope field. The
|
|
93
|
+
python-docx-parity alias `Document.settings.track_revisions` reads
|
|
94
|
+
and writes the same flag so call sites that walk `doc.settings`
|
|
95
|
+
see consistent state.
|
|
96
|
+
|
|
97
|
+
- **`Document.tracked_revisions()` context manager** — scope
|
|
98
|
+
`track_revisions=True` to a `with` block; restores the previous
|
|
99
|
+
value on exit (even on exception). Block-scoped track-changes is a
|
|
100
|
+
Pythonic ergonomic that doesn't exist in upstream python-docx
|
|
101
|
+
because the upstream package has no track-changes API at all.
|
|
102
|
+
|
|
103
|
+
- **`Document.user_name: str | None` / `Document.user_email: str |
|
|
104
|
+
None`** — author identity threaded through to SuperDoc on session-
|
|
105
|
+
open for tracked-change attribution. python-docx's
|
|
106
|
+
`Document.core_properties.last_modified_by` is the closest upstream
|
|
107
|
+
surface; we use the SuperDoc-native fields directly because SuperDoc
|
|
108
|
+
bakes them into the change records.
|
|
109
|
+
|
|
110
|
+
- **`Document.revisions: Revisions`** — collection of tracked changes
|
|
111
|
+
with iteration, `get(id)`, `accept_all()`, `reject_all()`,
|
|
112
|
+
`accept(id)`, `reject(id)`. Per-revision `Revision.accept()` /
|
|
113
|
+
`Revision.reject()` resolve a single change. python-docx upstream
|
|
114
|
+
has zero track-changes API, so this entire surface is an Athena
|
|
115
|
+
extension wired to SuperDoc's `doc.trackChanges.{list,get,decide}`.
|
|
116
|
+
|
|
79
117
|
### If you need a deviation
|
|
80
118
|
|
|
81
119
|
If there is a genuine technical reason why a deviation from python-docx is necessary:
|
|
@@ -107,6 +145,13 @@ This is a **thin HTTP client** that mimics the sync python-docx API.
|
|
|
107
145
|
resolve to `CommandBuffer.call(CreateParagraph(**p))`. Rewriting call
|
|
108
146
|
sites to construct `Command` dataclasses directly is a possible
|
|
109
147
|
follow-up — the wire format is typed end-to-end either way.
|
|
148
|
+
- **Programmatic Tool Calling (PTC):** every `CommandBuffer.call` and
|
|
149
|
+
`flush` emits begin/end events to agora via `docx/_ptc.py` when run
|
|
150
|
+
inside a Daytona sandbox with `ATHENA_PTC_URL` set. Each method call
|
|
151
|
+
surfaces as a nested sub-tool-card under the parent
|
|
152
|
+
`run_python_code` tool. See
|
|
153
|
+
[`docs/PROGRAMMATIC_TOOL_CALLING_GUIDE.md`](../../docs/PROGRAMMATIC_TOOL_CALLING_GUIDE.md)
|
|
154
|
+
at the monorepo root.
|
|
110
155
|
|
|
111
156
|
## Development
|
|
112
157
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.5.
|
|
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>
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""HTTP command buffer for the docx-studio SDK.
|
|
2
|
+
|
|
3
|
+
The buffer sits between SDK call sites and the HTTP transport. Every SDK
|
|
4
|
+
mutation routes through it; we flush eagerly for queries and response-bearing
|
|
5
|
+
ops (creates), and idle-batch pure mutations so a burst of setters becomes
|
|
6
|
+
one HTTP request instead of N.
|
|
7
|
+
|
|
8
|
+
Concurrency model: the SDK serializes calls through ``_batching.run_sync``
|
|
9
|
+
(one persistent event-loop thread), so the buffer's primary thread is the
|
|
10
|
+
loop thread. The auto-flush timer fires on a *different* thread, so we
|
|
11
|
+
guard the pending list with an RLock and treat the timer flush as a
|
|
12
|
+
best-effort coalesce — if the loop thread races us we just flush twice
|
|
13
|
+
(or zero times; the next loop-thread call drains it).
|
|
14
|
+
|
|
15
|
+
`flush_all` is a process-wide hook used by the Daytona sandbox prelude to
|
|
16
|
+
make sure pending writes hit Keryx before the sandbox is suspended. It walks
|
|
17
|
+
a weak-ref registry so dead Workbook instances don't keep their buffers
|
|
18
|
+
alive.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
import threading
|
|
25
|
+
import weakref
|
|
26
|
+
from contextlib import contextmanager
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Generator
|
|
28
|
+
|
|
29
|
+
from docx import _ptc
|
|
30
|
+
from docx.commands import Command, must_flush_immediately
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from docx._http_doc import HttpClient
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ptc_emit_end_batch(cmds: list[Command], *, is_error: bool) -> None:
|
|
37
|
+
"""Emit a PTC ``end`` event for every cmd that received a ``begin``.
|
|
38
|
+
|
|
39
|
+
Tolerant: missing call_ids (cmd was added before PTC env was set) and
|
|
40
|
+
PTC client exceptions both silently no-op.
|
|
41
|
+
"""
|
|
42
|
+
for c in cmds:
|
|
43
|
+
call_id = getattr(c, "_ptc_call_id", None)
|
|
44
|
+
if not call_id:
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
_ptc.emit_end(
|
|
48
|
+
call_id=call_id,
|
|
49
|
+
tool_name=type(c).__name__,
|
|
50
|
+
result={"ok": not is_error}
|
|
51
|
+
if not is_error
|
|
52
|
+
else {"ok": False, "error": "command batch failed"},
|
|
53
|
+
is_error=is_error,
|
|
54
|
+
)
|
|
55
|
+
except Exception: # noqa: BLE001
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# Process-wide registry for flush_all()
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
_active_buffers: list[weakref.ref["CommandBuffer"]] = []
|
|
64
|
+
_registry_lock = threading.Lock()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _register(buffer: "CommandBuffer") -> None:
|
|
68
|
+
with _registry_lock:
|
|
69
|
+
_active_buffers.append(weakref.ref(buffer))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _unregister(buffer: "CommandBuffer") -> None:
|
|
73
|
+
"""Drop ``buffer`` from the active-registry list.
|
|
74
|
+
|
|
75
|
+
Called from :meth:`CommandBuffer.close` so a long-lived process that
|
|
76
|
+
opens and closes many Documents doesn't accumulate dead weak-refs in
|
|
77
|
+
the registry between ``flush_all`` calls.
|
|
78
|
+
"""
|
|
79
|
+
with _registry_lock:
|
|
80
|
+
_active_buffers[:] = [
|
|
81
|
+
ref
|
|
82
|
+
for ref in _active_buffers
|
|
83
|
+
if (b := ref()) is not None and b is not buffer
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def flush_all() -> None:
|
|
88
|
+
"""Flush every live CommandBuffer in this process.
|
|
89
|
+
|
|
90
|
+
Used by the Daytona sandbox prelude after user code returns, so
|
|
91
|
+
buffered mutations make it to Keryx before the sandbox is suspended.
|
|
92
|
+
Safe to call when no Buffers exist (no-op). Failures are logged and
|
|
93
|
+
swallowed — we don't want one stuck buffer to mask the rest.
|
|
94
|
+
"""
|
|
95
|
+
with _registry_lock:
|
|
96
|
+
snapshot = list(_active_buffers)
|
|
97
|
+
# Compact dead refs while we hold the lock.
|
|
98
|
+
live = [ref for ref in snapshot if ref() is not None]
|
|
99
|
+
_active_buffers[:] = live
|
|
100
|
+
|
|
101
|
+
for ref in live:
|
|
102
|
+
buf = ref()
|
|
103
|
+
if buf is None:
|
|
104
|
+
continue
|
|
105
|
+
try:
|
|
106
|
+
buf.flush()
|
|
107
|
+
except Exception as e: # noqa: BLE001
|
|
108
|
+
sys.stderr.write(
|
|
109
|
+
f"[docx-sdk] flush_all: buffer {buf.asset_id} flush failed: {e}\n",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# CommandBuffer
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
# Idle window before a pure-mutation buffer auto-flushes. Short by design —
|
|
118
|
+
# the SDK's main consumer is agent code that fires sequential property
|
|
119
|
+
# setters within milliseconds of each other; we just need to coalesce those
|
|
120
|
+
# without holding writes back from Keryx for human-perceptible time.
|
|
121
|
+
DEFAULT_AUTO_FLUSH_SECONDS: float = 0.1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class CommandBuffer:
|
|
125
|
+
"""Buffers commands for one Document/asset.
|
|
126
|
+
|
|
127
|
+
Behaviour:
|
|
128
|
+
- Queries and response-bearing mutations (creates, inserts that return
|
|
129
|
+
ids) flush immediately. Pending pure mutations are flushed in the
|
|
130
|
+
same batch so ordering is preserved.
|
|
131
|
+
- Pure mutations (formatters, setters) are queued. An idle timer flushes
|
|
132
|
+
them after :data:`DEFAULT_AUTO_FLUSH_SECONDS` of inactivity, OR on
|
|
133
|
+
the next eager call, OR on explicit ``flush()``.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
client: "HttpClient",
|
|
139
|
+
asset_id: str,
|
|
140
|
+
*,
|
|
141
|
+
auto_flush_seconds: float = DEFAULT_AUTO_FLUSH_SECONDS,
|
|
142
|
+
) -> None:
|
|
143
|
+
self._client: "HttpClient" = client
|
|
144
|
+
self._asset_id: str = asset_id
|
|
145
|
+
self._pending: list[Command] = []
|
|
146
|
+
self._lock: threading.RLock = threading.RLock()
|
|
147
|
+
self._timer: threading.Timer | None = None
|
|
148
|
+
self._auto_flush_seconds: float = auto_flush_seconds
|
|
149
|
+
self._closed: bool = False
|
|
150
|
+
# Track-changes envelope state — propagated as request-level
|
|
151
|
+
# ``changeMode`` and ``user`` on every batch. ``Document``
|
|
152
|
+
# mutates these via :meth:`set_change_mode` / :meth:`set_user`,
|
|
153
|
+
# which flush before changing so prior buffered ops keep their
|
|
154
|
+
# original semantics.
|
|
155
|
+
self._change_mode: str | None = None
|
|
156
|
+
self._user: dict[str, str] | None = None
|
|
157
|
+
# Batch mode flag — when True, Create* commands buffer instead
|
|
158
|
+
# of flushing immediately, and proxies bind to a client-side
|
|
159
|
+
# UUID instead of waiting for the server response. See
|
|
160
|
+
# :meth:`Document.batch` and ``PERFORMANCE_BATCHING_ANALYSIS.md``.
|
|
161
|
+
self._batch_depth: int = 0
|
|
162
|
+
# client_node_id → list of (proxy, attr) pairs to update with the
|
|
163
|
+
# real nodeId after flush. Populated by ``add_paragraph`` /
|
|
164
|
+
# ``add_heading`` / etc. when they queue a Create with a
|
|
165
|
+
# client-assigned id. Drained on every flush.
|
|
166
|
+
self._proxy_id_refs: dict[
|
|
167
|
+
str, list[tuple[object, str]]
|
|
168
|
+
] = {}
|
|
169
|
+
_register(self)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def is_batching(self) -> bool:
|
|
173
|
+
"""``True`` if a ``with doc.batch():`` block is currently open.
|
|
174
|
+
|
|
175
|
+
SDK call sites read this to decide whether to defer Create*
|
|
176
|
+
commands or eager-flush them. Reentrant — nested ``batch()``
|
|
177
|
+
blocks increment the counter and decrement on exit; the outer
|
|
178
|
+
block is what actually flushes.
|
|
179
|
+
"""
|
|
180
|
+
return self._batch_depth > 0
|
|
181
|
+
|
|
182
|
+
def register_proxy_id_ref(
|
|
183
|
+
self, client_id: str, proxy: object, attr: str = "_node_id"
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Register that ``proxy.<attr>`` should be rewritten from
|
|
186
|
+
``client_id`` to the real node id once the current batch flushes.
|
|
187
|
+
|
|
188
|
+
Called by ``Document.add_paragraph`` (and equivalents) when they
|
|
189
|
+
queue a Create with a ``client_node_id``. The flush loop walks
|
|
190
|
+
the per-cmd result for ``real_node_id`` / ``real_entity_id``
|
|
191
|
+
echoes and applies setattr to every registered ref.
|
|
192
|
+
"""
|
|
193
|
+
with self._lock:
|
|
194
|
+
self._proxy_id_refs.setdefault(client_id, []).append((proxy, attr))
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def asset_id(self) -> str:
|
|
198
|
+
return self._asset_id
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def pending_count(self) -> int:
|
|
202
|
+
with self._lock:
|
|
203
|
+
return len(self._pending)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def change_mode(self) -> str | None:
|
|
207
|
+
return self._change_mode
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def user(self) -> "dict[str, str] | None":
|
|
211
|
+
return None if self._user is None else dict(self._user)
|
|
212
|
+
|
|
213
|
+
def set_change_mode(self, mode: str | None) -> None:
|
|
214
|
+
"""Set the batch-level change mode and flush prior pending ops.
|
|
215
|
+
|
|
216
|
+
Flushing first means commands queued under the previous mode
|
|
217
|
+
retain those semantics — switching modes mid-stream doesn't
|
|
218
|
+
retro-actively re-tag earlier mutations. ``mode`` is one of
|
|
219
|
+
``"direct"``, ``"tracked"``, or ``None`` to clear.
|
|
220
|
+
"""
|
|
221
|
+
if mode is not None and mode not in ("direct", "tracked"):
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"change_mode must be 'direct', 'tracked', or None; got {mode!r}"
|
|
224
|
+
)
|
|
225
|
+
with self._lock:
|
|
226
|
+
current = self._change_mode
|
|
227
|
+
if current == mode:
|
|
228
|
+
return
|
|
229
|
+
# Drain any queued commands with the *previous* mode before
|
|
230
|
+
# adopting the new one.
|
|
231
|
+
self.flush()
|
|
232
|
+
with self._lock:
|
|
233
|
+
self._change_mode = mode
|
|
234
|
+
|
|
235
|
+
def set_user(self, name: str | None, email: str | None = None) -> None:
|
|
236
|
+
"""Set the batch-level user identity.
|
|
237
|
+
|
|
238
|
+
Sent on every commands request envelope. SuperDoc honors the
|
|
239
|
+
identity at session-open time only — subsequent batches against
|
|
240
|
+
a pooled session ignore the value, but the payload is still
|
|
241
|
+
included so a fresh session picks it up.
|
|
242
|
+
"""
|
|
243
|
+
if name is None:
|
|
244
|
+
with self._lock:
|
|
245
|
+
self._user = None
|
|
246
|
+
return
|
|
247
|
+
if not name:
|
|
248
|
+
raise ValueError("user name must be a non-empty string")
|
|
249
|
+
with self._lock:
|
|
250
|
+
self._user = {"name": name}
|
|
251
|
+
if email:
|
|
252
|
+
self._user["email"] = email
|
|
253
|
+
|
|
254
|
+
def call(self, cmd: Command) -> Any:
|
|
255
|
+
"""Execute or buffer ``cmd``.
|
|
256
|
+
|
|
257
|
+
For eager commands (queries, response-bearing creates), drains
|
|
258
|
+
the pending queue and runs ``cmd`` in the same batch; returns
|
|
259
|
+
the per-cmd result dict.
|
|
260
|
+
|
|
261
|
+
For pure mutations, appends to the queue, resets the idle timer,
|
|
262
|
+
and returns ``None``. The caller MUST NOT rely on the return
|
|
263
|
+
value of a buffered mutation — it's not available until flush.
|
|
264
|
+
|
|
265
|
+
**Batch mode:** when ``self.is_batching`` is True AND the command
|
|
266
|
+
carries a non-None ``client_node_id`` / ``client_entity_id``
|
|
267
|
+
(Create*, CommentsCreate), the command is buffered rather than
|
|
268
|
+
eager-flushed. The caller is expected to have pre-stamped a
|
|
269
|
+
proxy with the client-side id; after the batch flushes, the
|
|
270
|
+
applier's ``real_node_id`` / ``real_entity_id`` echo lets the
|
|
271
|
+
SDK rewrite the proxy in-place.
|
|
272
|
+
"""
|
|
273
|
+
if self._closed:
|
|
274
|
+
raise RuntimeError(
|
|
275
|
+
f"CommandBuffer for {self._asset_id} is closed",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# PTC begin: one event per user-facing method call, before any
|
|
279
|
+
# batching. Failures here can't crash the user's code path.
|
|
280
|
+
try:
|
|
281
|
+
cmd._ptc_call_id = _ptc.emit_begin(type(cmd).__name__, cmd.to_dict()) # type: ignore[attr-defined]
|
|
282
|
+
except Exception: # noqa: BLE001
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
if must_flush_immediately(cmd) and not self._is_batched_create(cmd):
|
|
286
|
+
return self._eager_flush_with(cmd)
|
|
287
|
+
|
|
288
|
+
with self._lock:
|
|
289
|
+
self._pending.append(cmd)
|
|
290
|
+
self._reset_timer_locked()
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def _is_batched_create(self, cmd: Command) -> bool:
|
|
294
|
+
"""Return True iff this command can be safely deferred because
|
|
295
|
+
the caller pre-assigned a client-side id and the buffer is in
|
|
296
|
+
batch mode.
|
|
297
|
+
|
|
298
|
+
We only defer Create-shape commands (those with
|
|
299
|
+
``client_node_id`` / ``client_entity_id`` set). Pure queries
|
|
300
|
+
like ``BlocksList`` must still eager-flush — the caller is
|
|
301
|
+
awaiting their data. Read-bearing creates without a client id
|
|
302
|
+
(legacy callers / non-batch path) also stay eager so we don't
|
|
303
|
+
change their semantics.
|
|
304
|
+
"""
|
|
305
|
+
if not self.is_batching:
|
|
306
|
+
return False
|
|
307
|
+
cli_node = getattr(cmd, "client_node_id", None)
|
|
308
|
+
cli_ent = getattr(cmd, "client_entity_id", None)
|
|
309
|
+
return cli_node is not None or cli_ent is not None
|
|
310
|
+
|
|
311
|
+
def flush(self) -> list[Any]:
|
|
312
|
+
"""Flush pending commands as one HTTP batch.
|
|
313
|
+
|
|
314
|
+
Returns the list of per-command result dicts. Empty list if nothing
|
|
315
|
+
was pending. Cancels any active idle timer.
|
|
316
|
+
|
|
317
|
+
After the batch returns, walks per-cmd results for
|
|
318
|
+
``real_node_id`` / ``real_entity_id`` echoes and updates any
|
|
319
|
+
proxies registered via :meth:`register_proxy_id_ref` with the
|
|
320
|
+
resolved server ids. This is what completes the round-trip for
|
|
321
|
+
``with doc.batch():`` — the SDK's caller-facing proxies pick up
|
|
322
|
+
real ids transparently after the block exits.
|
|
323
|
+
"""
|
|
324
|
+
with self._lock:
|
|
325
|
+
self._cancel_timer_locked()
|
|
326
|
+
pending = self._pending
|
|
327
|
+
self._pending = []
|
|
328
|
+
change_mode = self._change_mode
|
|
329
|
+
user = None if self._user is None else dict(self._user)
|
|
330
|
+
proxy_id_refs = self._proxy_id_refs
|
|
331
|
+
self._proxy_id_refs = {}
|
|
332
|
+
if not pending:
|
|
333
|
+
return []
|
|
334
|
+
try:
|
|
335
|
+
results = self._client.execute_batch(
|
|
336
|
+
self._asset_id,
|
|
337
|
+
pending,
|
|
338
|
+
change_mode=change_mode,
|
|
339
|
+
user=user,
|
|
340
|
+
)
|
|
341
|
+
except Exception:
|
|
342
|
+
_ptc_emit_end_batch(pending, is_error=True)
|
|
343
|
+
raise
|
|
344
|
+
_ptc_emit_end_batch(pending, is_error=False)
|
|
345
|
+
# Rewrite registered proxies from client-side UUIDs to the real
|
|
346
|
+
# SuperDoc ids the applier echoed back. Defensive: tolerate
|
|
347
|
+
# missing fields (legacy or transitional applier without the
|
|
348
|
+
# client-id support).
|
|
349
|
+
if proxy_id_refs:
|
|
350
|
+
for result in results:
|
|
351
|
+
if not isinstance(result, dict):
|
|
352
|
+
continue
|
|
353
|
+
for cli_key, real_key in (
|
|
354
|
+
("client_node_id", "real_node_id"),
|
|
355
|
+
("client_entity_id", "real_entity_id"),
|
|
356
|
+
):
|
|
357
|
+
cli = result.get(cli_key)
|
|
358
|
+
real = result.get(real_key)
|
|
359
|
+
if not (isinstance(cli, str) and isinstance(real, str)):
|
|
360
|
+
continue
|
|
361
|
+
refs = proxy_id_refs.pop(cli, [])
|
|
362
|
+
for proxy, attr in refs:
|
|
363
|
+
try:
|
|
364
|
+
setattr(proxy, attr, real)
|
|
365
|
+
except Exception: # noqa: BLE001
|
|
366
|
+
# A proxy that rejected setattr (slots without
|
|
367
|
+
# the attr, frozen dataclass, etc.) silently
|
|
368
|
+
# keeps its client_id — the rewriter in the
|
|
369
|
+
# applier will still resolve it correctly
|
|
370
|
+
# for the next batch.
|
|
371
|
+
pass
|
|
372
|
+
return results
|
|
373
|
+
|
|
374
|
+
@contextmanager
|
|
375
|
+
def batch(self) -> Generator[None, None, None]:
|
|
376
|
+
"""Group calls into one HTTP batch.
|
|
377
|
+
|
|
378
|
+
Inside the ``with`` block:
|
|
379
|
+
|
|
380
|
+
- Queries (``BlocksList``, ``Find``, ``GetNodeById``, …) still
|
|
381
|
+
eager-flush — the caller is awaiting their result, so we have
|
|
382
|
+
no choice.
|
|
383
|
+
- Pure mutations (formatters, setters) accumulate without their
|
|
384
|
+
idle timer firing.
|
|
385
|
+
- Create*-shape commands that carry a ``client_node_id`` /
|
|
386
|
+
``client_entity_id`` (set by ``Document.add_paragraph`` and
|
|
387
|
+
friends when they detect batch mode) also accumulate — the
|
|
388
|
+
server resolves the client UUIDs to real SuperDoc ids in a
|
|
389
|
+
single batch via the per-request ``clientIdMap``.
|
|
390
|
+
|
|
391
|
+
On exit, drains anything left. Reentrant: nested ``batch()``
|
|
392
|
+
blocks share the same accumulating queue; only the outermost
|
|
393
|
+
block flushes.
|
|
394
|
+
"""
|
|
395
|
+
with self._lock:
|
|
396
|
+
self._cancel_timer_locked()
|
|
397
|
+
old_window = self._auto_flush_seconds
|
|
398
|
+
self._auto_flush_seconds = 0.0 # disable timer-scheduled flush
|
|
399
|
+
self._batch_depth += 1
|
|
400
|
+
try:
|
|
401
|
+
yield
|
|
402
|
+
finally:
|
|
403
|
+
with self._lock:
|
|
404
|
+
self._batch_depth -= 1
|
|
405
|
+
outermost = self._batch_depth == 0
|
|
406
|
+
if outermost:
|
|
407
|
+
self._auto_flush_seconds = old_window
|
|
408
|
+
if outermost:
|
|
409
|
+
self.flush()
|
|
410
|
+
|
|
411
|
+
def close(self) -> None:
|
|
412
|
+
"""Flush and disable. Idempotent."""
|
|
413
|
+
if self._closed:
|
|
414
|
+
return
|
|
415
|
+
try:
|
|
416
|
+
self.flush()
|
|
417
|
+
finally:
|
|
418
|
+
self._closed = True
|
|
419
|
+
_unregister(self)
|
|
420
|
+
|
|
421
|
+
# ----- internals -----
|
|
422
|
+
|
|
423
|
+
def _eager_flush_with(self, cmd: Command) -> Any:
|
|
424
|
+
"""Drain pending + run ``cmd`` in one batch; return cmd's result.
|
|
425
|
+
|
|
426
|
+
When pending commands include batched Creates with registered
|
|
427
|
+
proxy refs (e.g. a query mid-batch triggers an eager flush
|
|
428
|
+
before the ``with doc.batch():`` block ends), the proxies still
|
|
429
|
+
get their real ids written back via the same flush-time rewrite
|
|
430
|
+
that :meth:`flush` does.
|
|
431
|
+
"""
|
|
432
|
+
with self._lock:
|
|
433
|
+
self._cancel_timer_locked()
|
|
434
|
+
pending = self._pending
|
|
435
|
+
self._pending = []
|
|
436
|
+
change_mode = self._change_mode
|
|
437
|
+
user = None if self._user is None else dict(self._user)
|
|
438
|
+
proxy_id_refs = self._proxy_id_refs
|
|
439
|
+
self._proxy_id_refs = {}
|
|
440
|
+
all_cmds: list[Command] = [*pending, cmd]
|
|
441
|
+
try:
|
|
442
|
+
results: list[Any] = self._client.execute_batch(
|
|
443
|
+
self._asset_id,
|
|
444
|
+
all_cmds,
|
|
445
|
+
change_mode=change_mode,
|
|
446
|
+
user=user,
|
|
447
|
+
)
|
|
448
|
+
except Exception:
|
|
449
|
+
_ptc_emit_end_batch(all_cmds, is_error=True)
|
|
450
|
+
raise
|
|
451
|
+
_ptc_emit_end_batch(all_cmds, is_error=False)
|
|
452
|
+
# Apply the same proxy-rewrite pass as ``flush()`` — see comment
|
|
453
|
+
# there for the contract.
|
|
454
|
+
if proxy_id_refs:
|
|
455
|
+
for result in results:
|
|
456
|
+
if not isinstance(result, dict):
|
|
457
|
+
continue
|
|
458
|
+
for cli_key, real_key in (
|
|
459
|
+
("client_node_id", "real_node_id"),
|
|
460
|
+
("client_entity_id", "real_entity_id"),
|
|
461
|
+
):
|
|
462
|
+
cli = result.get(cli_key)
|
|
463
|
+
real = result.get(real_key)
|
|
464
|
+
if not (isinstance(cli, str) and isinstance(real, str)):
|
|
465
|
+
continue
|
|
466
|
+
refs = proxy_id_refs.pop(cli, [])
|
|
467
|
+
for proxy, attr in refs:
|
|
468
|
+
try:
|
|
469
|
+
setattr(proxy, attr, real)
|
|
470
|
+
except Exception: # noqa: BLE001
|
|
471
|
+
pass
|
|
472
|
+
if not results:
|
|
473
|
+
return {}
|
|
474
|
+
return results[-1]
|
|
475
|
+
|
|
476
|
+
def _reset_timer_locked(self) -> None:
|
|
477
|
+
# Caller must hold self._lock.
|
|
478
|
+
self._cancel_timer_locked()
|
|
479
|
+
if self._auto_flush_seconds <= 0:
|
|
480
|
+
return # disabled (e.g. inside batch())
|
|
481
|
+
self._timer = threading.Timer(
|
|
482
|
+
self._auto_flush_seconds,
|
|
483
|
+
self._auto_flush,
|
|
484
|
+
)
|
|
485
|
+
self._timer.daemon = True
|
|
486
|
+
self._timer.start()
|
|
487
|
+
|
|
488
|
+
def _cancel_timer_locked(self) -> None:
|
|
489
|
+
# Caller must hold self._lock.
|
|
490
|
+
if self._timer is not None:
|
|
491
|
+
self._timer.cancel()
|
|
492
|
+
self._timer = None
|
|
493
|
+
|
|
494
|
+
def _auto_flush(self) -> None:
|
|
495
|
+
try:
|
|
496
|
+
self.flush()
|
|
497
|
+
except Exception as e: # noqa: BLE001
|
|
498
|
+
sys.stderr.write(
|
|
499
|
+
f"[docx-sdk] auto-flush failed for {self._asset_id}: {e}\n",
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
__all__ = ["CommandBuffer", "flush_all", "DEFAULT_AUTO_FLUSH_SECONDS"]
|