excel-mcp-server 2.2.0__tar.gz → 2.2.1__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 (227) hide show
  1. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/PKG-INFO +4 -1
  2. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/README.md +3 -0
  3. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/pyproject.toml +1 -1
  4. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/pyproject.toml.orig +1 -1
  5. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/formulas.py +20 -0
  6. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/lazy_edit.py +32 -30
  7. excel_mcp_server-2.2.1/src/excel_mcp/lazy_find.py +83 -0
  8. excel_mcp_server-2.2.1/src/excel_mcp/lazy_lines.py +259 -0
  9. excel_mcp_server-2.2.1/src/excel_mcp/lazy_rows.py +198 -0
  10. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/lazy_shared.py +49 -3
  11. excel_mcp_server-2.2.1/src/excel_mcp/lazy_source.py +102 -0
  12. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/lazy_workbook.py +123 -110
  13. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/cells.py +25 -0
  14. excel_mcp_server-2.2.1/src/excel_mcp/operations/line_edit_styles.py +94 -0
  15. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/line_edits.py +28 -51
  16. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot.py +6 -7
  17. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_source.py +49 -1
  18. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/replace.py +11 -2
  19. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sheet_copy.py +35 -8
  20. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sheet_refs.py +10 -0
  21. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/shifting.py +32 -0
  22. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/workbook_rewrite.py +6 -2
  23. excel_mcp_server-2.2.1/src/excel_mcp/plain_refs.py +106 -0
  24. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/workspace.py +1 -1
  25. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/xlfn.py +4 -0
  26. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/LICENSE +0 -0
  27. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/__init__.py +0 -0
  28. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/__main__.py +0 -0
  29. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/cells.py +0 -0
  30. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/criteria.py +0 -0
  31. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/dependencies.py +0 -0
  32. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/dynamic.py +0 -0
  33. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/engine.py +0 -0
  34. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/__init__.py +0 -0
  35. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/arrays.py +0 -0
  36. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/conditional.py +0 -0
  37. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/datetime.py +0 -0
  38. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/depreciation.py +0 -0
  39. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/distributions.py +0 -0
  40. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/financial.py +0 -0
  41. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/helpers.py +0 -0
  42. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/indirect.py +0 -0
  43. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/information.py +0 -0
  44. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/logical.py +0 -0
  45. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/lookup.py +0 -0
  46. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/math.py +0 -0
  47. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/regression.py +0 -0
  48. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/securities.py +0 -0
  49. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/statistical.py +0 -0
  50. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/subtotals.py +0 -0
  51. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/functions/text.py +0 -0
  52. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/operators.py +0 -0
  53. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/parser.py +0 -0
  54. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/r1c1.py +0 -0
  55. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/registry.py +0 -0
  56. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/scheduler.py +0 -0
  57. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/solver.py +0 -0
  58. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/steps.py +0 -0
  59. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/tablerefs.py +0 -0
  60. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/textformat.py +0 -0
  61. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/values.py +0 -0
  62. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/calc/wildcard.py +0 -0
  63. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/cfb.py +0 -0
  64. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/chart_roundtrip.py +0 -0
  65. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/cli.py +0 -0
  66. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/config.py +0 -0
  67. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/errors.py +0 -0
  68. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/formula_syntax.py +0 -0
  69. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/function_arguments.json +0 -0
  70. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/inputs.py +0 -0
  71. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/links.py +0 -0
  72. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/macros.py +0 -0
  73. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/__init__.py +0 -0
  74. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/calculated.py +0 -0
  75. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chart_index.py +0 -0
  76. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chart_info.py +0 -0
  77. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex.py +0 -0
  78. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
  79. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
  80. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
  81. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
  82. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
  83. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
  84. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
  85. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/chartex_xml.py +0 -0
  86. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts.py +0 -0
  87. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts_axes.py +0 -0
  88. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts_check.py +0 -0
  89. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts_data.py +0 -0
  90. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts_look.py +0 -0
  91. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts_modern.py +0 -0
  92. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts_options.py +0 -0
  93. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts_series.py +0 -0
  94. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/charts_style.py +0 -0
  95. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/columns.py +0 -0
  96. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/comparison.py +0 -0
  97. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/conditional.py +0 -0
  98. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/conditional_kinds.py +0 -0
  99. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/conditional_rule.py +0 -0
  100. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/conditional_x14.py +0 -0
  101. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/csv_export.py +0 -0
  102. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/csv_format.py +0 -0
  103. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/csv_import.py +0 -0
  104. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/csv_values.py +0 -0
  105. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/date_width.py +0 -0
  106. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/drawings.py +0 -0
  107. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/duplicates.py +0 -0
  108. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/files.py +0 -0
  109. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/fill.py +0 -0
  110. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/filter_criteria.py +0 -0
  111. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/filter_stored.py +0 -0
  112. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/filters.py +0 -0
  113. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/formatting.py +0 -0
  114. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/goal_seek.py +0 -0
  115. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/hyperlinks.py +0 -0
  116. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/images.py +0 -0
  117. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/inspect.py +0 -0
  118. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/layout.py +0 -0
  119. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/line_edit_objects.py +0 -0
  120. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/line_edit_rules.py +0 -0
  121. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/names.py +0 -0
  122. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/notes.py +0 -0
  123. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/paste.py +0 -0
  124. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_axis.py +0 -0
  125. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_cache.py +0 -0
  126. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_calc.py +0 -0
  127. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_definition.py +0 -0
  128. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_fields.py +0 -0
  129. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_groups.py +0 -0
  130. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_index.py +0 -0
  131. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_options.py +0 -0
  132. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
  133. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_records.py +0 -0
  134. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_render.py +0 -0
  135. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_showas.py +0 -0
  136. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_specs.py +0 -0
  137. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/pivot_values.py +0 -0
  138. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/print_setup.py +0 -0
  139. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/protection.py +0 -0
  140. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/rule_clear.py +0 -0
  141. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/rules.py +0 -0
  142. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sheet_counts.py +0 -0
  143. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sheet_shapes.py +0 -0
  144. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sheet_view.py +0 -0
  145. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sheets.py +0 -0
  146. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/slicer_index.py +0 -0
  147. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/slicer_manage.py +0 -0
  148. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/slicer_pivot.py +0 -0
  149. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/slicer_place.py +0 -0
  150. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/slicer_table.py +0 -0
  151. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/slicer_xml.py +0 -0
  152. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/slicers.py +0 -0
  153. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sorting.py +0 -0
  154. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/spans.py +0 -0
  155. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sparkline_style.py +0 -0
  156. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sparkline_xml.py +0 -0
  157. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/sparklines.py +0 -0
  158. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/spill.py +0 -0
  159. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/table_options.py +0 -0
  160. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/tables.py +0 -0
  161. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/text_split.py +0 -0
  162. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/trace.py +0 -0
  163. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/transform.py +0 -0
  164. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/used_range.py +0 -0
  165. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/vba.py +0 -0
  166. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/vba_write.py +0 -0
  167. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/workbook_settings.py +0 -0
  168. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/operations/x14_xml.py +0 -0
  169. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/ovba.py +0 -0
  170. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/ovba_compress.py +0 -0
  171. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/ovba_write.py +0 -0
  172. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/__init__.py +0 -0
  173. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/anchors.py +0 -0
  174. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/arrays.py +0 -0
  175. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/capture.py +0 -0
  176. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/capture_sheet.py +0 -0
  177. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/consistency.py +0 -0
  178. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/extensions.py +0 -0
  179. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/guards.py +0 -0
  180. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/lazy_records.py +0 -0
  181. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/lines.py +0 -0
  182. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/metadata.py +0 -0
  183. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/model.py +0 -0
  184. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/opc.py +0 -0
  185. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/patch.py +0 -0
  186. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/pivot_records.py +0 -0
  187. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/pivots.py +0 -0
  188. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/properties.py +0 -0
  189. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/reader.py +0 -0
  190. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/references.py +0 -0
  191. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/restore.py +0 -0
  192. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/restore_sheet.py +0 -0
  193. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/scan.py +0 -0
  194. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/shape_names.py +0 -0
  195. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/package/vml.py +0 -0
  196. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/paths.py +0 -0
  197. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/py.typed +0 -0
  198. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/refs.py +0 -0
  199. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/rewrite.py +0 -0
  200. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/__init__.py +0 -0
  201. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/annotation_tools.py +0 -0
  202. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/areas.py +0 -0
  203. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/csv_tools.py +0 -0
  204. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/data_tools.py +0 -0
  205. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/format_tools.py +0 -0
  206. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/formula_tools.py +0 -0
  207. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/http.py +0 -0
  208. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/image_tools.py +0 -0
  209. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/object_tools.py +0 -0
  210. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/params.py +0 -0
  211. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/pivot_tools.py +0 -0
  212. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/registry.py +0 -0
  213. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/results.py +0 -0
  214. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/sheet_tools.py +0 -0
  215. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/slicer_tools.py +0 -0
  216. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/validation.py +0 -0
  217. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/vba_tools.py +0 -0
  218. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/vba_write_tools.py +0 -0
  219. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/server/workbook_tools.py +0 -0
  220. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/spill.py +0 -0
  221. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/structured.py +0 -0
  222. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/text.py +0 -0
  223. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/upload_links.py +0 -0
  224. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/upload_scan.py +0 -0
  225. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/values.py +0 -0
  226. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/src/excel_mcp/xlm.py +0 -0
  227. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.1}/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.0
