athena-python-docx 0.16.0__tar.gz → 0.17.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.16.0 → athena_python_docx-0.17.0}/CLAUDE.md +15 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/PKG-INFO +1 -1
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/__init__.py +1 -1
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_buffer.py +16 -1
- athena_python_docx-0.17.0/docx/_image_utils.py +214 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/commands.py +5 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/document.py +29 -16
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/text/paragraph.py +49 -7
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/text/run.py +8 -16
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/pyproject.toml +1 -1
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/fake_session.py +7 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/70_add_and_iterate_back.json +1 -5
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex18_read_back_large_doc.json +1 -101
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex19_mutate_all_runs.json +0 -5
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw09_bulk_run_iteration.json +0 -10
- athena_python_docx-0.17.0/tests/test_image_url_data_uri.py +283 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_iter_inner_content.py +65 -3
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_ptc.py +40 -0
- athena_python_docx-0.16.0/docx/_image_utils.py +0 -90
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/.gitignore +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/DOCX_EXEC_LAB.md +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/README.md +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_athena_extension.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_batching.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_execution.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_http.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_http_doc.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_postproc.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_ptc.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_table_styles.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/_timeouts.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/api.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/bookmarks.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/charts.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/client.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/comments.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/enum/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/enum/section.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/enum/style.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/enum/table.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/enum/text.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/errors.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/exceptions.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/fields.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/footnotes.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/math.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/opc/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/opc/coreprops.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/oxml/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/revisions.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/sdt.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/section.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/session.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/settings.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/shape.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/shared.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/styles/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/styles/style.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/styles/styles.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/table.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/text/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/text/font.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/text/hyperlink.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/text/pagebreak.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/text/parfmt.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/text/tabstops.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/toc.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/docx/typing.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/docx_exec_lab.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/docx_exec_lab_examples/fast_table_fill.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/docx_exec_lab_examples/find_replace_literal.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/publish.sh +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/release.sh +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/round_trip_smoke.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/smoke_test_block_not_found.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/scripts/validate_find_replace_asset.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/conftest.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/METHODOLOGY.md +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/README.md +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/ab_probe_cases.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/ab_probe_runner.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/auto_gen_cases.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/binary_round_trip.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/cases.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/complex_cases.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/coverage_report.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/extract.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/extreme_cases.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/firm_templates/README.md +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/firm_templates/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/firm_templates/_runner.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/firm_templates/extractor.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/firm_templates/test_pw_corpus.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/firm_templates/test_pw_research_digest.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/local_runner.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/mega_cases.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshot.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/01_basic_paragraph.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/02_multiple_headings.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/03_runs_with_formatting.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/04_font_name_and_size.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/05_font_color_rgb.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/06_font_character_properties.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/07_font_subscript_superscript.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/08_font_highlight.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/09_paragraph_alignment.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/100_table_negative_indexing.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/101_table_cells_flat_iteration.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/102_text_with_embedded_special_chars.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/104_core_properties_datetime.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/105_default_one_section.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/106_heading_paragraph_format.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/107_varying_row_heights.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/10_paragraph_indents.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/11_paragraph_spacing.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/12_paragraph_keep_options.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/13_paragraph_tab_stops.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/14_run_add_tab_and_break.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/15_run_add_break_page.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/16_paragraph_clear_and_insert_before.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/17_table_basic.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/18_table_cell_text.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/19_table_row_column_sizing.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/20_table_cell_vertical_alignment.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/21_table_alignment_and_autofit.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/22_table_cell_paragraphs_iteration.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/24_table_add_row_column.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/25_table_merge_cells.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/26_section_page_setup.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/27_section_margins.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/28_section_add_new.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/29_section_headers_linked.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/30_styles_iteration.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/31_styles_lookup_and_default.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/32_styles_add_paragraph_style.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/33_core_properties_set_and_get.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/34_inline_shapes_iterate_empty.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/35_full_report.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/36_replace_text_in_paragraph.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/37_iterate_runs_and_format_all_bold.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/38_font_all_properties.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/39_large_body_100_paragraphs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/40_large_table_10x10.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/41_unicode_and_emoji.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/42_very_long_paragraph.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/43_paragraph_text_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/44_paragraph_alignment_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/45_cell_text_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/46_run_text_setter_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/47_font_size_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/48_font_color_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/49_resume_layout.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/50_multi_section_doc.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/52_iterate_everything.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/53_apply_style_to_paragraphs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/54_empty_everything.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/55_single_character_runs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/56_everything_in_one.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/57_runs_after_multiple_text_appends.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/58_modify_runs_in_place.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/59_indent_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/60_space_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/61_cell_paragraph_with_runs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/62_many_cell_paragraphs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/63_table_style_round_trip.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/64_many_sections.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/65_20x20_table_formatted.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/66_toc_like_structure.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/67_paragraph_insert_before_chain.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/68_invoice.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/69_newsletter.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/71_academic_paper.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/72_legal_contract.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/73_form_with_many_tables.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/74_paragraph_with_10_runs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/75_paragraph_negative_first_line_indent.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/76_rgbcolor_from_string.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/77_length_unit_conversions.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/78_paragraph_copy_style.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/79_bulk_cell_formatting.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/80_apply_style_after_add_run.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/81_multi_page_with_breaks.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/82_add_text_on_existing_run.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/83_clear_then_repopulate_paragraph.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/84_table_reread_row_count.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/85_header_footer_access.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/86_font_read_unset_returns_none.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/87_500_paragraph_doc.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/88_mixed_content_iteration.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/89_alignment_clear_via_none.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/90_cell_add_paragraph_styled.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/91_many_small_tables.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/92_margins_every_section.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/93_font_bool_reads_after_set.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/94_page_break_before_paragraph.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/95_paragraph_hyperlinks_empty.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/96_paragraph_contains_page_break.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/97_document_styles_by_key.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/98_style_contains_check.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/99_run_add_picture_from_bytes.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex02_unicode_everywhere.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex03_1000_paragraphs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex04_50x50_table.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex05_long_text_in_cell.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex06_hundred_tiny_runs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex07_every_font_boolean.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex08_many_continuous_sections.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex09_many_tab_stops.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex10_complex_bom.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex11_banded_rows_formatting.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex12_section_reconfigure.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex13_cell_with_10_paragraphs.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex14_styled_report_table.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex15_paragraph_all_format_props.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex16_runs_interleaved_with_breaks.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex17_all_break_kinds.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/ex20_kitchen_sink_v2.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega01_book_chapter.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega02_research_proposal.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega03_financial_statement.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega04_recipe_card.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega05_user_manual.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega06_complex_newsletter.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega07_budget_spreadsheet.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega08_product_catalog.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega09_signed_contract.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/mega10_api_documentation.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw01_official_quickstart.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw02_paragraph_style_list.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw03_character_formatting.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw04_section_page_setup.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw05_toc_pattern.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw06_meeting_minutes.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw07_dense_formatting_demo.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw08_table_merged_header.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw10_colored_grid_table.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw11_header_text.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw12_first_page_footer.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw13_even_page_header.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/op_snapshots/rw15_paragraph_style_instance.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/ours_spec.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/parity_crawl.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/parity_diff.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/real_world_cases.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/round_trip_tests.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/runner.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/stock_spec.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/fidelity/test_e2e_against_staging.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/README.md +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/__init__.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/baseline_gaps.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/compare.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/intentional_deviations.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/introspect.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/reports/GAP_ANALYSIS.md +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/reports/gap_report.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/run_parity.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/snapshots/athena_latest.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/snapshots/upstream_python_docx_1.2.0.json +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/parity/test_parity_gap.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_add_section_extract_items.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_athena_extensions_contract.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_athena_extensions_registry.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_batching_perf.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_block_not_found_error.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_buffer.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_add_paragraph_wire_shape.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_add_table_not_supported.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_inner_add_hyperlink_stash.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_inner_add_run_via_cell_insert.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_inner_format_stash.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_inner_run_format_stash.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_inner_run_guard.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_text_plain_fastpath.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_cell_text_replace_semantics.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_collapsed_range_format.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_command_dataclasses.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_commands.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_comments.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_document_asset_id_property.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_document_clear.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_document_create.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_document_create_from_template.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_document_factory_validation.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_docx_exec_lab.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_docx_exec_lab_server.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_e2e_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_execution_scope.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_find_replace_session_open.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_http_transport.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_hyperlink_coalescing.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_insert_deferred.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_list_styles.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_merged_cell_secondary_slot.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_merged_cells.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_oxml_shim.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_paragraph_text_len_cache.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_parity_misc.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_parity_round2.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_partial_failure_cascade.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_phase_a_behavior.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_phase_b_headers_footers.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_phase_c_tables.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_postproc_cell_format_rewrite.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_postproc_cell_run_format_rewrite.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_postproc_ref_restore.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_pr19766_review_fixes.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_python_docx_api_parity.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_revisions.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_add_paragraph_style.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_add_picture.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_add_run.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_cell_add_paragraph.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_comments_add_comment.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_comments_get.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_document_audit.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_document_element.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_enum_section.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_font_audit.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_header_footer.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_hyperlink.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_inline_shape.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_insert_paragraph_before.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_misc.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_paragraph_strict.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_paragraph_style.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_paragraph_style_strict.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_parfmt.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_row_col_cell.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_run_add_break.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_run_bool_setters.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_run_style.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_run_style_strict.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_run_text.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_run_underline.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_section_audit.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_section_dimensions.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_section_onoff.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_settings.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_shared_audit.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_style.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_styles.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_table_audit.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_table_cell.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_table_dimensions.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_silent_stub_table_layout.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_smoke_integration.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_style_acceptance.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_style_font.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_style_setters_contract.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_table_set_cell_perf.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_table_style_id_resolution.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_temporarily_unavailable.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_validate_find_replace_asset_script.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_wire_contract.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_xml_attr_guard.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/tests/test_zod_wire_contract.py +0 -0
- {athena_python_docx-0.16.0 → athena_python_docx-0.17.0}/uv.lock +0 -0
|
@@ -93,6 +93,21 @@ remaining work* for the exact wire-op shape needed to unblock each.
|
|
|
93
93
|
These differ from stock python-docx because the SDK is asset-backed,
|
|
94
94
|
not file-backed. Each is documented in the relevant docstring.
|
|
95
95
|
|
|
96
|
+
- **`add_picture` accepts an image URL or `data:` URI, not just a path
|
|
97
|
+
or stream.** Stock python-docx routes ``image_path_or_stream`` through
|
|
98
|
+
``docx.image.image.Image.from_file``, which opens a ``str`` as a local
|
|
99
|
+
filesystem path and has no networking — so ``add_picture("https://…")``
|
|
100
|
+
raises ``FileNotFoundError``. Agents (and humans) routinely pass a
|
|
101
|
+
remote image URL, so ``Document.add_picture`` / ``Run.add_picture``
|
|
102
|
+
additionally fetch ``http(s)://`` URLs (via the already-required
|
|
103
|
+
``requests`` dependency, 30s timeout) and decode ``data:[…;base64],…``
|
|
104
|
+
URIs. Local paths and file-like streams behave exactly as upstream.
|
|
105
|
+
The dispatch lives in ``docx._image_utils.load_image_bytes``; a failed
|
|
106
|
+
fetch/decode raises a ``ValueError`` at the call site rather than a
|
|
107
|
+
bare transport error. This is the only image-input deviation — the
|
|
108
|
+
resulting bytes still flow through the same magic-byte
|
|
109
|
+
``sniff_content_type`` → base64 ``data:`` URI → ``create.image`` path.
|
|
110
|
+
|
|
96
111
|
- **Built-in table style friendly names resolve to OOXML styleIds.**
|
|
97
112
|
Word's ``styles.xml`` ships built-in table styles whose styleIds
|
|
98
113
|
(``LightGrid-Accent1``, ``MediumShading1-Accent2``, …) differ from
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-docx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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>
|
|
@@ -130,6 +130,21 @@ if TYPE_CHECKING:
|
|
|
130
130
|
_PTC_EMIT_TOOLS: frozenset[str] = frozenset({"CreateParagraph", "CreateHeading", "CreateTable"})
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
def _should_emit_ptc(cmd: Command) -> bool:
|
|
134
|
+
"""Whether ``cmd`` should produce a PTC sub-tool-card.
|
|
135
|
+
|
|
136
|
+
Only allow-listed block creates emit, and only when they target the
|
|
137
|
+
**body** story. Creates carrying an ``in_`` story locator (header,
|
|
138
|
+
footer, footnote, …) are skipped: the frontend locate affordance
|
|
139
|
+
scans the body editor only, so a header/footer card would highlight
|
|
140
|
+
an unrelated body block or nothing at all. Skip until the frontend
|
|
141
|
+
can route by ``args.in``.
|
|
142
|
+
"""
|
|
143
|
+
if type(cmd).__name__ not in _PTC_EMIT_TOOLS:
|
|
144
|
+
return False
|
|
145
|
+
return getattr(cmd, "in_", None) is None
|
|
146
|
+
|
|
147
|
+
|
|
133
148
|
def _ptc_emit_end_batch(cmds: list[Command], *, is_error: bool) -> None:
|
|
134
149
|
"""Emit a PTC ``end`` event for every cmd that received a ``begin``.
|
|
135
150
|
|
|
@@ -414,7 +429,7 @@ class CommandBuffer:
|
|
|
414
429
|
# commands still flow through the buffer (and through the batched
|
|
415
430
|
# HTTP POST), they just don't produce a sub-tool-card. ``emit_end``
|
|
416
431
|
# is automatically skipped because no ``_ptc_call_id`` was set.
|
|
417
|
-
if
|
|
432
|
+
if _should_emit_ptc(cmd):
|
|
418
433
|
try:
|
|
419
434
|
cmd._ptc_call_id = _ptc.emit_begin( # type: ignore[attr-defined]
|
|
420
435
|
type(cmd).__name__,
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Image format / content-type helpers shared by ``Run.add_picture``
|
|
2
|
+
and ``Document.add_picture``.
|
|
3
|
+
|
|
4
|
+
The previous implementations sniffed only by file extension, which
|
|
5
|
+
silently mistyped streams (default ``image/png``) and was wrong on
|
|
6
|
+
files with mismatched extensions (e.g. ``screenshot.png`` containing
|
|
7
|
+
JPEG bytes — common from OS screen-capture flows).
|
|
8
|
+
|
|
9
|
+
We sniff by **magic bytes** for the four formats SuperDoc accepts as
|
|
10
|
+
inline image data URIs (PNG, JPEG, GIF, WebP), with extension as a
|
|
11
|
+
secondary fallback for unknown / off-format streams.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
from typing import BinaryIO
|
|
18
|
+
|
|
19
|
+
# Magic-byte signatures for the formats SuperDoc accepts inline.
|
|
20
|
+
# (mime, signature_bytes, signature_offset). Most are at offset 0;
|
|
21
|
+
# WebP needs a two-part check (RIFF header + WEBP marker at offset 8).
|
|
22
|
+
_PNG_MAGIC: bytes = b"\x89PNG\r\n\x1a\n"
|
|
23
|
+
_JPEG_MAGIC: bytes = b"\xff\xd8\xff"
|
|
24
|
+
_GIF87_MAGIC: bytes = b"GIF87a"
|
|
25
|
+
_GIF89_MAGIC: bytes = b"GIF89a"
|
|
26
|
+
_WEBP_RIFF: bytes = b"RIFF"
|
|
27
|
+
_WEBP_MARKER: bytes = b"WEBP"
|
|
28
|
+
_BMP_MAGIC: bytes = b"BM"
|
|
29
|
+
_TIFF_LE_MAGIC: bytes = b"II*\x00"
|
|
30
|
+
_TIFF_BE_MAGIC: bytes = b"MM\x00*"
|
|
31
|
+
_SVG_HINT_DECL: bytes = b"<?xml"
|
|
32
|
+
_SVG_HINT_TAG: bytes = b"<svg"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def sniff_content_type(image_bytes: bytes, *, fallback_path: str = "") -> str:
|
|
36
|
+
"""Return a SuperDoc-acceptable image MIME for ``image_bytes``.
|
|
37
|
+
|
|
38
|
+
Magic-byte detection in priority order; if no signature matches,
|
|
39
|
+
fall back to the file extension on ``fallback_path``; if that
|
|
40
|
+
also fails, default to ``"image/png"`` (matches the prior
|
|
41
|
+
silent-default behavior so callers that previously got away with
|
|
42
|
+
it keep working).
|
|
43
|
+
|
|
44
|
+
Athena extension beyond python-docx 1.x: also detects SVG, BMP,
|
|
45
|
+
and TIFF. python-docx upstream supports PNG/JPEG/GIF/BMP/TIFF; SVG
|
|
46
|
+
and WEBP are missing from upstream (issues #651, #717, #1399).
|
|
47
|
+
"""
|
|
48
|
+
if image_bytes.startswith(_PNG_MAGIC):
|
|
49
|
+
return "image/png"
|
|
50
|
+
if image_bytes.startswith(_JPEG_MAGIC):
|
|
51
|
+
return "image/jpeg"
|
|
52
|
+
if image_bytes.startswith(_GIF87_MAGIC) or image_bytes.startswith(_GIF89_MAGIC):
|
|
53
|
+
return "image/gif"
|
|
54
|
+
if (
|
|
55
|
+
len(image_bytes) >= 12
|
|
56
|
+
and image_bytes.startswith(_WEBP_RIFF)
|
|
57
|
+
and image_bytes[8:12] == _WEBP_MARKER
|
|
58
|
+
):
|
|
59
|
+
return "image/webp"
|
|
60
|
+
if image_bytes.startswith(_BMP_MAGIC):
|
|
61
|
+
return "image/bmp"
|
|
62
|
+
if image_bytes.startswith(_TIFF_LE_MAGIC) or image_bytes.startswith(_TIFF_BE_MAGIC):
|
|
63
|
+
return "image/tiff"
|
|
64
|
+
# SVG: probe first ~1KB for either an XML declaration or a literal
|
|
65
|
+
# ``<svg`` tag. Case-insensitive on the tag because legitimate SVG
|
|
66
|
+
# files sometimes carry ``<SVG`` after author tools.
|
|
67
|
+
head = image_bytes[:1024]
|
|
68
|
+
if _SVG_HINT_TAG in head or _SVG_HINT_TAG in head.lower():
|
|
69
|
+
return "image/svg+xml"
|
|
70
|
+
if head.startswith(_SVG_HINT_DECL) and b"<svg" in head.lower():
|
|
71
|
+
return "image/svg+xml"
|
|
72
|
+
|
|
73
|
+
if fallback_path:
|
|
74
|
+
lower = fallback_path.lower()
|
|
75
|
+
if lower.endswith((".jpg", ".jpeg")):
|
|
76
|
+
return "image/jpeg"
|
|
77
|
+
if lower.endswith(".gif"):
|
|
78
|
+
return "image/gif"
|
|
79
|
+
if lower.endswith(".webp"):
|
|
80
|
+
return "image/webp"
|
|
81
|
+
if lower.endswith(".png"):
|
|
82
|
+
return "image/png"
|
|
83
|
+
if lower.endswith(".svg"):
|
|
84
|
+
return "image/svg+xml"
|
|
85
|
+
if lower.endswith(".bmp"):
|
|
86
|
+
return "image/bmp"
|
|
87
|
+
if lower.endswith((".tif", ".tiff")):
|
|
88
|
+
return "image/tiff"
|
|
89
|
+
|
|
90
|
+
return "image/png"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Generous default so large remote images over a slow link still land, while a
|
|
94
|
+
# dead host fails the call instead of hanging the agent's code-exec forever.
|
|
95
|
+
_URL_FETCH_TIMEOUT_SECONDS: float = 30.0
|
|
96
|
+
# Hard cap so a giant (or accidentally non-image) URL can't balloon memory in
|
|
97
|
+
# the document-exec sandbox — the bytes get base64-encoded into the doc next.
|
|
98
|
+
_MAX_REMOTE_IMAGE_BYTES: int = 10 * 1024 * 1024 # 10 MiB
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_image_bytes(image_path_or_stream: "str | BinaryIO") -> tuple[bytes, str]:
|
|
102
|
+
"""Read raw image bytes from a local path, ``http(s)://`` URL, ``data:``
|
|
103
|
+
URI, or binary stream.
|
|
104
|
+
|
|
105
|
+
Returns ``(image_bytes, fallback_path)`` where ``fallback_path`` feeds the
|
|
106
|
+
extension-based MIME fallback in :func:`sniff_content_type` (empty when the
|
|
107
|
+
source carries no usable filename).
|
|
108
|
+
|
|
109
|
+
Athena extension beyond python-docx: upstream ``add_picture`` only accepts a
|
|
110
|
+
local path or file-like object — a ``str`` is always opened as a filesystem
|
|
111
|
+
path. Agents routinely pass an image URL or a ``data:`` URI, so the Athena
|
|
112
|
+
SDK additionally fetches ``http(s)://`` URLs (via ``requests``) and decodes
|
|
113
|
+
``data:`` URIs here. Local paths and streams behave exactly as in
|
|
114
|
+
python-docx.
|
|
115
|
+
|
|
116
|
+
URL fetching runs inside the document-exec sandbox, which already executes
|
|
117
|
+
arbitrary agent-authored code (hence arbitrary outbound requests), so this
|
|
118
|
+
grants no network capability the caller lacks — it is not an SSRF allowlist
|
|
119
|
+
boundary. It does cap the download at ``_MAX_REMOTE_IMAGE_BYTES`` so an
|
|
120
|
+
oversized response can't exhaust sandbox memory.
|
|
121
|
+
"""
|
|
122
|
+
if isinstance(image_path_or_stream, str):
|
|
123
|
+
descriptor = image_path_or_stream
|
|
124
|
+
# URI schemes are case-insensitive (RFC 3986): dispatch on a lowercased
|
|
125
|
+
# scheme, but fetch/open the original value unchanged.
|
|
126
|
+
scheme = descriptor.split(":", 1)[0].lower() if ":" in descriptor else ""
|
|
127
|
+
if scheme in ("http", "https"):
|
|
128
|
+
from urllib.parse import urlsplit
|
|
129
|
+
|
|
130
|
+
# Sniff fallback off the URL *path* only — a signed URL's query
|
|
131
|
+
# string (``?token=…``) would otherwise defeat the extension check.
|
|
132
|
+
return _fetch_image_url(descriptor), urlsplit(descriptor).path
|
|
133
|
+
if scheme == "data":
|
|
134
|
+
return _decode_image_data_uri(descriptor), ""
|
|
135
|
+
with open(descriptor, "rb") as f:
|
|
136
|
+
return f.read(), descriptor
|
|
137
|
+
# Duck-type the stream: python-docx accepts anything with ``.read()`` (it
|
|
138
|
+
# never isinstance-checks), so a BinaryIO that doesn't subclass io.IOBase
|
|
139
|
+
# must work too.
|
|
140
|
+
reader = getattr(image_path_or_stream, "read", None)
|
|
141
|
+
if callable(reader):
|
|
142
|
+
data = reader()
|
|
143
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
144
|
+
raise TypeError(
|
|
145
|
+
f"Image stream .read() returned {type(data).__name__}, expected bytes",
|
|
146
|
+
)
|
|
147
|
+
return bytes(data), ""
|
|
148
|
+
raise TypeError(
|
|
149
|
+
"Expected a path str, http(s) URL, data: URI, or binary stream, got "
|
|
150
|
+
f"{type(image_path_or_stream).__name__}",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _fetch_image_url(url: str) -> bytes:
|
|
155
|
+
"""Download image bytes from an ``http(s)://`` URL, capped at
|
|
156
|
+
``_MAX_REMOTE_IMAGE_BYTES`` (Athena extension — see
|
|
157
|
+
:func:`load_image_bytes`)."""
|
|
158
|
+
# Lazy import: ``requests`` is already a hard SDK dependency (HTTP transport),
|
|
159
|
+
# but keep this format helper importable without paying for it up front.
|
|
160
|
+
import requests
|
|
161
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
162
|
+
|
|
163
|
+
def _redact(raw: str) -> str:
|
|
164
|
+
# Drop query + fragment so signed-URL tokens don't leak into errors/logs.
|
|
165
|
+
parts = urlsplit(raw)
|
|
166
|
+
return urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
with requests.get(url, timeout=_URL_FETCH_TIMEOUT_SECONDS, stream=True) as resp:
|
|
170
|
+
resp.raise_for_status()
|
|
171
|
+
declared = resp.headers.get("content-length")
|
|
172
|
+
if declared is not None and declared.isdigit() and int(declared) > _MAX_REMOTE_IMAGE_BYTES:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"add_picture image at {_redact(url)!r} declares {declared} bytes, "
|
|
175
|
+
f"over the {_MAX_REMOTE_IMAGE_BYTES}-byte cap"
|
|
176
|
+
)
|
|
177
|
+
chunks: list[bytes] = []
|
|
178
|
+
total = 0
|
|
179
|
+
for chunk in resp.iter_content(chunk_size=65536):
|
|
180
|
+
if not chunk:
|
|
181
|
+
continue
|
|
182
|
+
total += len(chunk)
|
|
183
|
+
if total > _MAX_REMOTE_IMAGE_BYTES:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"add_picture image at {_redact(url)!r} exceeds the "
|
|
186
|
+
f"{_MAX_REMOTE_IMAGE_BYTES}-byte cap"
|
|
187
|
+
)
|
|
188
|
+
chunks.append(chunk)
|
|
189
|
+
return b"".join(chunks)
|
|
190
|
+
except requests.RequestException as exc:
|
|
191
|
+
raise ValueError(f"add_picture could not fetch image URL {_redact(url)!r}: {exc}") from exc
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _decode_image_data_uri(uri: str) -> bytes:
|
|
195
|
+
"""Decode a ``data:[<mediatype>][;base64],<payload>`` URI to raw bytes
|
|
196
|
+
(Athena extension — see :func:`load_image_bytes`)."""
|
|
197
|
+
header, sep, payload = uri.partition(",")
|
|
198
|
+
if not sep or not payload:
|
|
199
|
+
raise ValueError("add_picture received a data: URI with no payload")
|
|
200
|
+
# ``;base64`` is a discrete, case-insensitive token (RFC 2397) — match it as
|
|
201
|
+
# a token, not a substring (so ``;notbase64x`` doesn't false-match), and use
|
|
202
|
+
# strict decoding so malformed base64 fails loudly instead of silently.
|
|
203
|
+
is_base64 = any(token.strip().lower() == "base64" for token in header.split(";")[1:])
|
|
204
|
+
try:
|
|
205
|
+
if is_base64:
|
|
206
|
+
return base64.b64decode(payload, validate=True)
|
|
207
|
+
from urllib.parse import unquote_to_bytes
|
|
208
|
+
|
|
209
|
+
return unquote_to_bytes(payload)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
raise ValueError(f"add_picture could not decode data: URI: {exc}") from exc
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
__all__ = ["load_image_bytes", "sniff_content_type"]
|
|
@@ -519,6 +519,11 @@ class ImagesGet(Command):
|
|
|
519
519
|
class BlocksList(Command):
|
|
520
520
|
node_types: list[str] | None = None
|
|
521
521
|
include_text: bool | None = None
|
|
522
|
+
# Ask the server to inline each paragraph/heading block's run breakdown
|
|
523
|
+
# (``paragraph.inlines``) so ``Document.paragraphs`` can hydrate
|
|
524
|
+
# ``Paragraph.text``/``.runs``/``.style`` from one list instead of a
|
|
525
|
+
# ``get_node_by_id`` per paragraph. Serialized to ``includeRuns`` on the wire.
|
|
526
|
+
include_runs: bool | None = None
|
|
522
527
|
|
|
523
528
|
|
|
524
529
|
@dataclass
|
|
@@ -15,7 +15,6 @@ Phase 1 supported methods:
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import base64
|
|
18
|
-
import io
|
|
19
18
|
import sys
|
|
20
19
|
from contextlib import contextmanager
|
|
21
20
|
from typing import TYPE_CHECKING, BinaryIO, Generator, Iterator
|
|
@@ -359,12 +358,23 @@ class Document:
|
|
|
359
358
|
# Include "heading" too so iterating over paragraphs covers both —
|
|
360
359
|
# python-docx does not distinguish headings from paragraphs at the
|
|
361
360
|
# Document.paragraphs level either (they're all Paragraph instances).
|
|
361
|
+
# ``includeRuns`` asks the server to inline each block's run breakdown
|
|
362
|
+
# (``paragraph.inlines``) so we can hydrate every returned proxy's
|
|
363
|
+
# ``.text`` / ``.runs`` / ``.style`` from this single list — instead of
|
|
364
|
+
# one ``get_node_by_id`` round-trip per paragraph per property. A
|
|
365
|
+
# read-heavy restyle loop over a large doc drops from ~N×3 round-trips
|
|
366
|
+
# to 1. See ``docx-studio/PERFORMANCE_BATCHING_ANALYSIS.md``.
|
|
362
367
|
raw: object = run_sync(
|
|
363
368
|
self._session.doc.blocks.list(
|
|
364
|
-
{
|
|
369
|
+
{
|
|
370
|
+
"nodeTypes": ["paragraph", "heading"],
|
|
371
|
+
"includeText": True,
|
|
372
|
+
"includeRuns": True,
|
|
373
|
+
},
|
|
365
374
|
),
|
|
366
375
|
)
|
|
367
|
-
# Real shape: {"blocks": [{"kind":"block","nodeType":..,"nodeId"
|
|
376
|
+
# Real shape: {"blocks": [{"kind":"block","nodeType":..,"nodeId":..,
|
|
377
|
+
# "styleId":..,"text":..,"paragraph":{"inlines":[..]}}], ...}
|
|
368
378
|
if isinstance(raw, dict):
|
|
369
379
|
blocks_obj: object = raw.get("blocks", [])
|
|
370
380
|
blocks: list = blocks_obj if isinstance(blocks_obj, list) else []
|
|
@@ -380,11 +390,20 @@ class Document:
|
|
|
380
390
|
if node_id:
|
|
381
391
|
nt_raw = b.get("nodeType")
|
|
382
392
|
nt: str = nt_raw if isinstance(nt_raw, str) and nt_raw else "paragraph"
|
|
393
|
+
# Seed the read cache when the server inlined this block's runs.
|
|
394
|
+
# The snapshot is ``get_node_by_id``-shaped so
|
|
395
|
+
# ``Paragraph._node_info`` and the text/runs/style getters consume
|
|
396
|
+
# it unchanged. Blocks without an ``inlines`` payload (e.g.
|
|
397
|
+
# tables) fall back to live queries.
|
|
398
|
+
snapshot: dict | None = (
|
|
399
|
+
{"node": b} if isinstance(b.get("paragraph"), dict) else None
|
|
400
|
+
)
|
|
383
401
|
out.append(
|
|
384
402
|
Paragraph(
|
|
385
403
|
session=self._session,
|
|
386
404
|
node_id=node_id,
|
|
387
405
|
node_type=nt,
|
|
406
|
+
node_info_snapshot=snapshot,
|
|
388
407
|
),
|
|
389
408
|
)
|
|
390
409
|
return out
|
|
@@ -1854,26 +1873,20 @@ class Document:
|
|
|
1854
1873
|
|
|
1855
1874
|
Mirrors python-docx ``Document.add_picture`` — returns the new
|
|
1856
1875
|
:class:`InlineShape` proxy.
|
|
1876
|
+
|
|
1877
|
+
``image_path_or_stream`` accepts a local file path, a binary stream
|
|
1878
|
+
(e.g. ``io.BytesIO``), an ``http(s)://`` image URL (fetched
|
|
1879
|
+
server-side), or a ``data:`` URI. The URL and ``data:`` forms are
|
|
1880
|
+
Athena extensions beyond python-docx, which is path/stream-only.
|
|
1857
1881
|
"""
|
|
1858
|
-
from docx._image_utils import sniff_content_type
|
|
1882
|
+
from docx._image_utils import load_image_bytes, sniff_content_type
|
|
1859
1883
|
from docx.shape import InlineShape
|
|
1860
1884
|
from docx.text.run import _build_inline_shape_info
|
|
1861
1885
|
|
|
1862
1886
|
self._ensure_open()
|
|
1863
1887
|
self._reset_list_chain()
|
|
1864
1888
|
|
|
1865
|
-
image_bytes
|
|
1866
|
-
fallback_path: str = ""
|
|
1867
|
-
if isinstance(image_path_or_stream, str):
|
|
1868
|
-
with open(image_path_or_stream, "rb") as f:
|
|
1869
|
-
image_bytes = f.read()
|
|
1870
|
-
fallback_path = image_path_or_stream
|
|
1871
|
-
elif isinstance(image_path_or_stream, io.IOBase):
|
|
1872
|
-
image_bytes = image_path_or_stream.read()
|
|
1873
|
-
else:
|
|
1874
|
-
raise TypeError(
|
|
1875
|
-
f"Expected path str or binary stream, got {type(image_path_or_stream).__name__}",
|
|
1876
|
-
)
|
|
1889
|
+
image_bytes, fallback_path = load_image_bytes(image_path_or_stream)
|
|
1877
1890
|
# An empty payload (closed file, empty BytesIO, truncated stream
|
|
1878
1891
|
# that's already been read to EOF) would silently produce a
|
|
1879
1892
|
# ``data:image/png;base64,`` URI — SuperDoc accepts that and
|
|
@@ -226,6 +226,7 @@ class Paragraph:
|
|
|
226
226
|
in_cell: bool = False,
|
|
227
227
|
cell_ooxml_address: "tuple[str | None, int, int, int, int] | None" = None,
|
|
228
228
|
cell: "object | None" = None,
|
|
229
|
+
node_info_snapshot: dict | None = None,
|
|
229
230
|
) -> None:
|
|
230
231
|
self._session: "Session" = session
|
|
231
232
|
self._node_id: str = node_id
|
|
@@ -272,6 +273,36 @@ class Paragraph:
|
|
|
272
273
|
# — defensive for paragraph proxies built outside the _Cell
|
|
273
274
|
# construction paths.
|
|
274
275
|
self._cell: object | None = cell
|
|
276
|
+
# Prefetched ``get_node_by_id``-shaped snapshot
|
|
277
|
+
# (``{"node": {..., "paragraph": {"inlines": [...]}}}``) seeded by
|
|
278
|
+
# ``Document.paragraphs`` via the bulk ``blocks.list(includeRuns=True)``
|
|
279
|
+
# read. When present, ``text`` / ``runs`` / ``style`` read it instead of
|
|
280
|
+
# issuing a per-property ``get_node_by_id`` round-trip. It is a
|
|
281
|
+
# point-in-time snapshot (consistent with ``Document.paragraphs`` already
|
|
282
|
+
# returning a snapshot list) and is dropped by ``_invalidate_node_info``
|
|
283
|
+
# the moment this proxy mutates its own content.
|
|
284
|
+
self._node_info_snapshot: dict | None = node_info_snapshot
|
|
285
|
+
|
|
286
|
+
def _node_info(self) -> object:
|
|
287
|
+
"""Return this paragraph's ``get_node_by_id``-shaped node info.
|
|
288
|
+
|
|
289
|
+
Served from the prefetch snapshot when one was seeded (one bulk
|
|
290
|
+
``blocks.list`` for the whole document instead of one
|
|
291
|
+
``get_node_by_id`` per ``.text`` / ``.runs`` / ``.style`` access);
|
|
292
|
+
otherwise a live query. ``_invalidate_node_info`` clears the snapshot
|
|
293
|
+
after a content mutation so the next read re-queries.
|
|
294
|
+
"""
|
|
295
|
+
snapshot = self._node_info_snapshot
|
|
296
|
+
if snapshot is not None:
|
|
297
|
+
return snapshot
|
|
298
|
+
return run_sync(
|
|
299
|
+
self._session.doc.get_node_by_id({"id": self._node_id}),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def _invalidate_node_info(self) -> None:
|
|
303
|
+
"""Drop the prefetched node snapshot after a content mutation, so the
|
|
304
|
+
next ``text`` / ``runs`` / ``style`` read reflects the new state."""
|
|
305
|
+
self._node_info_snapshot = None
|
|
275
306
|
|
|
276
307
|
def _check_cell_inner(self, op: str, **format_props: Any) -> bool:
|
|
277
308
|
"""Return ``True`` if this paragraph is inside a table cell and
|
|
@@ -356,7 +387,7 @@ class Paragraph:
|
|
|
356
387
|
@property
|
|
357
388
|
def text(self) -> str:
|
|
358
389
|
"""Return the plain-text content of this paragraph."""
|
|
359
|
-
return
|
|
390
|
+
return _extract_text(self._node_info())
|
|
360
391
|
|
|
361
392
|
@text.setter
|
|
362
393
|
def text(self, value: str) -> None:
|
|
@@ -399,6 +430,9 @@ class Paragraph:
|
|
|
399
430
|
# We just replaced the entire paragraph contents with ``value``;
|
|
400
431
|
# the new text-address length is simply ``len(value)``.
|
|
401
432
|
self._text_len = len(value)
|
|
433
|
+
# Content changed — drop the prefetched snapshot so a follow-up
|
|
434
|
+
# ``.text`` / ``.runs`` read reflects the replacement.
|
|
435
|
+
self._invalidate_node_info()
|
|
402
436
|
|
|
403
437
|
@property
|
|
404
438
|
def runs(self) -> list["Run"]:
|
|
@@ -413,9 +447,7 @@ class Paragraph:
|
|
|
413
447
|
"""
|
|
414
448
|
from docx.text.run import Run
|
|
415
449
|
|
|
416
|
-
info: object =
|
|
417
|
-
self._session.doc.get_node_by_id({"id": self._node_id}),
|
|
418
|
-
)
|
|
450
|
+
info: object = self._node_info()
|
|
419
451
|
inlines: list[dict] = _walk_inlines(info)
|
|
420
452
|
runs: list["Run"] = []
|
|
421
453
|
offset: int = 0
|
|
@@ -479,9 +511,7 @@ class Paragraph:
|
|
|
479
511
|
from docx.styles.style import ParagraphStyle
|
|
480
512
|
from docx.enum.style import WD_STYLE_TYPE
|
|
481
513
|
|
|
482
|
-
info: object =
|
|
483
|
-
self._session.doc.get_node_by_id({"id": self._node_id}),
|
|
484
|
-
)
|
|
514
|
+
info: object = self._node_info()
|
|
485
515
|
if not isinstance(info, dict):
|
|
486
516
|
return None
|
|
487
517
|
node: object = info.get("node")
|
|
@@ -537,6 +567,10 @@ class Paragraph:
|
|
|
537
567
|
},
|
|
538
568
|
),
|
|
539
569
|
)
|
|
570
|
+
# Style changed — drop the prefetched snapshot so a follow-up ``.style``
|
|
571
|
+
# read reflects the new style instead of the stale prefetched value
|
|
572
|
+
# (the restyle-loop read-after-write this PR's prefetch targets).
|
|
573
|
+
self._invalidate_node_info()
|
|
540
574
|
|
|
541
575
|
@property
|
|
542
576
|
def alignment(self) -> "WD_ALIGN_PARAGRAPH | None":
|
|
@@ -1227,6 +1261,9 @@ class Paragraph:
|
|
|
1227
1261
|
)
|
|
1228
1262
|
offset += 1
|
|
1229
1263
|
i = j + 1
|
|
1264
|
+
# Inserted content — drop the prefetched node snapshot so a follow-up
|
|
1265
|
+
# ``.text`` / ``.runs`` read on this proxy reflects the new runs.
|
|
1266
|
+
self._invalidate_node_info()
|
|
1230
1267
|
|
|
1231
1268
|
@athena_extension(
|
|
1232
1269
|
issue=74,
|
|
@@ -1397,6 +1434,11 @@ class Paragraph:
|
|
|
1397
1434
|
)
|
|
1398
1435
|
run_sync(self._session.send_command(cmd))
|
|
1399
1436
|
self._text_len = end_offset
|
|
1437
|
+
# Appended a hyperlink run — drop the prefetched snapshot so a follow-up
|
|
1438
|
+
# ``.text`` / ``.runs`` read reflects the new content. This path fires
|
|
1439
|
+
# ``CreateHyperlink`` directly via ``send_command`` (it bypasses
|
|
1440
|
+
# ``_insert_run_text_segments``), so it needs its own invalidation.
|
|
1441
|
+
self._invalidate_node_info()
|
|
1400
1442
|
|
|
1401
1443
|
run_dict = {"text": text, "hyperlink": {"url": url, "target": url}}
|
|
1402
1444
|
return Hyperlink(
|
|
@@ -594,26 +594,18 @@ class Run:
|
|
|
594
594
|
|
|
595
595
|
Returns an :class:`InlineShape` proxy for the newly-inserted
|
|
596
596
|
image, matching python-docx ``Run.add_picture``.
|
|
597
|
+
|
|
598
|
+
``image_path_or_stream`` accepts a local file path, a binary stream
|
|
599
|
+
(e.g. ``io.BytesIO``), an ``http(s)://`` image URL (fetched
|
|
600
|
+
server-side), or a ``data:`` URI. The URL and ``data:`` forms are
|
|
601
|
+
Athena extensions beyond python-docx, which is path/stream-only.
|
|
597
602
|
"""
|
|
598
|
-
from docx._image_utils import sniff_content_type
|
|
603
|
+
from docx._image_utils import load_image_bytes, sniff_content_type
|
|
599
604
|
from docx.document import _emu_to_px
|
|
600
605
|
from docx.shape import InlineShape
|
|
601
606
|
import base64
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
image_bytes: bytes
|
|
605
|
-
fallback_path: str = ""
|
|
606
|
-
if isinstance(image_path_or_stream, str):
|
|
607
|
-
with open(image_path_or_stream, "rb") as f:
|
|
608
|
-
image_bytes = f.read()
|
|
609
|
-
fallback_path = image_path_or_stream
|
|
610
|
-
elif isinstance(image_path_or_stream, io.IOBase):
|
|
611
|
-
image_bytes = image_path_or_stream.read()
|
|
612
|
-
else:
|
|
613
|
-
raise TypeError(
|
|
614
|
-
"Expected path str or binary stream, "
|
|
615
|
-
f"got {type(image_path_or_stream).__name__}",
|
|
616
|
-
)
|
|
607
|
+
|
|
608
|
+
image_bytes, fallback_path = load_image_bytes(image_path_or_stream)
|
|
617
609
|
# Empty payload would silently round-trip as
|
|
618
610
|
# ``data:image/png;base64,`` (broken placeholder image). Fail
|
|
619
611
|
# at the call site instead of at render time. Matches the
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "athena-python-docx"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.17.0"
|
|
8
8
|
description = "Drop-in replacement for python-docx that connects to Athena's Superdoc/Keryx collaborative document stack"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -296,6 +296,7 @@ class FakeDocState:
|
|
|
296
296
|
|
|
297
297
|
if op == "blocks.list":
|
|
298
298
|
types = params.get("nodeTypes") or None
|
|
299
|
+
include_runs = bool(params.get("includeRuns"))
|
|
299
300
|
out_blocks: list[dict] = []
|
|
300
301
|
for b in self.blocks:
|
|
301
302
|
if types and b.node_type not in types:
|
|
@@ -307,6 +308,12 @@ class FakeDocState:
|
|
|
307
308
|
"styleId": b.style_id,
|
|
308
309
|
"alignment": b.alignment,
|
|
309
310
|
}
|
|
311
|
+
# Mirror the real server: when runs are requested, inline each
|
|
312
|
+
# paragraph/heading block's run breakdown so the SDK hydrates
|
|
313
|
+
# Paragraph.text/.runs/.style from this one list (no per-node read).
|
|
314
|
+
if include_runs and b.node_type in ("paragraph", "heading"):
|
|
315
|
+
entry["text"] = b.text
|
|
316
|
+
entry["paragraph"] = {"inlines": list(b.inlines)}
|
|
310
317
|
out_blocks.append(entry)
|
|
311
318
|
# Mirror real SuperDoc: when "table" is in the requested
|
|
312
319
|
# nodeTypes, top-level tables come back too. The fake doesn't
|