excel-mcp-server 2.2.3__tar.gz → 2.2.5__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.3 → excel_mcp_server-2.2.5}/PKG-INFO +6 -4
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/README.md +5 -3
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/pyproject.toml +1 -1
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/pyproject.toml.orig +1 -1
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts.py +15 -2
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_check.py +1 -1
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_data.py +30 -11
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_modern.py +19 -11
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_options.py +1 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/layout.py +12 -1
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sheet_shapes.py +2 -1
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/slicer_manage.py +18 -10
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/slicer_place.py +8 -4
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/slicer_xml.py +2 -2
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/slicers.py +12 -1
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sparklines.py +2 -4
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/vba_write.py +0 -2
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/capture_sheet.py +1 -1
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/extensions.py +16 -5
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/patch.py +36 -9
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/references.py +6 -3
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/scan.py +1 -10
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/object_tools.py +1 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/registry.py +25 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/values.py +1 -3
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/workspace.py +15 -7
- excel_mcp_server-2.2.5/src/excel_mcp/xml_text.py +18 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/LICENSE +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/__init__.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/__main__.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/cells.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/criteria.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/dependencies.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/dynamic.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/engine.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/__init__.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/arrays.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/conditional.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/datetime.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/depreciation.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/distributions.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/financial.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/helpers.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/indirect.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/information.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/logical.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/lookup.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/math.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/regression.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/securities.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/statistical.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/subtotals.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/functions/text.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/operators.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/parser.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/r1c1.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/registry.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/scheduler.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/solver.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/steps.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/tablerefs.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/textformat.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/values.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/calc/wildcard.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/cell_budget.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/cfb.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/chart_roundtrip.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/cli.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/config.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/errors.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/formula_syntax.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/formulas.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/function_arguments.json +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/inputs.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/lazy_edit.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/lazy_find.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/lazy_lines.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/lazy_rows.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/lazy_shared.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/lazy_source.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/lazy_workbook.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/links.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/macros.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/__init__.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/calculated.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/cells.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chart_index.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chart_info.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/chartex_xml.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_axes.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_look.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_series.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_style.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/columns.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/comparison.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/conditional.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/conditional_kinds.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/conditional_rule.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/conditional_x14.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/csv_export.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/csv_format.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/csv_import.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/csv_values.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/date_width.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/drawings.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/duplicates.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/files.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/fill.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/filter_criteria.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/filter_stored.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/filters.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/formatting.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/goal_seek.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/hyperlinks.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/images.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/inspect.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/line_edit_objects.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/line_edit_rules.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/line_edit_styles.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/line_edits.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/names.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/notes.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/paste.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_axis.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_cache.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_calc.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_definition.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_fields.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_groups.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_index.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_options.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_records.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_render.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_showas.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_source.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_specs.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/pivot_values.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/print_setup.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/protection.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/replace.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/rule_clear.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/rules.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sheet_copy.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sheet_counts.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sheet_refs.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sheet_view.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sheets.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/shifting.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/slicer_index.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/slicer_pivot.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/slicer_table.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sorting.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/spans.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sparkline_style.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/sparkline_xml.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/spill.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/table_options.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/tables.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/text_split.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/trace.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/transform.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/used_range.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/vba.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/workbook_rewrite.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/workbook_settings.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/x14_xml.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/ovba.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/ovba_compress.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/ovba_write.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/__init__.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/anchors.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/arrays.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/capture.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/consistency.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/guards.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/lazy_records.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/lines.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/metadata.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/model.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/opc.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/pivot_records.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/pivots.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/properties.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/reader.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/restore.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/restore_sheet.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/shape_names.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/package/vml.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/paths.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/plain_refs.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/py.typed +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/refs.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/rewrite.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/__init__.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/annotation_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/areas.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/csv_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/data_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/format_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/formula_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/http.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/image_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/params.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/pivot_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/results.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/sheet_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/slicer_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/validation.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/vba_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/vba_write_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/server/workbook_tools.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/spill.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/structured.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/text.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/upload_links.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/upload_scan.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/xlfn.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/xlm.py +0 -0
- {excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/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.5
|
|
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
|
|
@@ -104,7 +104,8 @@ Excel**, and runs on Windows, macOS and Linux.
|
|
|
104
104
|
|
|
105
105
|
**Macros (opt-in)**
|
|
106
106
|
- Read the VBA code in `.xlsm` files. Writing VBA is off unless you start the server with
|
|
107
|
-
`--allow-vba-write
|
|
107
|
+
`--allow-vba-write`, which also decides whether `create_workbook` and `import_workbook` may
|
|
108
|
+
make `.xlsm`/`.xltm` files; the server never runs macros
|
|
108
109
|
|
|
109
110
|
**Safe by design**
|
|
110
111
|
- Optional folder confinement, a formula safety check, read-only mode, localhost-only HTTP by
|
|
@@ -323,8 +324,9 @@ next call can refer to it. Charts and images are selected by name, as listed by
|
|
|
323
324
|
|
|
324
325
|
With `--allow-vba-write` (or `EXCEL_MCP_ALLOW_VBA_WRITE=1`) the server can set the code of
|
|
325
326
|
standard, class, workbook and sheet modules in `.xlsm` and `.xltm` files, delete standard and
|
|
326
|
-
class modules, and create
|
|
327
|
-
|
|
327
|
+
class modules, and create or import `.xlsm`/`.xltm` workbooks (without the option both are
|
|
328
|
+
refused, and `import_workbook` removes macros from uploads to `.xlsx`/`.xltx`). The server only
|
|
329
|
+
stores the code; it never runs it, and Excel asks before enabling macros. Digitally signed VBA projects are
|
|
328
330
|
refused, since any change would invalidate the signature.
|
|
329
331
|
|
|
330
332
|
> **Warning:** macro code runs with your user's rights once you enable macros in Excel. A
|
|
@@ -68,7 +68,8 @@ Excel**, and runs on Windows, macOS and Linux.
|
|
|
68
68
|
|
|
69
69
|
**Macros (opt-in)**
|
|
70
70
|
- Read the VBA code in `.xlsm` files. Writing VBA is off unless you start the server with
|
|
71
|
-
`--allow-vba-write
|
|
71
|
+
`--allow-vba-write`, which also decides whether `create_workbook` and `import_workbook` may
|
|
72
|
+
make `.xlsm`/`.xltm` files; the server never runs macros
|
|
72
73
|
|
|
73
74
|
**Safe by design**
|
|
74
75
|
- Optional folder confinement, a formula safety check, read-only mode, localhost-only HTTP by
|
|
@@ -287,8 +288,9 @@ next call can refer to it. Charts and images are selected by name, as listed by
|
|
|
287
288
|
|
|
288
289
|
With `--allow-vba-write` (or `EXCEL_MCP_ALLOW_VBA_WRITE=1`) the server can set the code of
|
|
289
290
|
standard, class, workbook and sheet modules in `.xlsm` and `.xltm` files, delete standard and
|
|
290
|
-
class modules, and create
|
|
291
|
-
|
|
291
|
+
class modules, and create or import `.xlsm`/`.xltm` workbooks (without the option both are
|
|
292
|
+
refused, and `import_workbook` removes macros from uploads to `.xlsx`/`.xltx`). The server only
|
|
293
|
+
stores the code; it never runs it, and Excel asks before enabling macros. Digitally signed VBA projects are
|
|
292
294
|
refused, since any change would invalidate the signature.
|
|
293
295
|
|
|
294
296
|
> **Warning:** macro code runs with your user's rights once you enable macros in Excel. A
|
|
@@ -78,6 +78,7 @@ def create_chart(
|
|
|
78
78
|
options: ChartOptions,
|
|
79
79
|
replace: str | None,
|
|
80
80
|
name: str | None,
|
|
81
|
+
max_cells: int,
|
|
81
82
|
) -> tuple[str | None, str | None]:
|
|
82
83
|
"""Add a chart (or replace the chart called `replace`); return its name and cells."""
|
|
83
84
|
host = None if at is None else get_sheet(workbook, sheet)
|
|
@@ -96,7 +97,17 @@ def create_chart(
|
|
|
96
97
|
f"{chart_type} charts read their data in columns; list rows with `series`."
|
|
97
98
|
)
|
|
98
99
|
return _create_modern(
|
|
99
|
-
workbook,
|
|
100
|
+
workbook,
|
|
101
|
+
host,
|
|
102
|
+
anchor,
|
|
103
|
+
chart_type,
|
|
104
|
+
source,
|
|
105
|
+
series,
|
|
106
|
+
categories,
|
|
107
|
+
options,
|
|
108
|
+
replace,
|
|
109
|
+
name,
|
|
110
|
+
max_cells,
|
|
100
111
|
)
|
|
101
112
|
plots = resolve_series(
|
|
102
113
|
workbook,
|
|
@@ -106,6 +117,7 @@ def create_chart(
|
|
|
106
117
|
series_in,
|
|
107
118
|
series,
|
|
108
119
|
categories,
|
|
120
|
+
max_cells,
|
|
109
121
|
)
|
|
110
122
|
check_chart(plots, chart_type, options)
|
|
111
123
|
chart = build_chart(plots, chart_type, options)
|
|
@@ -146,10 +158,11 @@ def _create_modern(
|
|
|
146
158
|
options: ChartOptions,
|
|
147
159
|
replace: str | None,
|
|
148
160
|
name: str | None,
|
|
161
|
+
max_cells: int,
|
|
149
162
|
) -> tuple[str, str]:
|
|
150
163
|
if host is None or anchor is None:
|
|
151
164
|
raise InvalidArgumentError(f"A {chart_type} chart sits on the sheet: give at, e.g. 'E2'.")
|
|
152
|
-
plots = resolve_modern(workbook, host.title, chart_type, source, series, categories)
|
|
165
|
+
plots = resolve_modern(workbook, host.title, chart_type, source, series, categories, max_cells)
|
|
153
166
|
check_modern(plots, chart_type, options)
|
|
154
167
|
index, name = _place(host, replace, name)
|
|
155
168
|
if index is None:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from excel_mcp.errors import InvalidArgumentError
|
|
4
4
|
from excel_mcp.operations.charts_data import Plot
|
|
5
5
|
from excel_mcp.operations.charts_options import (
|
|
6
|
+
MAX_SERIES,
|
|
6
7
|
ROUND_TYPES,
|
|
7
8
|
Axis,
|
|
8
9
|
ChartOptions,
|
|
@@ -12,7 +13,6 @@ from excel_mcp.operations.charts_options import (
|
|
|
12
13
|
)
|
|
13
14
|
from excel_mcp.operations.formatting import parse_color
|
|
14
15
|
|
|
15
|
-
MAX_SERIES = 255
|
|
16
16
|
_COMBINABLE = ("column", "line", "area")
|
|
17
17
|
_SECONDARY = ("column", "line", "area", "scatter")
|
|
18
18
|
_GROUPED = ("column", "bar", "line", "area")
|
|
@@ -9,7 +9,7 @@ from openpyxl.chart.series import SeriesLabel
|
|
|
9
9
|
from openpyxl.utils import absolute_coordinate, quote_sheetname
|
|
10
10
|
|
|
11
11
|
from excel_mcp.errors import InvalidArgumentError
|
|
12
|
-
from excel_mcp.operations.charts_options import ChartType, SeriesSpec
|
|
12
|
+
from excel_mcp.operations.charts_options import MAX_SERIES, ChartType, SeriesSpec
|
|
13
13
|
from excel_mcp.refs import CellRange, parse_cell, parse_range
|
|
14
14
|
from excel_mcp.workspace import get_sheet
|
|
15
15
|
|
|
@@ -36,6 +36,7 @@ def resolve_series(
|
|
|
36
36
|
series_in: SeriesIn,
|
|
37
37
|
series: list[SeriesSpec],
|
|
38
38
|
categories: str | None,
|
|
39
|
+
max_cells: int,
|
|
39
40
|
) -> list[Plot]:
|
|
40
41
|
if (source is None) == (not series):
|
|
41
42
|
raise InvalidArgumentError(
|
|
@@ -43,24 +44,34 @@ def resolve_series(
|
|
|
43
44
|
"and not neither."
|
|
44
45
|
)
|
|
45
46
|
if source is not None:
|
|
46
|
-
series = _block_series(workbook, sheet, source, chart_type, series_in)
|
|
47
|
+
series = _block_series(workbook, sheet, source, chart_type, series_in, max_cells)
|
|
47
48
|
categories = None
|
|
48
|
-
|
|
49
|
+
check_series_count(len(series))
|
|
50
|
+
return [_resolve(workbook, sheet, item, categories, max_cells) for item in series]
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
def _resolve(
|
|
52
|
-
|
|
53
|
+
def _resolve(
|
|
54
|
+
workbook: Workbook, sheet: str, spec: SeriesSpec, categories: str | None, max_cells: int
|
|
55
|
+
) -> Plot:
|
|
56
|
+
values, points = one_line(workbook, sheet, spec.values, "values", max_cells)
|
|
53
57
|
labels = spec.categories or categories
|
|
54
58
|
return Plot(
|
|
55
59
|
spec=spec,
|
|
56
60
|
values=values,
|
|
57
|
-
categories=one_line(workbook, sheet, labels, "categories")[0]
|
|
58
|
-
|
|
61
|
+
categories=one_line(workbook, sheet, labels, "categories", max_cells)[0]
|
|
62
|
+
if labels
|
|
63
|
+
else None,
|
|
64
|
+
sizes=one_line(workbook, sheet, spec.sizes, "sizes", max_cells)[0] if spec.sizes else None,
|
|
59
65
|
name=series_name(workbook, spec.name),
|
|
60
66
|
points=points,
|
|
61
67
|
)
|
|
62
68
|
|
|
63
69
|
|
|
70
|
+
def check_series_count(count: int) -> None:
|
|
71
|
+
if count > MAX_SERIES:
|
|
72
|
+
raise InvalidArgumentError(f"A chart has at most {MAX_SERIES} series, got {count}.")
|
|
73
|
+
|
|
74
|
+
|
|
64
75
|
def split_sheet(workbook: Workbook, default: str, text: str) -> tuple[str, str]:
|
|
65
76
|
"""Split 'Data!B2:B9' (or 'My Sheet'!B2:B9) into the worksheet's title and the cells."""
|
|
66
77
|
qualifier, mark, cells = text.rpartition("!")
|
|
@@ -74,10 +85,12 @@ def qualified(sheet: str, area: CellRange) -> str:
|
|
|
74
85
|
return f"{quote_sheetname(sheet)}!{absolute_coordinate(str(area))}"
|
|
75
86
|
|
|
76
87
|
|
|
77
|
-
def one_line(
|
|
88
|
+
def one_line(
|
|
89
|
+
workbook: Workbook, default: str, text: str, what: str, max_cells: int
|
|
90
|
+
) -> tuple[str, int]:
|
|
78
91
|
"""A one-row or one-column range as an absolute formula, and its number of cells."""
|
|
79
92
|
sheet, cells = split_sheet(workbook, default, text)
|
|
80
|
-
area = parse_range(cells)
|
|
93
|
+
area = parse_range(cells).within(max_cells)
|
|
81
94
|
if area.rows > 1 and area.cols > 1:
|
|
82
95
|
raise InvalidArgumentError(
|
|
83
96
|
f"Series {what} must be one row or one column, got {text!r}. List one series per "
|
|
@@ -99,11 +112,16 @@ def series_name(workbook: Workbook, text: str | None) -> SeriesLabel | None:
|
|
|
99
112
|
|
|
100
113
|
|
|
101
114
|
def _block_series(
|
|
102
|
-
workbook: Workbook,
|
|
115
|
+
workbook: Workbook,
|
|
116
|
+
default: str,
|
|
117
|
+
source: str,
|
|
118
|
+
chart_type: ChartType,
|
|
119
|
+
series_in: SeriesIn,
|
|
120
|
+
max_cells: int,
|
|
103
121
|
) -> list[SeriesSpec]:
|
|
104
122
|
"""Series for a block with a header line and a label line, by columns or by rows."""
|
|
105
123
|
sheet, cells = split_sheet(workbook, default, source)
|
|
106
|
-
area = parse_range(cells)
|
|
124
|
+
area = parse_range(cells).within(max_cells)
|
|
107
125
|
if area.rows < 2 or area.cols < 2:
|
|
108
126
|
raise InvalidArgumentError(
|
|
109
127
|
"source needs a header row and a label column plus at least one series, "
|
|
@@ -112,6 +130,7 @@ def _block_series(
|
|
|
112
130
|
by_columns = series_in == "columns"
|
|
113
131
|
lines = area.cols if by_columns else area.rows
|
|
114
132
|
points = (area.rows if by_columns else area.cols) - 1
|
|
133
|
+
check_series_count(lines - 1)
|
|
115
134
|
|
|
116
135
|
def segment(line: int, start: int, stop: int) -> str:
|
|
117
136
|
"""Cells `start` to `stop` along line number `line`, the label line being 0."""
|
|
@@ -5,9 +5,10 @@ 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.operations.charts_check import
|
|
8
|
+
from excel_mcp.operations.charts_check import check_labels, require
|
|
9
9
|
from excel_mcp.operations.charts_data import (
|
|
10
10
|
Plot,
|
|
11
|
+
check_series_count,
|
|
11
12
|
one_line,
|
|
12
13
|
qualified,
|
|
13
14
|
series_name,
|
|
@@ -51,6 +52,7 @@ def resolve_modern(
|
|
|
51
52
|
source: str | None,
|
|
52
53
|
series: list[SeriesSpec],
|
|
53
54
|
categories: str | None,
|
|
55
|
+
max_cells: int,
|
|
54
56
|
) -> list[Plot]:
|
|
55
57
|
if (source is None) == (not series):
|
|
56
58
|
raise InvalidArgumentError(
|
|
@@ -58,33 +60,41 @@ def resolve_modern(
|
|
|
58
60
|
"and not neither."
|
|
59
61
|
)
|
|
60
62
|
if source is not None:
|
|
61
|
-
series, categories = _block(workbook, sheet, chart_type, source), None
|
|
62
|
-
|
|
63
|
+
series, categories = _block(workbook, sheet, chart_type, source, max_cells), None
|
|
64
|
+
check_series_count(len(series))
|
|
65
|
+
plots = [_plot(workbook, sheet, chart_type, spec, categories, max_cells) for spec in series]
|
|
63
66
|
if chart_type not in _MANY and len(plots) != 1:
|
|
64
67
|
raise InvalidArgumentError(f"A {chart_type} chart plots one series, got {len(plots)}.")
|
|
65
68
|
return plots
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
def _plot(
|
|
69
|
-
workbook: Workbook,
|
|
72
|
+
workbook: Workbook,
|
|
73
|
+
sheet: str,
|
|
74
|
+
chart_type: str,
|
|
75
|
+
spec: SeriesSpec,
|
|
76
|
+
categories: str | None,
|
|
77
|
+
max_cells: int,
|
|
70
78
|
) -> Plot:
|
|
71
|
-
values, points = one_line(workbook, sheet, spec.values, "values")
|
|
79
|
+
values, points = one_line(workbook, sheet, spec.values, "values", max_cells)
|
|
72
80
|
labels = spec.categories or categories
|
|
73
81
|
if labels is None:
|
|
74
82
|
found = None
|
|
75
83
|
elif chart_type in _HIERARCHIES:
|
|
76
84
|
title, cells = split_sheet(workbook, sheet, labels)
|
|
77
|
-
found = qualified(title, parse_range(cells))
|
|
85
|
+
found = qualified(title, parse_range(cells).within(max_cells))
|
|
78
86
|
else:
|
|
79
|
-
found = one_line(workbook, sheet, labels, "categories")[0]
|
|
87
|
+
found = one_line(workbook, sheet, labels, "categories", max_cells)[0]
|
|
80
88
|
return Plot(spec, values, found, None, series_name(workbook, spec.name), points)
|
|
81
89
|
|
|
82
90
|
|
|
83
|
-
def _block(
|
|
91
|
+
def _block(
|
|
92
|
+
workbook: Workbook, default: str, chart_type: str, source: str, max_cells: int
|
|
93
|
+
) -> list[SeriesSpec]:
|
|
84
94
|
"""Series for a block with a header row, read as Excel reads it: leading text columns are
|
|
85
95
|
labels (one, or the levels of a treemap or sunburst), the number columns are the series."""
|
|
86
96
|
sheet, cells = split_sheet(workbook, default, source)
|
|
87
|
-
area = parse_range(cells)
|
|
97
|
+
area = parse_range(cells).within(max_cells)
|
|
88
98
|
if area.rows < 2:
|
|
89
99
|
raise InvalidArgumentError("source needs a header row and at least one row of data.")
|
|
90
100
|
labels = _text_columns(workbook[sheet], area)
|
|
@@ -120,8 +130,6 @@ def _text_columns(sheet: Worksheet, area: CellRange) -> int:
|
|
|
120
130
|
|
|
121
131
|
|
|
122
132
|
def check_modern(plots: list[Plot], chart_type: ChartType, options: ChartOptions) -> None:
|
|
123
|
-
if len(plots) > MAX_SERIES:
|
|
124
|
-
raise InvalidArgumentError(f"A chart has at most {MAX_SERIES} series, got {len(plots)}.")
|
|
125
133
|
_check_options(options, chart_type)
|
|
126
134
|
for number, plot in enumerate(plots, start=1):
|
|
127
135
|
try:
|
{excel_mcp_server-2.2.3 → excel_mcp_server-2.2.5}/src/excel_mcp/operations/charts_options.py
RENAMED
|
@@ -10,6 +10,7 @@ ChartType = Literal[
|
|
|
10
10
|
"column", "bar", "line", "area", "pie", "doughnut", "radar", "scatter", "bubble",
|
|
11
11
|
"waterfall", "histogram", "pareto", "box_whisker", "treemap", "sunburst", "funnel",
|
|
12
12
|
] # fmt: skip
|
|
13
|
+
MAX_SERIES = 255
|
|
13
14
|
SeriesType = Literal["column", "line", "area"]
|
|
14
15
|
Grouping = Literal["standard", "stacked", "percent_stacked"]
|
|
15
16
|
LegendPosition = Literal["right", "left", "top", "bottom", "none"]
|
|
@@ -16,7 +16,7 @@ from excel_mcp.operations.formatting import parse_color
|
|
|
16
16
|
from excel_mcp.operations.print_setup import PrintSetup, apply_print_setup
|
|
17
17
|
from excel_mcp.operations.protection import Protection, apply_protection
|
|
18
18
|
from excel_mcp.operations.sheet_view import ViewOptions, apply_view, move_sheet
|
|
19
|
-
from excel_mcp.operations.spans import parse_span, to_spans
|
|
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
22
|
|
|
@@ -88,6 +88,7 @@ def apply_layout(
|
|
|
88
88
|
apply_auto_filter(sheet, layout.auto_filter, formula_values, max_cells)
|
|
89
89
|
if layout.tab_color is not None:
|
|
90
90
|
sheet.sheet_properties.tabColor = parse_color(layout.tab_color)
|
|
91
|
+
check_row_spans(layout.rows or [])
|
|
91
92
|
for action in layout.rows or []:
|
|
92
93
|
change_lines(sheet, "rows", action)
|
|
93
94
|
for action in layout.columns or []:
|
|
@@ -102,6 +103,16 @@ def apply_layout(
|
|
|
102
103
|
apply_view(sheet, layout.view)
|
|
103
104
|
|
|
104
105
|
|
|
106
|
+
def check_row_spans(actions: list[LineAction]) -> None:
|
|
107
|
+
"""Every row of a span gets an object, so the spans of one call share the span limit."""
|
|
108
|
+
total = 0
|
|
109
|
+
for action in actions:
|
|
110
|
+
first, last = parse_span(action.span, "rows")
|
|
111
|
+
total += last - first + 1
|
|
112
|
+
if total > MAX_ROW_SPAN:
|
|
113
|
+
raise InvalidArgumentError(f"One call can change at most {MAX_ROW_SPAN:,} rows.")
|
|
114
|
+
|
|
115
|
+
|
|
105
116
|
def split_column_dimensions(sheet: Worksheet) -> None:
|
|
106
117
|
"""Give every column of a multi-column definition its own, as openpyxl expects.
|
|
107
118
|
|
|
@@ -4,6 +4,7 @@ connectors, groups and form controls."""
|
|
|
4
4
|
import re
|
|
5
5
|
import uuid
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
+
from html import unescape
|
|
7
8
|
from xml.sax.saxutils import escape, quoteattr
|
|
8
9
|
|
|
9
10
|
from openpyxl.workbook import Workbook
|
|
@@ -15,7 +16,7 @@ from excel_mcp.package import PackageState, Part, SheetPackage, state_of
|
|
|
15
16
|
from excel_mcp.package.consistency import THREADED_COMMENTS
|
|
16
17
|
from excel_mcp.package.extensions import CONDITIONAL_FORMATS, SPARKLINES
|
|
17
18
|
from excel_mcp.package.model import cloned
|
|
18
|
-
from excel_mcp.package.scan import relationship_ids
|
|
19
|
+
from excel_mcp.package.scan import relationship_ids
|
|
19
20
|
|
|
20
21
|
# Charts of Excel 2016 and slicers are copied with the parts they need, by their own code.
|
|
21
22
|
_COPIED_ELSEWHERE = (
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import uuid
|
|
5
|
-
from xml.sax.saxutils import quoteattr
|
|
6
5
|
|
|
7
6
|
from openpyxl.pivot.table import TableDefinition
|
|
8
7
|
from openpyxl.workbook import Workbook
|
|
@@ -29,7 +28,9 @@ from excel_mcp.operations.slicer_index import (
|
|
|
29
28
|
from excel_mcp.operations.slicer_place import add_cache, add_entry, next_shape_id
|
|
30
29
|
from excel_mcp.package import state_of
|
|
31
30
|
from excel_mcp.package.anchors import Corner
|
|
31
|
+
from excel_mcp.package.scan import attributes
|
|
32
32
|
from excel_mcp.text import quoted
|
|
33
|
+
from excel_mcp.xml_text import attribute
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class SlicerInfo(BaseModel):
|
|
@@ -112,10 +113,12 @@ def placed_range(workbook: Workbook, sheet: Worksheet, name: str) -> str | None:
|
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
def _anchor_of(anchors: list[str], name: str) -> str | None:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
return next((a for a in anchors if _shape_name(a) == name), None)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _shape_name(anchor: str) -> str | None:
|
|
120
|
+
tag = re.search(r"<\w+:(?:slicer|timeslicer)\b[^>]*>", anchor)
|
|
121
|
+
return attributes(tag[0]).get("name") if tag else None
|
|
119
122
|
|
|
120
123
|
|
|
121
124
|
def delete_slicer(
|
|
@@ -141,7 +144,11 @@ def remove_entry(workbook: Workbook, entry: Entry) -> None:
|
|
|
141
144
|
"""Take a slicer or timeline out of its sheet; its cache goes when nothing uses it."""
|
|
142
145
|
package = state_of(workbook).sheet(entry.sheet)
|
|
143
146
|
text = entry.part.data.decode("utf-8")
|
|
144
|
-
text = re.sub(
|
|
147
|
+
text = re.sub(
|
|
148
|
+
r"<(?:slicer|timeline)\b[^>]*/>",
|
|
149
|
+
lambda tag: "" if attributes(tag[0]).get("name") == entry.name else tag[0],
|
|
150
|
+
text,
|
|
151
|
+
)
|
|
145
152
|
entry.part.data = text.encode("utf-8")
|
|
146
153
|
if not xml.read_entries(text):
|
|
147
154
|
package.links.remove(entry.link)
|
|
@@ -200,6 +207,7 @@ __all__ = ["Corner", "SlicerInfo", "delete_slicer", "list_slicers", "sheet_of"]
|
|
|
200
207
|
# -- copying a sheet ---------------------------------------------------------------------------
|
|
201
208
|
|
|
202
209
|
_SHAPE_ID = re.compile(r'(<xdr:cNvPr\b[^>]*?\bid=")\d+(")')
|
|
210
|
+
_SHAPE_NAME = re.compile(r'(<xdr:cNvPr\b[^>]*?\bname=)"[^"]*"')
|
|
203
211
|
_GUID = re.compile(r'(<a16:creationId\b[^>]*?\bid=")[^"]*(")')
|
|
204
212
|
_UID = re.compile(r'(\b\w+:uid=")[^"]*(")')
|
|
205
213
|
|
|
@@ -236,7 +244,7 @@ def copy_slicers(
|
|
|
236
244
|
def _join_cache(workbook: Workbook, name: str, pivots: list[tuple[int, str]], tab: int) -> None:
|
|
237
245
|
link = caches(workbook)[name][0]
|
|
238
246
|
text = link.target.data.decode("utf-8") # pyright: ignore[reportAttributeAccessIssue]
|
|
239
|
-
added = "".join(f'<pivotTable tabId="{tab}" name=
|
|
247
|
+
added = "".join(f'<pivotTable tabId="{tab}" name={attribute(p)}/>' for _, p in pivots)
|
|
240
248
|
link.target.data = text.replace("</pivotTables>", added + "</pivotTables>").encode("utf-8") # pyright: ignore[reportAttributeAccessIssue]
|
|
241
249
|
|
|
242
250
|
|
|
@@ -259,7 +267,7 @@ def _copy_cache(
|
|
|
259
267
|
number += 1
|
|
260
268
|
new = f"{stem}{number}"
|
|
261
269
|
text = link.target.data.decode("utf-8") # pyright: ignore[reportAttributeAccessIssue]
|
|
262
|
-
text =
|
|
270
|
+
text = re.sub(r'\bname="[^"]*"', lambda _: f"name={attribute(new)}", text, count=1)
|
|
263
271
|
text = _UID.sub(lambda m: m[1] + _new_guid() + m[2], text, count=1)
|
|
264
272
|
if tab_id is not None:
|
|
265
273
|
text = re.sub(r'(<pivotTable\b[^>]*?\btabId=")\d+', rf"\g<1>{tab_id}", text)
|
|
@@ -281,11 +289,11 @@ def _copy_entry(workbook: Workbook, entry: Entry, target: Worksheet, cache: str,
|
|
|
281
289
|
"name": name,
|
|
282
290
|
"cache": cache,
|
|
283
291
|
}
|
|
284
|
-
tag = entry.kind + "".join(f" {k}={
|
|
292
|
+
tag = entry.kind + "".join(f" {k}={attribute(v)}" for k, v in values.items()) + "/>"
|
|
285
293
|
package = state_of(workbook).sheet(entry.sheet)
|
|
286
294
|
shape = _anchor_of(package.anchors, entry.name)
|
|
287
295
|
assert shape is not None
|
|
288
|
-
shape =
|
|
296
|
+
shape = _SHAPE_NAME.sub(lambda m: f"{m[1]}{attribute(name)}", shape, count=1)
|
|
289
297
|
shape = _SHAPE_ID.sub(
|
|
290
298
|
rf"\g<1>{next_shape_id(state_of(workbook).sheet(target))}\g<2>", shape, count=1
|
|
291
299
|
)
|
|
@@ -7,6 +7,7 @@ from openpyxl.workbook import Workbook
|
|
|
7
7
|
from openpyxl.workbook.defined_name import DefinedName
|
|
8
8
|
from openpyxl.worksheet.worksheet import Worksheet
|
|
9
9
|
|
|
10
|
+
from excel_mcp.errors import WorkbookError
|
|
10
11
|
from excel_mcp.operations import slicer_xml as xml
|
|
11
12
|
from excel_mcp.package import Link, Part, SheetPackage, state_of
|
|
12
13
|
from excel_mcp.package.anchors import Geometry
|
|
@@ -17,6 +18,7 @@ from excel_mcp.package.scan import (
|
|
|
17
18
|
root_scope,
|
|
18
19
|
used_prefixes,
|
|
19
20
|
)
|
|
21
|
+
from excel_mcp.xml_text import attribute
|
|
20
22
|
|
|
21
23
|
_XDR = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
|
|
22
24
|
_A = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
@@ -119,7 +121,8 @@ def _list_in(extensions: dict[str, str], uri: str, element: str) -> None:
|
|
|
119
121
|
assert reference is not None
|
|
120
122
|
existing = extensions[uri]
|
|
121
123
|
closing = re.search(r"</(?:\w+:)(?:slicerCaches|timelineCacheRefs)>", existing)
|
|
122
|
-
|
|
124
|
+
if closing is None:
|
|
125
|
+
raise WorkbookError("The workbook's list of slicer or timeline caches is malformed.")
|
|
123
126
|
extensions[uri] = existing[: closing.start()] + reference[0] + existing[closing.start() :]
|
|
124
127
|
|
|
125
128
|
|
|
@@ -152,12 +155,13 @@ def anchor(
|
|
|
152
155
|
end_col, col_off = _end(geometry, False, column, left + size[0], left)
|
|
153
156
|
end_row, row_off = _end(geometry, True, row, top + size[1], top)
|
|
154
157
|
timeline = kind == "timeline"
|
|
158
|
+
shape_name = attribute(name)
|
|
155
159
|
requires = "tsle" if timeline else "sle15" if kind == "table" else "a14"
|
|
156
160
|
declared = {"tsle": _TSLE, "sle15": _SLE15, "a14": _A14}[requires]
|
|
157
161
|
graphic = (
|
|
158
|
-
f'<tsle:timeslicer xmlns:tsle="{_TSLE}" name=
|
|
162
|
+
f'<tsle:timeslicer xmlns:tsle="{_TSLE}" name={shape_name}/>'
|
|
159
163
|
if timeline
|
|
160
|
-
else f'<sle:slicer xmlns:sle="{_SLE}" name=
|
|
164
|
+
else f'<sle:slicer xmlns:sle="{_SLE}" name={shape_name}/>'
|
|
161
165
|
)
|
|
162
166
|
uri = _TSLE if timeline else _SLE
|
|
163
167
|
guid = "{" + str(uuid.uuid4()).upper() + "}"
|
|
@@ -171,7 +175,7 @@ def anchor(
|
|
|
171
175
|
f'<mc:AlternateContent xmlns:mc="{xml.MC}">'
|
|
172
176
|
f'<mc:Choice xmlns:{requires}="{declared}" Requires="{requires}">'
|
|
173
177
|
f'<xdr:graphicFrame macro=""><xdr:nvGraphicFramePr>'
|
|
174
|
-
f'<xdr:cNvPr id="{shape_id}" name=
|
|
178
|
+
f'<xdr:cNvPr id="{shape_id}" name={shape_name}><a:extLst>'
|
|
175
179
|
f'<a:ext uri="{{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}}">'
|
|
176
180
|
f'<a16:creationId xmlns:a16="{_A16}" id="{guid}"/></a:ext></a:extLst></xdr:cNvPr>'
|
|
177
181
|
f"<xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr>"
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import datetime as dt
|
|
4
4
|
import re
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from xml.sax.saxutils import quoteattr
|
|
7
6
|
|
|
8
7
|
from excel_mcp.package.opc import XML_DECLARATION
|
|
9
8
|
from excel_mcp.package.scan import attributes
|
|
9
|
+
from excel_mcp.xml_text import attribute
|
|
10
10
|
|
|
11
11
|
X14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"
|
|
12
12
|
X15 = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"
|
|
@@ -47,7 +47,7 @@ class CacheOptions:
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def _attrs(**values: object) -> str:
|
|
50
|
-
return "".join(f" {k}={
|
|
50
|
+
return "".join(f" {k}={attribute(str(v))}" for k, v in values.items() if v is not None)
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def _date(moment: dt.date | dt.datetime) -> str:
|
|
@@ -31,6 +31,7 @@ from excel_mcp.package import state_of
|
|
|
31
31
|
from excel_mcp.refs import parse_cell
|
|
32
32
|
|
|
33
33
|
EMU_PER_CM = 360_000
|
|
34
|
+
MAX_TEXT_LENGTH = 255
|
|
34
35
|
_SLICER_SIZE = (1_828_800, 2_667_000)
|
|
35
36
|
_TIMELINE_SIZE = (3_333_750, 1_371_600)
|
|
36
37
|
|
|
@@ -71,6 +72,8 @@ def add_slicer(
|
|
|
71
72
|
) -> tuple[str, bool]:
|
|
72
73
|
"""Add the slicer or timeline to ``sheet``; returns its name, and whether the figures of
|
|
73
74
|
PivotTables it limits were left for Excel to recalculate when it opens the file."""
|
|
75
|
+
if request.caption:
|
|
76
|
+
_check_text(request.caption, "caption")
|
|
74
77
|
owner, target = find_source(workbook, request.source.sheet, request.source.name)
|
|
75
78
|
if isinstance(target, Table):
|
|
76
79
|
if request.timeline or request.connect:
|
|
@@ -235,6 +238,7 @@ def _names(workbook: Workbook, request: SlicerRequest, stem: str) -> tuple[str,
|
|
|
235
238
|
"""The slicer's name and its cache's, unique in the workbook as Excel makes them."""
|
|
236
239
|
taken = {entry.name.casefold() for entry in entries(workbook)}
|
|
237
240
|
if request.name is not None:
|
|
241
|
+
_check_text(request.name, "name")
|
|
238
242
|
if request.name.casefold() in taken:
|
|
239
243
|
raise InvalidArgumentError(f"A slicer or timeline named {request.name!r} exists.")
|
|
240
244
|
name = request.name
|
|
@@ -243,7 +247,8 @@ def _names(workbook: Workbook, request: SlicerRequest, stem: str) -> tuple[str,
|
|
|
243
247
|
while name.casefold() in taken:
|
|
244
248
|
number += 1
|
|
245
249
|
name = f"{request.field} {number}"
|
|
246
|
-
|
|
250
|
+
# Excel keeps letters, digits and . \ ? in a cache name, and a defined name is at most 255.
|
|
251
|
+
stem += re.sub(r"[^\w.\\?]", "_", request.field).rstrip("_")[: MAX_TEXT_LENGTH - 20]
|
|
247
252
|
used = {n.casefold() for n in workbook.defined_names} | {c.casefold() for c in caches(workbook)}
|
|
248
253
|
cache, number = stem, 0
|
|
249
254
|
while cache.casefold() in used:
|
|
@@ -252,6 +257,12 @@ def _names(workbook: Workbook, request: SlicerRequest, stem: str) -> tuple[str,
|
|
|
252
257
|
return name, cache
|
|
253
258
|
|
|
254
259
|
|
|
260
|
+
def _check_text(text: str, what: str) -> None:
|
|
261
|
+
"""Excel takes any slicer name, caption and cache name of 1 to 255 characters."""
|
|
262
|
+
if not text.strip() or len(text) > MAX_TEXT_LENGTH:
|
|
263
|
+
raise InvalidArgumentError(f"A slicer's {what} must be 1 to {MAX_TEXT_LENGTH} characters.")
|
|
264
|
+
|
|
265
|
+
|
|
255
266
|
def _check_style(style: str | None, kind: str) -> None:
|
|
256
267
|
wanted = "TimeSlicerStyle" if kind == "timeline" else "SlicerStyle"
|
|
257
268
|
if style is not None and not re.fullmatch(wanted + r"(Light|Dark|Other)\d", style):
|
|
@@ -17,7 +17,7 @@ from excel_mcp.operations.sparkline_style import SparklineInfo, SparklineStyle,
|
|
|
17
17
|
from excel_mcp.operations.sparkline_xml import build_group, read_groups
|
|
18
18
|
from excel_mcp.operations.x14_xml import X14, XM, sheet_prefix
|
|
19
19
|
from excel_mcp.package import state_of
|
|
20
|
-
from excel_mcp.package.extensions import SPARKLINES, edit_sparklines
|
|
20
|
+
from excel_mcp.package.extensions import SPARKLINES, edit_sparklines, sparkline_location
|
|
21
21
|
from excel_mcp.refs import CellRange, cell_name, parse_range
|
|
22
22
|
from excel_mcp.workspace import get_sheet
|
|
23
23
|
|
|
@@ -117,9 +117,7 @@ def _delete(extensions: dict[str, str], doomed: Callable[[str], bool]) -> int:
|
|
|
117
117
|
|
|
118
118
|
def handle(item: str) -> list[str]:
|
|
119
119
|
nonlocal removed
|
|
120
|
-
|
|
121
|
-
assert location is not None
|
|
122
|
-
if doomed(location[1]):
|
|
120
|
+
if doomed(sparkline_location(item)):
|
|
123
121
|
removed += 1
|
|
124
122
|
return []
|
|
125
123
|
return [item]
|
|
@@ -30,8 +30,6 @@ def write_module(
|
|
|
30
30
|
raise InvalidArgumentError(
|
|
31
31
|
"Code lines cannot start with 'Attribute': the server writes module attributes itself."
|
|
32
32
|
)
|
|
33
|
-
if "\0" in code:
|
|
34
|
-
raise InvalidArgumentError("The code contains a NUL character.")
|
|
35
33
|
project = _project(workbook)
|
|
36
34
|
existing = project.find(name)
|
|
37
35
|
if existing is None and not _MODULE_NAME.fullmatch(name):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Reading from the original file what openpyxl is going to drop from a worksheet."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
from html import unescape
|
|
4
5
|
from typing import Literal
|
|
5
6
|
from xml.etree import ElementTree
|
|
6
7
|
|
|
@@ -21,7 +22,6 @@ from excel_mcp.package.scan import (
|
|
|
21
22
|
relationship_ids,
|
|
22
23
|
scan,
|
|
23
24
|
scan_fragment,
|
|
24
|
-
unescape,
|
|
25
25
|
with_namespaces,
|
|
26
26
|
)
|
|
27
27
|
from excel_mcp.package.shape_names import anchored_name, restore_names
|