3
+ Version: 2.2.1
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
@@ -98,6 +98,9 @@ Excel**, and runs on Windows, macOS and Linux.
98
98
  **Large files**
99
99
  - Reads stream the workbook and return pages, so memory stays flat on sheets with hundreds of
100
100
  thousands of rows; every call is bounded by size and cell limits
101
+ - Edits of a sheet of 1 MB of XML or more parse only the rows they touch, so inserting or
102
+ deleting rows and columns, sorting, replacing, copying a sheet and creating a PivotTable
103
+ take seconds on a 200,000-row sheet instead of minutes
101
104
 
102
105
  **Macros (opt-in)**
103
106
  - Read the VBA code in `.xlsm` files. Writing VBA is off unless you start the server with
@@ -62,6 +62,9 @@ Excel**, and runs on Windows, macOS and Linux.
62
62
  **Large files**
63
63
  - Reads stream the workbook and return pages, so memory stays flat on sheets with hundreds of
64
64
  thousands of rows; every call is bounded by size and cell limits
65
+ - Edits of a sheet of 1 MB of XML or more parse only the rows they touch, so inserting or
66
+ deleting rows and columns, sorting, replacing, copying a sheet and creating a PivotTable
67
+ take seconds on a 200,000-row sheet instead of minutes
65
68
 
