excel-mcp-server 2.2.0__tar.gz → 2.2.2__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.
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/PKG-INFO +6 -2
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/README.md +4 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/pyproject.toml +3 -3
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/pyproject.toml.orig +3 -3
- excel_mcp_server-2.2.2/src/excel_mcp/cell_budget.py +73 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/cli.py +8 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/config.py +2 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/formulas.py +20 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_edit.py +35 -35
- excel_mcp_server-2.2.2/src/excel_mcp/lazy_find.py +83 -0
- excel_mcp_server-2.2.2/src/excel_mcp/lazy_lines.py +261 -0
- excel_mcp_server-2.2.2/src/excel_mcp/lazy_rows.py +198 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_shared.py +49 -3
- excel_mcp_server-2.2.2/src/excel_mcp/lazy_source.py +141 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_workbook.py +150 -119
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/calculated.py +9 -2
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/cells.py +33 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/hyperlinks.py +2 -1
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/inspect.py +2 -2
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/layout.py +1 -1
- excel_mcp_server-2.2.2/src/excel_mcp/operations/line_edit_styles.py +94 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edits.py +28 -51
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/notes.py +2 -1
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot.py +6 -7
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_source.py +49 -1
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/replace.py +11 -2
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_copy.py +35 -8
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_refs.py +10 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/shifting.py +32 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/workbook_rewrite.py +6 -2
- excel_mcp_server-2.2.2/src/excel_mcp/plain_refs.py +106 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/sheet_tools.py +1 -1
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/workspace.py +32 -17
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/xlfn.py +4 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/LICENSE +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/__init__.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/__main__.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/cells.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/criteria.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/dependencies.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/dynamic.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/engine.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/__init__.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/arrays.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/conditional.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/datetime.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/depreciation.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/distributions.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/financial.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/helpers.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/indirect.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/information.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/logical.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/lookup.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/math.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/regression.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/securities.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/statistical.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/subtotals.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/text.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/operators.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/parser.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/r1c1.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/registry.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/scheduler.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/solver.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/steps.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/tablerefs.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/textformat.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/values.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/wildcard.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/cfb.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/chart_roundtrip.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/errors.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/formula_syntax.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/function_arguments.json +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/inputs.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/links.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/macros.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/__init__.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chart_index.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chart_info.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_xml.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_axes.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_check.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_data.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_look.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_modern.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_options.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_series.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_style.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/columns.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/comparison.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_kinds.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_rule.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_x14.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_export.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_format.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_import.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_values.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/date_width.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/drawings.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/duplicates.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/files.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/fill.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filter_criteria.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filter_stored.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filters.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/formatting.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/goal_seek.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/images.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_objects.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_rules.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/names.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/paste.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_axis.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_cache.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_calc.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_definition.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_fields.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_groups.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_index.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_options.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_records.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_render.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_showas.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_specs.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_values.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/print_setup.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/protection.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/rule_clear.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/rules.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_counts.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_shapes.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_view.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheets.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_index.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_manage.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_pivot.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_place.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_table.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_xml.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicers.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sorting.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/spans.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparkline_style.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparkline_xml.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparklines.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/spill.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/table_options.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/tables.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/text_split.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/trace.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/transform.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/used_range.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/vba.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/vba_write.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/workbook_settings.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/x14_xml.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba_compress.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba_write.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/__init__.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/anchors.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/arrays.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/capture.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/capture_sheet.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/consistency.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/extensions.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/guards.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/lazy_records.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/lines.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/metadata.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/model.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/opc.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/patch.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/pivot_records.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/pivots.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/properties.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/reader.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/references.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/restore.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/restore_sheet.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/scan.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/shape_names.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/vml.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/paths.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/py.typed +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/refs.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/rewrite.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/__init__.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/annotation_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/areas.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/csv_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/data_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/format_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/formula_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/http.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/image_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/object_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/params.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/pivot_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/registry.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/results.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/slicer_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/validation.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/vba_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/vba_write_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/workbook_tools.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/spill.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/structured.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/text.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/upload_links.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/upload_scan.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/values.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/xlm.py +0 -0
- {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/zip_limits.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: excel-mcp-server
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: MCP server for creating, reading and editing Excel workbooks without Microsoft Excel
|
|
5
5
|
Keywords: mcp,mcp-server,model-context-protocol,excel,xlsx,xlsm,spreadsheet,openpyxl,charts,pivot-table,formulas,vba,llm,ai,claude,cursor
|
|
6
6
|
Author: Haris Musa
|
|
@@ -23,7 +23,7 @@ Classifier: Typing :: Typed
|
|
|
23
23
|
Requires-Dist: mcp>=2.2,<3
|
|
24
24
|
Requires-Dist: olefile>=0.47,<1
|
|
25
25
|
Requires-Dist: openpyxl>=3.1.5,<4
|
|
26
|
-
Requires-Dist: pillow>=
|
|
26
|
+
Requires-Dist: pillow>=13,<14
|
|
27
27
|
Requires-Dist: pydantic>=2.12,<3
|
|
28
28
|
Requires-Dist: starlette>=1,<2
|
|
29
29
|
Requires-Dist: uvicorn>=0.31,<1
|
|
@@ -98,6 +98,9 @@ Excel**, and runs on Windows, macOS and Linux.
|
|
|
98
98
|
**Large files**
|
|
99
99
|
- Reads stream the workbook and return pages, so memory stays flat on sheets with hundreds of
|
|
100
100
|
thousands of rows; every call is bounded by size and cell limits
|
|
101
|
+
- Edits of a sheet of 1 MB of XML or more parse only the rows they touch, so inserting or
|
|
102
|
+
deleting rows and columns, sorting, replacing, copying a sheet and creating a PivotTable
|
|
103
|
+
take seconds on a 200,000-row sheet instead of minutes
|
|
101
104
|
|
|
102
105
|
**Macros (opt-in)**
|
|
103
106
|
- Read the VBA code in `.xlsm` files. Writing VBA is off unless you start the server with
|
|
@@ -231,6 +234,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
|
|
|
231
234
|
| `--max-file-mb N` | `EXCEL_MCP_MAX_FILE_MB` | `100` | Largest workbook the server opens. Also sets the unpacked-size limit (10 times this) that every opened or uploaded workbook must stay under; parts compressed over 500:1 are refused |
|
|
232
235
|
| `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
|
|
233
236
|
| `--max-pivot-cells N` | `EXCEL_MCP_MAX_PIVOT_CELLS` | `3000000` | Largest PivotTable source in cells (a 230,000-row, 13-column sheet); creating one from the biggest sources takes a minute and about 2 GB of memory |
|
|
237
|
+
| `--max-loaded-cells N` | `EXCEL_MCP_MAX_LOADED_CELLS` | `3000000` | Most cells and rows of a workbook that may be loaded into memory at once, about 0.5 KB each (a 3,000,000 limit is about 1.5 GB). A workbook with more is refused by the tools that must load whole sheets; `read_range` and the other streaming reads, and edits of large sheets that parse only some rows, are not limited by the size of the sheet |
|
|
234
238
|
| `--log-level LEVEL` | | `WARNING` | Logging on stderr |
|
|
235
239
|
| `--host HOST` | `EXCEL_MCP_HOST` | `127.0.0.1` | HTTP listen address |
|
|
236
240
|
| `--port PORT` | `EXCEL_MCP_PORT` | `8017` | HTTP port |
|
|
@@ -62,6 +62,9 @@ Excel**, and runs on Windows, macOS and Linux.
|
|
|
62
62
|
**Large files**
|
|
63
63
|
- Reads stream the workbook and return pages, so memory stays flat on sheets with hundreds of
|
|
64
64
|
thousands of rows; every call is bounded by size and cell limits
|
|
65
|
+
- Edits of a sheet of 1 MB of XML or more parse only the rows they touch, so inserting or
|
|
66
|
+
deleting rows and columns, sorting, replacing, copying a sheet and creating a PivotTable
|
|
67
|
+
take seconds on a 200,000-row sheet instead of minutes
|
|
65
68
|
|
|
66
69
|
**Macros (opt-in)**
|
|
67
70
|
- Read the VBA code in `.xlsm` files. Writing VBA is off unless you start the server with
|
|
@@ -195,6 +198,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
|
|
|
195
198
|
| `--max-file-mb N` | `EXCEL_MCP_MAX_FILE_MB` | `100` | Largest workbook the server opens. Also sets the unpacked-size limit (10 times this) that every opened or uploaded workbook must stay under; parts compressed over 500:1 are refused |
|
|
196
199
|
| `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
|
|
197
200
|
| `--max-pivot-cells N` | `EXCEL_MCP_MAX_PIVOT_CELLS` | `3000000` | Largest PivotTable source in cells (a 230,000-row, 13-column sheet); creating one from the biggest sources takes a minute and about 2 GB of memory |
|
|
201
|
+
| `--max-loaded-cells N` | `EXCEL_MCP_MAX_LOADED_CELLS` | `3000000` | Most cells and rows of a workbook that may be loaded into memory at once, about 0.5 KB each (a 3,000,000 limit is about 1.5 GB). A workbook with more is refused by the tools that must load whole sheets; `read_range` and the other streaming reads, and edits of large sheets that parse only some rows, are not limited by the size of the sheet |
|
|
198
202
|
| `--log-level LEVEL` | | `WARNING` | Logging on stderr |
|
|
199
203
|
| `--host HOST` | `EXCEL_MCP_HOST` | `127.0.0.1` | HTTP listen address |
|
|
200
204
|
| `--port PORT` | `EXCEL_MCP_PORT` | `8017` | HTTP port |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "excel-mcp-server"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.2"
|
|
4
4
|
description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -43,7 +43,7 @@ dependencies = [
|
|
|
43
43
|
"mcp>=2.2,<3",
|
|
44
44
|
"olefile>=0.47,<1",
|
|
45
45
|
"openpyxl>=3.1.5,<4",
|
|
46
|
-
"pillow>=
|
|
46
|
+
"pillow>=13,<14",
|
|
47
47
|
"pydantic>=2.12,<3",
|
|
48
48
|
"starlette>=1,<2",
|
|
49
49
|
"uvicorn>=0.31,<1",
|
|
@@ -71,7 +71,7 @@ dev = [
|
|
|
71
71
|
]
|
|
72
72
|
|
|
73
73
|
[build-system]
|
|
74
|
-
requires = ["uv_build>=0.
|
|
74
|
+
requires = ["uv_build>=0.13.1,<0.14"]
|
|
75
75
|
build-backend = "uv_build"
|
|
76
76
|
|
|
77
77
|
[tool.ruff]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "excel-mcp-server"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.2"
|
|
4
4
|
description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -44,7 +44,7 @@ dependencies = [
|
|
|
44
44
|
"mcp>=2.2,<3",
|
|
45
45
|
"olefile>=0.47,<1",
|
|
46
46
|
"openpyxl>=3.1.5,<4",
|
|
47
|
-
"pillow>=
|
|
47
|
+
"pillow>=13,<14",
|
|
48
48
|
"pydantic>=2.12,<3",
|
|
49
49
|
"starlette>=1,<2",
|
|
50
50
|
"uvicorn>=0.31,<1",
|
|
@@ -68,7 +68,7 @@ dev = [
|
|
|
68
68
|
]
|
|
69
69
|
|
|
70
70
|
[build-system]
|
|
71
|
-
requires = ["uv_build>=0.
|
|
71
|
+
requires = ["uv_build>=0.13.1,<0.14"]
|
|
72
72
|
build-backend = "uv_build"
|
|
73
73
|
|
|
74
74
|
[tool.ruff]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""A limit on the cells that a workbook's sheets make openpyxl create.
|
|
2
|
+
|
|
3
|
+
Memory use is set by the number of cells created, not by the size of the file: a sheet of
|
|
4
|
+
millions of empty cells is a few hundred kilobytes. So every sheet is counted before it is
|
|
5
|
+
parsed, and what is created cannot pass the limit. Rows count as cells (openpyxl keeps their
|
|
6
|
+
attributes), and so does every cell of a merged range or hyperlink range, which it creates
|
|
7
|
+
from the one element that names the range.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import IO
|
|
12
|
+
from xml.parsers import expat
|
|
13
|
+
|
|
14
|
+
from openpyxl.reader.excel import ExcelReader
|
|
15
|
+
from openpyxl.xml.constants import SHEET_MAIN_NS, WORKSHEET_TYPE
|
|
16
|
+
|
|
17
|
+
from excel_mcp.errors import LimitExceededError
|
|
18
|
+
from excel_mcp.refs import parse_range
|
|
19
|
+
|
|
20
|
+
_CHUNK = 1 << 20
|
|
21
|
+
_NAMESPACE = f"{SHEET_MAIN_NS} "
|
|
22
|
+
_COUNTED = {f"{_NAMESPACE}c", f"{_NAMESPACE}row"}
|
|
23
|
+
_MERGED = f"{_NAMESPACE}mergeCell"
|
|
24
|
+
_LINKED = f"{_NAMESPACE}hyperlink"
|
|
25
|
+
# A cell that holds a link takes about three times the memory of one that does not.
|
|
26
|
+
_LINK_COST = 3
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CellBudget:
|
|
30
|
+
"""The cells that may still be created for one workbook, shared by all its sheets."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, limit: int) -> None:
|
|
33
|
+
self.limit = limit
|
|
34
|
+
self.used = 0
|
|
35
|
+
|
|
36
|
+
def spend(self, count: int) -> None:
|
|
37
|
+
self.used += count
|
|
38
|
+
if self.used > self.limit:
|
|
39
|
+
raise LimitExceededError(
|
|
40
|
+
f"The workbook has more than {self.limit:,} cells and rows to load, which "
|
|
41
|
+
"would use too much memory. Read it with read_range, split it into smaller "
|
|
42
|
+
"workbooks, or raise --max-loaded-cells."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def spend_sheet(source: IO[bytes], budget: CellBudget) -> None:
|
|
47
|
+
"""Spend the cells and rows of one worksheet's XML, stopping at the first one over the limit."""
|
|
48
|
+
parser = expat.ParserCreate(namespace_separator=" ")
|
|
49
|
+
|
|
50
|
+
def start(name: str, attributes: dict[str, str]) -> None:
|
|
51
|
+
if name in _COUNTED:
|
|
52
|
+
budget.spend(1)
|
|
53
|
+
elif name == _MERGED:
|
|
54
|
+
budget.spend(parse_range(attributes["ref"]).size)
|
|
55
|
+
elif name == _LINKED:
|
|
56
|
+
budget.spend(_LINK_COST * parse_range(attributes["ref"]).size)
|
|
57
|
+
|
|
58
|
+
parser.StartElementHandler = start
|
|
59
|
+
while chunk := source.read(_CHUNK):
|
|
60
|
+
parser.Parse(chunk, False)
|
|
61
|
+
parser.Parse(b"", True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_workbook(path: Path, budget: CellBudget) -> None:
|
|
65
|
+
"""Spend the cells and rows that loading all of a workbook's worksheets would create."""
|
|
66
|
+
reader = ExcelReader(path)
|
|
67
|
+
try:
|
|
68
|
+
reader.read_manifest()
|
|
69
|
+
for part in reader.package.findall(WORKSHEET_TYPE):
|
|
70
|
+
with reader.archive.open(part.PartName.lstrip("/")) as source:
|
|
71
|
+
spend_sheet(source, budget)
|
|
72
|
+
finally:
|
|
73
|
+
reader.archive.close()
|
|
@@ -78,6 +78,7 @@ def _settings(args: argparse.Namespace) -> Settings:
|
|
|
78
78
|
max_file_bytes=args.max_file_mb * MB,
|
|
79
79
|
max_cells=args.max_cells,
|
|
80
80
|
max_pivot_source_cells=args.max_pivot_cells,
|
|
81
|
+
max_loaded_cells=args.max_loaded_cells,
|
|
81
82
|
),
|
|
82
83
|
log_level=args.log_level,
|
|
83
84
|
)
|
|
@@ -159,6 +160,13 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
159
160
|
help="Largest PivotTable source, in cells (default %(default)s). "
|
|
160
161
|
"Also EXCEL_MCP_MAX_PIVOT_CELLS.",
|
|
161
162
|
)
|
|
163
|
+
common.add_argument(
|
|
164
|
+
"--max-loaded-cells",
|
|
165
|
+
type=_positive_int,
|
|
166
|
+
default=_env_default("EXCEL_MCP_MAX_LOADED_CELLS", DEFAULT_LIMITS.max_loaded_cells),
|
|
167
|
+
help="Most cells and rows of a workbook that may be loaded into memory at once, about "
|
|
168
|
+
"0.5 KB each (default %(default)s). Also EXCEL_MCP_MAX_LOADED_CELLS.",
|
|
169
|
+
)
|
|
162
170
|
common.add_argument(
|
|
163
171
|
"--log-level",
|
|
164
172
|
default="WARNING",
|
|
@@ -14,6 +14,8 @@ class Limits:
|
|
|
14
14
|
max_cells: int = 100_000
|
|
15
15
|
max_pivot_source_cells: int = 3_000_000
|
|
16
16
|
max_copy_cells: int = 1_000_000
|
|
17
|
+
# Cells and rows that all the sheets of a workbook may create in memory, about 0.5 KB each.
|
|
18
|
+
max_loaded_cells: int = 3_000_000
|
|
17
19
|
# Zip bomb guards, applied to every workbook the server opens or receives. Real workbooks
|
|
18
20
|
# compress 10 to 30 times and sparse sheets far more, so these leave room for them.
|
|
19
21
|
max_unpack_factor: int = 10
|
|
@@ -16,10 +16,12 @@ else before a "!" (a file, a path, a URL, "[1]") points at another workbook.
|
|
|
16
16
|
|
|
17
17
|
import re
|
|
18
18
|
from collections.abc import Iterable, Iterator
|
|
19
|
+
from functools import lru_cache
|
|
19
20
|
|
|
20
21
|
from openpyxl.formula import Tokenizer
|
|
21
22
|
from openpyxl.formula.tokenizer import Token, TokenizerError
|
|
22
23
|
|
|
24
|
+
from excel_mcp import plain_refs
|
|
23
25
|
from excel_mcp.errors import InvalidFormulaError, UnsafeFormulaError
|
|
24
26
|
from excel_mcp.formula_syntax import check_syntax, invalid_formula
|
|
25
27
|
from excel_mcp.spill import store_spills
|
|
@@ -118,10 +120,28 @@ def storable_formula(formula: str, sheet_names: Iterable[str]) -> str:
|
|
|
118
120
|
|
|
119
121
|
Every formula that is written anywhere in a workbook goes through here.
|
|
120
122
|
"""
|
|
123
|
+
pattern = plain_refs.shape(formula)
|
|
124
|
+
if pattern is not None and _is_plain_and_allowed(pattern):
|
|
125
|
+
return formula
|
|
121
126
|
check_formula(formula, sheet_names)
|
|
122
127
|
return add_prefixes(store_spills(formula))
|
|
123
128
|
|
|
124
129
|
|
|
130
|
+
@lru_cache(maxsize=4096)
|
|
131
|
+
def _is_plain_and_allowed(pattern: str) -> bool:
|
|
132
|
+
"""Whether formulas of this shape pass the check and need no prefixes.
|
|
133
|
+
|
|
134
|
+
Which cells a plain formula refers to cannot change the outcome, so a sheet with hundreds
|
|
135
|
+
of thousands of formulas filled down from a few is checked a few times, not that often.
|
|
136
|
+
A formula that does not pass is checked again as itself, to raise its own error.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
check_formula(pattern, ())
|
|
140
|
+
except (InvalidFormulaError, UnsafeFormulaError):
|
|
141
|
+
return False
|
|
142
|
+
return add_prefixes(pattern) == pattern
|
|
143
|
+
|
|
144
|
+
|
|
125
145
|
def storable_operand(operand: str, sheet_names: Iterable[str]) -> str:
|
|
126
146
|
"""`storable_formula` for rule operands and names, which are stored without the "="."""
|
|
127
147
|
return storable_formula(f"={operand.removeprefix('=')}", sheet_names).removeprefix("=")
|
|
@@ -32,16 +32,10 @@ from openpyxl.worksheet.dimensions import DimensionHolder
|
|
|
32
32
|
from openpyxl.worksheet.worksheet import Worksheet
|
|
33
33
|
from openpyxl.xml.constants import SHARED_STRINGS
|
|
34
34
|
|
|
35
|
-
from excel_mcp import lazy_shared
|
|
36
|
-
from excel_mcp.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Parts,
|
|
40
|
-
SheetSource,
|
|
41
|
-
lazy_sheets,
|
|
42
|
-
open_lazily,
|
|
43
|
-
split_sheet,
|
|
44
|
-
)
|
|
35
|
+
from excel_mcp import lazy_rows, lazy_shared
|
|
36
|
+
from excel_mcp.cell_budget import CellBudget
|
|
37
|
+
from excel_mcp.lazy_source import SheetSource, split_sheet
|
|
38
|
+
from excel_mcp.lazy_workbook import BLOCK_ROWS, LazyCells, Parts, lazy_sheets, open_lazily
|
|
45
39
|
from excel_mcp.package.opc import REL_BASE, Package, Rel, free_id
|
|
46
40
|
from excel_mcp.package.restore import MAIN, sheet_parts
|
|
47
41
|
from excel_mcp.package.scan import declare_in_root, root_scope
|
|
@@ -148,27 +142,31 @@ class LazyEdit:
|
|
|
148
142
|
|
|
149
143
|
path: Path
|
|
150
144
|
strings_part: str | None
|
|
151
|
-
|
|
152
|
-
frozen: dict[Worksheet, LazyCells] = field(default_factory=dict)
|
|
145
|
+
frozen: dict[Worksheet, tuple[LazyCells, dict[int, bytes]]] = field(default_factory=dict)
|
|
153
146
|
|
|
154
|
-
def freeze(self) -> None:
|
|
155
|
-
"""
|
|
147
|
+
def freeze(self, workbook: Workbook) -> None:
|
|
148
|
+
"""Prepare each sheet for saving, with the rows that were parsed written as text.
|
|
156
149
|
|
|
157
|
-
openpyxl writes what a sheet holds,
|
|
158
|
-
|
|
150
|
+
openpyxl writes what a sheet holds, which is only the rows that need it (those with a
|
|
151
|
+
link, a note or a value it writes in a form of its own). The other rows of blocks that
|
|
152
|
+
were parsed are written by `lazy_rows`, and the rows of blocks that were not parsed are
|
|
153
|
+
put in from the file; `merge` puts them all together.
|
|
159
154
|
"""
|
|
160
|
-
for sheet in
|
|
155
|
+
for sheet in workbook.worksheets:
|
|
161
156
|
cells = sheet._cells
|
|
162
|
-
if isinstance(cells, LazyCells):
|
|
163
|
-
|
|
157
|
+
if not isinstance(cells, LazyCells):
|
|
158
|
+
continue
|
|
159
|
+
lazy_shared.settle(cells)
|
|
164
160
|
dimensions = DimensionHolder(worksheet=sheet, default_factory=sheet._add_row)
|
|
165
161
|
for row, dimension in dict.items(sheet.row_dimensions):
|
|
166
|
-
if
|
|
162
|
+
if cells.is_loaded(row):
|
|
167
163
|
dimensions[row] = dimension
|
|
168
|
-
sheet.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
rendered, sheet._cells = lazy_rows.render(cells.held(), dimensions)
|
|
165
|
+
kept = {row for row, _ in sheet._cells}
|
|
166
|
+
sheet.row_dimensions = DimensionHolder(worksheet=sheet, default_factory=sheet._add_row)
|
|
167
|
+
for row in kept & dimensions.keys():
|
|
168
|
+
sheet.row_dimensions[row] = dimensions[row]
|
|
169
|
+
self.frozen[sheet] = (cells, rendered)
|
|
172
170
|
|
|
173
171
|
def merge(self, workbook: Workbook, content: bytes) -> bytes:
|
|
174
172
|
"""The saved workbook with the rows that were not parsed put back, as the file has them."""
|
|
@@ -179,8 +177,9 @@ class LazyEdit:
|
|
|
179
177
|
return content
|
|
180
178
|
package = Package(content)
|
|
181
179
|
parts = sheet_parts(package)
|
|
182
|
-
for sheet, cells in spliced.items():
|
|
183
|
-
|
|
180
|
+
for sheet, (cells, rendered) in spliced.items():
|
|
181
|
+
xml = _splice(package.read(parts[sheet.title]), cells, rendered)
|
|
182
|
+
package.write(parts[sheet.title], xml)
|
|
184
183
|
if self.strings_part:
|
|
185
184
|
self._restore_strings(package)
|
|
186
185
|
return package.to_bytes()
|
|
@@ -199,22 +198,22 @@ class LazyEdit:
|
|
|
199
198
|
rels.append(Rel(free_id(rels, ""), REL_BASE + "sharedStrings", name))
|
|
200
199
|
|
|
201
200
|
|
|
202
|
-
def load(path: Path) -> tuple[Workbook, LazyEdit | None]:
|
|
201
|
+
def load(path: Path, budget: CellBudget) -> tuple[Workbook, LazyEdit | None]:
|
|
203
202
|
"""Load a workbook for editing, its large sheets without their rows."""
|
|
204
203
|
parts: Parts = {}
|
|
205
|
-
reader = open_lazily(path, parts, _split)
|
|
204
|
+
reader = open_lazily(path, parts, _split, budget)
|
|
206
205
|
reader.read()
|
|
207
206
|
strings = reader.package.find(SHARED_STRINGS)
|
|
208
207
|
edit = LazyEdit(path, strings.PartName.lstrip("/") if strings is not None else None)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return reader.wb, edit if
|
|
208
|
+
lazy = list(lazy_sheets(reader, parts))
|
|
209
|
+
for sheet, source in lazy:
|
|
210
|
+
_install(sheet, source, reader.shared_strings, budget)
|
|
211
|
+
return reader.wb, edit if lazy else None
|
|
213
212
|
|
|
214
213
|
|
|
215
|
-
def _install(sheet: Worksheet, source: SheetSource, strings: list[str]) -> None:
|
|
214
|
+
def _install(sheet: Worksheet, source: SheetSource, strings: list[str], budget: CellBudget) -> None:
|
|
216
215
|
carried = sheet._cells
|
|
217
|
-
cells = LazyCells(sheet, source, strings)
|
|
216
|
+
cells = LazyCells(sheet, source, strings, budget)
|
|
218
217
|
sheet._cells = cells
|
|
219
218
|
_bind_row_dimensions(sheet, source, strings)
|
|
220
219
|
for (row, column), carried_cell in carried.items():
|
|
@@ -250,13 +249,14 @@ def _self_closed(tag: bytes) -> bytes:
|
|
|
250
249
|
return tag if tag.endswith(b"/>") else tag[:-1] + b"/>"
|
|
251
250
|
|
|
252
251
|
|
|
253
|
-
def _splice(xml: bytes, cells: LazyCells) -> bytes:
|
|
252
|
+
def _splice(xml: bytes, cells: LazyCells, rendered: dict[int, bytes]) -> bytes:
|
|
254
253
|
"""The sheet openpyxl wrote, holding the rows of the parsed blocks, with the others added."""
|
|
255
254
|
source = cells.source
|
|
256
255
|
data = _SHEET_DATA.search(xml)
|
|
257
256
|
if data is None:
|
|
258
257
|
raise ValueError("A saved worksheet has no sheetData.")
|
|
259
258
|
produced = {_row_number(row[0]): row[0] for row in _ROW.finditer(data[0])}
|
|
259
|
+
produced.update(rendered)
|
|
260
260
|
lazy_shared.restore(cells, produced)
|
|
261
261
|
pieces = _rows(cells, produced)
|
|
262
262
|
head, tail = xml[: data.start()], xml[data.end() :]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Finding which rows of a sheet that is not parsed can hold a text, without parsing them.
|
|
2
|
+
|
|
3
|
+
Replacing text looks at every cell, but most of a large sheet holds none of it. Only the blocks
|
|
4
|
+
of rows that might hold a match are parsed; the others are as in the file. "Might" is decided
|
|
5
|
+
from the XML and errs on the side of parsing: a block that holds nothing is parsed at worst.
|
|
6
|
+
A cell is searched as `replace` searches it: text, numbers as their text, formulas as they are
|
|
7
|
+
written (without the storage prefixes), so a formula that is only a master of the shared
|
|
8
|
+
formulas below it matches for the cells that follow it too.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from bisect import bisect_right
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from html import unescape
|
|
15
|
+
|
|
16
|
+
from excel_mcp.lazy_workbook import LazyCells
|
|
17
|
+
|
|
18
|
+
_STRING = re.compile(rb'<c\b[^>]*\bt="s"[^>]*><v>(\d+)</v>')
|
|
19
|
+
_INLINE = re.compile(rb"<is>(.*?)</is>", re.S)
|
|
20
|
+
_TAGS = re.compile(rb"<[^>]*>")
|
|
21
|
+
_FORMULA = re.compile(rb"<f\b([^>]*)>([^<]*)</f>")
|
|
22
|
+
_NUMBER = re.compile(rb'<c\b(?![^>]*\bt="(?!n")\w+")[^>]*><v>')
|
|
23
|
+
_GROUP = re.compile(rb'\bsi="(\d+)"')
|
|
24
|
+
_TEXT_OF_NUMBERS = frozenset("0123456789.-+e")
|
|
25
|
+
# What a stored formula has and its text as shown does not, or the reverse.
|
|
26
|
+
_STORAGE_MARKS = ("_xl", "anchorarray")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_matches(
|
|
30
|
+
cells: LazyCells, matches: Callable[[str], bool], query: str, *, in_formulas: bool
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Parse the blocks that may hold a cell for which ``matches(text)`` is true.
|
|
33
|
+
|
|
34
|
+
``query`` is what ``matches`` looks for, which tells what kinds of cell it can match.
|
|
35
|
+
"""
|
|
36
|
+
source = cells.source
|
|
37
|
+
starts = [start for start, _ in source.spans]
|
|
38
|
+
rows: set[int] = set()
|
|
39
|
+
|
|
40
|
+
def mark(position: int) -> None:
|
|
41
|
+
rows.add(source.row_numbers[bisect_right(starts, position) - 1])
|
|
42
|
+
|
|
43
|
+
hits = {index for index, text in enumerate(cells.strings) if matches(text)}
|
|
44
|
+
if hits:
|
|
45
|
+
for found in _STRING.finditer(source.data):
|
|
46
|
+
if int(found[1]) in hits:
|
|
47
|
+
mark(found.start())
|
|
48
|
+
for found in _INLINE.finditer(source.data):
|
|
49
|
+
if matches(unescape(_TAGS.sub(b"", found[1]).decode())):
|
|
50
|
+
mark(found.start())
|
|
51
|
+
if set(query.lower()) <= _TEXT_OF_NUMBERS:
|
|
52
|
+
rows.update(_rows_with_numbers(cells))
|
|
53
|
+
if in_formulas:
|
|
54
|
+
_mark_formulas(cells, matches, query, mark)
|
|
55
|
+
for number in rows:
|
|
56
|
+
cells.load(number)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _rows_with_numbers(cells: LazyCells) -> list[int]:
|
|
60
|
+
source = cells.source
|
|
61
|
+
return [
|
|
62
|
+
number
|
|
63
|
+
for number, (start, stop) in zip(source.row_numbers, source.spans, strict=True)
|
|
64
|
+
if _NUMBER.search(source.data, start, stop)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _mark_formulas(
|
|
69
|
+
cells: LazyCells, matches: Callable[[str], bool], query: str, mark: Callable[[int], None]
|
|
70
|
+
) -> None:
|
|
71
|
+
source = cells.source
|
|
72
|
+
digits = any(character.isdigit() for character in query)
|
|
73
|
+
for found in _FORMULA.finditer(source.data):
|
|
74
|
+
shown = "=" + unescape(found[2].decode())
|
|
75
|
+
hit = matches(shown) or any(part in shown.lower() for part in _STORAGE_MARKS)
|
|
76
|
+
if hit:
|
|
77
|
+
mark(found.start())
|
|
78
|
+
group = _GROUP.search(found[1])
|
|
79
|
+
master = source.masters.get(group[1].decode()) if group else None
|
|
80
|
+
# A cell that follows its master differs from it in digits, and in letters when the
|
|
81
|
+
# group spans columns.
|
|
82
|
+
if master is not None and (hit or digits or master.wide):
|
|
83
|
+
cells.load_between(*master.rows)
|