excel-mcp-server 2.2.2__tar.gz → 2.2.4__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.
Files changed (228) hide show
  1. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/PKG-INFO +1 -1
  2. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/pyproject.toml +1 -1
  3. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/pyproject.toml.orig +1 -1
  4. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/slicer_place.py +3 -1
  5. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sparklines.py +2 -4
  6. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/consistency.py +7 -6
  7. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/extensions.py +37 -23
  8. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/opc.py +26 -14
  9. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/patch.py +36 -8
  10. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/references.py +5 -2
  11. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/restore.py +8 -3
  12. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/LICENSE +0 -0
  13. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/README.md +0 -0
  14. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/__init__.py +0 -0
  15. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/__main__.py +0 -0
  16. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/cells.py +0 -0
  17. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/criteria.py +0 -0
  18. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/dependencies.py +0 -0
  19. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/dynamic.py +0 -0
  20. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/engine.py +0 -0
  21. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/__init__.py +0 -0
  22. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/arrays.py +0 -0
  23. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/conditional.py +0 -0
  24. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/datetime.py +0 -0
  25. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/depreciation.py +0 -0
  26. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/distributions.py +0 -0
  27. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/financial.py +0 -0
  28. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/helpers.py +0 -0
  29. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/indirect.py +0 -0
  30. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/information.py +0 -0
  31. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/logical.py +0 -0
  32. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/lookup.py +0 -0
  33. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/math.py +0 -0
  34. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/regression.py +0 -0
  35. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/securities.py +0 -0
  36. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/statistical.py +0 -0
  37. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/subtotals.py +0 -0
  38. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/functions/text.py +0 -0
  39. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/operators.py +0 -0
  40. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/parser.py +0 -0
  41. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/r1c1.py +0 -0
  42. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/registry.py +0 -0
  43. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/scheduler.py +0 -0
  44. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/solver.py +0 -0
  45. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/steps.py +0 -0
  46. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/tablerefs.py +0 -0
  47. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/textformat.py +0 -0
  48. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/values.py +0 -0
  49. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/calc/wildcard.py +0 -0
  50. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/cell_budget.py +0 -0
  51. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/cfb.py +0 -0
  52. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/chart_roundtrip.py +0 -0
  53. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/cli.py +0 -0
  54. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/config.py +0 -0
  55. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/errors.py +0 -0
  56. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/formula_syntax.py +0 -0
  57. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/formulas.py +0 -0
  58. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/function_arguments.json +0 -0
  59. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/inputs.py +0 -0
  60. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/lazy_edit.py +0 -0
  61. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/lazy_find.py +0 -0
  62. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/lazy_lines.py +0 -0
  63. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/lazy_rows.py +0 -0
  64. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/lazy_shared.py +0 -0
  65. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/lazy_source.py +0 -0
  66. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/lazy_workbook.py +0 -0
  67. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/links.py +0 -0
  68. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/macros.py +0 -0
  69. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/__init__.py +0 -0
  70. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/calculated.py +0 -0
  71. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/cells.py +0 -0
  72. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chart_index.py +0 -0
  73. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chart_info.py +0 -0
  74. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex.py +0 -0
  75. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
  76. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
  77. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
  78. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
  79. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
  80. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
  81. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
  82. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/chartex_xml.py +0 -0
  83. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts.py +0 -0
  84. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts_axes.py +0 -0
  85. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts_check.py +0 -0
  86. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts_data.py +0 -0
  87. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts_look.py +0 -0
  88. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts_modern.py +0 -0
  89. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts_options.py +0 -0
  90. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts_series.py +0 -0
  91. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/charts_style.py +0 -0
  92. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/columns.py +0 -0
  93. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/comparison.py +0 -0
  94. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/conditional.py +0 -0
  95. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/conditional_kinds.py +0 -0
  96. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/conditional_rule.py +0 -0
  97. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/conditional_x14.py +0 -0
  98. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/csv_export.py +0 -0
  99. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/csv_format.py +0 -0
  100. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/csv_import.py +0 -0
  101. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/csv_values.py +0 -0
  102. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/date_width.py +0 -0
  103. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/drawings.py +0 -0
  104. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/duplicates.py +0 -0
  105. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/files.py +0 -0
  106. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/fill.py +0 -0
  107. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/filter_criteria.py +0 -0
  108. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/filter_stored.py +0 -0
  109. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/filters.py +0 -0
  110. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/formatting.py +0 -0
  111. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/goal_seek.py +0 -0
  112. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/hyperlinks.py +0 -0
  113. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/images.py +0 -0
  114. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/inspect.py +0 -0
  115. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/layout.py +0 -0
  116. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/line_edit_objects.py +0 -0
  117. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/line_edit_rules.py +0 -0
  118. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/line_edit_styles.py +0 -0
  119. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/line_edits.py +0 -0
  120. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/names.py +0 -0
  121. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/notes.py +0 -0
  122. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/paste.py +0 -0
  123. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot.py +0 -0
  124. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_axis.py +0 -0
  125. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_cache.py +0 -0
  126. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_calc.py +0 -0
  127. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_definition.py +0 -0
  128. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_fields.py +0 -0
  129. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_groups.py +0 -0
  130. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_index.py +0 -0
  131. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_options.py +0 -0
  132. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
  133. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_records.py +0 -0
  134. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_render.py +0 -0
  135. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_showas.py +0 -0
  136. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_source.py +0 -0
  137. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_specs.py +0 -0
  138. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/pivot_values.py +0 -0
  139. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/print_setup.py +0 -0
  140. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/protection.py +0 -0
  141. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/replace.py +0 -0
  142. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/rule_clear.py +0 -0
  143. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/rules.py +0 -0
  144. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sheet_copy.py +0 -0
  145. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sheet_counts.py +0 -0
  146. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sheet_refs.py +0 -0
  147. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sheet_shapes.py +0 -0
  148. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sheet_view.py +0 -0
  149. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sheets.py +0 -0
  150. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/shifting.py +0 -0
  151. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/slicer_index.py +0 -0
  152. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/slicer_manage.py +0 -0
  153. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/slicer_pivot.py +0 -0
  154. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/slicer_table.py +0 -0
  155. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/slicer_xml.py +0 -0
  156. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/slicers.py +0 -0
  157. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sorting.py +0 -0
  158. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/spans.py +0 -0
  159. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sparkline_style.py +0 -0
  160. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/sparkline_xml.py +0 -0
  161. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/spill.py +0 -0
  162. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/table_options.py +0 -0
  163. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/tables.py +0 -0
  164. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/text_split.py +0 -0
  165. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/trace.py +0 -0
  166. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/transform.py +0 -0
  167. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/used_range.py +0 -0
  168. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/vba.py +0 -0
  169. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/vba_write.py +0 -0
  170. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/workbook_rewrite.py +0 -0
  171. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/workbook_settings.py +0 -0
  172. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/operations/x14_xml.py +0 -0
  173. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/ovba.py +0 -0
  174. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/ovba_compress.py +0 -0
  175. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/ovba_write.py +0 -0
  176. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/__init__.py +0 -0
  177. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/anchors.py +0 -0
  178. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/arrays.py +0 -0
  179. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/capture.py +0 -0
  180. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/capture_sheet.py +0 -0
  181. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/guards.py +0 -0
  182. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/lazy_records.py +0 -0
  183. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/lines.py +0 -0
  184. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/metadata.py +0 -0
  185. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/model.py +0 -0
  186. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/pivot_records.py +0 -0
  187. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/pivots.py +0 -0
  188. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/properties.py +0 -0
  189. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/reader.py +0 -0
  190. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/restore_sheet.py +0 -0
  191. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/scan.py +0 -0
  192. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/shape_names.py +0 -0
  193. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/package/vml.py +0 -0
  194. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/paths.py +0 -0
  195. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/plain_refs.py +0 -0
  196. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/py.typed +0 -0
  197. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/refs.py +0 -0
  198. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/rewrite.py +0 -0
  199. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/__init__.py +0 -0
  200. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/annotation_tools.py +0 -0
  201. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/areas.py +0 -0
  202. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/csv_tools.py +0 -0
  203. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/data_tools.py +0 -0
  204. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/format_tools.py +0 -0
  205. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/formula_tools.py +0 -0
  206. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/http.py +0 -0
  207. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/image_tools.py +0 -0
  208. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/object_tools.py +0 -0
  209. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/params.py +0 -0
  210. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/pivot_tools.py +0 -0
  211. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/registry.py +0 -0
  212. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/results.py +0 -0
  213. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/sheet_tools.py +0 -0
  214. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/slicer_tools.py +0 -0
  215. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/validation.py +0 -0
  216. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/vba_tools.py +0 -0
  217. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/vba_write_tools.py +0 -0
  218. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/server/workbook_tools.py +0 -0
  219. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/spill.py +0 -0
  220. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/structured.py +0 -0
  221. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/text.py +0 -0
  222. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/upload_links.py +0 -0
  223. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/upload_scan.py +0 -0
  224. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/values.py +0 -0
  225. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/workspace.py +0 -0
  226. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/xlfn.py +0 -0
  227. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/src/excel_mcp/xlm.py +0 -0
  228. {excel_mcp_server-2.2.2 → excel_mcp_server-2.2.4}/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.2