66
69
  **Macros (opt-in)**
67
70
  - Read the VBA code in `.xlsm` files. Writing VBA is off unless you start the server with
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.0"
3
+ version = "2.2.1"
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.0"
3
+ version = "2.2.1"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -16,10 +16,12 @@ else before a "!" (a file, a path, a URL, "[1]") points at another workbook.
16
16
 
17
17
  import re
18
18
  from collections.abc import Iterable, Iterator
19
+ from functools import lru_cache
19
20
 
20
21
  from openpyxl.formula import Tokenizer
21
22
  from openpyxl.formula.tokenizer import Token, TokenizerError
22
23
 
24
+ from excel_mcp import plain_refs
23
25
  from excel_mcp.errors import InvalidFormulaError, UnsafeFormulaError
24
26
  from excel_mcp.formula_syntax import check_syntax, invalid_formula
25
27
  from excel_mcp.spill import store_spills
@@ -118,10 +120,28 @@ def storable_formula(formula: str, sheet_names: Iterable[str]) -> str:
118
120
 
119
121
  Every formula that is written anywhere in a workbook goes through here.
120
122
  """
123
+ pattern = plain_refs.shape(formula)
124
+ if pattern is not None and _is_plain_and_allowed(pattern):
125
+ return formula
121
126
  check_formula(formula, sheet_names)
122
127
  return add_prefixes(store_spills(formula))
123
128
 
124
129
 
130
+ @lru_cache(maxsize=4096)
131
+ def _is_plain_and_allowed(pattern: str) -> bool:
132
+ """Whether formulas of this shape pass the check and need no prefixes.
133
+
134
+ Which cells a plain formula refers to cannot change the outcome, so a sheet with hundreds
135
+ of thousands of formulas filled down from a few is checked a few times, not that often.
136
+ A formula that does not pass is checked again as itself, to raise its own error.
137
+ """
138
+ try:
139
+ check_formula(pattern, ())
140
+ except (InvalidFormulaError, UnsafeFormulaError):
141
+ return False
142
+ return add_prefixes(pattern) == pattern
143
+
144
+
125
145
  def storable_operand(operand: str, sheet_names: Iterable[str]) -> str:
126
146
  """`storable_formula` for rule operands and names, which are stored without the "="."""
127
147
  return storable_formula(f"={operand.removeprefix('=')}", sheet_names).removeprefix("=")
@@ -32,16 +32,9 @@ from openpyxl.worksheet.dimensions import DimensionHolder
32
32
  from openpyxl.worksheet.worksheet import Worksheet
33
33
  from openpyxl.xml.constants import SHARED_STRINGS
34
34
 
35
- from excel_mcp import lazy_shared
36
- from excel_mcp.lazy_workbook import (
37
- BLOCK_ROWS,
38
- LazyCells,
39
- Parts,
40
- SheetSource,
41
- lazy_sheets,
42
- open_lazily,
43
- split_sheet,
44
- )
35
+ from excel_mcp import lazy_rows, lazy_shared
36
+ from excel_mcp.lazy_source import SheetSource, split_sheet
37
+ from excel_mcp.lazy_workbook import BLOCK_ROWS, LazyCells, Parts, lazy_sheets, open_lazily
45
38
  from excel_mcp.package.opc import REL_BASE, Package, Rel, free_id
46
39
  from excel_mcp.package.restore import MAIN, sheet_parts
47
40
  from excel_mcp.package.scan import declare_in_root, root_scope
@@ -55,6 +48,7 @@ _ROW = re.compile(rb"<row\b[^>]*?/>|<row\b[^>]*>.*?</row>", re.S)
55
48
  _ROW_NUMBER = re.compile(rb'\br="(\d+)"')
56
49
  _DIMENSION = re.compile(rb'<dimension\s+ref="[^"]*"\s*/>')
57
50
  _CELL_COLUMN = re.compile(rb'<c\b[^>]*?\br="([A-Z]+)\d')
51
+ _UNADDRESSED_CELL = re.compile(rb"<c\b(?![^>]*\br=)")
58
52
  # A row with attributes of its own (a height, a style, hidden), not just its number.
59
53
  _ATTRIBUTED_ROW = re.compile(rb"<row\b[^>]*\s(?!r=|spans=|[\w.-]+:)[\w.-]+=[^>]*>")
60
54
  # What a formula stored as its result when Excel last calculated it, which an edit makes stale.
@@ -73,6 +67,8 @@ def _split(data: bytes) -> tuple[bytes, SheetSource] | None:
73
67
  cross_cell = (b't="array"', b't="dataTable"', b' cm="', b' vm="')
74
68
  if len(data) < LAZY_SHEET_BYTES or any(marker in data for marker in cross_cell):
75
69
  return None
70
+ if _UNADDRESSED_CELL.search(data): # Its column is not known until the row is read.
71
+ return None
76
72
  split = split_sheet(data)
77
73
  return split if split and split[1].row_numbers else None
78
74
 
@@ -148,27 +144,31 @@ class LazyEdit:
148
144
 
149
145
  path: Path
150
146
  strings_part: str | None
151
- sheets: list[Worksheet] = field(default_factory=list)
152
- frozen: dict[Worksheet, LazyCells] = field(default_factory=dict)
147
+ frozen: dict[Worksheet, tuple[LazyCells, dict[int, bytes]]] = field(default_factory=dict)
153
148
 
154
- def freeze(self) -> None:
155
- """Leave each sheet with the cells and row dimensions that were parsed, for saving.
149
+ def freeze(self, workbook: Workbook) -> None:
150
+ """Prepare each sheet for saving, with the rows that were parsed written as text.
156
151
 
157
- openpyxl writes what a sheet holds, and the rows of blocks that were not parsed are
158
- put in from the file by `merge`.
152
+ openpyxl writes what a sheet holds, which is only the rows that need it (those with a
153
+ link, a note or a value it writes in a form of its own). The other rows of blocks that
154
+ were parsed are written by `lazy_rows`, and the rows of blocks that were not parsed are
155
+ put in from the file; `merge` puts them all together.
159
156
  """
