athena-python-docx 0.4.0__tar.gz → 0.5.1__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.4.0 → athena_python_docx-0.5.1}/CLAUDE.md +48 -5
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/PKG-INFO +7 -3
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/__init__.py +3 -1
- athena_python_docx-0.5.1/docx/_buffer.py +248 -0
- athena_python_docx-0.5.1/docx/_http_doc.py +518 -0
- athena_python_docx-0.5.1/docx/_image_utils.py +63 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/api.py +9 -20
- athena_python_docx-0.5.1/docx/client.py +208 -0
- athena_python_docx-0.5.1/docx/commands.py +752 -0
- athena_python_docx-0.5.1/docx/comments.py +319 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/document.py +195 -59
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/enum/section.py +17 -0
- athena_python_docx-0.5.1/docx/enum/style.py +227 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/enum/text.py +13 -3
- athena_python_docx-0.5.1/docx/exceptions.py +42 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/opc/coreprops.py +62 -3
- athena_python_docx-0.5.1/docx/section.py +689 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/shape.py +97 -10
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/shared.py +12 -12
- athena_python_docx-0.5.1/docx/styles/style.py +360 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/styles/styles.py +90 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/table.py +328 -22
- athena_python_docx-0.5.1/docx/text/font.py +15 -0
- athena_python_docx-0.5.1/docx/text/pagebreak.py +96 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/text/paragraph.py +262 -44
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/text/parfmt.py +4 -57
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/text/run.py +248 -48
- athena_python_docx-0.5.1/docx/text/tabstops.py +238 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/pyproject.toml +14 -3
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/scripts/release.sh +8 -8
- athena_python_docx-0.5.1/scripts/round_trip_smoke.py +232 -0
- athena_python_docx-0.5.1/tests/fidelity/ab_probe_cases.py +520 -0
- athena_python_docx-0.5.1/tests/fidelity/ab_probe_runner.py +537 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/binary_round_trip.py +19 -6
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/fake_session.py +276 -50
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/local_runner.py +5 -2
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshot.py +2 -1
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +12 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +2 -2
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/29_section_headers_linked.json +6 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/35_full_report.json +1 -1
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +2 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/56_everything_in_one.json +1 -1
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +2 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +5 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex10_complex_bom.json +3 -3
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +10 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +5 -5
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega01_book_chapter.json +4 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega02_research_proposal.json +7 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega03_financial_statement.json +1 -1
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +3 -0
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +12 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +4 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +20 -0
- athena_python_docx-0.4.0/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json → athena_python_docx-0.5.1/tests/fidelity/op_snapshots/rw11_header_text.json +1 -0
- athena_python_docx-0.4.0/tests/fidelity/op_snapshots/29_section_headers_linked.json → athena_python_docx-0.5.1/tests/fidelity/op_snapshots/rw12_first_page_footer.json +2 -1
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/rw13_even_page_header.json +5 -0
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/rw14_nested_cell_table.json +13 -0
- athena_python_docx-0.5.1/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +5 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/ours_spec.json +17 -142
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/parity_diff.json +3 -2
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/real_world_cases.py +69 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/runner.py +4 -1
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/stock_spec.json +16 -16
- athena_python_docx-0.5.1/tests/parity/README.md +170 -0
- athena_python_docx-0.5.1/tests/parity/__init__.py +5 -0
- athena_python_docx-0.5.1/tests/parity/baseline_gaps.json +7 -0
- athena_python_docx-0.5.1/tests/parity/compare.py +768 -0
- athena_python_docx-0.5.1/tests/parity/intentional_deviations.json +189 -0
- athena_python_docx-0.5.1/tests/parity/introspect.py +362 -0
- athena_python_docx-0.5.1/tests/parity/reports/GAP_ANALYSIS.md +396 -0
- athena_python_docx-0.5.1/tests/parity/reports/gap_report.json +5524 -0
- athena_python_docx-0.5.1/tests/parity/run_parity.py +212 -0
- athena_python_docx-0.5.1/tests/parity/snapshots/athena_latest.json +12717 -0
- athena_python_docx-0.5.1/tests/parity/snapshots/upstream_python_docx_1.2.0.json +68840 -0
- athena_python_docx-0.5.1/tests/parity/test_parity_gap.py +183 -0
- athena_python_docx-0.5.1/tests/test_buffer.py +174 -0
- athena_python_docx-0.5.1/tests/test_command_dataclasses.py +144 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/test_commands.py +12 -2
- athena_python_docx-0.5.1/tests/test_comments.py +238 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/test_document_create.py +4 -26
- athena_python_docx-0.5.1/tests/test_http_transport.py +177 -0
- athena_python_docx-0.5.1/tests/test_iter_inner_content.py +75 -0
- athena_python_docx-0.5.1/tests/test_merged_cells.py +76 -0
- athena_python_docx-0.5.1/tests/test_parity_misc.py +227 -0
- athena_python_docx-0.5.1/tests/test_parity_round2.py +566 -0
- athena_python_docx-0.5.1/tests/test_phase_a_behavior.py +162 -0
- athena_python_docx-0.5.1/tests/test_phase_b_headers_footers.py +193 -0
- athena_python_docx-0.5.1/tests/test_phase_c_tables.py +138 -0
- athena_python_docx-0.5.1/tests/test_pr19766_review_fixes.py +307 -0
- athena_python_docx-0.5.1/tests/test_style_acceptance.py +113 -0
- athena_python_docx-0.5.1/tests/test_style_font.py +168 -0
- athena_python_docx-0.5.1/tests/test_style_setters_contract.py +125 -0
- athena_python_docx-0.5.1/tests/test_wire_contract.py +426 -0
- athena_python_docx-0.5.1/tests/test_zod_wire_contract.py +109 -0
- athena_python_docx-0.5.1/uv.lock +715 -0
- athena_python_docx-0.4.0/docx/_http_doc.py +0 -181
- athena_python_docx-0.4.0/docx/client.py +0 -316
- athena_python_docx-0.4.0/docx/enum/style.py +0 -64
- athena_python_docx-0.4.0/docx/section.py +0 -358
- athena_python_docx-0.4.0/docx/styles/style.py +0 -77
- athena_python_docx-0.4.0/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -7
- athena_python_docx-0.4.0/tests/test_http_transport.py +0 -172
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/.gitignore +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/README.md +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/_batching.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/_http.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/enum/table.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/errors.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/settings.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/text/hyperlink.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/docx/typing.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/scripts/publish.sh +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/__init__.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/conftest.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/METHODOLOGY.md +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/23_nested_table.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.4.0 → athena_python_docx-0.5.1}/tests/test_smoke_integration.py +0 -0
|
@@ -34,6 +34,35 @@ These standard python-docx members don't apply to a Superdoc-backed SDK:
|
|
|
34
34
|
- `Document.core_properties.last_modified_by` — we use Keryx attribution instead
|
|
35
35
|
- `Document.settings` — Word app settings (not surfaced by Superdoc)
|
|
36
36
|
- `InlineShape.chart` — charts (Phase 2+)
|
|
37
|
+
- `Comment.add_paragraph` / `Comment.add_table` /
|
|
38
|
+
`Comment.iter_inner_content` / `Comment.paragraphs` /
|
|
39
|
+
`Comment.tables` — SuperDoc models a comment as a single text body,
|
|
40
|
+
not a `BlockItemContainer`. Use `Comment.text` instead. The
|
|
41
|
+
paragraph/table-bearing surface raises `CommentsNotImplementedError`.
|
|
42
|
+
|
|
43
|
+
### Phase-3b upstream-blocked surface
|
|
44
|
+
|
|
45
|
+
These are *shipped at the surface* but partially or fully no-op at
|
|
46
|
+
runtime, pending SuperDoc SDK changes outside this repo. The
|
|
47
|
+
upstream constraints have been verified against
|
|
48
|
+
[SuperDoc's published docs](https://docs.superdoc.dev/document-api/reference/styles/apply)
|
|
49
|
+
and the bundled TypeScript types in
|
|
50
|
+
`node_modules/@superdoc-dev/sdk/dist/generated/client.d.ts`. See
|
|
51
|
+
`docx-studio/PYTHON_DOCX_PARITY_GAPS.md` § *What unblocks the
|
|
52
|
+
remaining work* for the exact wire-op shape needed to unblock each.
|
|
53
|
+
|
|
54
|
+
- **24 setters on `BaseStyle` / `CharacterStyle` / `ParagraphStyle`**
|
|
55
|
+
(`name`, `style_id`, `hidden`, `locked`, `priority`, `quick_style`,
|
|
56
|
+
`unhide_when_used`, `base_style`, `next_paragraph_style`) emit
|
|
57
|
+
`PendingStyleMutationWarning` and stash on `_overrides`.
|
|
58
|
+
SuperDoc 1.7's `doc.styles.apply` is constrained to
|
|
59
|
+
`target.scope: "docDefaults"` — no per-style metadata variant.
|
|
60
|
+
Behavior pinned by `tests/test_style_setters_contract.py` (11
|
|
61
|
+
tests); when SuperDoc lands a metadata op, those tests will need
|
|
62
|
+
to flip to assert real bus calls.
|
|
63
|
+
- **`Styles.latent_styles`** returns an empty `_LatentStyles` —
|
|
64
|
+
`DocInfoResult.styles` only surfaces `paragraphStyles[]` (a flat
|
|
65
|
+
usage-count list), not the OOXML `<w:latentStyles>` block.
|
|
37
66
|
|
|
38
67
|
### Intentional deviations (additions / different semantics)
|
|
39
68
|
|
|
@@ -56,14 +85,28 @@ If there is a genuine technical reason why a deviation from python-docx is neces
|
|
|
56
85
|
3. Get explicit confirmation that the deviation is acceptable
|
|
57
86
|
4. Document the deviation in the "Intentionally omitted" list above
|
|
58
87
|
|
|
59
|
-
## Architecture
|
|
88
|
+
## Architecture (0.5.0+)
|
|
60
89
|
|
|
61
|
-
This is
|
|
90
|
+
This is a **thin HTTP client** that mimics the sync python-docx API.
|
|
62
91
|
|
|
63
92
|
- Sync façade (matches python-docx) — `doc.save()`, `paragraph.add_run()`
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
- Every internal call constructs a typed `Command` dataclass
|
|
94
|
+
(`docx.commands`) and ships it through a `CommandBuffer`
|
|
95
|
+
(`docx._buffer`) which POSTs to docx-studio's `/docs/:id/commands`.
|
|
96
|
+
- HTTP transport: `requests.Session` with a Retry (3 attempts, 0.5s
|
|
97
|
+
backoff, 429/502/503/504). The legacy `transport="direct"` (embedded
|
|
98
|
+
Superdoc CLI + y-websocket from Python) was removed in 0.5.0; agent
|
|
99
|
+
pods no longer embed Superdoc.
|
|
100
|
+
- Batching: queries and response-bearing creates flush eagerly (and
|
|
101
|
+
drain pending pure mutations in the same batch); pure mutations
|
|
102
|
+
buffer with a 100 ms idle timer. `Document.save()` and the context
|
|
103
|
+
manager exit drain explicitly. `docx.flush_all()` is the Daytona
|
|
104
|
+
prelude hook — flushes every live buffer in the process.
|
|
105
|
+
- The path-proxy in `_http_doc.py` is an internal translation layer:
|
|
106
|
+
call sites that look like `await self._session.doc.create.paragraph(p)`
|
|
107
|
+
resolve to `CommandBuffer.call(CreateParagraph(**p))`. Rewriting call
|
|
108
|
+
sites to construct `Command` dataclasses directly is a possible
|
|
109
|
+
follow-up — the wire format is typed end-to-end either way.
|
|
67
110
|
|
|
68
111
|
## Development
|
|
69
112
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx collaborative document stack
|
|
5
5
|
Project-URL: Homepage, https://athenaintelligence.ai
|
|
6
6
|
Author-email: Athena Intelligence <engineering@athenaintelligence.ai>
|
|
@@ -11,14 +11,18 @@ Classifier: Programming Language :: Python :: 3
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist:
|
|
14
|
+
Requires-Dist: requests>=2.28
|
|
15
|
+
Requires-Dist: urllib3>=1.26.6
|
|
16
16
|
Provides-Extra: dev
|
|
17
17
|
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
18
18
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
19
19
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
20
|
Requires-Dist: python-docx>=1.1; extra == 'dev'
|
|
21
|
+
Requires-Dist: responses>=0.24; extra == 'dev'
|
|
21
22
|
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
23
|
+
Provides-Extra: fidelity
|
|
24
|
+
Requires-Dist: httpx>=0.27; extra == 'fidelity'
|
|
25
|
+
Requires-Dist: superdoc-sdk>=1.8.0; extra == 'fidelity'
|
|
22
26
|
Description-Content-Type: text/markdown
|
|
23
27
|
|
|
24
28
|
# athena-python-docx
|
|
@@ -6,9 +6,10 @@ See CLAUDE.md for the API parity contract.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
__version__ = "0.
|
|
9
|
+
__version__ = "0.5.1"
|
|
10
10
|
|
|
11
11
|
from docx.api import Document
|
|
12
|
+
from docx._buffer import flush_all
|
|
12
13
|
# Re-exports python-docx ships at docx top-level for convenience.
|
|
13
14
|
from docx.shared import Emu, Inches, Pt, Cm, Mm, Twips, Length, RGBColor
|
|
14
15
|
|
|
@@ -22,5 +23,6 @@ __all__ = [
|
|
|
22
23
|
"Twips",
|
|
23
24
|
"Length",
|
|
24
25
|
"RGBColor",
|
|
26
|
+
"flush_all",
|
|
25
27
|
"__version__",
|
|
26
28
|
]
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""HTTP command buffer for the docx-studio SDK.
|
|
2
|
+
|
|
3
|
+
The buffer sits between SDK call sites and the HTTP transport. Every SDK
|
|
4
|
+
mutation routes through it; we flush eagerly for queries and response-bearing
|
|
5
|
+
ops (creates), and idle-batch pure mutations so a burst of setters becomes
|
|
6
|
+
one HTTP request instead of N.
|
|
7
|
+
|
|
8
|
+
Concurrency model: the SDK serializes calls through ``_batching.run_sync``
|
|
9
|
+
(one persistent event-loop thread), so the buffer's primary thread is the
|
|
10
|
+
loop thread. The auto-flush timer fires on a *different* thread, so we
|
|
11
|
+
guard the pending list with an RLock and treat the timer flush as a
|
|
12
|
+
best-effort coalesce — if the loop thread races us we just flush twice
|
|
13
|
+
(or zero times; the next loop-thread call drains it).
|
|
14
|
+
|
|
15
|
+
`flush_all` is a process-wide hook used by the Daytona sandbox prelude to
|
|
16
|
+
make sure pending writes hit Keryx before the sandbox is suspended. It walks
|
|
17
|
+
a weak-ref registry so dead Workbook instances don't keep their buffers
|
|
18
|
+
alive.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
import threading
|
|
25
|
+
import weakref
|
|
26
|
+
from contextlib import contextmanager
|
|
27
|
+
from typing import TYPE_CHECKING, Any, Generator
|
|
28
|
+
|
|
29
|
+
from docx.commands import Command, must_flush_immediately
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from docx._http_doc import HttpClient
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Process-wide registry for flush_all()
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
_active_buffers: list[weakref.ref["CommandBuffer"]] = []
|
|
40
|
+
_registry_lock = threading.Lock()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _register(buffer: "CommandBuffer") -> None:
|
|
44
|
+
with _registry_lock:
|
|
45
|
+
_active_buffers.append(weakref.ref(buffer))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _unregister(buffer: "CommandBuffer") -> None:
|
|
49
|
+
"""Drop ``buffer`` from the active-registry list.
|
|
50
|
+
|
|
51
|
+
Called from :meth:`CommandBuffer.close` so a long-lived process that
|
|
52
|
+
opens and closes many Documents doesn't accumulate dead weak-refs in
|
|
53
|
+
the registry between ``flush_all`` calls.
|
|
54
|
+
"""
|
|
55
|
+
with _registry_lock:
|
|
56
|
+
_active_buffers[:] = [
|
|
57
|
+
ref
|
|
58
|
+
for ref in _active_buffers
|
|
59
|
+
if (b := ref()) is not None and b is not buffer
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def flush_all() -> None:
|
|
64
|
+
"""Flush every live CommandBuffer in this process.
|
|
65
|
+
|
|
66
|
+
Used by the Daytona sandbox prelude after user code returns, so
|
|
67
|
+
buffered mutations make it to Keryx before the sandbox is suspended.
|
|
68
|
+
Safe to call when no Buffers exist (no-op). Failures are logged and
|
|
69
|
+
swallowed — we don't want one stuck buffer to mask the rest.
|
|
70
|
+
"""
|
|
71
|
+
with _registry_lock:
|
|
72
|
+
snapshot = list(_active_buffers)
|
|
73
|
+
# Compact dead refs while we hold the lock.
|
|
74
|
+
live = [ref for ref in snapshot if ref() is not None]
|
|
75
|
+
_active_buffers[:] = live
|
|
76
|
+
|
|
77
|
+
for ref in live:
|
|
78
|
+
buf = ref()
|
|
79
|
+
if buf is None:
|
|
80
|
+
continue
|
|
81
|
+
try:
|
|
82
|
+
buf.flush()
|
|
83
|
+
except Exception as e: # noqa: BLE001
|
|
84
|
+
sys.stderr.write(
|
|
85
|
+
f"[docx-sdk] flush_all: buffer {buf.asset_id} flush failed: {e}\n",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# CommandBuffer
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
# Idle window before a pure-mutation buffer auto-flushes. Short by design —
|
|
94
|
+
# the SDK's main consumer is agent code that fires sequential property
|
|
95
|
+
# setters within milliseconds of each other; we just need to coalesce those
|
|
96
|
+
# without holding writes back from Keryx for human-perceptible time.
|
|
97
|
+
DEFAULT_AUTO_FLUSH_SECONDS: float = 0.1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CommandBuffer:
|
|
101
|
+
"""Buffers commands for one Document/asset.
|
|
102
|
+
|
|
103
|
+
Behaviour:
|
|
104
|
+
- Queries and response-bearing mutations (creates, inserts that return
|
|
105
|
+
ids) flush immediately. Pending pure mutations are flushed in the
|
|
106
|
+
same batch so ordering is preserved.
|
|
107
|
+
- Pure mutations (formatters, setters) are queued. An idle timer flushes
|
|
108
|
+
them after :data:`DEFAULT_AUTO_FLUSH_SECONDS` of inactivity, OR on
|
|
109
|
+
the next eager call, OR on explicit ``flush()``.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
client: "HttpClient",
|
|
115
|
+
asset_id: str,
|
|
116
|
+
*,
|
|
117
|
+
auto_flush_seconds: float = DEFAULT_AUTO_FLUSH_SECONDS,
|
|
118
|
+
) -> None:
|
|
119
|
+
self._client: "HttpClient" = client
|
|
120
|
+
self._asset_id: str = asset_id
|
|
121
|
+
self._pending: list[Command] = []
|
|
122
|
+
self._lock: threading.RLock = threading.RLock()
|
|
123
|
+
self._timer: threading.Timer | None = None
|
|
124
|
+
self._auto_flush_seconds: float = auto_flush_seconds
|
|
125
|
+
self._closed: bool = False
|
|
126
|
+
_register(self)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def asset_id(self) -> str:
|
|
130
|
+
return self._asset_id
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def pending_count(self) -> int:
|
|
134
|
+
with self._lock:
|
|
135
|
+
return len(self._pending)
|
|
136
|
+
|
|
137
|
+
def call(self, cmd: Command) -> Any:
|
|
138
|
+
"""Execute or buffer ``cmd``.
|
|
139
|
+
|
|
140
|
+
For eager commands (queries, creates), drains the pending queue and
|
|
141
|
+
runs ``cmd`` in the same batch; returns the per-cmd result dict.
|
|
142
|
+
|
|
143
|
+
For pure mutations, appends to the queue, resets the idle timer,
|
|
144
|
+
and returns ``None``. The caller MUST NOT rely on the return value
|
|
145
|
+
of a buffered mutation — it's not available until flush.
|
|
146
|
+
"""
|
|
147
|
+
if self._closed:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
f"CommandBuffer for {self._asset_id} is closed",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if must_flush_immediately(cmd):
|
|
153
|
+
return self._eager_flush_with(cmd)
|
|
154
|
+
|
|
155
|
+
with self._lock:
|
|
156
|
+
self._pending.append(cmd)
|
|
157
|
+
self._reset_timer_locked()
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def flush(self) -> list[Any]:
|
|
161
|
+
"""Flush pending commands as one HTTP batch.
|
|
162
|
+
|
|
163
|
+
Returns the list of per-command result dicts. Empty list if nothing
|
|
164
|
+
was pending. Cancels any active idle timer.
|
|
165
|
+
"""
|
|
166
|
+
with self._lock:
|
|
167
|
+
self._cancel_timer_locked()
|
|
168
|
+
pending = self._pending
|
|
169
|
+
self._pending = []
|
|
170
|
+
if not pending:
|
|
171
|
+
return []
|
|
172
|
+
return self._client.execute_batch(self._asset_id, pending)
|
|
173
|
+
|
|
174
|
+
@contextmanager
|
|
175
|
+
def batch(self) -> Generator[None, None, None]:
|
|
176
|
+
"""Group calls into one HTTP batch.
|
|
177
|
+
|
|
178
|
+
Inside the ``with`` block, eager commands still flush immediately
|
|
179
|
+
(we can't buffer reads — the caller is awaiting their result), but
|
|
180
|
+
pure mutations accumulate without their idle timer firing.
|
|
181
|
+
On exit, drains anything left.
|
|
182
|
+
"""
|
|
183
|
+
# Cancel the idle timer for the duration of the block; we'll flush
|
|
184
|
+
# explicitly on exit. New adds inside the block go straight onto
|
|
185
|
+
# _pending without rescheduling the timer.
|
|
186
|
+
with self._lock:
|
|
187
|
+
self._cancel_timer_locked()
|
|
188
|
+
old_window = self._auto_flush_seconds
|
|
189
|
+
self._auto_flush_seconds = 0.0 # disable scheduling
|
|
190
|
+
try:
|
|
191
|
+
yield
|
|
192
|
+
finally:
|
|
193
|
+
with self._lock:
|
|
194
|
+
self._auto_flush_seconds = old_window
|
|
195
|
+
self.flush()
|
|
196
|
+
|
|
197
|
+
def close(self) -> None:
|
|
198
|
+
"""Flush and disable. Idempotent."""
|
|
199
|
+
if self._closed:
|
|
200
|
+
return
|
|
201
|
+
try:
|
|
202
|
+
self.flush()
|
|
203
|
+
finally:
|
|
204
|
+
self._closed = True
|
|
205
|
+
_unregister(self)
|
|
206
|
+
|
|
207
|
+
# ----- internals -----
|
|
208
|
+
|
|
209
|
+
def _eager_flush_with(self, cmd: Command) -> Any:
|
|
210
|
+
"""Drain pending + run ``cmd`` in one batch; return cmd's result."""
|
|
211
|
+
with self._lock:
|
|
212
|
+
self._cancel_timer_locked()
|
|
213
|
+
pending = self._pending
|
|
214
|
+
self._pending = []
|
|
215
|
+
all_cmds: list[Command] = [*pending, cmd]
|
|
216
|
+
results: list[Any] = self._client.execute_batch(self._asset_id, all_cmds)
|
|
217
|
+
if not results:
|
|
218
|
+
return {}
|
|
219
|
+
return results[-1]
|
|
220
|
+
|
|
221
|
+
def _reset_timer_locked(self) -> None:
|
|
222
|
+
# Caller must hold self._lock.
|
|
223
|
+
self._cancel_timer_locked()
|
|
224
|
+
if self._auto_flush_seconds <= 0:
|
|
225
|
+
return # disabled (e.g. inside batch())
|
|
226
|
+
self._timer = threading.Timer(
|
|
227
|
+
self._auto_flush_seconds,
|
|
228
|
+
self._auto_flush,
|
|
229
|
+
)
|
|
230
|
+
self._timer.daemon = True
|
|
231
|
+
self._timer.start()
|
|
232
|
+
|
|
233
|
+
def _cancel_timer_locked(self) -> None:
|
|
234
|
+
# Caller must hold self._lock.
|
|
235
|
+
if self._timer is not None:
|
|
236
|
+
self._timer.cancel()
|
|
237
|
+
self._timer = None
|
|
238
|
+
|
|
239
|
+
def _auto_flush(self) -> None:
|
|
240
|
+
try:
|
|
241
|
+
self.flush()
|
|
242
|
+
except Exception as e: # noqa: BLE001
|
|
243
|
+
sys.stderr.write(
|
|
244
|
+
f"[docx-sdk] auto-flush failed for {self._asset_id}: {e}\n",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
__all__ = ["CommandBuffer", "flush_all", "DEFAULT_AUTO_FLUSH_SECONDS"]
|