athena-python-docx 0.2.3__tar.gz → 0.4.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.2.3 → athena_python_docx-0.4.0}/CLAUDE.md +12 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/PKG-INFO +1 -1
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/__init__.py +1 -1
- athena_python_docx-0.4.0/docx/_http.py +189 -0
- athena_python_docx-0.4.0/docx/_http_doc.py +181 -0
- athena_python_docx-0.4.0/docx/api.py +84 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/client.py +95 -17
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/document.py +88 -2
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/pyproject.toml +1 -1
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/README.md +1 -1
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/fake_session.py +10 -1
- athena_python_docx-0.4.0/tests/test_document_create.py +187 -0
- athena_python_docx-0.4.0/tests/test_http_transport.py +172 -0
- athena_python_docx-0.2.3/docx/api.py +0 -41
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/.gitignore +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/README.md +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/_batching.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/section.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/style.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/table.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/enum/text.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/errors.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/opc/coreprops.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/section.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/settings.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/shape.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/shared.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/styles/style.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/styles/styles.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/table.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/hyperlink.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/paragraph.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/parfmt.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/text/run.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/docx/typing.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/scripts/publish.sh +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/scripts/release.sh +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/conftest.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/METHODOLOGY.md +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/binary_round_trip.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/local_runner.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshot.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/103_cell_tables_enumeration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/23_nested_table.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/51_nested_tables_deep.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex01_five_levels_deep_tables.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/ours_spec.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/parity_diff.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/real_world_cases.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/runner.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/fidelity/stock_spec.json +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/test_commands.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.2.3 → athena_python_docx-0.4.0}/tests/test_smoke_integration.py +0 -0
|
@@ -35,6 +35,18 @@ These standard python-docx members don't apply to a Superdoc-backed SDK:
|
|
|
35
35
|
- `Document.settings` — Word app settings (not surfaced by Superdoc)
|
|
36
36
|
- `InlineShape.chart` — charts (Phase 2+)
|
|
37
37
|
|
|
38
|
+
### Intentional deviations (additions / different semantics)
|
|
39
|
+
|
|
40
|
+
These differ from stock python-docx because the SDK is asset-backed,
|
|
41
|
+
not file-backed. Each is documented in the relevant docstring.
|
|
42
|
+
|
|
43
|
+
- **`Document.create(name=, base_url=, api_key=, parent_folder_id=, workspace_id=)`**
|
|
44
|
+
classmethod. Stock python-docx returns a blank in-memory document
|
|
45
|
+
for `Document(None)`; we can't fabricate a SuperDocument asset
|
|
46
|
+
client-side, so net-new asset creation is a separate factory that
|
|
47
|
+
hits `POST {base_url}/docs/empty`. The constructor positional-arg
|
|
48
|
+
shape (`Document(asset_id)`) is preserved for parity.
|
|
49
|
+
|
|
38
50
|
### If you need a deviation
|
|
39
51
|
|
|
40
52
|
If there is a genuine technical reason why a deviation from python-docx is necessary:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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>
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""HTTP-backed Superdoc document handle.
|
|
2
|
+
|
|
3
|
+
This is the core of the ``transport="http"`` path. It mirrors the
|
|
4
|
+
shape of the bound doc API that ``superdoc-sdk`` exposes — every
|
|
5
|
+
existing SDK call site uses ``self._session.doc.<dotted.path>(params)``,
|
|
6
|
+
and ``HttpDocHandle`` matches that shape via a lazy attribute walker.
|
|
7
|
+
|
|
8
|
+
When the user calls ``doc.create.paragraph({...})`` the path proxy
|
|
9
|
+
accumulates the dotted name and the terminal ``__call__`` POSTs a
|
|
10
|
+
single-command request to docx-studio's ``POST /docs/:id/commands``,
|
|
11
|
+
unpacks the dispatcher's ``applied[0].result``, and returns it. The
|
|
12
|
+
existing SDK code expects a Python dict back, which the server returns
|
|
13
|
+
verbatim from Superdoc.
|
|
14
|
+
|
|
15
|
+
The HTTP path is a 1:1 substitute for the embedded Superdoc CLI; no
|
|
16
|
+
SDK call site has to change to opt in.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.request
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from docx.errors import (
|
|
27
|
+
AuthenticationError,
|
|
28
|
+
DocxError,
|
|
29
|
+
SessionError,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _user_agent() -> str:
|
|
34
|
+
# Lazy import — docx/__init__.py imports api.py at package load, but
|
|
35
|
+
# _http_doc is only imported lazily from client.py at session-open
|
|
36
|
+
# time, so the package is fully initialized by the time this runs.
|
|
37
|
+
from docx import __version__
|
|
38
|
+
|
|
39
|
+
return f"athena-python-docx/{__version__}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _http_post_json(
|
|
43
|
+
*,
|
|
44
|
+
url: str,
|
|
45
|
+
api_key: str,
|
|
46
|
+
body: dict,
|
|
47
|
+
timeout: float = 60.0,
|
|
48
|
+
) -> dict:
|
|
49
|
+
"""POST JSON, parse JSON, raise typed errors on non-2xx."""
|
|
50
|
+
payload: bytes = json.dumps(body).encode("utf-8")
|
|
51
|
+
req = urllib.request.Request( # noqa: S310
|
|
52
|
+
url,
|
|
53
|
+
data=payload,
|
|
54
|
+
method="POST",
|
|
55
|
+
headers={
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
"Authorization": f"Bearer {api_key}",
|
|
58
|
+
"Accept": "application/json",
|
|
59
|
+
"User-Agent": _user_agent(),
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
try:
|
|
63
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
|
|
64
|
+
raw: bytes = resp.read()
|
|
65
|
+
return json.loads(raw.decode("utf-8"))
|
|
66
|
+
except urllib.error.HTTPError as e:
|
|
67
|
+
err_body: str = ""
|
|
68
|
+
try:
|
|
69
|
+
err_body = e.read().decode("utf-8", errors="replace")
|
|
70
|
+
except Exception: # noqa: BLE001
|
|
71
|
+
pass
|
|
72
|
+
if e.code in (401, 403):
|
|
73
|
+
raise AuthenticationError(
|
|
74
|
+
f"docx-studio rejected the request (HTTP {e.code}): {err_body}",
|
|
75
|
+
) from e
|
|
76
|
+
if e.code == 207:
|
|
77
|
+
# Partial-success — caller may want to inspect. Re-raise as
|
|
78
|
+
# DocxError but parse the body so they can see what failed.
|
|
79
|
+
try:
|
|
80
|
+
parsed = json.loads(err_body) if err_body else {}
|
|
81
|
+
except json.JSONDecodeError:
|
|
82
|
+
parsed = {"raw": err_body}
|
|
83
|
+
raise DocxError(
|
|
84
|
+
f"docx-studio batch reported a partial failure: {parsed!r}",
|
|
85
|
+
) from e
|
|
86
|
+
raise DocxError(
|
|
87
|
+
f"docx-studio returned HTTP {e.code}: {err_body}",
|
|
88
|
+
) from e
|
|
89
|
+
except urllib.error.URLError as e:
|
|
90
|
+
raise SessionError(
|
|
91
|
+
f"Unable to reach docx-studio at {url}: {e.reason}",
|
|
92
|
+
) from e
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class HttpClient:
|
|
96
|
+
"""Owns the HTTP transport for a Document.
|
|
97
|
+
|
|
98
|
+
Stateless wrt batching today — every SDK primitive maps to one
|
|
99
|
+
POST /commands with a single-element commands array. Future:
|
|
100
|
+
add a batch buffer that flushes on `with doc.batch():` exit.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, *, base_url: str, api_key: str) -> None:
|
|
104
|
+
self._base_url: str = base_url.rstrip("/")
|
|
105
|
+
self._api_key: str = api_key
|
|
106
|
+
|
|
107
|
+
def execute(self, asset_id: str, op: str, params: dict) -> Any:
|
|
108
|
+
"""Send one command to /docs/:id/commands and return its result."""
|
|
109
|
+
url: str = f"{self._base_url}/docs/{asset_id}/commands"
|
|
110
|
+
body: dict = {
|
|
111
|
+
"commands": [{"op": op, "params": params}],
|
|
112
|
+
"return": {"snapshot": False},
|
|
113
|
+
}
|
|
114
|
+
resp: dict = _http_post_json(url=url, api_key=self._api_key, body=body)
|
|
115
|
+
applied = resp.get("applied")
|
|
116
|
+
if not isinstance(applied, list) or not applied:
|
|
117
|
+
err = resp.get("error")
|
|
118
|
+
if err:
|
|
119
|
+
raise DocxError(
|
|
120
|
+
f"docx-studio command {op!r} failed: {err!r}",
|
|
121
|
+
)
|
|
122
|
+
raise DocxError(
|
|
123
|
+
f"docx-studio returned no applied entry for {op!r}: {resp!r}",
|
|
124
|
+
)
|
|
125
|
+
result = applied[0].get("result")
|
|
126
|
+
return result if isinstance(result, dict) else {}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _PathProxy:
|
|
130
|
+
"""Lazy attribute walker — turns ``doc.create.paragraph(p)`` into
|
|
131
|
+
``client.execute(asset_id, "create.paragraph", p)``.
|
|
132
|
+
|
|
133
|
+
The Python SDK's call sites use snake_case dotted paths
|
|
134
|
+
(e.g. ``doc.format.paragraph.set_alignment``) which match the
|
|
135
|
+
Python ``superdoc-sdk`` Bound API. The server-side dispatcher
|
|
136
|
+
converts to camelCase before resolving on the npm SDK's bound API,
|
|
137
|
+
so the wire op-name stays in snake_case and clients across
|
|
138
|
+
languages can share it.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
__slots__ = ("_client", "_asset_id", "_path")
|
|
142
|
+
|
|
143
|
+
def __init__(self, client: HttpClient, asset_id: str, path: str) -> None:
|
|
144
|
+
self._client: HttpClient = client
|
|
145
|
+
self._asset_id: str = asset_id
|
|
146
|
+
self._path: str = path
|
|
147
|
+
|
|
148
|
+
def __getattr__(self, name: str) -> "_PathProxy":
|
|
149
|
+
# Block Python special names so iteration / hash / etc. don't
|
|
150
|
+
# silently deepen the path.
|
|
151
|
+
if name.startswith("__"):
|
|
152
|
+
raise AttributeError(name)
|
|
153
|
+
new_path: str = f"{self._path}.{name}" if self._path else name
|
|
154
|
+
return _PathProxy(self._client, self._asset_id, new_path)
|
|
155
|
+
|
|
156
|
+
async def __call__(self, params: dict | None = None) -> Any:
|
|
157
|
+
# The existing SDK is async-style (calls go through run_sync);
|
|
158
|
+
# __call__ is async to match. _http_post_json is blocking under
|
|
159
|
+
# the hood, but run_sync runs us on a persistent event-loop
|
|
160
|
+
# thread, so blocking HTTP is fine — it doesn't starve the
|
|
161
|
+
# main thread.
|
|
162
|
+
return self._client.execute(self._asset_id, self._path, params or {})
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class HttpDocHandle:
|
|
166
|
+
"""Drop-in replacement for ``superdoc.AsyncSuperDocClient.open()``'s
|
|
167
|
+
return value. Existing SDK call sites use ``self._session.doc.<X>(p)``
|
|
168
|
+
where ``X`` is a dotted path on the bound doc API; this object
|
|
169
|
+
forwards every such access to the HTTP transport via _PathProxy.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
__slots__ = ("_client", "_asset_id")
|
|
173
|
+
|
|
174
|
+
def __init__(self, client: HttpClient, asset_id: str) -> None:
|
|
175
|
+
self._client: HttpClient = client
|
|
176
|
+
self._asset_id: str = asset_id
|
|
177
|
+
|
|
178
|
+
def __getattr__(self, name: str) -> _PathProxy:
|
|
179
|
+
if name.startswith("__"):
|
|
180
|
+
raise AttributeError(name)
|
|
181
|
+
return _PathProxy(self._client, self._asset_id, name)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""The `Document` factory — matches python-docx's `docx.Document(path)`.
|
|
2
|
+
|
|
3
|
+
python-docx signature:
|
|
4
|
+
Document(docx=None) -> Document
|
|
5
|
+
|
|
6
|
+
Our signature deviates from the path-based one because we don't open
|
|
7
|
+
.docx files from disk — we open Y.Doc assets from Keryx. The parameter
|
|
8
|
+
is reused: you pass an asset_id string where python-docx would take a
|
|
9
|
+
file path.
|
|
10
|
+
|
|
11
|
+
For net-new asset creation, use the classmethod ``Document.create()``
|
|
12
|
+
(see ``docx.document``). Stock python-docx returns a blank document
|
|
13
|
+
when ``Document(None)`` is called, but our SDK can't fabricate a
|
|
14
|
+
SuperDocument asset out of thin air — there's an Athena-side asset
|
|
15
|
+
write that has to happen first. ``Document.create()`` handles that via
|
|
16
|
+
``POST /docs/empty`` on docx-studio.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from docx.document import Document as _Document
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def Document(
|
|
25
|
+
docx: str | None = None,
|
|
26
|
+
*,
|
|
27
|
+
transport: str = "http",
|
|
28
|
+
base_url: str | None = None,
|
|
29
|
+
api_key: str | None = None,
|
|
30
|
+
) -> _Document:
|
|
31
|
+
"""Open a Word document for editing.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
docx: Athena asset_id of the SuperDoc document to open.
|
|
35
|
+
In python-docx this takes a file path; here it takes
|
|
36
|
+
an asset_id. To create a NEW document instead of opening
|
|
37
|
+
an existing one, call ``Document.create(...)`` (a
|
|
38
|
+
classmethod on the Document class).
|
|
39
|
+
transport: Either ``"http"`` (default since 0.4.0 — every
|
|
40
|
+
primitive routes through docx-studio's
|
|
41
|
+
``POST /docs/:id/commands``; pure-Python client, no
|
|
42
|
+
embedded binary) or ``"direct"`` (legacy — embedded
|
|
43
|
+
Superdoc CLI talks to Keryx via y-websocket; requires
|
|
44
|
+
``SUPERDOC_COLLAB_TOKEN`` env vars; emits a
|
|
45
|
+
``DeprecationWarning`` and will be removed in 1.0).
|
|
46
|
+
The ``http`` transport requires ``base_url`` +
|
|
47
|
+
``api_key`` (or the ``ATHENA_DOCX_*`` env vars).
|
|
48
|
+
base_url: docx-studio API base for ``transport="http"``. Falls
|
|
49
|
+
back to ``$ATHENA_DOCX_BASE_URL``.
|
|
50
|
+
api_key: Athena API key / PropelAuth bearer for
|
|
51
|
+
``transport="http"``. Falls back to ``$ATHENA_DOCX_API_KEY``.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A Document instance bound to the asset.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: if ``docx`` is None or empty. Use ``Document.create()``
|
|
58
|
+
to make a fresh asset.
|
|
59
|
+
"""
|
|
60
|
+
if not docx:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"athena-python-docx requires an asset_id. "
|
|
63
|
+
"Pass the SuperDoc asset ID as the first argument: "
|
|
64
|
+
"Document('asset_xxx...'). "
|
|
65
|
+
"To create a brand-new document, call "
|
|
66
|
+
"Document.create(name=..., base_url=..., api_key=...).",
|
|
67
|
+
)
|
|
68
|
+
return _Document(
|
|
69
|
+
asset_id=docx,
|
|
70
|
+
transport=transport,
|
|
71
|
+
http_base_url=base_url,
|
|
72
|
+
http_api_key=api_key,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Re-export the classmethod factories at module level so callers can
|
|
77
|
+
# write either:
|
|
78
|
+
# from docx import Document
|
|
79
|
+
# Document.create(name=...)
|
|
80
|
+
# or:
|
|
81
|
+
# from docx.api import Document
|
|
82
|
+
# Document.create(name=...)
|
|
83
|
+
# (matching python-docx's flat API surface.)
|
|
84
|
+
Document.create = _Document.create # type: ignore[attr-defined]
|
|
@@ -64,12 +64,28 @@ class Session:
|
|
|
64
64
|
*,
|
|
65
65
|
asset_id: str,
|
|
66
66
|
user_info: dict[str, str] | None = None,
|
|
67
|
+
bundle: dict[str, str] | None = None,
|
|
68
|
+
transport: str = "direct",
|
|
69
|
+
http_base_url: str | None = None,
|
|
70
|
+
http_api_key: str | None = None,
|
|
67
71
|
) -> None:
|
|
68
72
|
self._asset_id: str = asset_id
|
|
69
73
|
self._user_info: dict[str, str] = user_info or {
|
|
70
74
|
"name": "Athena Agent",
|
|
71
75
|
"email": "agent@athenaintel.com",
|
|
72
76
|
}
|
|
77
|
+
# Optional pre-minted collab bundle. When set, open() uses these
|
|
78
|
+
# values instead of reading from os.environ. Used by Document.create()
|
|
79
|
+
# so the freshly-minted Keryx JWT returned from the create API is
|
|
80
|
+
# consumed directly without a round-trip through env vars.
|
|
81
|
+
self._bundle: dict[str, str] | None = bundle
|
|
82
|
+
# Transport: "direct" → embedded superdoc-sdk talks Keryx directly
|
|
83
|
+
# (legacy path, requires SUPERDOC_COLLAB_TOKEN env vars).
|
|
84
|
+
# "http" → all primitives go through docx-studio's
|
|
85
|
+
# POST /docs/:id/commands; no embedded Superdoc, no Keryx WS in-process.
|
|
86
|
+
self._transport: str = transport
|
|
87
|
+
self._http_base_url: str | None = http_base_url
|
|
88
|
+
self._http_api_key: str | None = http_api_key
|
|
73
89
|
self._client: Any | None = None
|
|
74
90
|
self._doc_handle: Any | None = None
|
|
75
91
|
self._opened: bool = False
|
|
@@ -84,10 +100,19 @@ class Session:
|
|
|
84
100
|
return self._opened and not self._closed
|
|
85
101
|
|
|
86
102
|
async def open(self) -> None:
|
|
87
|
-
"""Open
|
|
103
|
+
"""Open a session against the configured transport.
|
|
104
|
+
|
|
105
|
+
For ``transport="http"`` this is a near no-op — we just construct
|
|
106
|
+
the HTTP doc handle. The first command-level call hits docx-studio,
|
|
107
|
+
which performs the ownership check and acquires the Superdoc
|
|
108
|
+
session server-side.
|
|
109
|
+
|
|
110
|
+
For ``transport="direct"`` (legacy) this opens an embedded
|
|
111
|
+
Superdoc session and y-websocket to Keryx. Requires the
|
|
112
|
+
``SUPERDOC_COLLAB_TOKEN`` env var (or a pre-minted bundle).
|
|
88
113
|
|
|
89
114
|
Raises:
|
|
90
|
-
AuthenticationError: if
|
|
115
|
+
AuthenticationError: if creds are missing/invalid.
|
|
91
116
|
SessionError: on any other open-time failure.
|
|
92
117
|
"""
|
|
93
118
|
if self._closed:
|
|
@@ -97,9 +122,57 @@ class Session:
|
|
|
97
122
|
if self._opened:
|
|
98
123
|
return
|
|
99
124
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
if self._transport == "direct":
|
|
126
|
+
import warnings
|
|
127
|
+
|
|
128
|
+
warnings.warn(
|
|
129
|
+
"transport='direct' is deprecated and will be removed in "
|
|
130
|
+
"athena-python-docx 1.0. Pass transport='http' (the new "
|
|
131
|
+
"default) or remove the argument. The HTTP transport is "
|
|
132
|
+
"pure-Python, has no embedded Superdoc binary, and works "
|
|
133
|
+
"without the SUPERDOC_COLLAB_TOKEN env-var dance.",
|
|
134
|
+
DeprecationWarning,
|
|
135
|
+
stacklevel=3,
|
|
136
|
+
)
|
|
137
|
+
elif self._transport == "http":
|
|
138
|
+
from docx._http_doc import HttpClient, HttpDocHandle
|
|
139
|
+
|
|
140
|
+
base_url: str | None = self._http_base_url or os.environ.get(
|
|
141
|
+
"ATHENA_DOCX_BASE_URL",
|
|
142
|
+
)
|
|
143
|
+
api_key: str | None = self._http_api_key or os.environ.get(
|
|
144
|
+
"ATHENA_DOCX_API_KEY",
|
|
145
|
+
)
|
|
146
|
+
if not base_url:
|
|
147
|
+
raise SessionError(
|
|
148
|
+
"Missing base_url for HTTP transport. Pass base_url= "
|
|
149
|
+
"to Document(...) or set ATHENA_DOCX_BASE_URL.",
|
|
150
|
+
)
|
|
151
|
+
if not api_key:
|
|
152
|
+
raise AuthenticationError(
|
|
153
|
+
"Missing api_key for HTTP transport. Pass api_key= "
|
|
154
|
+
"to Document(...) or set ATHENA_DOCX_API_KEY.",
|
|
155
|
+
)
|
|
156
|
+
client = HttpClient(base_url=base_url, api_key=api_key)
|
|
157
|
+
self._client = client
|
|
158
|
+
self._doc_handle = HttpDocHandle(client, self._asset_id)
|
|
159
|
+
self._opened = True
|
|
160
|
+
_log_info(f"Opened {self._asset_id} (http)")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
# ----- legacy direct transport -----
|
|
164
|
+
|
|
165
|
+
# Prefer the in-memory bundle over env vars when present — that
|
|
166
|
+
# path is taken by Document.create(), which receives a fresh
|
|
167
|
+
# bundle in the create-asset HTTP response.
|
|
168
|
+
if self._bundle is not None:
|
|
169
|
+
token = self._bundle.get(_TOKEN_ENV)
|
|
170
|
+
ws_url = self._bundle.get(_WS_URL_ENV)
|
|
171
|
+
workspace_id = self._bundle.get(_WORKSPACE_ENV)
|
|
172
|
+
else:
|
|
173
|
+
token = os.environ.get(_TOKEN_ENV)
|
|
174
|
+
ws_url = os.environ.get(_WS_URL_ENV)
|
|
175
|
+
workspace_id = os.environ.get(_WORKSPACE_ENV)
|
|
103
176
|
|
|
104
177
|
if not token:
|
|
105
178
|
raise AuthenticationError(
|
|
@@ -173,33 +246,38 @@ class Session:
|
|
|
173
246
|
return self._doc_handle
|
|
174
247
|
|
|
175
248
|
async def save(self, *, in_place: bool = True) -> None: # noqa: ARG002
|
|
176
|
-
"""Ensure pending mutations have flushed
|
|
249
|
+
"""Ensure pending mutations have flushed.
|
|
177
250
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
to
|
|
181
|
-
targets file-backed docs and errors with "this session has no
|
|
182
|
-
source path" for collab sessions. So save() is a thin flush:
|
|
183
|
-
sleep briefly so in-flight WebSocket frames land, then return.
|
|
251
|
+
For ``transport="http"`` this is a true no-op — every primitive
|
|
252
|
+
was a synchronous HTTP round-trip that the server already
|
|
253
|
+
committed to Keryx before responding.
|
|
184
254
|
|
|
185
|
-
|
|
186
|
-
|
|
255
|
+
For ``transport="direct"`` we sleep briefly to let in-flight
|
|
256
|
+
y-websocket frames land before close, matching the legacy
|
|
257
|
+
superdoc_write_utils.py 1-second pre-close delay.
|
|
187
258
|
"""
|
|
188
259
|
if not self._opened:
|
|
189
260
|
raise SessionError("Cannot save a session that was never opened.")
|
|
190
261
|
if self._closed:
|
|
191
262
|
raise DocumentClosedError(f"Session {self._asset_id} is closed.")
|
|
192
263
|
|
|
193
|
-
|
|
194
|
-
|
|
264
|
+
if self._transport == "http":
|
|
265
|
+
_log_info(f"Saved (no-op for http transport) {self._asset_id}")
|
|
266
|
+
return
|
|
267
|
+
|
|
195
268
|
await asyncio.sleep(1)
|
|
196
269
|
_log_info(f"Saved (flushed) {self._asset_id}")
|
|
197
270
|
|
|
198
271
|
async def close(self) -> None:
|
|
199
|
-
"""Close the
|
|
272
|
+
"""Close the session. Idempotent."""
|
|
200
273
|
if self._closed:
|
|
201
274
|
return
|
|
202
275
|
|
|
276
|
+
if self._transport == "http":
|
|
277
|
+
self._closed = True
|
|
278
|
+
_log_info(f"Closed {self._asset_id} (http)")
|
|
279
|
+
return
|
|
280
|
+
|
|
203
281
|
if self._opened and self._doc_handle is not None:
|
|
204
282
|
try:
|
|
205
283
|
# Match the 1-second flush delay from superdoc_write_utils
|