160
- for sheet in self.sheets:
157
+ for sheet in workbook.worksheets:
161
158
  cells = sheet._cells
162
- if isinstance(cells, LazyCells):
163
- lazy_shared.settle(cells)
159
+ if not isinstance(cells, LazyCells):
160
+ continue
161
+ lazy_shared.settle(cells)
164
162
  dimensions = DimensionHolder(worksheet=sheet, default_factory=sheet._add_row)
165
163
  for row, dimension in dict.items(sheet.row_dimensions):
166
- if not isinstance(cells, LazyCells) or cells.is_loaded(row):
164
+ if cells.is_loaded(row):
167
165
  dimensions[row] = dimension
168
- sheet.row_dimensions = dimensions
169
- if isinstance(cells, LazyCells):
170
- sheet._cells = cells.held()
171
- self.frozen[sheet] = cells
166
+ rendered, sheet._cells = lazy_rows.render(cells.held(), dimensions)
167
+ kept = {row for row, _ in sheet._cells}
168
+ sheet.row_dimensions = DimensionHolder(worksheet=sheet, default_factory=sheet._add_row)
169
+ for row in kept & dimensions.keys():
170
+ sheet.row_dimensions[row] = dimensions[row]
171
+ self.frozen[sheet] = (cells, rendered)
172
172
 
173
173
  def merge(self, workbook: Workbook, content: bytes) -> bytes:
174
174
  """The saved workbook with the rows that were not parsed put back, as the file has them."""
@@ -179,8 +179,9 @@ class LazyEdit:
179
179
  return content
180
180
  package = Package(content)
181
181
  parts = sheet_parts(package)