3
+ Version: 2.2.4
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.2"
3
+ version = "2.2.4"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.2"
3
+ version = "2.2.4"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
@@ -119,7 +120,8 @@ def _list_in(extensions: dict[str, str], uri: str, element: str) -> None:
119
120
  assert reference is not None
120
121
  existing = extensions[uri]
121
122
  closing = re.search(r"</(?:\w+:)(?:slicerCaches|timelineCacheRefs)>", existing)
122
- assert closing is not None
123
+ if closing is None:
124
+ raise WorkbookError("The workbook's list of slicer or timeline caches is malformed.")
123
125
  extensions[uri] = existing[: closing.start()] + reference[0] + existing[closing.start() :]
124
126
 
125
127
 
@@ -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
- location = _LOCATION.search(item)
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]
@@ -27,7 +27,8 @@ _CACHE_LISTS = {
27
27
  }
28
28
  _SLICER_REFERENCE = re.compile(r'<(?:[\w.-]+:)?(?:slicer|timeline)\b[^>]*?\bcache="([^"]*)"')
29
29
  PIVOT_ENTRY = re.compile(r"<(?:[\w.-]+:)?pivotTable\b[^>]*/>")
30
- _CACHE_ENTRY = r'<(?:[\w.-]+:)?(?:slicerCache|timelineCacheRef)\b[^>]*?\b[\w.-]+:id="{id}"[^>]*/>'
30
+ _CACHE_ENTRY = re.compile(r"<(?:[\w.-]+:)?(?:slicerCache|timelineCacheRef)\b[^>]*/>")
31
+ _RELATIONSHIP_ID = re.compile(r'\b[\w.-]+:id="([^"]*)"')
31
32
 
