excel-mcp-server 2.2.1__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.1 → excel_mcp_server-2.2.2}/PKG-INFO +3 -2
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/README.md +1 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/pyproject.toml +3 -3
- {excel_mcp_server-2.2.1 → 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.1 → excel_mcp_server-2.2.2}/src/excel_mcp/cli.py +8 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/config.py +2 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_edit.py +6 -8
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_lines.py +3 -1
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_source.py +47 -8
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_workbook.py +27 -9
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/calculated.py +9 -2
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/cells.py +8 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/hyperlinks.py +2 -1
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/inspect.py +2 -2
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/layout.py +1 -1
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/notes.py +2 -1
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/sheet_tools.py +1 -1
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/workspace.py +31 -16
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/LICENSE +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/__init__.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/__main__.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/cells.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/criteria.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/dependencies.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/dynamic.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/engine.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/__init__.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/arrays.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/conditional.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/datetime.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/depreciation.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/distributions.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/financial.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/helpers.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/indirect.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/information.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/logical.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/lookup.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/math.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/regression.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/securities.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/statistical.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/subtotals.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/text.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/operators.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/parser.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/r1c1.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/registry.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/scheduler.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/solver.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/steps.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/tablerefs.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/textformat.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/values.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/wildcard.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/cfb.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/chart_roundtrip.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/errors.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/formula_syntax.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/formulas.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/function_arguments.json +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/inputs.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_find.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_rows.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_shared.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/links.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/macros.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/__init__.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chart_index.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chart_info.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_xml.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_axes.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_check.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_data.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_look.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_modern.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_options.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_series.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_style.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/columns.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/comparison.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_kinds.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_rule.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_x14.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_export.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_format.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_import.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_values.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/date_width.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/drawings.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/duplicates.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/files.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/fill.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filter_criteria.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filter_stored.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filters.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/formatting.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/goal_seek.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/images.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_objects.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_rules.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_styles.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edits.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/names.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/paste.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_axis.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_cache.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_calc.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_definition.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_fields.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_groups.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_index.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_options.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_records.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_render.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_showas.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_source.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_specs.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_values.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/print_setup.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/protection.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/replace.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/rule_clear.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/rules.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_copy.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_counts.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_refs.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_shapes.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_view.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheets.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/shifting.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_index.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_manage.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_pivot.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_place.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_table.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_xml.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicers.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sorting.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/spans.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparkline_style.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparkline_xml.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparklines.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/spill.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/table_options.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/tables.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/text_split.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/trace.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/transform.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/used_range.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/vba.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/vba_write.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/workbook_rewrite.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/workbook_settings.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/x14_xml.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba_compress.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba_write.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/__init__.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/anchors.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/arrays.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/capture.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/capture_sheet.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/consistency.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/extensions.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/guards.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/lazy_records.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/lines.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/metadata.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/model.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/opc.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/patch.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/pivot_records.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/pivots.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/properties.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/reader.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/references.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/restore.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/restore_sheet.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/scan.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/shape_names.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/package/vml.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/paths.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/plain_refs.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/py.typed +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/refs.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/rewrite.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/__init__.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/annotation_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/areas.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/csv_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/data_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/format_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/formula_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/http.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/image_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/object_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/params.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/pivot_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/registry.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/results.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/slicer_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/validation.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/vba_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/vba_write_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/server/workbook_tools.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/spill.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/structured.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/text.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/upload_links.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/upload_scan.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/values.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/xlfn.py +0 -0
- {excel_mcp_server-2.2.1 → excel_mcp_server-2.2.2}/src/excel_mcp/xlm.py +0 -0
- {excel_mcp_server-2.2.1 → 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
|
|
@@ -234,6 +234,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
|
|
|
234
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 |
|
|
235
235
|
| `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
|
|
236
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 |
|
|
237
238
|
| `--log-level LEVEL` | | `WARNING` | Logging on stderr |
|
|
238
239
|
| `--host HOST` | `EXCEL_MCP_HOST` | `127.0.0.1` | HTTP listen address |
|
|
239
240
|
| `--port PORT` | `EXCEL_MCP_PORT` | `8017` | HTTP port |
|
|
@@ -198,6 +198,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
|
|
|
198
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 |
|
|
199
199
|
| `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
|
|
200
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 |
|
|
201
202
|
| `--log-level LEVEL` | | `WARNING` | Logging on stderr |
|
|
202
203
|
| `--host HOST` | `EXCEL_MCP_HOST` | `127.0.0.1` | HTTP listen address |
|
|
203
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
|
|
@@ -33,6 +33,7 @@ from openpyxl.worksheet.worksheet import Worksheet
|
|
|
33
33
|
from openpyxl.xml.constants import SHARED_STRINGS
|
|
34
34
|
|
|
35
35
|
from excel_mcp import lazy_rows, lazy_shared
|
|
36
|
+
from excel_mcp.cell_budget import CellBudget
|
|
36
37
|
from excel_mcp.lazy_source import SheetSource, split_sheet
|
|
37
38
|
from excel_mcp.lazy_workbook import BLOCK_ROWS, LazyCells, Parts, lazy_sheets, open_lazily
|
|
38
39
|
from excel_mcp.package.opc import REL_BASE, Package, Rel, free_id
|
|
@@ -48,7 +49,6 @@ _ROW = re.compile(rb"<row\b[^>]*?/>|<row\b[^>]*>.*?</row>", re.S)
|
|
|
48
49
|
_ROW_NUMBER = re.compile(rb'\br="(\d+)"')
|
|
49
50
|
_DIMENSION = re.compile(rb'<dimension\s+ref="[^"]*"\s*/>')
|
|
50
51
|
_CELL_COLUMN = re.compile(rb'<c\b[^>]*?\br="([A-Z]+)\d')
|
|
51
|
-
_UNADDRESSED_CELL = re.compile(rb"<c\b(?![^>]*\br=)")
|
|
52
52
|
# A row with attributes of its own (a height, a style, hidden), not just its number.
|
|
53
53
|
_ATTRIBUTED_ROW = re.compile(rb"<row\b[^>]*\s(?!r=|spans=|[\w.-]+:)[\w.-]+=[^>]*>")
|
|
54
54
|
# What a formula stored as its result when Excel last calculated it, which an edit makes stale.
|
|
@@ -67,8 +67,6 @@ def _split(data: bytes) -> tuple[bytes, SheetSource] | None:
|
|
|
67
67
|
cross_cell = (b't="array"', b't="dataTable"', b' cm="', b' vm="')
|
|
68
68
|
if len(data) < LAZY_SHEET_BYTES or any(marker in data for marker in cross_cell):
|
|
69
69
|
return None
|
|
70
|
-
if _UNADDRESSED_CELL.search(data): # Its column is not known until the row is read.
|
|
71
|
-
return None
|
|
72
70
|
split = split_sheet(data)
|
|
73
71
|
return split if split and split[1].row_numbers else None
|
|
74
72
|
|
|
@@ -200,22 +198,22 @@ class LazyEdit:
|
|
|
200
198
|
rels.append(Rel(free_id(rels, ""), REL_BASE + "sharedStrings", name))
|
|
201
199
|
|
|
202
200
|
|
|
203
|
-
def load(path: Path) -> tuple[Workbook, LazyEdit | None]:
|
|
201
|
+
def load(path: Path, budget: CellBudget) -> tuple[Workbook, LazyEdit | None]:
|
|
204
202
|
"""Load a workbook for editing, its large sheets without their rows."""
|
|
205
203
|
parts: Parts = {}
|
|
206
|
-
reader = open_lazily(path, parts, _split)
|
|
204
|
+
reader = open_lazily(path, parts, _split, budget)
|
|
207
205
|
reader.read()
|
|
208
206
|
strings = reader.package.find(SHARED_STRINGS)
|
|
209
207
|
edit = LazyEdit(path, strings.PartName.lstrip("/") if strings is not None else None)
|
|
210
208
|
lazy = list(lazy_sheets(reader, parts))
|
|
211
209
|
for sheet, source in lazy:
|
|
212
|
-
_install(sheet, source, reader.shared_strings)
|
|
210
|
+
_install(sheet, source, reader.shared_strings, budget)
|
|
213
211
|
return reader.wb, edit if lazy else None
|
|
214
212
|
|
|
215
213
|
|
|
216
|
-
def _install(sheet: Worksheet, source: SheetSource, strings: list[str]) -> None:
|
|
214
|
+
def _install(sheet: Worksheet, source: SheetSource, strings: list[str], budget: CellBudget) -> None:
|
|
217
215
|
carried = sheet._cells
|
|
218
|
-
cells = LazyCells(sheet, source, strings)
|
|
216
|
+
cells = LazyCells(sheet, source, strings, budget)
|
|
219
217
|
sheet._cells = cells
|
|
220
218
|
_bind_row_dimensions(sheet, source, strings)
|
|
221
219
|
for (row, column), carried_cell in carried.items():
|
|
@@ -154,7 +154,9 @@ def copied(cells: LazyCells, sheet: Worksheet) -> LazyCells:
|
|
|
154
154
|
if not cells.is_loaded(number)
|
|
155
155
|
]
|
|
156
156
|
masters = dict(source.masters)
|
|
157
|
-
return LazyCells(
|
|
157
|
+
return LazyCells(
|
|
158
|
+
sheet, _assembled(source, rows, masters, source.hidden_rows), cells.strings, cells.budget
|
|
159
|
+
)
|
|
158
160
|
|
|
159
161
|
|
|
160
162
|
def _moved_masters(source: SheetSource, edit: LineEdit) -> dict[str, SharedMaster]:
|
|
@@ -4,8 +4,9 @@ import re
|
|
|
4
4
|
from bisect import bisect_left, bisect_right
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from functools import cached_property
|
|
7
|
+
from itertools import pairwise
|
|
7
8
|
|
|
8
|
-
from openpyxl.utils import column_index_from_string
|
|
9
|
+
from openpyxl.utils import column_index_from_string, get_column_letter
|
|
9
10
|
from openpyxl.xml.constants import SHEET_MAIN_NS
|
|
10
11
|
|
|
11
12
|
from excel_mcp import lazy_shared
|
|
@@ -17,6 +18,9 @@ _ROW_NUMBER = re.compile(rb'\br="(\d+)"')
|
|
|
17
18
|
_HIDDEN = re.compile(rb'\bhidden="(?:1|true)"')
|
|
18
19
|
_CELL = re.compile(rb"<c[ >/]")
|
|
19
20
|
_CELL_COLUMN = re.compile(rb'<c\b[^>]*?\br="([A-Z]+)\d')
|
|
21
|
+
_UNADDRESSED = re.compile(rb"<row\b(?![^>]*\br=)|<c\b(?![^>]*\br=)")
|
|
22
|
+
_TAG = re.compile(rb"<(row|c)\b([^>]*)>")
|
|
23
|
+
_CELL_LETTERS = re.compile(rb'\br="([A-Z]+)\d')
|
|
20
24
|
_ROOT = re.compile(rb'<worksheet\b[^>]*?\bxmlns="([^"]+)"[^>]*>')
|
|
21
25
|
|
|
22
26
|
|
|
@@ -73,24 +77,24 @@ class SheetSource:
|
|
|
73
77
|
def split_sheet(data: bytes) -> tuple[bytes, SheetSource] | None:
|
|
74
78
|
"""The sheet without its rows, and the rows; None for a layout this does not handle.
|
|
75
79
|
|
|
76
|
-
Rows without a number
|
|
77
|
-
masters of shared formulas are indexed, as a row's formula can depend on the
|
|
78
|
-
declares it.
|
|
80
|
+
Rows and cells without a number or address are given one first, as their position
|
|
81
|
+
says. The masters of shared formulas are indexed, as a row's formula can depend on the
|
|
82
|
+
row that declares it.
|
|
79
83
|
"""
|
|
80
84
|
root = _ROOT.search(data)
|
|
81
85
|
start = data.find(_SHEET_DATA)
|
|
82
86
|
end = data.find(_SHEET_DATA_END)
|
|
83
87
|
if root is None or root[1] != SHEET_MAIN_NS.encode() or start < 0 or end < start:
|
|
84
88
|
return None
|
|
89
|
+
if not _all_addressed(data, start, end) and _UNADDRESSED.search(data, start, end):
|
|
90
|
+
data = data[:start] + _address(data[start:end]) + data[end:]
|
|
91
|
+
end = data.find(_SHEET_DATA_END)
|
|
85
92
|
numbers: list[int] = []
|
|
86
93
|
spans: list[tuple[int, int]] = []
|
|
87
94
|
hidden: list[int] = []
|
|
88
95
|
starts = list(_ROW.finditer(data, start, end))
|
|
89
96
|
for position, tag in enumerate(starts):
|
|
90
|
-
|
|
91
|
-
if number is None:
|
|
92
|
-
return None
|
|
93
|
-
numbers.append(int(number.group(1)))
|
|
97
|
+
numbers.append(int(_ROW_NUMBER.search(tag.group())[1])) # pyright: ignore[reportOptionalSubscript]
|
|
94
98
|
following = starts[position + 1].start() if position + 1 < len(starts) else end
|
|
95
99
|
spans.append((tag.start(), following))
|
|
96
100
|
if _HIDDEN.search(tag.group()):
|
|
@@ -100,3 +104,38 @@ def split_sheet(data: bytes) -> tuple[bytes, SheetSource] | None:
|
|
|
100
104
|
without_rows = data[:start] + _SHEET_DATA + b"/>" + data[end + len(_SHEET_DATA_END) :]
|
|
101
105
|
masters = lazy_shared.index(data, start, end) if b't="shared"' in data else {}
|
|
102
106
|
return without_rows, SheetSource(data, root.group(), numbers, spans, hidden, masters)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _all_addressed(data: bytes, start: int, end: int) -> bool:
|
|
110
|
+
"""Whether every row and cell opens with its address, as counting them (quickly) shows."""
|
|
111
|
+
return all(
|
|
112
|
+
data.count(tag, start, end) == data.count(tag + b' r="', start, end)
|
|
113
|
+
for tag in (b"<row", b"<c")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _address(rows: bytes) -> bytes:
|
|
118
|
+
"""The rows with the number of each row and the address of each cell that has none.
|
|
119
|
+
|
|
120
|
+
Excel and openpyxl number them by position: a row after the one before it, a cell after
|
|
121
|
+
the one before it in its row. Writers leave the numbers out to make the file smaller.
|
|
122
|
+
"""
|
|
123
|
+
row = column = 0
|
|
124
|
+
|
|
125
|
+
def addressed(tag: re.Match[bytes]) -> bytes:
|
|
126
|
+
nonlocal row, column
|
|
127
|
+
attributes = tag[2]
|
|
128
|
+
if tag[1] == b"row":
|
|
129
|
+
given = _ROW_NUMBER.search(attributes)
|
|
130
|
+
row = int(given[1]) if given else row + 1
|
|
131
|
+
column = 0
|
|
132
|
+
return tag[0] if given else b'<row r="%d"%b>' % (row, attributes)
|
|
133
|
+
given = _CELL_LETTERS.search(attributes)
|
|
134
|
+
column = column_index_from_string(given[1].decode()) if given else column + 1
|
|
135
|
+
if given:
|
|
136
|
+
return tag[0]
|
|
137
|
+
return b'<c r="%b%d"%b>' % (get_column_letter(column).encode(), row, attributes)
|
|
138
|
+
|
|
139
|
+
bounds = [0, *(tag.start() for tag in _ROW.finditer(rows)), len(rows)]
|
|
140
|
+
# One row at a time, so that no list of all the pieces of a sheet is held besides the sheet.
|
|
141
|
+
return b"".join(_TAG.sub(addressed, rows[a:b]) for a, b in pairwise(bounds))
|
|
@@ -22,6 +22,7 @@ from openpyxl.worksheet.worksheet import Worksheet
|
|
|
22
22
|
from openpyxl.xml.constants import WORKSHEET_TYPE
|
|
23
23
|
|
|
24
24
|
from excel_mcp import lazy_rows, lazy_shared
|
|
25
|
+
from excel_mcp.cell_budget import CellBudget, spend_sheet
|
|
25
26
|
from excel_mcp.lazy_source import SheetSource, split_sheet
|
|
26
27
|
|
|
27
28
|
BLOCK_ROWS = 128
|
|
@@ -39,11 +40,14 @@ class LazyCells(dict[CellKey, Any]):
|
|
|
39
40
|
parsed holds all of its rows: what is not in ``loaded`` is as the file has it.
|
|
40
41
|
"""
|
|
41
42
|
|
|
42
|
-
def __init__(
|
|
43
|
+
def __init__(
|
|
44
|
+
self, sheet: Worksheet, source: SheetSource, strings: list[str], budget: CellBudget
|
|
45
|
+
) -> None:
|
|
43
46
|
super().__init__()
|
|
44
47
|
self.sheet = sheet
|
|
45
48
|
self.source = source
|
|
46
49
|
self.strings = strings
|
|
50
|
+
self.budget = budget
|
|
47
51
|
self.loaded: set[int] = set()
|
|
48
52
|
self.columns: dict[int, list[int]] = {}
|
|
49
53
|
self.last_row = self.last_column = 0
|
|
@@ -246,6 +250,9 @@ class LazyCells(dict[CellKey, Any]):
|
|
|
246
250
|
|
|
247
251
|
def _parse(self, spans: list[tuple[int, int]]) -> None:
|
|
248
252
|
"""Parse rows: those in the usual forms as text, the others with openpyxl's parser."""
|
|
253
|
+
data = self.source.data
|
|
254
|
+
# The rows and their cells, counted before they are parsed as `spend_sheet` counts a sheet.
|
|
255
|
+
self.budget.spend(len(spans) + sum(data.count(b"<c", start, end) for start, end in spans))
|
|
249
256
|
book: Any = self.sheet.parent
|
|
250
257
|
shared = lazy_shared.Translators(self.source.masters)
|
|
251
258
|
reading = lazy_rows.Reading(
|
|
@@ -307,11 +314,14 @@ Splitter = Callable[[bytes], tuple[bytes, SheetSource] | None]
|
|
|
307
314
|
class _SheetArchive(ZipFile):
|
|
308
315
|
"""An archive that serves each worksheet without its rows, which are kept in ``parts``."""
|
|
309
316
|
|
|
310
|
-
def __init__(
|
|
317
|
+
def __init__(
|
|
318
|
+
self, path: Path, parts: Parts, sheet_parts: set[str], split: Splitter, budget: CellBudget
|
|
319
|
+
) -> None:
|
|
311
320
|
super().__init__(path)
|
|
312
321
|
self.parts = parts
|
|
313
322
|
self.sheet_parts = sheet_parts
|
|
314
323
|
self.split = split
|
|
324
|
+
self.budget = budget
|
|
315
325
|
self.worksheets: list[str] = []
|
|
316
326
|
|
|
317
327
|
def open(self, name: Any, mode: Any = "r", pwd: Any = None, **options: Any) -> IO[bytes]:
|
|
@@ -324,16 +334,24 @@ class _SheetArchive(ZipFile):
|
|
|
324
334
|
split = self.split(data)
|
|
325
335
|
self.parts[name] = split if split else (data, None)
|
|
326
336
|
self.worksheets.append(name)
|
|
327
|
-
|
|
337
|
+
parsed = self.parts[name][0]
|
|
338
|
+
spend_sheet(BytesIO(parsed), self.budget)
|
|
339
|
+
return BytesIO(parsed)
|
|
328
340
|
|
|
329
341
|
|
|
330
|
-
def open_lazily(
|
|
331
|
-
|
|
342
|
+
def open_lazily(
|
|
343
|
+
path: Path, parts: Parts, split: Splitter, budget: CellBudget, **options: Any
|
|
344
|
+
) -> ExcelReader:
|
|
345
|
+
"""A reader that has not read the workbook yet, whose sheets are served by ``split``.
|
|
346
|
+
|
|
347
|
+
The cells and rows of the sheets that are not split are spent from ``budget`` when they
|
|
348
|
+
are read, and those of the split sheets when their rows are parsed.
|
|
349
|
+
"""
|
|
332
350
|
reader = ExcelReader(path, **options)
|
|
333
351
|
reader.read_manifest()
|
|
334
352
|
sheet_parts = {part.PartName.lstrip("/") for part in reader.package.findall(WORKSHEET_TYPE)}
|
|
335
353
|
reader.archive.close()
|
|
336
|
-
reader.archive = _SheetArchive(path, parts, sheet_parts, split)
|
|
354
|
+
reader.archive = _SheetArchive(path, parts, sheet_parts, split, budget)
|
|
337
355
|
return reader
|
|
338
356
|
|
|
339
357
|
|
|
@@ -346,16 +364,16 @@ def lazy_sheets(reader: ExcelReader, parts: Parts) -> Iterator[tuple[Worksheet,
|
|
|
346
364
|
yield sheet, source
|
|
347
365
|
|
|
348
366
|
|
|
349
|
-
def load_lazy(path: Path, parts: Parts, *, data_only: bool) -> Workbook:
|
|
367
|
+
def load_lazy(path: Path, parts: Parts, budget: CellBudget, *, data_only: bool) -> Workbook:
|
|
350
368
|
"""Load a workbook the way ``load_workbook`` does, leaving the cells to be read on demand.
|
|
351
369
|
|
|
352
370
|
A sheet this cannot split into rows is loaded whole. ``parts`` is shared by the loads of
|
|
353
371
|
one file, so that each sheet is read from the file once.
|
|
354
372
|
"""
|
|
355
|
-
reader = open_lazily(path, parts, split_sheet, data_only=data_only, keep_links=False)
|
|
373
|
+
reader = open_lazily(path, parts, split_sheet, budget, data_only=data_only, keep_links=False)
|
|
356
374
|
reader.read()
|
|
357
375
|
for sheet, source in lazy_sheets(reader, parts):
|
|
358
|
-
cells = LazyCells(sheet, source, reader.shared_strings)
|
|
376
|
+
cells = LazyCells(sheet, source, reader.shared_strings, budget)
|
|
359
377
|
for cell in sheet._cells.values():
|
|
360
378
|
if isinstance(cell, MergedCell):
|
|
361
379
|
cells.keep(cell)
|
|
@@ -12,7 +12,7 @@ from openpyxl.worksheet.worksheet import Worksheet
|
|
|
12
12
|
|
|
13
13
|
from excel_mcp.calc.engine import Engine
|
|
14
14
|
from excel_mcp.calc.values import ExcelError, Scalar, UncalculableError
|
|
15
|
-
from excel_mcp.errors import InvalidArgumentError
|
|
15
|
+
from excel_mcp.errors import InvalidArgumentError, LimitExceededError
|
|
16
16
|
from excel_mcp.operations import cells
|
|
17
17
|
from excel_mcp.operations.cells import RangeData
|
|
18
18
|
from excel_mcp.operations.comparison import FormulaResults
|
|
@@ -70,7 +70,14 @@ def formula_values(
|
|
|
70
70
|
]
|
|
71
71
|
if not formula_cells:
|
|
72
72
|
return {}
|
|
73
|
-
|
|
73
|
+
try:
|
|
74
|
+
data = read_calculated(workspace, path, sheet.title, str(area), area.size)
|
|
75
|
+
except LimitExceededError as error:
|
|
76
|
+
raise LimitExceededError(
|
|
77
|
+
f"{error} Calculating the formulas of {area} needs the cells in memory because "
|
|
78
|
+
"the file stores no results for them. Open the workbook in Excel and save it, or "
|
|
79
|
+
"use a smaller range."
|
|
80
|
+
) from None
|
|
74
81
|
if data.uncalculated:
|
|
75
82
|
cells_list = ", ".join(data.uncalculated)
|
|
76
83
|
raise InvalidArgumentError(
|
|
@@ -57,6 +57,14 @@ def stored_cells(sheet: Worksheet) -> Iterator[Cell]:
|
|
|
57
57
|
yield cell
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def described_used_range(sheet: Worksheet) -> CellRange:
|
|
61
|
+
"""`used_range` of a sheet that is not being changed: a large sheet's is read from its XML."""
|
|
62
|
+
cells = sheet._cells
|
|
63
|
+
if isinstance(cells, LazyCells) and (found := used_range_in(cells.source.data)):
|
|
64
|
+
return found
|
|
65
|
+
return used_range(sheet)
|
|
66
|
+
|
|
67
|
+
|
|
60
68
|
def stored_cells_in(sheet: Worksheet, area: CellRange) -> Iterator[Cell]:
|
|
61
69
|
"""The cells that hold a value inside ``area``; a large sheet parses only those rows."""
|
|
62
70
|
cells = sheet._cells
|
|
@@ -12,6 +12,7 @@ from pydantic import BaseModel, Field
|
|
|
12
12
|
from excel_mcp.errors import InvalidArgumentError
|
|
13
13
|
from excel_mcp.formulas import split_top_level, unquote
|
|
14
14
|
from excel_mcp.inputs import InputModel
|
|
15
|
+
from excel_mcp.lazy_workbook import held_cells
|
|
15
16
|
from excel_mcp.links import is_allowed_address
|
|
16
17
|
from excel_mcp.refs import parse_range
|
|
17
18
|
|
|
@@ -58,7 +59,7 @@ def list_links(sheet: Worksheet) -> list[LinkInfo]:
|
|
|
58
59
|
+ ("#" + cell.hyperlink.location if cell.hyperlink.location else ""),
|
|
59
60
|
tooltip=cell.hyperlink.tooltip,
|
|
60
61
|
)
|
|
61
|
-
for _, cell in sorted(sheet.
|
|
62
|
+
for _, cell in sorted(held_cells(sheet).items())
|
|
62
63
|
if cell.hyperlink
|
|
63
64
|
]
|
|
64
65
|
|
|
@@ -7,7 +7,7 @@ from openpyxl.workbook import Workbook
|
|
|
7
7
|
from openpyxl.worksheet.worksheet import Worksheet
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
10
|
-
from excel_mcp.operations.cells import
|
|
10
|
+
from excel_mcp.operations.cells import described_used_range, streamed_used_range
|
|
11
11
|
from excel_mcp.operations.chart_index import list_charts
|
|
12
12
|
from excel_mcp.operations.chart_info import ChartInfo
|
|
13
13
|
from excel_mcp.operations.conditional_x14 import extended_rules
|
|
@@ -139,7 +139,7 @@ def describe_workbook(
|
|
|
139
139
|
|
|
140
140
|
def describe_sheet(sheet: Worksheet) -> SheetDetails:
|
|
141
141
|
return SheetDetails(
|
|
142
|
-
used_range=str(
|
|
142
|
+
used_range=str(described_used_range(sheet)),
|
|
143
143
|
freeze_panes=sheet.freeze_panes,
|
|
144
144
|
auto_filter=sheet.auto_filter.ref,
|
|
145
145
|
merged_ranges=sorted(str(merged) for merged in sheet.merged_cells.ranges),
|
|
@@ -157,7 +157,7 @@ def set_visibility(sheet: Worksheet, visibility: Literal["visible", "hidden"]) -
|
|
|
157
157
|
|
|
158
158
|
def hidden_lines(sheet: Worksheet, axis: Axis) -> list[str]:
|
|
159
159
|
if axis == "rows":
|
|
160
|
-
indices = {index for index, row in sheet.row_dimensions
|
|
160
|
+
indices = {index for index, row in dict.items(sheet.row_dimensions) if row.hidden}
|
|
161
161
|
else:
|
|
162
162
|
indices = set()
|
|
163
163
|
for column in sheet.column_dimensions.values():
|
|
@@ -5,6 +5,7 @@ from openpyxl.worksheet.worksheet import Worksheet
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
7
|
from excel_mcp.errors import InvalidArgumentError
|
|
8
|
+
from excel_mcp.lazy_workbook import held_cells
|
|
8
9
|
from excel_mcp.operations.cells import writable_cell
|
|
9
10
|
from excel_mcp.refs import parse_cell
|
|
10
11
|
|
|
@@ -18,7 +19,7 @@ class NoteInfo(BaseModel):
|
|
|
18
19
|
def list_notes(sheet: Worksheet) -> list[NoteInfo]:
|
|
19
20
|
return [
|
|
20
21
|
NoteInfo(cell=cell.coordinate, text=cell.comment.text, author=cell.comment.author or "")
|
|
21
|
-
for _, cell in sorted(sheet.
|
|
22
|
+
for _, cell in sorted(held_cells(sheet).items())
|
|
22
23
|
if cell.comment
|
|
23
24
|
]
|
|
24
25
|
|
|
@@ -23,7 +23,7 @@ def register(tools: ToolRegistry, workspace: Workspace) -> None:
|
|
|
23
23
|
"""Describe a sheet: used range, panes, merges and everything on it (tables, charts,
|
|
24
24
|
PivotTables, slicers, images, notes, hyperlinks, validation, conditional formats,
|
|
25
25
|
sparklines...) with names and cells. Slow on very large files."""
|
|
26
|
-
with workspace.read(path
|
|
26
|
+
with workspace.read(path) as workbook:
|
|
27
27
|
return inspect.describe_sheet(get_sheet(workbook, sheet))
|
|
28
28
|
|
|
29
29
|
@tools.writer("Create sheet")
|