182
- for sheet, cells in spliced.items():
183
- package.write(parts[sheet.title], _splice(package.read(parts[sheet.title]), cells))
182
+ for sheet, (cells, rendered) in spliced.items():
183
+ xml = _splice(package.read(parts[sheet.title]), cells, rendered)
184
+ package.write(parts[sheet.title], xml)
184
185
  if self.strings_part:
185
186
  self._restore_strings(package)
186
187
  return package.to_bytes()
@@ -206,10 +207,10 @@ def load(path: Path) -> tuple[Workbook, LazyEdit | None]:
206
207
  reader.read()
207
208
  strings = reader.package.find(SHARED_STRINGS)
208
209
  edit = LazyEdit(path, strings.PartName.lstrip("/") if strings is not None else None)
209
- for sheet, source in lazy_sheets(reader, parts):
210
+ lazy = list(lazy_sheets(reader, parts))
211
+ for sheet, source in lazy:
210
212
  _install(sheet, source, reader.shared_strings)
211
- edit.sheets.append(sheet)
212
- return reader.wb, edit if edit.sheets else None
213
+ return reader.wb, edit if lazy else None
213
214
 
214
215
 
215
216
  def _install(sheet: Worksheet, source: SheetSource, strings: list[str]) -> None:
@@ -250,13 +251,14 @@ def _self_closed(tag: bytes) -> bytes:
250
251
  return tag if tag.endswith(b"/>") else tag[:-1] + b"/>"
251
252
 
252
253
 
253
- def _splice(xml: bytes, cells: LazyCells) -> bytes:
254
+ def _splice(xml: bytes, cells: LazyCells, rendered: dict[int, bytes]) -> bytes:
254
255
  """The sheet openpyxl wrote, holding the rows of the parsed blocks, with the others added."""
255
256
  source = cells.source
256
257
  data = _SHEET_DATA.search(xml)
257
258
  if data is None:
258
259
  raise ValueError("A saved worksheet has no sheetData.")
259
260
  produced = {_row_number(row[0]): row[0] for row in _ROW.finditer(data[0])}
261
+ produced.update(rendered)
260
262
  lazy_shared.restore(cells, produced)
261
263
  pieces = _rows(cells, produced)
262
264
  head, tail = xml[: data.start()], xml[data.end() :]
