excel-mcp-server 2.2.6__tar.gz → 2.2.7__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.6 → excel_mcp_server-2.2.7}/PKG-INFO +3 -3
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/README.md +2 -2
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/pyproject.toml +1 -1
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/pyproject.toml.orig +1 -1
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/engine.py +1 -1
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/cells.py +38 -4
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/csv_export.py +9 -2
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/csv_import.py +22 -5
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/duplicates.py +12 -2
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/fill.py +17 -5
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/layout.py +27 -14
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot.py +9 -6
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_records.py +11 -4
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_render.py +9 -3
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_source.py +15 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/replace.py +41 -5
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/slicers.py +4 -1
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sorting.py +19 -11
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/table_options.py +3 -3
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/text_split.py +65 -20
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/transform.py +3 -1
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/data_tools.py +2 -1
- excel_mcp_server-2.2.7/src/excel_mcp/text_budget.py +35 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/workspace.py +19 -1
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/LICENSE +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/__init__.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/__main__.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/cells.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/criteria.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/dependencies.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/dynamic.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/__init__.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/arrays.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/conditional.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/datetime.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/depreciation.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/distributions.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/financial.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/helpers.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/indirect.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/information.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/logical.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/lookup.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/math.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/regression.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/securities.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/statistical.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/subtotals.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/functions/text.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/operators.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/parser.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/r1c1.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/registry.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/scheduler.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/solver.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/steps.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/tablerefs.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/textformat.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/values.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/calc/wildcard.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/cell_budget.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/cfb.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/chart_roundtrip.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/cli.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/config.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/errors.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/formula_syntax.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/formulas.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/function_arguments.json +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/inputs.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/lazy_edit.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/lazy_find.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/lazy_lines.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/lazy_rows.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/lazy_shared.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/lazy_source.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/lazy_workbook.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/links.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/macros.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/__init__.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/calculated.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chart_index.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chart_info.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/chartex_xml.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts_axes.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts_check.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts_data.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts_look.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts_modern.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts_options.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts_series.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/charts_style.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/columns.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/comparison.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/conditional.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/conditional_kinds.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/conditional_rule.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/conditional_x14.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/csv_format.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/csv_values.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/date_width.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/drawings.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/files.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/filter_criteria.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/filter_stored.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/filters.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/formatting.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/goal_seek.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/hyperlinks.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/images.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/inspect.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/line_edit_objects.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/line_edit_rules.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/line_edit_styles.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/line_edits.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/names.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/notes.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/paste.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_axis.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_cache.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_calc.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_definition.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_fields.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_groups.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_index.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_options.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_showas.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_specs.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/pivot_values.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/print_setup.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/protection.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/rule_clear.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/rules.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sheet_copy.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sheet_counts.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sheet_refs.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sheet_shapes.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sheet_view.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sheets.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/shifting.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/slicer_index.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/slicer_manage.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/slicer_pivot.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/slicer_place.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/slicer_table.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/slicer_xml.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/spans.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sparkline_style.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sparkline_xml.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/sparklines.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/spill.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/tables.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/trace.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/used_range.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/vba.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/vba_write.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/workbook_rewrite.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/workbook_settings.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/operations/x14_xml.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/ovba.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/ovba_compress.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/ovba_write.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/__init__.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/anchors.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/arrays.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/capture.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/capture_sheet.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/consistency.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/extensions.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/guards.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/lazy_records.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/lines.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/metadata.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/model.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/opc.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/patch.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/pivot_records.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/pivots.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/properties.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/reader.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/references.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/restore.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/restore_sheet.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/scan.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/shape_names.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/package/vml.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/paths.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/plain_refs.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/py.typed +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/refs.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/rewrite.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/__init__.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/annotation_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/areas.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/csv_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/format_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/formula_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/http.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/image_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/object_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/params.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/pivot_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/registry.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/results.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/sheet_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/slicer_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/validation.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/vba_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/vba_write_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/server/workbook_tools.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/spill.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/structured.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/text.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/upload_links.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/upload_scan.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/values.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/xlfn.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/xlm.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/src/excel_mcp/xml_text.py +0 -0
- {excel_mcp_server-2.2.6 → excel_mcp_server-2.2.7}/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.7
|
|
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
|
|
@@ -233,7 +233,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
|
|
|
233
233
|
| `--allow-vba-write` | `EXCEL_MCP_ALLOW_VBA_WRITE=1` | off | Add tools that write VBA macros (see warning below); ignored with `--read-only` |
|
|
234
234
|
| `--tools AREAS` | `EXCEL_MCP_TOOLS` | all | Only offer the core tools and these comma-separated areas, to save context (see [Choosing tools](#choosing-tools)) |
|
|
235
235
|
| `--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 |
|
|
236
|
-
| `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
|
|
236
|
+
| `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages; also bounds text to 256 characters per cell on average |
|
|
237
237
|
| `--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 |
|
|
238
238
|
| `--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 |
|
|
239
239
|
| `--log-level LEVEL` | | `WARNING` | Logging on stderr |
|
|
@@ -343,7 +343,7 @@ refused, since any change would invalidate the signature.
|
|
|
343
343
|
`http(s)://`, `mailto:` or `#Sheet!A1` link only.
|
|
344
344
|
- Paths are resolved, including symlinks, before they are checked against the allowed
|
|
345
345
|
folders. Network (UNC) and Windows device paths are always rejected.
|
|
346
|
-
- A single call processes at most 100,000 cells (`--max-cells`), and large reads are returned in pages.
|
|
346
|
+
- A single call processes at most 100,000 cells (`--max-cells`) and, on average, 256 characters of text per cell (25.6 million characters at the default), and large reads are returned in pages. Text a workbook stores once but many cells hold is counted in every cell, and a workbook that saving would expand past the unpacked-size limit is refused.
|
|
347
347
|
- Cell contents are data from files. The server tells the model not to follow instructions
|
|
348
348
|
found in them, but review what an assistant does with workbooks from untrusted sources.
|
|
349
349
|
|
|
@@ -197,7 +197,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
|
|
|
197
197
|
| `--allow-vba-write` | `EXCEL_MCP_ALLOW_VBA_WRITE=1` | off | Add tools that write VBA macros (see warning below); ignored with `--read-only` |
|
|
198
198
|
| `--tools AREAS` | `EXCEL_MCP_TOOLS` | all | Only offer the core tools and these comma-separated areas, to save context (see [Choosing tools](#choosing-tools)) |
|
|
199
199
|
| `--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 |
|
|
200
|
-
| `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
|
|
200
|
+
| `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages; also bounds text to 256 characters per cell on average |
|
|
201
201
|
| `--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 |
|
|
202
202
|
| `--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 |
|
|
203
203
|
| `--log-level LEVEL` | | `WARNING` | Logging on stderr |
|
|
@@ -307,7 +307,7 @@ refused, since any change would invalidate the signature.
|
|
|
307
307
|
`http(s)://`, `mailto:` or `#Sheet!A1` link only.
|
|
308
308
|
- Paths are resolved, including symlinks, before they are checked against the allowed
|
|
309
309
|
folders. Network (UNC) and Windows device paths are always rejected.
|
|
310
|
-
- A single call processes at most 100,000 cells (`--max-cells`), and large reads are returned in pages.
|
|
310
|
+
- A single call processes at most 100,000 cells (`--max-cells`) and, on average, 256 characters of text per cell (25.6 million characters at the default), and large reads are returned in pages. Text a workbook stores once but many cells hold is counted in every cell, and a workbook that saving would expand past the unpacked-size limit is refused.
|
|
311
311
|
- Cell contents are data from files. The server tells the model not to follow instructions
|
|
312
312
|
found in them, but review what an assistant does with workbooks from untrusted sources.
|
|
313
313
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""Reading, writing, clearing, copying and searching cell contents."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterator
|
|
4
|
+
from contextlib import closing
|
|
4
5
|
|
|
5
6
|
from openpyxl.cell.cell import Cell, MergedCell
|
|
6
7
|
from openpyxl.worksheet._read_only import ReadOnlyWorksheet
|
|
7
8
|
from openpyxl.worksheet.worksheet import Worksheet
|
|
8
9
|
from pydantic import BaseModel
|
|
9
10
|
|
|
11
|
+
from excel_mcp.config import Limits
|
|
10
12
|
from excel_mcp.errors import InvalidArgumentError, LimitExceededError
|
|
11
13
|
from excel_mcp.lazy_workbook import LazyCells, held_cells
|
|
12
14
|
from excel_mcp.operations.date_width import FITTED_WIDTHS, widen_for_dates
|
|
@@ -22,6 +24,7 @@ from excel_mcp.refs import (
|
|
|
22
24
|
parse_clamped_range,
|
|
23
25
|
parse_range,
|
|
24
26
|
)
|
|
27
|
+
from excel_mcp.text_budget import CHARS_PER_CELL, TextBudget
|
|
25
28
|
from excel_mcp.values import CellValue, date_number_format, to_cell, to_json, typed_value
|
|
26
29
|
from excel_mcp.workspace import sheet_names
|
|
27
30
|
from excel_mcp.xlfn import display_formula
|
|
@@ -192,7 +195,24 @@ def read_range(sheet: ReadOnlyWorksheet, ref: str | None, max_cells: int) -> Ran
|
|
|
192
195
|
max_col=target.max_col,
|
|
193
196
|
values_only=True,
|
|
194
197
|
)
|
|
195
|
-
values = [
|
|
198
|
+
values = []
|
|
199
|
+
characters, limit, cut = 0, max_cells * CHARS_PER_CELL, False
|
|
200
|
+
with closing(rows):
|
|
201
|
+
for row in rows:
|
|
202
|
+
line = [_displayed(to_json(value)) for value in row]
|
|
203
|
+
size = sum(len(value) for value in line if isinstance(value, str))
|
|
204
|
+
if characters + size > limit:
|
|
205
|
+
if not values:
|
|
206
|
+
raise LimitExceededError(
|
|
207
|
+
f"Row {target.min_row} of {target} holds more than {limit:,} characters of "
|
|
208
|
+
"text, the limit per page. Read fewer columns."
|
|
209
|
+
)
|
|
210
|
+
cut = True
|
|
211
|
+
break
|
|
212
|
+
characters += size
|
|
213
|
+
values.append(line)
|
|
214
|
+
if cut:
|
|
215
|
+
last_row = target.min_row + len(values) - 1
|
|
196
216
|
for row in values:
|
|
197
217
|
while row and row[-1] is None:
|
|
198
218
|
row.pop()
|
|
@@ -279,21 +299,35 @@ def find_cells(
|
|
|
279
299
|
exact: bool,
|
|
280
300
|
case_sensitive: bool,
|
|
281
301
|
max_results: int,
|
|
302
|
+
limits: Limits,
|
|
282
303
|
) -> FindResult:
|
|
304
|
+
"""Cells whose text contains (or equals) ``query``. The text searched and the text returned
|
|
305
|
+
are counted, so a workbook of many cells sharing one long text cannot make a search long or
|
|
306
|
+
its result large."""
|
|
307
|
+
|
|
283
308
|
def normalize(text: str) -> str:
|
|
284
309
|
return text if case_sensitive else text.casefold()
|
|
285
310
|
|
|
311
|
+
scanned = TextBudget(
|
|
312
|
+
limits.max_loaded_cells,
|
|
313
|
+
"Searching would read more than {limit} characters of text. Search one sheet at a "
|
|
314
|
+
"time, or raise --max-loaded-cells.",
|
|
315
|
+
)
|
|
316
|
+
page = limits.max_read_cells * CHARS_PER_CELL
|
|
286
317
|
needle = normalize(query)
|
|
287
318
|
matches: dict[str, dict[str, CellValue]] = {}
|
|
288
|
-
count = 0
|
|
319
|
+
count = returned = 0
|
|
289
320
|
for sheet in sheets:
|
|
290
321
|
for row, col, value in streamed_cells(sheet):
|
|
291
322
|
shown = _displayed(to_json(value))
|
|
292
|
-
text =
|
|
323
|
+
text = str(shown)
|
|
324
|
+
scanned.spend(len(text))
|
|
325
|
+
text = normalize(text)
|
|
293
326
|
if not (text == needle if exact else needle in text):
|
|
294
327
|
continue
|
|
295
|
-
if count == max_results:
|
|
328
|
+
if count == max_results or (count and returned + len(text) > page):
|
|
296
329
|
return FindResult(matches=matches, truncated=True)
|
|
297
330
|
matches.setdefault(sheet.title, {})[cell_name(row, col)] = shown
|
|
298
331
|
count += 1
|
|
332
|
+
returned += len(text)
|
|
299
333
|
return FindResult(matches=matches)
|
|
@@ -14,6 +14,7 @@ from excel_mcp.errors import InvalidArgumentError, LimitExceededError
|
|
|
14
14
|
from excel_mcp.operations.calculated import read_calculated
|
|
15
15
|
from excel_mcp.operations.csv_format import DELIMITER_CHARACTERS, FORMULA_STARTS, Encoding
|
|
16
16
|
from excel_mcp.refs import CellRange, cell_name, parse_range
|
|
17
|
+
from excel_mcp.text_budget import CHARS_PER_CELL, TextBudget
|
|
17
18
|
from excel_mcp.values import CellValue
|
|
18
19
|
from excel_mcp.workspace import Workspace, get_streamed_sheet
|
|
19
20
|
|
|
@@ -42,9 +43,14 @@ def export_csv(
|
|
|
42
43
|
data = read_calculated(workspace, path, sheet, ref, max_cells)
|
|
43
44
|
if data.next_range:
|
|
44
45
|
raise LimitExceededError(
|
|
45
|
-
f"The range has more than {max_cells:,} cells
|
|
46
|
-
"smaller range or raise --max-cells."
|
|
46
|
+
f"The range has more than {max_cells:,} cells or {max_cells * CHARS_PER_CELL:,} "
|
|
47
|
+
"characters of text, the limit per call. Export a smaller range or raise --max-cells."
|
|
47
48
|
)
|
|
49
|
+
budget = TextBudget(
|
|
50
|
+
max_cells,
|
|
51
|
+
"The range as displayed has more than {limit} characters of text, the limit per call. "
|
|
52
|
+
"Export a smaller range or raise --max-cells.",
|
|
53
|
+
)
|
|
48
54
|
area = parse_range(data.range)
|
|
49
55
|
formats = _formats(workspace, path, sheet, area) if formatted else None
|
|
50
56
|
buffer = io.StringIO(newline="")
|
|
@@ -57,6 +63,7 @@ def export_csv(
|
|
|
57
63
|
number_format = formats[row][column] if formats else None
|
|
58
64
|
try:
|
|
59
65
|
fields.append(_field(value, number_format, escape_formulas))
|
|
66
|
+
budget.spend(len(fields[-1]))
|
|
60
67
|
except UncalculableError as reason:
|
|
61
68
|
where = cell_name(area.min_row + row, area.min_col + column)
|
|
62
69
|
raise InvalidArgumentError(
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Reading a CSV file into a worksheet."""
|
|
2
2
|
|
|
3
3
|
import csv
|
|
4
|
+
from collections.abc import Iterator
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
7
|
+
from typing import TextIO
|
|
6
8
|
|
|
7
9
|
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
|
|
8
10
|
from openpyxl.worksheet.worksheet import Worksheet
|
|
@@ -48,7 +50,8 @@ def import_csv(
|
|
|
48
50
|
with file.open(encoding=codec, newline="") as stream:
|
|
49
51
|
separator = _separator(stream.read(_SAMPLE_CHARACTERS), delimiter)
|
|
50
52
|
stream.seek(0)
|
|
51
|
-
|
|
53
|
+
lines = _lines_within(stream, separator, max_cells)
|
|
54
|
+
for offset, record in enumerate(csv.reader(lines, delimiter=separator), start=1):
|
|
52
55
|
row = first_row + offset - 1
|
|
53
56
|
if record and (row > MAX_ROW or first_col + len(record) - 1 > MAX_COLUMN):
|
|
54
57
|
raise InvalidArgumentError(
|
|
@@ -56,10 +59,7 @@ def import_csv(
|
|
|
56
59
|
)
|
|
57
60
|
cells += len(record)
|
|
58
61
|
if cells > max_cells:
|
|
59
|
-
raise
|
|
60
|
-
f"The file has more than {max_cells:,} cells, the limit per call. "
|
|
61
|
-
"Split the file or raise --max-cells."
|
|
62
|
-
)
|
|
62
|
+
raise _too_many_cells(max_cells)
|
|
63
63
|
for index, text in enumerate(record):
|
|
64
64
|
column = first_col + index
|
|
65
65
|
if width := _store(sheet, row, column, text, offset, rules):
|
|
@@ -79,6 +79,23 @@ def import_csv(
|
|
|
79
79
|
return WriteResult(sheet=sheet.title, range=str(written), cells_written=cells)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _too_many_cells(max_cells: int) -> LimitExceededError:
|
|
83
|
+
return LimitExceededError(
|
|
84
|
+
f"The file has more than {max_cells:,} cells, the limit per call. "
|
|
85
|
+
"Split the file or raise --max-cells."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _lines_within(stream: TextIO, separator: str, max_cells: int) -> Iterator[str]:
|
|
90
|
+
"""The lines of the file, refusing one with more delimiters than the limit has cells before
|
|
91
|
+
the reader makes a field of each: a record is held whole, so its length is not bounded by
|
|
92
|
+
the count of cells made so far."""
|
|
93
|
+
for line in stream:
|
|
94
|
+
if line.count(separator) >= max_cells:
|
|
95
|
+
raise _too_many_cells(max_cells)
|
|
96
|
+
yield line
|
|
97
|
+
|
|
98
|
+
|
|
82
99
|
def _separator(sample: str, delimiter: str) -> str:
|
|
83
100
|
"""The delimiter named, or for 'auto' the most frequent usual one in the first record."""
|
|
84
101
|
if delimiter != "auto":
|
|
@@ -8,6 +8,7 @@ from excel_mcp.operations.columns import find_column
|
|
|
8
8
|
from excel_mcp.operations.comparison import FormulaResults, as_number, cell_value
|
|
9
9
|
from excel_mcp.operations.sorting import save_cells, write_rows
|
|
10
10
|
from excel_mcp.refs import CellRange
|
|
11
|
+
from excel_mcp.text_budget import TextBudget
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def remove_duplicates(
|
|
@@ -16,6 +17,7 @@ def remove_duplicates(
|
|
|
16
17
|
columns: list[str] | None,
|
|
17
18
|
has_header: bool,
|
|
18
19
|
results: FormulaResults,
|
|
20
|
+
max_cells: int,
|
|
19
21
|
) -> int:
|
|
20
22
|
"""Move the rows to keep up, empty the rows left at the bottom, and return how many went.
|
|
21
23
|
|
|
@@ -35,12 +37,17 @@ def remove_duplicates(
|
|
|
35
37
|
if columns
|
|
36
38
|
else list(range(area.min_col, area.max_col + 1))
|
|
37
39
|
)
|
|
40
|
+
budget = TextBudget(
|
|
41
|
+
max_cells,
|
|
42
|
+
"Comparing rows would read more than {limit} characters of text. Use fewer rows, or "
|
|
43
|
+
"raise --max-cells.",
|
|
44
|
+
)
|
|
38
45
|
seen: set[tuple[object, ...]] = set()
|
|
39
46
|
kept = []
|
|
40
47
|
for cells in sheet.iter_rows(
|
|
41
48
|
min_row=first_row, max_row=area.max_row, min_col=area.min_col, max_col=area.max_col
|
|
42
49
|
):
|
|
43
|
-
key = tuple(_comparable(cells[col - area.min_col], results) for col in chosen)
|
|
50
|
+
key = tuple(_comparable(cells[col - area.min_col], results, budget) for col in chosen)
|
|
44
51
|
if key not in seen:
|
|
45
52
|
seen.add(key)
|
|
46
53
|
kept.append(save_cells(cells))
|
|
@@ -60,12 +67,15 @@ def remove_duplicates(
|
|
|
60
67
|
return removed
|
|
61
68
|
|
|
62
69
|
|
|
63
|
-
def _comparable(
|
|
70
|
+
def _comparable(
|
|
71
|
+
cell: Cell | MergedCell, results: FormulaResults, budget: TextBudget
|
|
72
|
+
) -> tuple[str, object]:
|
|
64
73
|
value = cell_value(cell, results)
|
|
65
74
|
if isinstance(value, bool):
|
|
66
75
|
return "boolean", value
|
|
67
76
|
if (number := as_number(value)) is not None:
|
|
68
77
|
return "number", number
|
|
69
78
|
if isinstance(value, str):
|
|
79
|
+
budget.spend(len(value))
|
|
70
80
|
return "text", value.casefold()
|
|
71
81
|
return "blank", None
|
|
@@ -134,9 +134,14 @@ def _values(
|
|
|
134
134
|
raise InvalidArgumentError("A date series starts from a date cell.")
|
|
135
135
|
if step != int(step):
|
|
136
136
|
raise InvalidArgumentError("A date series needs a whole-number step.")
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
try:
|
|
138
|
+
values: list[int | float | dt.date] = [
|
|
139
|
+
_add_dates(seed, unit, int(step) * index) for index in range(count)
|
|
140
|
+
]
|
|
141
|
+
except (OverflowError, ValueError):
|
|
142
|
+
raise InvalidArgumentError(
|
|
143
|
+
"The series goes past the last date a cell can hold."
|
|
144
|
+
) from None
|
|
140
145
|
numbers = [float(to_excel(value)) for value in values]
|
|
141
146
|
else:
|
|
142
147
|
if isinstance(seed, bool) or not isinstance(seed, int | float):
|
|
@@ -172,9 +177,16 @@ def _add_dates(seed: dt.date, unit: DateUnit, amount: int) -> dt.date:
|
|
|
172
177
|
|
|
173
178
|
|
|
174
179
|
def _add_weekdays(seed: dt.date, amount: int) -> dt.date:
|
|
175
|
-
|
|
180
|
+
"""``amount`` weekdays on from ``seed``: whole weeks at once, then the few days left."""
|
|
181
|
+
if amount == 0:
|
|
182
|
+
return seed
|
|
183
|
+
direction = 1 if amount > 0 else -1
|
|
176
184
|
result = seed
|
|
177
|
-
|
|
185
|
+
while result.weekday() >= 5: # a weekend seed counts from the weekday it follows
|
|
186
|
+
result -= dt.timedelta(days=direction)
|
|
187
|
+
weeks, days = divmod(abs(amount), 5)
|
|
188
|
+
result += dt.timedelta(days=7 * weeks * direction)
|
|
189
|
+
for _ in range(days):
|
|
178
190
|
result += dt.timedelta(days=direction)
|
|
179
191
|
while result.weekday() >= 5:
|
|
180
192
|
result += dt.timedelta(days=direction)
|
|
@@ -19,6 +19,7 @@ from excel_mcp.operations.sheet_view import ViewOptions, apply_view, move_sheet
|
|
|
19
19
|
from excel_mcp.operations.spans import MAX_ROW_SPAN, parse_span, to_spans
|
|
20
20
|
from excel_mcp.package.lines import Axis
|
|
21
21
|
from excel_mcp.refs import MAX_ROW, parse_cell
|
|
22
|
+
from excel_mcp.text_budget import TextBudget
|
|
22
23
|
|
|
23
24
|
MIN_AUTOFIT_WIDTH = 8
|
|
24
25
|
MAX_AUTOFIT_WIDTH = 80
|
|
@@ -78,9 +79,12 @@ def apply_layout(
|
|
|
78
79
|
if not 1 <= row <= MAX_ROW:
|
|
79
80
|
raise InvalidArgumentError(f"Row {row} is outside 1 to {MAX_ROW}.")
|
|
80
81
|
sheet.row_dimensions[row].height = height
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
if layout.autofit_columns:
|
|
83
|
+
fitted = estimate_widths(
|
|
84
|
+
sheet, [column_letter(c) for c in layout.autofit_columns], max_cells
|
|
85
|
+
)
|
|
86
|
+
for letter, width in fitted.items():
|
|
87
|
+
sheet.column_dimensions[letter].width = width
|
|
84
88
|
if layout.freeze_panes is not None:
|
|
85
89
|
parse_cell(layout.freeze_panes)
|
|
86
90
|
sheet.freeze_panes = layout.freeze_panes.upper()
|
|
@@ -187,14 +191,23 @@ def column_letter(column: str) -> str:
|
|
|
187
191
|
) from None
|
|
188
192
|
|
|
189
193
|
|
|
190
|
-
def
|
|
191
|
-
"""Approximate
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
longest =
|
|
200
|
-
|
|
194
|
+
def estimate_widths(sheet: Worksheet, letters: list[str], max_cells: int) -> dict[str, float]:
|
|
195
|
+
"""Approximate fitting widths from the longest text line in each column, in one pass over the
|
|
196
|
+
cells however many columns are asked for."""
|
|
197
|
+
budget = TextBudget(
|
|
198
|
+
max_cells,
|
|
199
|
+
"Fitting columns would read more than {limit} characters of text. Fit fewer columns, "
|
|
200
|
+
"or raise --max-cells.",
|
|
201
|
+
)
|
|
202
|
+
wanted = {column_index_from_string(letter): letter for letter in letters}
|
|
203
|
+
longest = dict.fromkeys(wanted, 0)
|
|
204
|
+
for cell in stored_cells(sheet):
|
|
205
|
+
if cell.column in wanted:
|
|
206
|
+
text = str(cell.value)
|
|
207
|
+
budget.spend(len(text))
|
|
208
|
+
lines = max(map(len, text.splitlines()), default=0)
|
|
209
|
+
longest[cell.column] = max(longest[cell.column], lines)
|
|
210
|
+
return {
|
|
211
|
+
letter: min(max(longest[index] + 2, MIN_AUTOFIT_WIDTH), MAX_AUTOFIT_WIDTH)
|
|
212
|
+
for index, letter in wanted.items()
|
|
213
|
+
}
|
|
@@ -32,7 +32,7 @@ from excel_mcp.operations.pivot_options import (
|
|
|
32
32
|
PivotValue,
|
|
33
33
|
ValuesIn,
|
|
34
34
|
)
|
|
35
|
-
from excel_mcp.operations.pivot_render import Shown, item_label, number_format, render
|
|
35
|
+
from excel_mcp.operations.pivot_render import Shown, item_label, measure, number_format, render
|
|
36
36
|
from excel_mcp.operations.pivot_showas import Figures
|
|
37
37
|
from excel_mcp.operations.pivot_source import Results, Source, read_source, serial
|
|
38
38
|
from excel_mcp.operations.pivot_specs import (
|
|
@@ -103,19 +103,22 @@ def create_pivot(
|
|
|
103
103
|
rows, columns, aggregator = _axes(
|
|
104
104
|
source, calculated, specs, on_rows, on_columns, [*pages, *hidden], request
|
|
105
105
|
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
# The table is as big as the items of its axes make it, which is not bounded by the source's
|
|
107
|
+
# cells (a few thousand records can make millions), so it is checked before it is built.
|
|
108
|
+
size = measure(rows, columns, request.layout)
|
|
109
109
|
start_row, start_col = parse_cell(target_cell)
|
|
110
110
|
filter_height = len(pages) + 1 if pages else 0
|
|
111
111
|
body = CellRange(
|
|
112
112
|
start_row + filter_height,
|
|
113
113
|
start_col,
|
|
114
|
-
start_row + filter_height +
|
|
115
|
-
start_col +
|
|
114
|
+
start_row + filter_height + size.header_rows + size.rows - 1,
|
|
115
|
+
start_col + size.label_columns + size.columns - 1,
|
|
116
116
|
)
|
|
117
117
|
area = CellRange(start_row, start_col, body.max_row, body.max_col).within(limits.max_cells)
|
|
118
118
|
_check_free(target_sheet, area, source_sheet, source.ref)
|
|
119
|
+
formats = [number_format(spec, _source_format(source, spec)) for spec in specs]
|
|
120
|
+
figures = Figures(aggregator, rows, columns, specs)
|
|
121
|
+
table = render(rows, columns, specs, figures, formats, request.layout)
|
|
119
122
|
|
|
120
123
|
pivot_name = _pivot_name(workbook, request.name, copy_of_name)
|
|
121
124
|
page_filters = [_page_filter(axis) for axis in pages]
|
|
@@ -26,7 +26,7 @@ ESCAPES = str.maketrans(
|
|
|
26
26
|
"\t": "	",
|
|
27
27
|
}
|
|
28
28
|
)
|
|
29
|
-
|
|
29
|
+
BATCH_CHARACTERS = 1 << 20
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class SharedIndex(int):
|
|
@@ -57,9 +57,16 @@ class PivotRecords(RecordList):
|
|
|
57
57
|
|
|
58
58
|
def _write_xml(stream: IO[bytes], rows: Sequence[Row]) -> None:
|
|
59
59
|
stream.write(f'<pivotCacheRecords xmlns="{SHEET_MAIN_NS}" count="{len(rows)}">'.encode())
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
batch: list[str] = []
|
|
61
|
+
size = 0
|
|
62
|
+
for row in rows:
|
|
63
|
+
record = f"<r>{''.join(_items(row))}</r>"
|
|
64
|
+
batch.append(record)
|
|
65
|
+
size += len(record)
|
|
66
|
+
if size >= BATCH_CHARACTERS:
|
|
67
|
+
stream.write("".join(batch).encode())
|
|
68
|
+
batch, size = [], 0
|
|
69
|
+
stream.write("".join(batch).encode())
|
|
63
70
|
stream.write(b"</pivotCacheRecords>")
|
|
64
71
|
|
|
65
72
|
|
|
@@ -33,6 +33,13 @@ class Table:
|
|
|
33
33
|
columns: int
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def measure(rows: Axis, columns: Axis, layout: Layout) -> Table:
|
|
37
|
+
"""The size of the table the axes make, before any of its cells are."""
|
|
38
|
+
label_columns = 1 if layout == "compact" else len(rows.fields) + rows.value_levels
|
|
39
|
+
header_rows = 1 + len(columns.fields) + columns.value_levels
|
|
40
|
+
return Table({}, header_rows, label_columns, len(rows.lines), len(columns.lines))
|
|
41
|
+
|
|
42
|
+
|
|
36
43
|
def render(
|
|
37
44
|
rows: Axis,
|
|
38
45
|
columns: Axis,
|
|
@@ -42,9 +49,8 @@ def render(
|
|
|
42
49
|
layout: Layout,
|
|
43
50
|
) -> Table:
|
|
44
51
|
"""``formats`` holds each values field's number format."""
|
|
45
|
-
|
|
46
|
-
header_rows =
|
|
47
|
-
table = Table({}, header_rows, label_columns, len(rows.lines), len(columns.lines))
|
|
52
|
+
table = measure(rows, columns, layout)
|
|
53
|
+
header_rows, label_columns = table.header_rows, table.label_columns
|
|
48
54
|
cells = table.cells
|
|
49
55
|
_headers(cells, rows, columns, specs, layout, header_rows, label_columns)
|
|
50
56
|
_column_labels(cells, columns, specs, label_columns)
|
|
@@ -17,6 +17,7 @@ from excel_mcp.lazy_workbook import LazyCells
|
|
|
17
17
|
from excel_mcp.operations.comparison import FormulaResults
|
|
18
18
|
from excel_mcp.refs import CellRange, cell_name, parse_range
|
|
19
19
|
from excel_mcp.text import quoted
|
|
20
|
+
from excel_mcp.text_budget import TextBudget
|
|
20
21
|
|
|
21
22
|
Value = str | int | float | bool | dt.datetime | ExcelError | None
|
|
22
23
|
"""A cell as a PivotTable sees it. Dates, times and durations are moments counted from `EPOCH`."""
|
|
@@ -95,9 +96,20 @@ def read_source(sheet: Worksheet, source_range: str, max_cells: int, results: Re
|
|
|
95
96
|
)
|
|
96
97
|
if area.rows < 2:
|
|
97
98
|
raise InvalidArgumentError(f"source {area} needs a header row and at least one data row.")
|
|
99
|
+
# The cache stores a text once per record unless it is an item of an axis, so the text of the
|
|
100
|
+
# source is counted, not only its cells.
|
|
101
|
+
budget = TextBudget(
|
|
102
|
+
max_cells,
|
|
103
|
+
f"Source {area} has more than {{limit}} characters of text; PivotTables can read at most "
|
|
104
|
+
"that. Use a smaller source, or raise --max-pivot-cells (environment variable "
|
|
105
|
+
"EXCEL_MCP_MAX_PIVOT_CELLS).",
|
|
106
|
+
)
|
|
98
107
|
if isinstance(sheet._cells, LazyCells) and (
|
|
99
108
|
stored := sheet._cells.stored_rows(area.min_row, area.max_row)
|
|
100
109
|
):
|
|
110
|
+
budget.spend(
|
|
111
|
+
sum(len(c[1]) for row in stored for c in row.values() if isinstance(c[1], str))
|
|
112
|
+
)
|
|
101
113
|
return _read_stored(sheet, area, stored)
|
|
102
114
|
rows = [
|
|
103
115
|
list(row)
|
|
@@ -105,6 +117,9 @@ def read_source(sheet: Worksheet, source_range: str, max_cells: int, results: Re
|
|
|
105
117
|
min_row=area.min_row, max_row=area.max_row, min_col=area.min_col, max_col=area.max_col
|
|
106
118
|
)
|
|
107
119
|
]
|
|
120
|
+
budget.spend(
|
|
121
|
+
sum(len(cell.value) for row in rows for cell in row if isinstance(cell.value, str))
|
|
122
|
+
)
|
|
108
123
|
names = [_header(cell) for cell in rows[0]]
|
|
109
124
|
folded = [name.strip().casefold() for name in names]
|
|
110
125
|
if len(set(folded)) < len(folded):
|
|
@@ -6,8 +6,12 @@ from openpyxl.worksheet.worksheet import Worksheet
|
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
8
|
from excel_mcp import lazy_find
|
|
9
|
+
from excel_mcp.calc.values import MAX_TEXT
|
|
10
|
+
from excel_mcp.config import Limits
|
|
11
|
+
from excel_mcp.errors import LimitExceededError
|
|
9
12
|
from excel_mcp.lazy_workbook import LazyCells
|
|
10
13
|
from excel_mcp.operations.cells import parsed_cells, store_typed
|
|
14
|
+
from excel_mcp.text_budget import TextBudget
|
|
11
15
|
from excel_mcp.workspace import sheet_names
|
|
12
16
|
from excel_mcp.xlfn import display_formula
|
|
13
17
|
|
|
@@ -24,6 +28,7 @@ def replace_cells(
|
|
|
24
28
|
exact: bool,
|
|
25
29
|
case_sensitive: bool,
|
|
26
30
|
in_formulas: bool,
|
|
31
|
+
limits: Limits,
|
|
27
32
|
) -> ReplaceResult:
|
|
28
33
|
"""Replace text in text, number and (optionally) formula cells; the result is retyped.
|
|
29
34
|
|
|
@@ -38,6 +43,14 @@ def replace_cells(
|
|
|
38
43
|
def matches(text: str) -> bool:
|
|
39
44
|
return found(text) is not None
|
|
40
45
|
|
|
46
|
+
grown = TextBudget(
|
|
47
|
+
limits.max_cells,
|
|
48
|
+
"Replacing would add more than {limit} characters of text. Replace in one sheet at a "
|
|
49
|
+
"time, with shorter text, or raise --max-cells.",
|
|
50
|
+
)
|
|
51
|
+
# A workbook can hold one long text and point millions of cells at it: each text is worked
|
|
52
|
+
# out once, and its result is shared by every cell that holds it.
|
|
53
|
+
results: dict[str, str | None] = {}
|
|
41
54
|
replaced: dict[str, int] = {}
|
|
42
55
|
for sheet in sheets:
|
|
43
56
|
names = sheet_names(sheet)
|
|
@@ -48,11 +61,11 @@ def replace_cells(
|
|
|
48
61
|
text = _searchable_text(cell.value, cell.data_type, in_formulas)
|
|
49
62
|
if text is None:
|
|
50
63
|
continue
|
|
51
|
-
if
|
|
52
|
-
new_text =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if new_text
|
|
64
|
+
if text not in results:
|
|
65
|
+
new_text = _replaced(pattern, text, query, replacement, exact=exact)
|
|
66
|
+
grown.spend(max(0, len(new_text) - len(text)))
|
|
67
|
+
results[text] = None if new_text == text else new_text
|
|
68
|
+
if (new_text := results[text]) is not None:
|
|
56
69
|
store_typed(cell, new_text, names)
|
|
57
70
|
changed += 1
|
|
58
71
|
if changed:
|
|
@@ -60,6 +73,29 @@ def replace_cells(
|
|
|
60
73
|
return ReplaceResult(replaced=replaced)
|
|
61
74
|
|
|
62
75
|
|
|
76
|
+
def _replaced(
|
|
77
|
+
pattern: re.Pattern[str], text: str, query: str, replacement: str, *, exact: bool
|
|
78
|
+
) -> str:
|
|
79
|
+
"""``text`` with the matches replaced; refused, before it is built, if too long for a cell."""
|
|
80
|
+
if exact:
|
|
81
|
+
if pattern.fullmatch(text) is None:
|
|
82
|
+
return text
|
|
83
|
+
_fit(len(replacement))
|
|
84
|
+
return replacement
|
|
85
|
+
if len(replacement) > len(query):
|
|
86
|
+
found = list(pattern.finditer(text))
|
|
87
|
+
_fit(len(text) + sum(len(replacement) - f.end() + f.start() for f in found))
|
|
88
|
+
return pattern.sub(lambda _: replacement, text)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fit(size: int) -> None:
|
|
92
|
+
if size > MAX_TEXT:
|
|
93
|
+
raise LimitExceededError(
|
|
94
|
+
f"Replacing would make a cell longer than {MAX_TEXT:,} characters, the limit of "
|
|
95
|
+
"an Excel cell."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
63
99
|
def _searchable_text(value: object, data_type: str, in_formulas: bool) -> str | None:
|
|
64
100
|
if data_type == "f":
|
|
65
101
|
return display_formula(value) if in_formulas and isinstance(value, str) else None
|
|
@@ -80,7 +80,7 @@ def add_slicer(
|
|
|
80
80
|
raise InvalidArgumentError(
|
|
81
81
|
"A table slicer connects to its table only, and tables have no timelines."
|
|
82
82
|
)
|
|
83
|
-
return _table_slicer(workbook, sheet, owner, target, request, results), False
|
|
83
|
+
return _table_slicer(workbook, sheet, owner, target, request, limits, results), False
|
|
84
84
|
pivots.share_caches(workbook)
|
|
85
85
|
group = _connected(workbook, owner, target, request)
|
|
86
86
|
if request.timeline:
|
|
@@ -94,6 +94,7 @@ def _table_slicer(
|
|
|
94
94
|
owner: Worksheet,
|
|
95
95
|
table: Table,
|
|
96
96
|
request: SlicerRequest,
|
|
97
|
+
limits: Limits,
|
|
97
98
|
results: Results,
|
|
98
99
|
) -> str:
|
|
99
100
|
_check_style(request.style, "slicer")
|
|
@@ -101,6 +102,8 @@ def _table_slicer(
|
|
|
101
102
|
_check_free_column(workbook, table, column.name)
|
|
102
103
|
name, cache = _names(workbook, request, "Slicer_")
|
|
103
104
|
if request.selected:
|
|
105
|
+
# The size of a table is whatever its file says, so it is checked like any range.
|
|
106
|
+
table_area(table).within(limits.max_cells)
|
|
104
107
|
slicer_table.show_only(
|
|
105
108
|
owner, table, column.name, request.selected, results(owner, table_area(table))
|
|
106
109
|
)
|