32
33
 
33
34
  def cache_name(part: Part) -> str:
@@ -80,11 +81,11 @@ def without_caches(
80
81
  and link.target.content_type in _CACHES
81
82
  and cache_name(link.target) in dropped
82
83
  }
83
- kept = []
84
- for text in texts:
85
- for identifier in gone:
86
- text = re.sub(_CACHE_ENTRY.format(id=re.escape(identifier)), "", text)
87
- kept.append(text)
84
+
85
+ def entry(match: re.Match[str]) -> str:
86
+ return "" if gone.intersection(_RELATIONSHIP_ID.findall(match[0])) else match[0]
87
+
88
+ kept = [_CACHE_ENTRY.sub(entry, text) for text in texts] if gone else texts
88
89
  return [link for link in links if link.id not in gone], kept
89
90
 
90
91
 
@@ -12,6 +12,7 @@ from xml.sax.saxutils import escape
12
12
 
13
13
  from openpyxl.utils.cell import column_index_from_string, get_column_letter
14
14
 
15
+ from excel_mcp.errors import WorkbookError
15
16
  from excel_mcp.package.lines import LineEdit
16
17
  from excel_mcp.package.scan import unescape
17
18
  from excel_mcp.refs import CellRange, clip_areas, parse_range
@@ -41,21 +42,27 @@ class Rewriter(Protocol):
41
42
 
42
43
 
43
44
  _SPARKLINE = re.compile(r"<x14:sparkline>.*?</x14:sparkline>", re.S)