@@ -0,0 +1,83 @@
1
+ """Finding which rows of a sheet that is not parsed can hold a text, without parsing them.
2
+
3
+ Replacing text looks at every cell, but most of a large sheet holds none of it. Only the blocks
4
+ of rows that might hold a match are parsed; the others are as in the file. "Might" is decided
5
+ from the XML and errs on the side of parsing: a block that holds nothing is parsed at worst.
6
+ A cell is searched as `replace` searches it: text, numbers as their text, formulas as they are
7
+ written (without the storage prefixes), so a formula that is only a master of the shared
8
+ formulas below it matches for the cells that follow it too.
9
+ """
10
+
11
+ import re
12
+ from bisect import bisect_right
13
+ from collections.abc import Callable
14
+ from html import unescape
15
+
16
+ from excel_mcp.lazy_workbook import LazyCells
17
+
18
+ _STRING = re.compile(rb'<c\b[^>]*\bt="s"[^>]*><v>(\d+)</v>')
19
+ _INLINE = re.compile(rb"<is>(.*?)</is>", re.S)
20
+ _TAGS = re.compile(rb"<[^>]*>")
21
+ _FORMULA = re.compile(rb"<f\b([^>]*)>([^<]*)</f>")
22
+ _NUMBER = re.compile(rb'<c\b(?![^>]*\bt="(?!n")\w+")[^>]*><v>')
23
+ _GROUP = re.compile(rb'\bsi="(\d+)"')
24
+ _TEXT_OF_NUMBERS = frozenset("0123456789.-+e")
25
+ # What a stored formula has and its text as shown does not, or the reverse.
26
+ _STORAGE_MARKS = ("_xl", "anchorarray")
27
+
28
+
29
+ def load_matches(
30
+ cells: LazyCells, matches: Callable[[str], bool], query: str, *, in_formulas: bool
31
+ ) -> None:
32
+ """Parse the blocks that may hold a cell for which ``matches(text)`` is true.
33
+
34
+ ``query`` is what ``matches`` looks for, which tells what kinds of cell it can match.
35
+ """
36
+ source = cells.source
37
+ starts = [start for start, _ in source.spans]
38
+ rows: set[int] = set()
39
+
40
+ def mark(position: int) -> None:
41
+ rows.add(source.row_numbers[bisect_right(starts, position) - 1])
42
+
43
+ hits = {index for index, text in enumerate(cells.strings) if matches(text)}
44
+ if hits:
45
+ for found in _STRING.finditer(source.data):
46
+ if int(found[1]) in hits:
47
+ mark(found.start())
48
+ for found in _INLINE.finditer(source.data):
49
+ if matches(unescape(_TAGS.sub(b"", found[1]).decode())):
50
+ mark(found.start())
51
+ if set(query.lower()) <= _TEXT_OF_NUMBERS:
52
+ rows.update(_rows_with_numbers(cells))
53
+ if in_formulas:
54
+ _mark_formulas(cells, matches, query, mark)
55
+ for number in rows:
56
+ cells.load(number)
57
+
58
+
59
+ def _rows_with_numbers(cells: LazyCells) -> list[int]:
60
+ source = cells.source
61
+ return [
62
+ number
63
+ for number, (start, stop) in zip(source.row_numbers, source.spans, strict=True)
64
+ if _NUMBER.search(source.data, start, stop)
65
+ ]
66
+
67
+
68
+ def _mark_formulas(
69
+ cells: LazyCells, matches: Callable[[str], bool], query: str, mark: Callable[[int], None]
70
+ ) -> None:
71
+ source = cells.source
72
+ digits = any(character.isdigit() for character in query)
73
+ for found in _FORMULA.finditer(source.data):
74
+ shown = "=" + unescape(found[2].decode())
75
+ hit = matches(shown) or any(part in shown.lower() for part in _STORAGE_MARKS)
76
+ if hit:
77
+ mark(found.start())
78
+ group = _GROUP.search(found[1])
79
+ master = source.masters.get(group[1].decode()) if group else None
80
+ # A cell that follows its master differs from it in digits, and in letters when the
81
+ # group spans columns.
82
+ if master is not None and (hit or digits or master.wide):
83
+ cells.load_between(*master.rows)
@@ -0,0 +1,259 @@
1
+ """Rows inserted or deleted, and references moved, in a sheet whose rows are not parsed.
2
+
3
+ A large sheet is held as the XML of its rows (`lazy_workbook`). Inserting rows above most of
4
+ them renumbers the rows and rewrites the references in their formulas, which needs no cells:
5
+ both are done on the XML, row by row, and only the rows around the edit are parsed.
6
+
7
+ Formulas filled down are stored as one master and the cells that follow it (`lazy_shared`).
8
+ They stay that way when every cell of the group moves alike: rewriting the master then
9
+ rewrites them all. A group whose cells or references fall on both sides of the edit (such a
10
+ cell now refers to another one than its master does) is parsed first and becomes ordinary
11
+ formulas, as Excel splits it.
12
+ """
13
+
14
+ import re
15
+ from collections.abc import Callable
16
+ from dataclasses import replace
17
+ from functools import lru_cache
18
+ from html import unescape
19
+ from typing import Any
20
+ from xml.sax.saxutils import escape
21
+
22
+ from openpyxl.formula.translate import Translator
23
+ from openpyxl.utils import column_index_from_string, get_column_letter
24
+ from openpyxl.worksheet.worksheet import Worksheet
25
+
26
+ from excel_mcp.formulas import storable_formula
27
+ from excel_mcp.lazy_shared import SharedMaster
28
+ from excel_mcp.lazy_source import SheetSource
29
+ from excel_mcp.lazy_workbook import CellKey, LazyCells
30
+ from excel_mcp.package.lines import LineEdit
31
+ from excel_mcp.refs import parse_range
32
+ from excel_mcp.rewrite import ReferenceRewriter
33
+
34
+ _FORMULA = re.compile(rb"<f\b([^>]*)>([^<]*)</f>")
35
+ _ROW_NUMBERS = re.compile(rb'(<row\b[^>]*?\br="|<c\b[^>]*?\br="[A-Z]+)(\d+)(")')
36
+ _REF = re.compile(rb' ref="([^"]*)"')
37
+ _CELLS = re.compile(rb"<c\b[^>]*?(?:/>|>.*?</c>)", re.S)
38
+ _CELL_REF = re.compile(rb'<c\b[^>]*?\br="([A-Z]+)(\d+)"')
39
+ _STYLE = re.compile(rb' s="(\d+)"')
40
+ _SPANS = re.compile(rb' spans="[^"]*"')
41
+
42
+ Row = tuple[int, bytes]
43
+
44
+
45
+ def unshare(cells: LazyCells, host: str, rewriter: ReferenceRewriter, edit: LineEdit) -> None:
46
+ """Parse the groups of shared formulas that ``edit`` would not leave alike."""
47
+ for master in cells.source.masters.values():
48
+ if not _moves_alike(master, host, rewriter, edit):
49
+ cells.load_between(*master.rows)
50
+
51
+
52
+ def _moves_alike(
53
+ master: SharedMaster, host: str, rewriter: ReferenceRewriter, edit: LineEdit
54
+ ) -> bool:
55
+ """Whether every cell of the group ends up with the formula its master gives it.
56
+
57
+ The group moves alike if the master's rewritten formula, copied to the farthest cell of the
58
+ group, is what rewriting the formula of that cell gives. The shift of a reference is the
59
+ same for the cells in between, because it changes only once, at the edit.
60
+ """
61
+ area = parse_range(master.ref)
62
+ if host.casefold() == edit.sheet.casefold():
63
+ first, last = (
64
+ (area.min_row, area.max_row) if edit.axis == "rows" else (area.min_col, area.max_col)
65
+ )
66
+ if edit.cuts(first, last):
67
+ return False
68
+ far = f"{get_column_letter(area.max_col)}{area.max_row}"
69
+ formula = "=" + master.text
70
+ copied = Translator(formula, master.coordinate).translate_formula(far)
71
+ moved = Translator(rewriter.formula(formula, host), master.coordinate).translate_formula(far)
72
+ return moved == rewriter.formula(copied, host)
73
+
74
+
75
+ def rewrite_formulas(
76
+ cells: LazyCells,
77
+ host: str,
78
+ rewriter: ReferenceRewriter,
79
+ names: list[str],
80
+ *,
81
+ check_all: bool = False,
82
+ ) -> None:
83
+ """Rewrite the references of the formulas in the rows that are not parsed.
84
+
85
+ A formula that changes is checked as every written formula is; ``check_all`` also checks
86
+ those that stay as they are (a copy of a sheet writes them all).
87
+ """
88
+ source = cells.source
89
+ cache: dict[str, str] = {}
90
+
91
+ def rewrite(text: str) -> str:
92
+ if text not in cache:
93
+ formula = "=" + text
94
+ moved = rewriter.formula(formula, host)
95
+ unchanged = moved == formula and not check_all
96
+ cache[text] = text if unchanged else storable_formula(moved, names)[1:]
97
+ return cache[text]
98
+
99
+ def replace_formula(match: re.Match[bytes]) -> bytes:
100
+ text = unescape(match[2].decode())
101
+ moved = rewrite(text)
102
+ if moved == text:
103
+ return match[0]
104
+ return b"<f%b>%b</f>" % (match[1], escape(moved).encode())
105
+
106
+ rows: list[Row] = []
107
+ for number, (start, stop) in zip(source.row_numbers, source.spans, strict=True):
108
+ row = source.data[start:stop]
109
+ if b"<f" in row and not cells.is_loaded(number):
110
+ row = _FORMULA.sub(replace_formula, row)
111
+ rows.append((number, row))
112
+ masters = {group: replace(m, text=rewrite(m.text)) for group, m in source.masters.items()}
113
+ cells.source = _assembled(source, rows, masters, source.hidden_rows)
114
+
115
+
116
+ def move_lines(
117
+ cells: LazyCells,
118
+ edit: LineEdit,
119
+ moved: dict[CellKey, Any],
120
+ inherit: Callable[[int, int | None], int] | None,
121
+ ) -> None:
122
+ """Insert or delete rows or columns: move the rows that are not parsed, in their XML, and
123
+ hold ``moved`` (the cells that are parsed, already moved) with them.
124
+
125
+ ``inherit`` gives the style of a cell that is inserted next to one: from the style of that
126
+ cell and of the one on its other side.
127
+ """
128
+ source = cells.source
129
+ rows: list[Row] = []
130
+ hidden = source.hidden_rows
131
+ parsed = cells.parsed_rows()
132
+ if edit.axis == "rows":
133
+ move_row = _row_mover(edit)
134
+ for number, (start, stop) in zip(source.row_numbers, source.spans, strict=True):
135
+ if (index := edit.index(number)) is not None and not cells.is_loaded(number):
136
+ rows.append((index, move_row(source.data[start:stop], index - number)))
137
+ hidden = [index for row in hidden if (index := edit.index(row)) is not None]
138
+ parsed = [span for first, last in parsed if (span := edit.span(first, last))]
139
+ else:
140
+ move_cells = _column_mover(edit, inherit)
141
+ for number, (start, stop) in zip(source.row_numbers, source.spans, strict=True):
142
+ if not cells.is_loaded(number):
143
+ rows.append((number, move_cells(source.data[start:stop])))
144
+ cells.replace(moved, _assembled(source, rows, _moved_masters(source, edit), hidden), parsed)
145
+
146
+
147
+ def copied(cells: LazyCells, sheet: Worksheet) -> LazyCells:
148
+ """The cells of a copy of a sheet: the rows that are not parsed are shared with the original,
149
+ which keeps the cells that are parsed."""
150
+ source = cells.source
151
+ rows = [
152
+ (number, source.data[start:stop])
153
+ for number, (start, stop) in zip(source.row_numbers, source.spans, strict=True)
154
+ if not cells.is_loaded(number)
155
+ ]
156
+ masters = dict(source.masters)
157
+ return LazyCells(sheet, _assembled(source, rows, masters, source.hidden_rows), cells.strings)
158
+
159
+
160
+ def _moved_masters(source: SheetSource, edit: LineEdit) -> dict[str, SharedMaster]:
161
+ masters = {}
162
+ rows = edit.axis == "rows"
163
+ for group, master in source.masters.items():
164
+ area = edit.range(parse_range(master.ref))
165
+ line = edit.index(master.row if rows else master.column)
166
+ if line is not None and area is not None:
167
+ position = {"row": line} if rows else {"column": line}
168
+ masters[group] = replace(master, ref=str(area), **position)
169
+ return masters
170
+
171
+
172
+ def _ref_mover(edit: LineEdit) -> Callable[[re.Match[bytes]], bytes]:
173
+ """What a shared formula's ``ref`` becomes."""
174
+
175
+ def move(match: re.Match[bytes]) -> bytes:
176
+ area = edit.range(parse_range(match[1].decode()))
177
+ return b' ref="%b"' % str(area).encode()
178
+
179
+ return move
180
+
181
+
182
+ def _row_mover(edit: LineEdit) -> Callable[[bytes, int], bytes]:
183
+ """A function that moves a row of XML down by some rows: its number, its cells, and the
184
+ range of a shared formula it holds."""
185
+ move_ref = _ref_mover(edit)
186
+
187
+ def move(row: bytes, delta: int) -> bytes:
188
+ if not delta:
189
+ return row
190
+ row = _ROW_NUMBERS.sub(lambda m: m[1] + b"%d" % (int(m[2]) + delta) + m[3], row)
191
+ return _REF.sub(move_ref, row) if b" ref=" in row else row
192
+
193
+ return move
194
+
195
+
196
+ def _column_mover(
197
+ edit: LineEdit, inherit: Callable[[int, int | None], int] | None
198
+ ) -> Callable[[bytes], bytes]:
199
+ """A function that moves the cells of a row of XML sideways: it drops those in deleted
200
+ columns, renames the others and gives the cells inserted next to one its format."""
201
+ move_ref = _ref_mover(edit)
202
+
203
+ def move(row: bytes) -> bytes:
204
+ found = list(_CELLS.finditer(row))
205
+ if not found:
206
+ return row
207
+ cells = []
208
+ for match in found:
209
+ ref = _CELL_REF.match(match[0])
210
+ assert ref is not None # `split_sheet` leaves out sheets with cells without an address.
211
+ cells.append((_column(ref[1]), ref, match[0]))
212
+ styles = {column: _style(cell) for column, _, cell in cells} if inherit else {}
213
+ kept = []
214
+ for column, ref, cell in cells:
215
+ line = edit.index(column)
216
+ if line is None:
217
+ continue
218
+ if line != column:
219
+ cell = cell[: ref.start(1)] + _letters(line) + cell[ref.end(1) :]
220
+ cell = _REF.sub(move_ref, cell) if b" ref=" in cell else cell
221
+ kept.append(cell)
222
+ if inherit and column == edit.at - 1:
223
+ style = inherit(styles[column], styles.get(edit.at))
224
+ kept.extend(
225
+ b'<c r="%b%b" s="%d"/>' % (_letters(new), ref[2], style)
226
+ for new in range(edit.at, edit.at + edit.count)
227
+ if style
228
+ )
229
+ head = _SPANS.sub(b"", row[: found[0].start()])
230
+ return head + b"".join(kept) + row[found[-1].end() :]
231
+
232
+ return move
233
+
234
+
235
+ @lru_cache(maxsize=1024)
236
+ def _column(letters: bytes) -> int:
237
+ return column_index_from_string(letters.decode())
238
+
239
+
240
+ @lru_cache(maxsize=1024)
241
+ def _letters(column: int) -> bytes:
242
+ return get_column_letter(column).encode()
243
+
244
+
245
+ def _style(cell: bytes) -> int:
246
+ found = _STYLE.search(cell, 0, cell.index(b">"))
247
+ return int(found[1]) if found else 0
248
+
249
+
250
+ def _assembled(
251
+ source: SheetSource, rows: list[Row], masters: dict[str, SharedMaster], hidden: list[int]
252
+ ) -> SheetSource:
253
+ spans = []
254
+ offset = 0
255
+ for _, row in rows:
256
+ spans.append((offset, offset + len(row)))
257
+ offset += len(row)
258
+ data = b"".join(row for _, row in rows)
259
+ return SheetSource(data, source.root, [number for number, _ in rows], spans, hidden, masters)