excel-mcp-server 2.2.0__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/PKG-INFO +6 -2
  2. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/README.md +4 -0
  3. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/pyproject.toml +3 -3
  4. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/pyproject.toml.orig +3 -3
  5. excel_mcp_server-2.2.2/src/excel_mcp/cell_budget.py +73 -0
  6. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/cli.py +8 -0
  7. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/config.py +2 -0
  8. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/formulas.py +20 -0
  9. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_edit.py +35 -35
  10. excel_mcp_server-2.2.2/src/excel_mcp/lazy_find.py +83 -0
  11. excel_mcp_server-2.2.2/src/excel_mcp/lazy_lines.py +261 -0
  12. excel_mcp_server-2.2.2/src/excel_mcp/lazy_rows.py +198 -0
  13. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_shared.py +49 -3
  14. excel_mcp_server-2.2.2/src/excel_mcp/lazy_source.py +141 -0
  15. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/lazy_workbook.py +150 -119
  16. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/calculated.py +9 -2
  17. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/cells.py +33 -0
  18. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/hyperlinks.py +2 -1
  19. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/inspect.py +2 -2
  20. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/layout.py +1 -1
  21. excel_mcp_server-2.2.2/src/excel_mcp/operations/line_edit_styles.py +94 -0
  22. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edits.py +28 -51
  23. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/notes.py +2 -1
  24. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot.py +6 -7
  25. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_source.py +49 -1
  26. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/replace.py +11 -2
  27. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_copy.py +35 -8
  28. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_refs.py +10 -0
  29. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/shifting.py +32 -0
  30. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/workbook_rewrite.py +6 -2
  31. excel_mcp_server-2.2.2/src/excel_mcp/plain_refs.py +106 -0
  32. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/sheet_tools.py +1 -1
  33. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/workspace.py +32 -17
  34. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/xlfn.py +4 -0
  35. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/LICENSE +0 -0
  36. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/__init__.py +0 -0
  37. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/__main__.py +0 -0
  38. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/cells.py +0 -0
  39. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/criteria.py +0 -0
  40. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/dependencies.py +0 -0
  41. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/dynamic.py +0 -0
  42. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/engine.py +0 -0
  43. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/__init__.py +0 -0
  44. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/arrays.py +0 -0
  45. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/conditional.py +0 -0
  46. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/datetime.py +0 -0
  47. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/depreciation.py +0 -0
  48. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/distributions.py +0 -0
  49. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/financial.py +0 -0
  50. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/helpers.py +0 -0
  51. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/indirect.py +0 -0
  52. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/information.py +0 -0
  53. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/logical.py +0 -0
  54. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/lookup.py +0 -0
  55. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/math.py +0 -0
  56. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/regression.py +0 -0
  57. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/securities.py +0 -0
  58. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/statistical.py +0 -0
  59. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/subtotals.py +0 -0
  60. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/functions/text.py +0 -0
  61. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/operators.py +0 -0
  62. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/parser.py +0 -0
  63. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/r1c1.py +0 -0
  64. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/registry.py +0 -0
  65. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/scheduler.py +0 -0
  66. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/solver.py +0 -0
  67. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/steps.py +0 -0
  68. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/tablerefs.py +0 -0
  69. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/textformat.py +0 -0
  70. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/values.py +0 -0
  71. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/calc/wildcard.py +0 -0
  72. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/cfb.py +0 -0
  73. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/chart_roundtrip.py +0 -0
  74. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/errors.py +0 -0
  75. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/formula_syntax.py +0 -0
  76. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/function_arguments.json +0 -0
  77. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/inputs.py +0 -0
  78. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/links.py +0 -0
  79. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/macros.py +0 -0
  80. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/__init__.py +0 -0
  81. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chart_index.py +0 -0
  82. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chart_info.py +0 -0
  83. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex.py +0 -0
  84. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/box_whisker_style.xml +0 -0
  85. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/colors.xml +0 -0
  86. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/funnel_style.xml +0 -0
  87. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/histogram_style.xml +0 -0
  88. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/sunburst_style.xml +0 -0
  89. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/treemap_style.xml +0 -0
  90. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_parts/waterfall_style.xml +0 -0
  91. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/chartex_xml.py +0 -0
  92. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts.py +0 -0
  93. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_axes.py +0 -0
  94. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_check.py +0 -0
  95. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_data.py +0 -0
  96. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_look.py +0 -0
  97. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_modern.py +0 -0
  98. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_options.py +0 -0
  99. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_series.py +0 -0
  100. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/charts_style.py +0 -0
  101. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/columns.py +0 -0
  102. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/comparison.py +0 -0
  103. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional.py +0 -0
  104. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_kinds.py +0 -0
  105. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_rule.py +0 -0
  106. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/conditional_x14.py +0 -0
  107. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_export.py +0 -0
  108. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_format.py +0 -0
  109. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_import.py +0 -0
  110. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/csv_values.py +0 -0
  111. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/date_width.py +0 -0
  112. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/drawings.py +0 -0
  113. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/duplicates.py +0 -0
  114. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/files.py +0 -0
  115. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/fill.py +0 -0
  116. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filter_criteria.py +0 -0
  117. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filter_stored.py +0 -0
  118. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/filters.py +0 -0
  119. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/formatting.py +0 -0
  120. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/goal_seek.py +0 -0
  121. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/images.py +0 -0
  122. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_objects.py +0 -0
  123. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/line_edit_rules.py +0 -0
  124. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/names.py +0 -0
  125. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/paste.py +0 -0
  126. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_axis.py +0 -0
  127. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_cache.py +0 -0
  128. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_calc.py +0 -0
  129. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_definition.py +0 -0
  130. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_fields.py +0 -0
  131. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_groups.py +0 -0
  132. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_index.py +0 -0
  133. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_options.py +0 -0
  134. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_rebuild.py +0 -0
  135. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_records.py +0 -0
  136. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_render.py +0 -0
  137. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_showas.py +0 -0
  138. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_specs.py +0 -0
  139. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/pivot_values.py +0 -0
  140. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/print_setup.py +0 -0
  141. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/protection.py +0 -0
  142. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/rule_clear.py +0 -0
  143. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/rules.py +0 -0
  144. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_counts.py +0 -0
  145. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_shapes.py +0 -0
  146. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheet_view.py +0 -0
  147. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sheets.py +0 -0
  148. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_index.py +0 -0
  149. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_manage.py +0 -0
  150. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_pivot.py +0 -0
  151. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_place.py +0 -0
  152. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_table.py +0 -0
  153. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicer_xml.py +0 -0
  154. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/slicers.py +0 -0
  155. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sorting.py +0 -0
  156. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/spans.py +0 -0
  157. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparkline_style.py +0 -0
  158. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparkline_xml.py +0 -0
  159. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/sparklines.py +0 -0
  160. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/spill.py +0 -0
  161. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/table_options.py +0 -0
  162. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/tables.py +0 -0
  163. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/text_split.py +0 -0
  164. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/trace.py +0 -0
  165. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/transform.py +0 -0
  166. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/used_range.py +0 -0
  167. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/vba.py +0 -0
  168. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/vba_write.py +0 -0
  169. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/workbook_settings.py +0 -0
  170. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/operations/x14_xml.py +0 -0
  171. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba.py +0 -0
  172. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba_compress.py +0 -0
  173. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/ovba_write.py +0 -0
  174. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/__init__.py +0 -0
  175. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/anchors.py +0 -0
  176. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/arrays.py +0 -0
  177. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/capture.py +0 -0
  178. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/capture_sheet.py +0 -0
  179. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/consistency.py +0 -0
  180. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/extensions.py +0 -0
  181. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/guards.py +0 -0
  182. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/lazy_records.py +0 -0
  183. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/lines.py +0 -0
  184. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/metadata.py +0 -0
  185. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/model.py +0 -0
  186. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/opc.py +0 -0
  187. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/patch.py +0 -0
  188. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/pivot_records.py +0 -0
  189. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/pivots.py +0 -0
  190. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/properties.py +0 -0
  191. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/reader.py +0 -0
  192. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/references.py +0 -0
  193. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/restore.py +0 -0
  194. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/restore_sheet.py +0 -0
  195. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/scan.py +0 -0
  196. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/shape_names.py +0 -0
  197. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/package/vml.py +0 -0
  198. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/paths.py +0 -0
  199. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/py.typed +0 -0
  200. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/refs.py +0 -0
  201. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/rewrite.py +0 -0
  202. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/__init__.py +0 -0
  203. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/annotation_tools.py +0 -0
  204. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/areas.py +0 -0
  205. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/csv_tools.py +0 -0
  206. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/data_tools.py +0 -0
  207. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/format_tools.py +0 -0
  208. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/formula_tools.py +0 -0
  209. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/http.py +0 -0
  210. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/image_tools.py +0 -0
  211. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/object_tools.py +0 -0
  212. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/params.py +0 -0
  213. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/pivot_tools.py +0 -0
  214. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/registry.py +0 -0
  215. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/results.py +0 -0
  216. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/slicer_tools.py +0 -0
  217. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/validation.py +0 -0
  218. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/vba_tools.py +0 -0
  219. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/vba_write_tools.py +0 -0
  220. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/server/workbook_tools.py +0 -0
  221. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/spill.py +0 -0
  222. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/structured.py +0 -0
  223. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/text.py +0 -0
  224. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/upload_links.py +0 -0
  225. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/upload_scan.py +0 -0
  226. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/values.py +0 -0
  227. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/xlm.py +0 -0
  228. {excel_mcp_server-2.2.0 → excel_mcp_server-2.2.2}/src/excel_mcp/zip_limits.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: excel-mcp-server
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: MCP server for creating, reading and editing Excel workbooks without Microsoft Excel
5
5
  Keywords: mcp,mcp-server,model-context-protocol,excel,xlsx,xlsm,spreadsheet,openpyxl,charts,pivot-table,formulas,vba,llm,ai,claude,cursor