45
+ # An element is its start tag, then either "/>" or its content up to its end tag. Written
46
+ # like this, a self-closing element never makes the match run on to some later end tag, so
47
+ # a document of many such elements is scanned once and not once per element.
44
48
  _EMPTY_GROUP = re.compile(
45
- r"<x14:sparklineGroup\b[^>]*>(?:(?!<x14:sparkline>).)*?</x14:sparklineGroup>", re.S
49
+ r"<x14:sparklineGroup\b[^>]*?(?:/>|>(?:(?!<x14:sparkline>).)*?</x14:sparklineGroup>)", re.S
46
50
  )
47
- _RULE_GROUP = re.compile(r"<x14:conditionalFormatting\b[^>]*>.*?</x14:conditionalFormatting>", re.S)
48
- _VALIDATION = re.compile(r"<x14:dataValidation\b[^>]*>.*?</x14:dataValidation>", re.S)
51
+ _RULE_GROUP = re.compile(
52
+ r"<x14:conditionalFormatting\b[^>]*?(?:/>|>.*?</x14:conditionalFormatting>)", re.S
53
+ )
54
+ _RULE = re.compile(r"<x14:cfRule\b[^>]*?(?:/>|>.*?</x14:cfRule>)", re.S)
55
+ _VALIDATION = re.compile(r"<x14:dataValidation\b[^>]*?(?:/>|>.*?</x14:dataValidation>)", re.S)
49
56
  _DATED_GROUP = re.compile(
50
- r"(<x14:sparklineGroup(?!s)[^>]*>(?:(?!<x14:sparklines>).)*?)<xm:f>([^<]*)</xm:f>(<x14:sparklines>)",
57
+ r"(<x14:sparklineGroup(?!s)[^>]*(?<!/)>(?:(?!<x14:sparklines>|</x14:sparklineGroup>).)*?)"
58
+ r"<xm:f>([^<]*)</xm:f>(<x14:sparklines>)",
51
59
  re.S,
52
60
  )
53
61
  _GROUP_DATES = re.compile(r"<xm:f>([^<]*)</xm:f>(?=<x14:sparklines>)")
54
62
  _RANGE = re.compile(r"<xm:sqref>([^<]*)</xm:sqref>")
55
63
  _FORMULA = re.compile(r"<xm:f>([^<]*)</xm:f>")
56
- _FORMULA_ITEM = re.compile(
57
- r"<x14:cfRule\b.*?</x14:cfRule>|<x14:dataValidation\b.*?</x14:dataValidation>", re.S
58
- )
64
+ _FORMULA_ITEM = re.compile(f"{_RULE.pattern}|{_VALIDATION.pattern}", re.S)
65
+ _RULE_ID_ATTRIBUTE = re.compile(r'\bid="([^"]*)"')
59
66
  _GUID = re.compile(r'(<x14:id>|\b(?:xr2:uid|id)=")(\{[0-9A-Fa-f-]{36}\})')
60
67
  _CELL = re.compile(r"(\$?)\b([A-Z]{1,3})(\$?)(\d+)\b(?![!(\w])")
61
68
 
@@ -196,21 +203,18 @@ def rule_sub(xml: str, rule: Callable[[str], str]) -> str:
196
203
 
197
204
 
198
205
  def without_rules(extensions: dict[str, str], identifiers: set[str]) -> None:
199
- """Remove the Excel 2010 conditional format rules with these ids (``{GUID}``)."""
206
+ """Remove the Excel 2010 conditional format rules with these ids (``{GUID}``), and the
207
+ groups left without a rule."""
200
208
  xml = extensions.get(CONDITIONAL_FORMATS)
201
209
  if xml is None:
202
210
  return
203
- for identifier in identifiers:
204
- pattern = (
205
- rf'<x14:cfRule\b(?=[^>]*\bid="{re.escape(identifier)}")'
206
- r"(?:[^>]*/>|[^>]*>.*?</x14:cfRule>)"
207
- )
208
- xml = re.sub(pattern, "", xml, flags=re.S)
209
- emptied = (
210
- r"<x14:conditionalFormatting\b[^>]*>(?:(?!<x14:cfRule\b).)*?"
211
- r"</x14:conditionalFormatting>"
212
- )
213
- xml = re.sub(emptied, "", xml, flags=re.S)
211
+
212
+ def dropped(rule: re.Match[str]) -> str:
213
+ start_tag = rule[0][: rule[0].index(">") + 1]
214
+ return "" if identifiers.intersection(_RULE_ID_ATTRIBUTE.findall(start_tag)) else rule[0]
215
+
216
+ xml = _RULE.sub(dropped, xml)
217
+ xml = _RULE_GROUP.sub(lambda group: group[0] if "<x14:cfRule" in group[0] else "", xml)
214
218
  if "<x14:cfRule" in xml:
215
219
  extensions[CONDITIONAL_FORMATS] = xml
216
220
  else:
@@ -229,11 +233,21 @@ def _sparklines(xml: str, handle: Callable[[str], list[str]]) -> str | None:
229
233
  return xml if re.search(r"<x14:sparklineGroup\b", xml) else None
230
234
 
231
235
 
232
- def _moved_sparkline(item: str, rewriter: Rewriter) -> list[str]:
236
+ def sparkline_location(item: str) -> str:
237
+ """The cell of a ``<x14:sparkline>``. Excel refuses a file with a sparkline that has none."""
233
238
  location = _RANGE.search(item)
239
+ if location is None:
240
+ raise WorkbookError(
241
+ "The workbook has a sparkline without a location (<xm:sqref>), which Excel "
242
+ "cannot open. Remove it from the file."
243
+ )
244
+ return location[1]
245
+
246
+
247
+ def _moved_sparkline(item: str, rewriter: Rewriter) -> list[str]:
248
+ location = sparkline_location(item)
234
249
  source = _FORMULA.search(item)
235
- assert location is not None
236
- moved = rewriter.ranges(location[1], grow=False)
250
+ moved = rewriter.ranges(location, grow=False)
237
251
  if moved is None:
238
252
  return []
239
253
  item = _RANGE.sub(lambda _: f"<xm:sqref>{moved}</xm:sqref>", item)
@@ -243,7 +257,7 @@ def _moved_sparkline(item: str, rewriter: Rewriter) -> list[str]:
243
257
  item = item.replace(source[0], "")
244
258
  else:
245
259
  item = item.replace(source[0], f"<xm:f>{escape(read)}</xm:f>")
246
- return [item, *_inserted_copies(item, location[1], rewriter)]
260
+ return [item, *_inserted_copies(item, location, rewriter)]
247
261
 
248
262
 
249
263
  def _moved_formula(match: re.Match[str], rewriter: Rewriter) -> str:
@@ -4,7 +4,9 @@ import io
4
4
  import posixpath
5
5
  import re
6
6
  import zipfile
7
+ from collections.abc import Callable
7
8
  from dataclasses import dataclass
9
+ from itertools import count
8
10
  from xml.sax.saxutils import quoteattr
9
11
 
10
12
  from excel_mcp.errors import WorkbookError
@@ -75,15 +77,25 @@ def write_rels(rels: list[Rel]) -> bytes:
75
77
  return (XML_DECLARATION + "".join(lines)).encode()
76
78
 
77
79
 
78
- def free_id(rels: list[Rel], wanted: str) -> str:
79
- """``wanted`` if it is a free ``rIdN``, else the first free one."""
80
+ def free_ids(rels: list[Rel]) -> Callable[[str], str]:
81
+ """A function that gives, for each id wanted, that id if it is a free ``rIdN`` and else the
82
+ first free one, and then counts it as taken. Many calls cost no more than one scan."""
80
83
  taken = {rel.id for rel in rels}
81
- if re.fullmatch(r"rId\d+", wanted) and wanted not in taken:
82
- return wanted
83
- number = 1
84
- while f"rId{number}" in taken:
85
- number += 1
86
- return f"rId{number}"
84
+ numbers = count(1)
85
+
86
+ def allocate(wanted: str) -> str:
87
+ if re.fullmatch(r"rId\d+", wanted) and wanted not in taken:
88
+ chosen = wanted
89
+ else:
90
+ chosen = next(f"rId{n}" for n in numbers if f"rId{n}" not in taken)
91
+ taken.add(chosen)
92
+ return chosen
93
+
94
+ return allocate
95
+
96
+
97
+ def free_id(rels: list[Rel], wanted: str) -> str:
98
+ return free_ids(rels)(wanted)
87
99
 
88
100
 
89
101
  class ContentTypes:
@@ -135,15 +147,16 @@ class Package:
135
147
 
136
148
  def __init__(self, data: bytes) -> None:
137
149
  self._source = zipfile.ZipFile(io.BytesIO(data))
150
+ self._source_names = set(self._source.namelist())
138
151
  self._changes: dict[str, bytes] = {}
139
152
  self._rels: dict[str, list[Rel]] = {}
140
153
  self.content_types = ContentTypes(self.read(CONTENT_TYPES))
141
154
 
142
155
  def names(self) -> set[str]:
143
- return set(self._source.namelist()) | set(self._changes)
156
+ return self._source_names | set(self._changes)
144
157
 
145
158
  def exists(self, name: str) -> bool:
146
- return name in self._changes or name in self._source.namelist()
159
+ return name in self._changes or name in self._source_names
147
160
 
148
161
  def read(self, name: str) -> bytes:
149
162
  if name in self._changes:
@@ -162,13 +175,12 @@ class Package:
162
175
 
163
176
  def unique_name(self, wanted: str) -> str:
164
177
  """``wanted``, or the same name with the next free number in it."""
165
- taken = self.names()
166
- if wanted not in taken:
178
+ if not self.exists(wanted):
167
179
  return wanted
168
180
  match = re.fullmatch(r"(.*?)(\d*)(\.[^./]*)?", wanted)
169
181
  stem, number, extension = match.groups() if match else (wanted, "", "")
170
182
  counter = int(number or 0) + 1
171
- while f"{stem}{counter}{extension or ''}" in taken:
183
+ while self.exists(f"{stem}{counter}{extension or ''}"):
172
184
  counter += 1
173
185
  return f"{stem}{counter}{extension or ''}"
174
186
 
@@ -189,6 +201,6 @@ class Package:
189
201
  elif content:
190
202
  target.writestr(name, content)
191
203
  for name, content in self._changes.items():
192
- if content and name not in self._source.namelist():
204
+ if content and name not in self._source_names:
193
205
  target.writestr(name, content)
194
206
  return output.getvalue()
@@ -2,6 +2,7 @@
2
2
 
3
3
  import re
4
4
  from collections.abc import Callable
5
+ from itertools import pairwise
5
6
 
6
7
  from excel_mcp.errors import WorkbookError
7
8
  from excel_mcp.package.scan import Scan, scan
@@ -146,17 +147,44 @@ def _with_extension(rule: bytes, qname: str, extension: str, offset: int) -> Edi
146
147
  return (offset, offset + len(rule), text)
147
148
 
148
149
 
150
+ # Looking each cell up by its address reads the sheet once per cell, which is fastest for a
151
+ # few cells; for more, one pass over all the cells does not grow with the number to patch.
152
+ _FEW_CELLS = 16
153
+ _CELL = re.compile(rb'<c r="([^"]*)"([^>]*?)(?:/>|>(.*?)</c>)', re.S)
154
+
155
+
149
156
  def patch_cells(data: bytes, edits: dict[str, CellEdit]) -> bytes:
150
157
  """Rewrite chosen cells of ``sheetData``; an edit gets a cell's attributes and its content."""
151
158
  if not edits:
152
159
  return data
153
- names = b"|".join(re.escape(name.encode()) for name in edits)
154
- cell = re.compile(rb'<c r="(' + names + rb')"([^>]*?)(?:/>|>(.*?)</c>)', re.S)
160
+ if len(edits) <= _FEW_CELLS:
161
+ return splice(data, _located_edits(data, edits))
155
162
 
156
163
  def rewrite(match: re.Match[bytes]) -> bytes:
157
- coordinate = match[1].decode()
158
- attributes, body = edits[coordinate](match[2].decode(), (match[3] or b"").decode())
159
- start = f'<c r="{coordinate}"{attributes}'
160
- return f"{start}>{body}</c>".encode() if body else f"{start}/>".encode()
161
-
162
- return cell.sub(rewrite, data)
164
+ edit = edits.get(match[1].decode())
165
+ return _rewritten(match, edit) if edit else match[0]
166
+
167
+ return _CELL.sub(rewrite, data)
168
+
169
+
170
+ def _located_edits(data: bytes, edits: dict[str, CellEdit]) -> list[Edit]:
171
+ found: list[Edit] = []
172
+ for coordinate, edit in edits.items():
173
+ opening = b'<c r="%b"' % coordinate.encode()
174
+ start = data.find(opening)
175
+ while start >= 0:
176
+ match = _CELL.match(data, start)
177
+ if match is None:
178
+ raise WorkbookError(f"Cell {coordinate} of a worksheet is malformed.")
179
+ found.append((match.start(), match.end(), _rewritten(match, edit)))
180
+ start = data.find(opening, match.end())
181
+ found.sort()
182
+ if any(before[1] > after[0] for before, after in pairwise(found)):
183
+ raise WorkbookError("A worksheet has cells inside other cells.")
184
+ return found
185
+
186
+
187
+ def _rewritten(match: re.Match[bytes], edit: CellEdit) -> bytes:
188
+ attributes, body = edit(match[2].decode(), (match[3] or b"").decode())
189
+ start = f'<c r="{match[1].decode()}"{attributes}'
190
+ return f"{start}>{body}</c>".encode() if body else f"{start}/>".encode()
@@ -26,6 +26,7 @@ from xml.sax.saxutils import escape
26
26
  from openpyxl import Workbook
27
27
  from openpyxl.worksheet.worksheet import Worksheet
28
28
 
29
+ from excel_mcp.errors import WorkbookError
29
30
  from excel_mcp.package import extensions
30
31
  from excel_mcp.package.anchors import Geometry, Moved, move_anchors, move_vml_shape
31
32
  from excel_mcp.package.lines import LineEdit
@@ -120,7 +121,8 @@ def _ranged_items(xml: str, item: str, attribute: str, rewriter: _Rewriter) -> s
120
121
  def move(match: re.Match[str]) -> str:
121
122
  text = match[0]
122
123
  value = re.search(rf'\b{attribute}="([^"]*)"', text)
123
- assert value is not None
124
+ if value is None:
125
+ raise WorkbookError(f"A <{item}> of the workbook has no {attribute} attribute.")
124
126
  moved = rewriter.ranges(value[1], grow=attribute == "sqref")
125
127
  return "" if moved is None else text.replace(value[0], f'{attribute}="{moved}"', 1)
126
128
 
@@ -134,7 +136,8 @@ def _sort_state(xml: str, rewriter: _Rewriter) -> str | None:
134
136
 
135
137
  def move(match: re.Match[str]) -> str:
136
138
  found = ref.search(match[0])
137
- assert found is not None
139
+ if found is None:
140
+ raise WorkbookError("A <sortCondition> of the workbook has no ref attribute.")
138
141
  moved = rewriter.ranges(found[1])
139
142
  return "" if moved is None else match[0].replace(found[0], f'ref="{moved}"', 1)
140
143
 
@@ -14,7 +14,7 @@ from excel_mcp.package.model import (
14
14
  state_of,
15
15
  workbook_sheets,
16
16
  )
17
- from excel_mcp.package.opc import Package, Rel, free_id
17
+ from excel_mcp.package.opc import Package, Rel, free_ids
18
18
  from excel_mcp.package.patch import WORKBOOK_ORDER, append_to_root, insertions, splice
19
19
  from excel_mcp.package.restore_sheet import restore_sheet
20
20
  from excel_mcp.package.scan import attributes, rel_id, remap_relationship_ids, scan, tags
@@ -103,13 +103,18 @@ class Restorer:
103
103
  ) -> list[str]:
104
104
  """Add relationships to a part that is being merged into, and renumber the XML to match."""
105
105
  rels = self.package.rels(owner)
106
+ free_id = free_ids(rels)
107
+ known: dict[tuple[str, str], Rel] = {}
108
+ for rel in rels:
109
+ known.setdefault((rel.type, rel.target), rel)
106
110
  mapping: dict[str, str] = {}
107
111
  for link in links:
108
112
  rel = Rel(link.id, link.type, self.target(link), isinstance(link.target, str))
109
- existing = next((r for r in rels if (r.type, r.target) == (rel.type, rel.target)), None)
113
+ existing = known.get((rel.type, rel.target))
110
114
  if existing is None:
111
- rel.id = free_id(rels, link.id)
115
+ rel.id = free_id(link.id)
112
116
  rels.append(rel)
117
+ known[rel.type, rel.target] = rel
113
118
  mapping[link.id] = (existing or rel).id
114
119
  return [remap_relationship_ids(text, mapping, scope) for text in texts]
115
120