athena-python-docx 0.2.3__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- athena_python_docx-0.5.0/CLAUDE.md +118 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/PKG-INFO +7 -3
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/__init__.py +3 -1
- athena_python_docx-0.5.0/docx/_buffer.py +248 -0
- athena_python_docx-0.5.0/docx/_http.py +189 -0
- athena_python_docx-0.5.0/docx/_http_doc.py +518 -0
- athena_python_docx-0.5.0/docx/_image_utils.py +63 -0
- athena_python_docx-0.5.0/docx/api.py +73 -0
- athena_python_docx-0.5.0/docx/client.py +208 -0
- athena_python_docx-0.5.0/docx/commands.py +752 -0
- athena_python_docx-0.5.0/docx/comments.py +319 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/document.py +260 -38
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/enum/section.py +17 -0
- athena_python_docx-0.5.0/docx/enum/style.py +227 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/enum/text.py +13 -3
- athena_python_docx-0.5.0/docx/exceptions.py +42 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/opc/coreprops.py +62 -3
- athena_python_docx-0.5.0/docx/section.py +689 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/shape.py +97 -10
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/shared.py +12 -12
- athena_python_docx-0.5.0/docx/styles/style.py +360 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/styles/styles.py +90 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/table.py +328 -22
- athena_python_docx-0.5.0/docx/text/font.py +15 -0
- athena_python_docx-0.5.0/docx/text/pagebreak.py +96 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/paragraph.py +262 -44
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/parfmt.py +4 -57
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/run.py +248 -48
- athena_python_docx-0.5.0/docx/text/tabstops.py +238 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/pyproject.toml +14 -3
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/README.md +1 -1
- athena_python_docx-0.5.0/tests/fidelity/ab_probe_cases.py +520 -0
- athena_python_docx-0.5.0/tests/fidelity/ab_probe_runner.py +537 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/binary_round_trip.py +19 -6
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/fake_session.py +286 -51
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/local_runner.py +5 -2
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshot.py +2 -1
- athena_python_docx-0.5.0/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +12 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +2 -2
- athena_python_docx-0.5.0/tests/fidelity/op_snapshots/29_section_headers_linked.json +6 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/35_full_report.json +1 -1
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +2 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +1 -1
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +2 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +5 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +3 -3
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +10 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +5 -5
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +4 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +7 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +1 -1
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +3 -0
- athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +12 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +4 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +20 -0
- athena_python_docx-0.2.3/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json → athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw11_header_text.json +1 -0
- athena_python_docx-0.2.3/tests/fidelity/op_snapshots/29_section_headers_linked.json → athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw12_first_page_footer.json +2 -1
- athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw13_even_page_header.json +5 -0
- athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw14_nested_cell_table.json +13 -0
- athena_python_docx-0.5.0/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +5 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/ours_spec.json +17 -142
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/parity_diff.json +3 -2
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/real_world_cases.py +69 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/runner.py +4 -1
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/stock_spec.json +16 -16
- athena_python_docx-0.5.0/tests/parity/README.md +170 -0
- athena_python_docx-0.5.0/tests/parity/__init__.py +5 -0
- athena_python_docx-0.5.0/tests/parity/baseline_gaps.json +7 -0
- athena_python_docx-0.5.0/tests/parity/compare.py +768 -0
- athena_python_docx-0.5.0/tests/parity/intentional_deviations.json +189 -0
- athena_python_docx-0.5.0/tests/parity/introspect.py +362 -0
- athena_python_docx-0.5.0/tests/parity/reports/GAP_ANALYSIS.md +396 -0
- athena_python_docx-0.5.0/tests/parity/reports/gap_report.json +5524 -0
- athena_python_docx-0.5.0/tests/parity/run_parity.py +212 -0
- athena_python_docx-0.5.0/tests/parity/snapshots/athena_latest.json +12717 -0
- athena_python_docx-0.5.0/tests/parity/snapshots/upstream_python_docx_1.2.0.json +68840 -0
- athena_python_docx-0.5.0/tests/parity/test_parity_gap.py +183 -0
- athena_python_docx-0.5.0/tests/test_buffer.py +174 -0
- athena_python_docx-0.5.0/tests/test_command_dataclasses.py +144 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/test_commands.py +12 -2
- athena_python_docx-0.5.0/tests/test_comments.py +238 -0
- athena_python_docx-0.5.0/tests/test_document_create.py +165 -0
- athena_python_docx-0.5.0/tests/test_http_transport.py +177 -0
- athena_python_docx-0.5.0/tests/test_iter_inner_content.py +75 -0
- athena_python_docx-0.5.0/tests/test_merged_cells.py +76 -0
- athena_python_docx-0.5.0/tests/test_parity_misc.py +227 -0
- athena_python_docx-0.5.0/tests/test_parity_round2.py +566 -0
- athena_python_docx-0.5.0/tests/test_phase_a_behavior.py +162 -0
- athena_python_docx-0.5.0/tests/test_phase_b_headers_footers.py +193 -0
- athena_python_docx-0.5.0/tests/test_phase_c_tables.py +138 -0
- athena_python_docx-0.5.0/tests/test_pr19766_review_fixes.py +307 -0
- athena_python_docx-0.5.0/tests/test_style_acceptance.py +113 -0
- athena_python_docx-0.5.0/tests/test_style_font.py +168 -0
- athena_python_docx-0.5.0/tests/test_style_setters_contract.py +125 -0
- athena_python_docx-0.5.0/tests/test_wire_contract.py +426 -0
- athena_python_docx-0.5.0/tests/test_zod_wire_contract.py +109 -0
- athena_python_docx-0.5.0/uv.lock +715 -0
- athena_python_docx-0.2.3/CLAUDE.md +0 -63
- athena_python_docx-0.2.3/docx/api.py +0 -41
- athena_python_docx-0.2.3/docx/client.py +0 -238
- athena_python_docx-0.2.3/docx/enum/style.py +0 -64
- athena_python_docx-0.2.3/docx/section.py +0 -358
- athena_python_docx-0.2.3/docx/styles/style.py +0 -77
- athena_python_docx-0.2.3/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -7
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/.gitignore +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/README.md +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/_batching.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/enum/table.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/errors.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/settings.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/text/hyperlink.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/docx/typing.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/scripts/publish.sh +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/scripts/release.sh +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/conftest.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/METHODOLOGY.md +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/23_nested_table.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.5.0}/tests/test_smoke_integration.py +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# athena-python-docx SDK — Claude Instructions
|
|
2
|
+
|
|
3
|
+
## API Parity Rule (MANDATORY)
|
|
4
|
+
|
|
5
|
+
**This SDK MUST be a 100% exact replica of the standard [python-docx](https://python-docx.readthedocs.io/) API.**
|
|
6
|
+
|
|
7
|
+
Every class, method, property, and parameter name must match python-docx exactly. The goal is that any code written for python-docx works identically with this SDK — no surprises, no differences.
|
|
8
|
+
|
|
9
|
+
### What this means in practice
|
|
10
|
+
|
|
11
|
+
- **Do NOT add new methods** that don't exist in python-docx
|
|
12
|
+
- **Do NOT add new properties** that don't exist in python-docx
|
|
13
|
+
- **Do NOT rename parameters** — use the exact same parameter names as python-docx
|
|
14
|
+
- **Do NOT change method signatures** — if python-docx's `add_heading()` takes `(text, level=1)`, ours must too
|
|
15
|
+
- **Do NOT change return types** — if python-docx's `paragraph.runs` returns `list[Run]`, ours must too
|
|
16
|
+
|
|
17
|
+
### How to verify parity
|
|
18
|
+
|
|
19
|
+
Before adding or modifying any API surface:
|
|
20
|
+
|
|
21
|
+
1. Check the [python-docx documentation](https://python-docx.readthedocs.io/)
|
|
22
|
+
2. Check the [python-docx source code](https://github.com/python-openxml/python-docx)
|
|
23
|
+
3. Confirm the method/property/parameter exists with the same name and signature
|
|
24
|
+
4. If it doesn't exist in python-docx, **do not add it** without explicit user approval
|
|
25
|
+
|
|
26
|
+
Run `uv run pytest tests/test_python_docx_api_parity.py -v -s` to verify parity.
|
|
27
|
+
|
|
28
|
+
### Intentionally omitted (Superdoc SDK limitations)
|
|
29
|
+
|
|
30
|
+
These standard python-docx members don't apply to a Superdoc-backed SDK:
|
|
31
|
+
|
|
32
|
+
- `Paragraph._p`, `Run._r` — XML element access (no local XML)
|
|
33
|
+
- `Document.part`, `Paragraph.part`, `Run.part` — package part access
|
|
34
|
+
- `Document.core_properties.last_modified_by` — we use Keryx attribution instead
|
|
35
|
+
- `Document.settings` — Word app settings (not surfaced by Superdoc)
|
|
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.
|
|
66
|
+
|
|
67
|
+
### Intentional deviations (additions / different semantics)
|
|
68
|
+
|
|
69
|
+
These differ from stock python-docx because the SDK is asset-backed,
|
|
70
|
+
not file-backed. Each is documented in the relevant docstring.
|
|
71
|
+
|
|
72
|
+
- **`Document.create(name=, base_url=, api_key=, parent_folder_id=, workspace_id=)`**
|
|
73
|
+
classmethod. Stock python-docx returns a blank in-memory document
|
|
74
|
+
for `Document(None)`; we can't fabricate a SuperDocument asset
|
|
75
|
+
client-side, so net-new asset creation is a separate factory that
|
|
76
|
+
hits `POST {base_url}/docs/empty`. The constructor positional-arg
|
|
77
|
+
shape (`Document(asset_id)`) is preserved for parity.
|
|
78
|
+
|
|
79
|
+
### If you need a deviation
|
|
80
|
+
|
|
81
|
+
If there is a genuine technical reason why a deviation from python-docx is necessary:
|
|
82
|
+
|
|
83
|
+
1. **Stop and ask the user** before implementing
|
|
84
|
+
2. Explain what the deviation is and why it's needed
|
|
85
|
+
3. Get explicit confirmation that the deviation is acceptable
|
|
86
|
+
4. Document the deviation in the "Intentionally omitted" list above
|
|
87
|
+
|
|
88
|
+
## Architecture (0.5.0+)
|
|
89
|
+
|
|
90
|
+
This is a **thin HTTP client** that mimics the sync python-docx API.
|
|
91
|
+
|
|
92
|
+
- Sync façade (matches python-docx) — `doc.save()`, `paragraph.add_run()`
|
|
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.
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
uv venv
|
|
115
|
+
uv pip install -e ".[dev]"
|
|
116
|
+
uv run pytest tests/ -x -q
|
|
117
|
+
uv run pytest tests/test_python_docx_api_parity.py -v
|
|
118
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx collaborative document stack
|
|
5
5
|
Project-URL: Homepage, https://athenaintelligence.ai
|
|
6
6
|
Author-email: Athena Intelligence <engineering@athenaintelligence.ai>
|
|
@@ -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.0"
|
|
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"]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""HTTP bootstrap for Document.create().
|
|
2
|
+
|
|
3
|
+
The SDK's primary transport is y-websocket via Superdoc; this module is
|
|
4
|
+
the *only* HTTP client in the package. It exists solely so that
|
|
5
|
+
``Document.create()`` can hit ``POST /docs/empty`` on docx-studio to
|
|
6
|
+
provision a new SuperDocument asset and receive a collab bundle to open
|
|
7
|
+
the document with.
|
|
8
|
+
|
|
9
|
+
We use ``urllib`` from stdlib to avoid pulling ``httpx`` / ``requests``
|
|
10
|
+
into the runtime — this code runs in Daytona sandboxes where every MB
|
|
11
|
+
matters, and the existing SDK already keeps its dep tree tight.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
from typing import TypedDict
|
|
21
|
+
|
|
22
|
+
from docx.errors import (
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
DocxError,
|
|
25
|
+
SessionError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _CollabBundle(TypedDict):
|
|
30
|
+
"""The collab credentials returned alongside a freshly-created asset.
|
|
31
|
+
|
|
32
|
+
Note: env-var-shape (matches what ``Session`` reads), even though
|
|
33
|
+
the wire format from docx-studio uses different key names. The
|
|
34
|
+
keys are renamed at parse time.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
SUPERDOC_COLLAB_TOKEN: str
|
|
38
|
+
KERYX_WS_URL: str
|
|
39
|
+
ATHENA_WORKSPACE_ID: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CreateAssetResult(TypedDict):
|
|
43
|
+
"""Parsed response from ``POST /docs/empty``."""
|
|
44
|
+
|
|
45
|
+
asset_id: str
|
|
46
|
+
name: str
|
|
47
|
+
workspace_id: str
|
|
48
|
+
collab: _CollabBundle
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_BASE_URL_ENV = "ATHENA_DOCX_BASE_URL"
|
|
52
|
+
_API_KEY_ENV = "ATHENA_DOCX_API_KEY" # noqa: S105
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_empty_document(
|
|
56
|
+
*,
|
|
57
|
+
base_url: str | None = None,
|
|
58
|
+
api_key: str | None = None,
|
|
59
|
+
name: str | None = None,
|
|
60
|
+
parent_folder_id: str | None = None,
|
|
61
|
+
workspace_id: str | None = None,
|
|
62
|
+
timeout: float = 30.0,
|
|
63
|
+
) -> CreateAssetResult:
|
|
64
|
+
"""Call ``POST {base_url}/docs/empty`` and parse the response.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
base_url: docx-studio API base, e.g. ``https://docx-studio.stg.athenaintel.com``.
|
|
68
|
+
Falls back to ``$ATHENA_DOCX_BASE_URL``.
|
|
69
|
+
api_key: Athena API key (PropelAuth user API key) or PropelAuth
|
|
70
|
+
access token. Sent as ``Authorization: Bearer <api_key>``.
|
|
71
|
+
Falls back to ``$ATHENA_DOCX_API_KEY``.
|
|
72
|
+
name: Optional display title.
|
|
73
|
+
parent_folder_id: Optional parent folder; defaults to workspace root.
|
|
74
|
+
workspace_id: Optional workspace UUID; defaults to caller's
|
|
75
|
+
current workspace.
|
|
76
|
+
timeout: Request timeout in seconds.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Parsed response dict with the new asset_id and a collab bundle
|
|
80
|
+
ready to feed into ``Session(..., bundle=...)``.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
SessionError: missing base_url or unparseable response.
|
|
84
|
+
AuthenticationError: missing api_key, or the server returned 401/403.
|
|
85
|
+
DocxError: any other 4xx/5xx from the server.
|
|
86
|
+
"""
|
|
87
|
+
resolved_base: str | None = base_url or os.environ.get(_BASE_URL_ENV)
|
|
88
|
+
resolved_key: str | None = api_key or os.environ.get(_API_KEY_ENV)
|
|
89
|
+
|
|
90
|
+
if not resolved_base:
|
|
91
|
+
raise SessionError(
|
|
92
|
+
f"Missing base_url and {_BASE_URL_ENV} env var. "
|
|
93
|
+
"Pass base_url= to Document.create() or set the env var.",
|
|
94
|
+
)
|
|
95
|
+
if not resolved_key:
|
|
96
|
+
raise AuthenticationError(
|
|
97
|
+
f"Missing api_key and {_API_KEY_ENV} env var. "
|
|
98
|
+
"Pass api_key= to Document.create() or set the env var.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
url: str = resolved_base.rstrip("/") + "/docs/empty"
|
|
102
|
+
body: dict[str, str] = {}
|
|
103
|
+
if name is not None:
|
|
104
|
+
body["name"] = name
|
|
105
|
+
if parent_folder_id is not None:
|
|
106
|
+
body["parentFolderId"] = parent_folder_id
|
|
107
|
+
if workspace_id is not None:
|
|
108
|
+
body["workspaceId"] = workspace_id
|
|
109
|
+
|
|
110
|
+
# Lazy import — _http is loaded lazily by Document.create(), so the
|
|
111
|
+
# package is fully initialized by the time we reach this code.
|
|
112
|
+
from docx import __version__
|
|
113
|
+
|
|
114
|
+
payload: bytes = json.dumps(body).encode("utf-8")
|
|
115
|
+
req = urllib.request.Request( # noqa: S310
|
|
116
|
+
url,
|
|
117
|
+
data=payload,
|
|
118
|
+
method="POST",
|
|
119
|
+
headers={
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
"Authorization": f"Bearer {resolved_key}",
|
|
122
|
+
"Accept": "application/json",
|
|
123
|
+
"User-Agent": f"athena-python-docx/{__version__}",
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
|
|
129
|
+
raw: bytes = resp.read()
|
|
130
|
+
except urllib.error.HTTPError as e:
|
|
131
|
+
err_body: str = ""
|
|
132
|
+
try:
|
|
133
|
+
err_body = e.read().decode("utf-8", errors="replace")
|
|
134
|
+
except Exception: # noqa: BLE001
|
|
135
|
+
pass
|
|
136
|
+
if e.code in (401, 403):
|
|
137
|
+
raise AuthenticationError(
|
|
138
|
+
f"docx-studio rejected the API key (HTTP {e.code}): {err_body}",
|
|
139
|
+
) from e
|
|
140
|
+
raise DocxError(
|
|
141
|
+
f"docx-studio /docs/empty returned HTTP {e.code}: {err_body}",
|
|
142
|
+
) from e
|
|
143
|
+
except urllib.error.URLError as e:
|
|
144
|
+
raise SessionError(
|
|
145
|
+
f"Unable to reach docx-studio at {url}: {e.reason}",
|
|
146
|
+
) from e
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
parsed: dict = json.loads(raw.decode("utf-8"))
|
|
150
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
151
|
+
raise SessionError(
|
|
152
|
+
f"docx-studio returned non-JSON response: {raw[:200]!r}",
|
|
153
|
+
) from e
|
|
154
|
+
|
|
155
|
+
asset_id_obj = parsed.get("assetId")
|
|
156
|
+
name_obj = parsed.get("name")
|
|
157
|
+
ws_obj = parsed.get("workspaceId")
|
|
158
|
+
collab_obj = parsed.get("collab")
|
|
159
|
+
if not (
|
|
160
|
+
isinstance(asset_id_obj, str)
|
|
161
|
+
and isinstance(name_obj, str)
|
|
162
|
+
and isinstance(ws_obj, str)
|
|
163
|
+
and isinstance(collab_obj, dict)
|
|
164
|
+
):
|
|
165
|
+
raise SessionError(
|
|
166
|
+
f"docx-studio response is missing required fields: {parsed!r}",
|
|
167
|
+
)
|
|
168
|
+
token_obj = collab_obj.get("token")
|
|
169
|
+
ws_url_obj = collab_obj.get("wsUrl")
|
|
170
|
+
bundle_ws_obj = collab_obj.get("workspaceId")
|
|
171
|
+
if not (
|
|
172
|
+
isinstance(token_obj, str)
|
|
173
|
+
and isinstance(ws_url_obj, str)
|
|
174
|
+
and isinstance(bundle_ws_obj, str)
|
|
175
|
+
):
|
|
176
|
+
raise SessionError(
|
|
177
|
+
f"docx-studio collab bundle is malformed: {collab_obj!r}",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return CreateAssetResult(
|
|
181
|
+
asset_id=asset_id_obj,
|
|
182
|
+
name=name_obj,
|
|
183
|
+
workspace_id=ws_obj,
|
|
184
|
+
collab=_CollabBundle(
|
|
185
|
+
SUPERDOC_COLLAB_TOKEN=token_obj,
|
|
186
|
+
KERYX_WS_URL=ws_url_obj,
|
|
187
|
+
ATHENA_WORKSPACE_ID=bundle_ws_obj,
|
|
188
|
+
),
|
|
189
|
+
)
|