6
6
  Author: Haris Musa
@@ -23,7 +23,7 @@ Classifier: Typing :: Typed
23
23
  Requires-Dist: mcp>=2.2,<3
24
24
  Requires-Dist: olefile>=0.47,<1
25
25
  Requires-Dist: openpyxl>=3.1.5,<4
26
- Requires-Dist: pillow>=12,<13
26
+ Requires-Dist: pillow>=13,<14
27
27
  Requires-Dist: pydantic>=2.12,<3
28
28
  Requires-Dist: starlette>=1,<2
29
29
  Requires-Dist: uvicorn>=0.31,<1
@@ -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
@@ -231,6 +234,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
231
234
  | `--max-file-mb N` | `EXCEL_MCP_MAX_FILE_MB` | `100` | Largest workbook the server opens. Also sets the unpacked-size limit (10 times this) that every opened or uploaded workbook must stay under; parts compressed over 500:1 are refused |
232
235
  | `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
233
236
  | `--max-pivot-cells N` | `EXCEL_MCP_MAX_PIVOT_CELLS` | `3000000` | Largest PivotTable source in cells (a 230,000-row, 13-column sheet); creating one from the biggest sources takes a minute and about 2 GB of memory |
237
+ | `--max-loaded-cells N` | `EXCEL_MCP_MAX_LOADED_CELLS` | `3000000` | Most cells and rows of a workbook that may be loaded into memory at once, about 0.5 KB each (a 3,000,000 limit is about 1.5 GB). A workbook with more is refused by the tools that must load whole sheets; `read_range` and the other streaming reads, and edits of large sheets that parse only some rows, are not limited by the size of the sheet |
234
238
  | `--log-level LEVEL` | | `WARNING` | Logging on stderr |
235
239
  | `--host HOST` | `EXCEL_MCP_HOST` | `127.0.0.1` | HTTP listen address |
236
240
  | `--port PORT` | `EXCEL_MCP_PORT` | `8017` | HTTP port |
@@ -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
@@ -195,6 +198,7 @@ docker run -p 8017:8017 -v "$PWD/workbooks:/data" -e EXCEL_MCP_AUTH_TOKEN=change
195
198
  | `--max-file-mb N` | `EXCEL_MCP_MAX_FILE_MB` | `100` | Largest workbook the server opens. Also sets the unpacked-size limit (10 times this) that every opened or uploaded workbook must stay under; parts compressed over 500:1 are refused |
196
199
  | `--max-cells N` | `EXCEL_MCP_MAX_CELLS` | `100000` | Most cells one call may read, write or format; reads are still returned in pages |
197
200
  | `--max-pivot-cells N` | `EXCEL_MCP_MAX_PIVOT_CELLS` | `3000000` | Largest PivotTable source in cells (a 230,000-row, 13-column sheet); creating one from the biggest sources takes a minute and about 2 GB of memory |
201
+ | `--max-loaded-cells N` | `EXCEL_MCP_MAX_LOADED_CELLS` | `3000000` | Most cells and rows of a workbook that may be loaded into memory at once, about 0.5 KB each (a 3,000,000 limit is about 1.5 GB). A workbook with more is refused by the tools that must load whole sheets; `read_range` and the other streaming reads, and edits of large sheets that parse only some rows, are not limited by the size of the sheet |
198
202
  | `--log-level LEVEL` | | `WARNING` | Logging on stderr |
199
203
  | `--host HOST` | `EXCEL_MCP_HOST` | `127.0.0.1` | HTTP listen address |
200
204
  | `--port PORT` | `EXCEL_MCP_PORT` | `8017` | HTTP port |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.0"
3
+ version = "2.2.2"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -43,7 +43,7 @@ dependencies = [
43
43
  "mcp>=2.2,<3",
44
44
  "olefile>=0.47,<1",
45
45
  "openpyxl>=3.1.5,<4",
46
- "pillow>=12,<13",
46
+ "pillow>=13,<14",
47
47
  "pydantic>=2.12,<3",
48
48
  "starlette>=1,<2",
49
49
  "uvicorn>=0.31,<1",
@@ -71,7 +71,7 @@ dev = [
71
71
  ]
72
72
 
73
73
  [build-system]
74
- requires = ["uv_build>=0.12.19,<0.13"]
74
+ requires = ["uv_build>=0.13.1,<0.14"]
75
75
  build-backend = "uv_build"
76
76
 
77
77
  [tool.ruff]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "excel-mcp-server"
3
- version = "2.2.0"
3
+ version = "2.2.2"
4
4
  description = "MCP server for creating, reading and editing Excel workbooks without Microsoft Excel"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -44,7 +44,7 @@ dependencies = [
44
44
  "mcp>=2.2,<3",
45
45
  "olefile>=0.47,<1",
46
46
  "openpyxl>=3.1.5,<4",
47
- "pillow>=12,<13",
47
+ "pillow>=13,<14",
48
48
  "pydantic>=2.12,<3",
49
49
  "starlette>=1,<2",
50
50
  "uvicorn>=0.31,<1",
@@ -68,7 +68,7 @@ dev = [
68
68
  ]
69
69
 
70
70
  [build-system]
71
- requires = ["uv_build>=0.12.19,<0.13"]
71
+ requires = ["uv_build>=0.13.1,<0.14"]
72
72
  build-backend = "uv_build"
73
73
 
74
74
  [tool.ruff]
@@ -0,0 +1,73 @@
1
+ """A limit on the cells that a workbook's sheets make openpyxl create.
2
+
3
+ Memory use is set by the number of cells created, not by the size of the file: a sheet of
4
+ millions of empty cells is a few hundred kilobytes. So every sheet is counted before it is
5
+ parsed, and what is created cannot pass the limit. Rows count as cells (openpyxl keeps their
6
+ attributes), and so does every cell of a merged range or hyperlink range, which it creates
7
+ from the one element that names the range.
8
+ """
9
+
10
+ from pathlib import Path
11
+ from typing import IO
12
+ from xml.parsers import expat
13
+
14
+ from openpyxl.reader.excel import ExcelReader
15
+ from openpyxl.xml.constants import SHEET_MAIN_NS, WORKSHEET_TYPE
16
+
17
+ from excel_mcp.errors import LimitExceededError
18
+ from excel_mcp.refs import parse_range
19
+
20
+ _CHUNK = 1 << 20
21
+ _NAMESPACE = f"{SHEET_MAIN_NS} "
22
+ _COUNTED = {f"{_NAMESPACE}c", f"{_NAMESPACE}row"}
23
+ _MERGED = f"{_NAMESPACE}mergeCell"
24
+ _LINKED = f"{_NAMESPACE}hyperlink"
25
+ # A cell that holds a link takes about three times the memory of one that does not.
26
+ _LINK_COST = 3
27
+
28
+
29
+ class CellBudget:
30
+ """The cells that may still be created for one workbook, shared by all its sheets."""
31
+
32
+ def __init__(self, limit: int) -> None:
33
+ self.limit = limit
34
+ self.used = 0
35
+
36
+ def spend(self, count: int) -> None:
37
+ self.used += count
38
+ if self.used > self.limit:
39
+ raise LimitExceededError(
40
+ f"The workbook has more than {self.limit:,} cells and rows to load, which "
41
+ "would use too much memory. Read it with read_range, split it into smaller "
42
+ "workbooks, or raise --max-loaded-cells."
43
+ )
44
+
45
+
46
+ def spend_sheet(source: IO[bytes], budget: CellBudget) -> None:
47
+ """Spend the cells and rows of one worksheet's XML, stopping at the first one over the limit."""
48
+ parser = expat.ParserCreate(namespace_separator=" ")
49
+
50
+ def start(name: str, attributes: dict[str, str]) -> None:
51
+ if name in _COUNTED:
52
+ budget.spend(1)
53
+ elif name == _MERGED:
54
+ budget.spend(parse_range(attributes["ref"]).size)
55
+ elif name == _LINKED:
56
+ budget.spend(_LINK_COST * parse_range(attributes["ref"]).size)
57
+
58
+ parser.StartElementHandler = start
59
+ while chunk := source.read(_CHUNK):
60
+ parser.Parse(chunk, False)
61
+ parser.Parse(b"", True)
62
+
63
+
64
+ def check_workbook(path: Path, budget: CellBudget) -> None:
65
+ """Spend the cells and rows that loading all of a workbook's worksheets would create."""
66
+ reader = ExcelReader(path)
67
+ try:
68
+ reader.read_manifest()
69
+ for part in reader.package.findall(WORKSHEET_TYPE):
70
+ with reader.archive.open(part.PartName.lstrip("/")) as source:
71
+ spend_sheet(source, budget)
72
+ finally:
73
+ reader.archive.close()
@@ -78,6 +78,7 @@ def _settings(args: argparse.Namespace) -> Settings:
78
78
  max_file_bytes=args.max_file_mb * MB,
79
79
  max_cells=args.max_cells,
80
80
  max_pivot_source_cells=args.max_pivot_cells,
81
+ max_loaded_cells=args.max_loaded_cells,
81
82
  ),
82
83
  log_level=args.log_level,
83
84
  )
@@ -159,6 +160,13 @@ def _parser() -> argparse.ArgumentParser:
159
160
  help="Largest PivotTable source, in cells (default %(default)s). "
160
161
  "Also EXCEL_MCP_MAX_PIVOT_CELLS.",
161
162
  )
163
+ common.add_argument(
164
+ "--max-loaded-cells",
165
+ type=_positive_int,
166
+ default=_env_default("EXCEL_MCP_MAX_LOADED_CELLS", DEFAULT_LIMITS.max_loaded_cells),
167
+ help="Most cells and rows of a workbook that may be loaded into memory at once, about "
168
+ "0.5 KB each (default %(default)s). Also EXCEL_MCP_MAX_LOADED_CELLS.",
169
+ )
162
170
  common.add_argument(
163
171
  "--log-level",
164
172
  default="WARNING",
@@ -14,6 +14,8 @@ class Limits:
14
14
  max_cells: int = 100_000
15
15
  max_pivot_source_cells: int = 3_000_000
16
16
  max_copy_cells: int = 1_000_000
17
+ # Cells and rows that all the sheets of a workbook may create in memory, about 0.5 KB each.
18
+ max_loaded_cells: int = 3_000_000
17
19
  # Zip bomb guards, applied to every workbook the server opens or receives. Real workbooks
18
20
  # compress 10 to 30 times and sparse sheets far more, so these leave room for them.
19
21
  max_unpack_factor: int = 10
@@ -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,10 @@ 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.cell_budget import CellBudget
37
+ from excel_mcp.lazy_source import SheetSource, split_sheet
38
+ from excel_mcp.lazy_workbook import BLOCK_ROWS, LazyCells, Parts, lazy_sheets, open_lazily
45
39
  from excel_mcp.package.opc import REL_BASE, Package, Rel, free_id
46
40
  from excel_mcp.package.restore import MAIN, sheet_parts
47
41
  from excel_mcp.package.scan import declare_in_root, root_scope
@@ -148,27 +142,31 @@ class LazyEdit:
148
142
 
149
143
  path: Path
150
144
  strings_part: str | None
151
- sheets: list[Worksheet] = field(default_factory=list)
152
- frozen: dict[Worksheet, LazyCells] = field(default_factory=dict)
145
+ frozen: dict[Worksheet, tuple[LazyCells, dict[int, bytes]]] = field(default_factory=dict)
153
146
 
154
- def freeze(self) -> None:
155
- """Leave each sheet with the cells and row dimensions that were parsed, for saving.
147
+ def freeze(self, workbook: Workbook) -> None:
148
+ """Prepare each sheet for saving, with the rows that were parsed written as text.
156
149
 
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`.
150
+ openpyxl writes what a sheet holds, which is only the rows that need it (those with a
151
+ link, a note or a value it writes in a form of its own). The other rows of blocks that
152
+ were parsed are written by `lazy_rows`, and the rows of blocks that were not parsed are
153
+ put in from the file; `merge` puts them all together.
159
154
  """
160
- for sheet in self.sheets:
155
+ for sheet in workbook.worksheets:
161
156
  cells = sheet._cells
162
- if isinstance(cells, LazyCells):
163
- lazy_shared.settle(cells)
157
+ if not isinstance(cells, LazyCells):
158
+ continue
159
+ lazy_shared.settle(cells)
164
160
  dimensions = DimensionHolder(worksheet=sheet, default_factory=sheet._add_row)
165
161
  for row, dimension in dict.items(sheet.row_dimensions):
166
- if not isinstance(cells, LazyCells) or cells.is_loaded(row):
162
+ if cells.is_loaded(row):
167
163
  dimensions[row] = dimension
168
- sheet.row_dimensions = dimensions
169
- if isinstance(cells, LazyCells):
170
- sheet._cells = cells.held()
171
- self.frozen[sheet] = cells
164
+ rendered, sheet._cells = lazy_rows.render(cells.held(), dimensions)
165
+ kept = {row for row, _ in sheet._cells}
166
+ sheet.row_dimensions = DimensionHolder(worksheet=sheet, default_factory=sheet._add_row)
167
+ for row in kept & dimensions.keys():
168
+ sheet.row_dimensions[row] = dimensions[row]
169
+ self.frozen[sheet] = (cells, rendered)
172
170
 
173
171
  def merge(self, workbook: Workbook, content: bytes) -> bytes:
174
172
  """The saved workbook with the rows that were not parsed put back, as the file has them."""
@@ -179,8 +177,9 @@ class LazyEdit:
179
177
  return content
180
178
  package = Package(content)
181
179
  parts = sheet_parts(package)
182
- for sheet, cells in spliced.items():
183
- package.write(parts[sheet.title], _splice(package.read(parts[sheet.title]), cells))
180
+ for sheet, (cells, rendered) in spliced.items():
181
+ xml = _splice(package.read(parts[sheet.title]), cells, rendered)
182
+ package.write(parts[sheet.title], xml)
184
183
  if self.strings_part:
185
184
  self._restore_strings(package)
186
185
  return package.to_bytes()
@@ -199,22 +198,22 @@ class LazyEdit:
199
198
  rels.append(Rel(free_id(rels, ""), REL_BASE + "sharedStrings", name))
200
199
 
201
200
 
202
- def load(path: Path) -> tuple[Workbook, LazyEdit | None]:
201
+ def load(path: Path, budget: CellBudget) -> tuple[Workbook, LazyEdit | None]:
203
202
  """Load a workbook for editing, its large sheets without their rows."""
204
203
  parts: Parts = {}
205
- reader = open_lazily(path, parts, _split)
204
+ reader = open_lazily(path, parts, _split, budget)
206
205
  reader.read()
207
206
  strings = reader.package.find(SHARED_STRINGS)
208
207
  edit = LazyEdit(path, strings.PartName.lstrip("/") if strings is not None else None)
209
- for sheet, source in lazy_sheets(reader, parts):
210
- _install(sheet, source, reader.shared_strings)
211
- edit.sheets.append(sheet)
212
- return reader.wb, edit if edit.sheets else None
208
+ lazy = list(lazy_sheets(reader, parts))
209
+ for sheet, source in lazy:
210
+ _install(sheet, source, reader.shared_strings, budget)
211
+ return reader.wb, edit if lazy else None
213
212
 
214
213
 
215
- def _install(sheet: Worksheet, source: SheetSource, strings: list[str]) -> None:
214
+ def _install(sheet: Worksheet, source: SheetSource, strings: list[str], budget: CellBudget) -> None:
216
215
  carried = sheet._cells
217
- cells = LazyCells(sheet, source, strings)
216
+ cells = LazyCells(sheet, source, strings, budget)
218
217
  sheet._cells = cells
219
218
  _bind_row_dimensions(sheet, source, strings)
220
219
  for (row, column), carried_cell in carried.items():
@@ -250,13 +249,14 @@ def _self_closed(tag: bytes) -> bytes:
250
249
  return tag if tag.endswith(b"/>") else tag[:-1] + b"/>"
251
250
 
252
251
 
253
- def _splice(xml: bytes, cells: LazyCells) -> bytes:
252
+ def _splice(xml: bytes, cells: LazyCells, rendered: dict[int, bytes]) -> bytes:
254
253
  """The sheet openpyxl wrote, holding the rows of the parsed blocks, with the others added."""
255
254
  source = cells.source
256
255
  data = _SHEET_DATA.search(xml)
257
256
  if data is None:
258
257
  raise ValueError("A saved worksheet has no sheetData.")
259
258
  produced = {_row_number(row[0]): row[0] for row in _ROW.finditer(data[0])}
259
+ produced.update(rendered)
260
260
  lazy_shared.restore(cells, produced)
261
261
  pieces = _rows(cells, produced)
262
262
  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)