dsh-office-kit 0.2.0 → 0.3.0

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 (197) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/README.md +3 -1
  3. package/assets/skills/excel-live-control/SKILL.md +234 -0
  4. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  5. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  6. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  7. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  8. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  9. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  10. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  11. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  12. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  13. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  14. package/assets/skills/office-documents/SKILL.md +90 -77
  15. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  16. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  17. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  18. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  19. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  20. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  21. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  22. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  23. package/assets/skills/office-documents/render_docx.py +92 -0
  24. package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
  25. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
  26. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
  27. package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
  28. package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
  29. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
  30. package/assets/skills/office-documents/scripts/comments_add.py +136 -0
  31. package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
  32. package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
  33. package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
  34. package/assets/skills/office-documents/scripts/content_controls.py +192 -0
  35. package/assets/skills/office-documents/scripts/core_properties.py +124 -0
  36. package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
  37. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
  38. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
  39. package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
  40. package/assets/skills/office-documents/scripts/fields_report.py +59 -0
  41. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
  42. package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
  43. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
  44. package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
  45. package/assets/skills/office-documents/scripts/images_audit.py +80 -0
  46. package/assets/skills/office-documents/scripts/insert_note.py +126 -0
  47. package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
  48. package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
  49. package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
  50. package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
  51. package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
  52. package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
  53. package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
  54. package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
  55. package/assets/skills/office-documents/scripts/replace_text.py +154 -0
  56. package/assets/skills/office-documents/scripts/section_audit.py +91 -0
  57. package/assets/skills/office-documents/scripts/set_protection.py +92 -0
  58. package/assets/skills/office-documents/scripts/style_lint.py +111 -0
  59. package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
  60. package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
  61. package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
  62. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
  63. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
  64. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  65. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  66. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  67. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  68. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  69. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  70. package/assets/skills/office-documents/tasks/equations.md +59 -0
  71. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  72. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  73. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  74. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  75. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  76. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  77. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  78. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  79. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  80. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  81. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  82. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  83. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  84. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  85. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  86. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  87. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  88. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  89. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  90. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  91. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  92. package/assets/skills/office-pdf/SKILL.md +73 -56
  93. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  94. package/assets/skills/office-pdf/references/geometry.md +44 -0
  95. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  96. package/assets/skills/office-pdf/references/limits.md +58 -0
  97. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  98. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  99. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  100. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  101. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  102. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  103. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  104. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  105. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  106. package/assets/skills/office-pdf/tasks/create.md +75 -0
  107. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  108. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  109. package/assets/skills/office-pdf/tasks/read.md +62 -0
  110. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  111. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  112. package/assets/skills/office-presentations/SKILL.md +141 -87
  113. package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
  114. package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
  115. package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
  116. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
  118. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
  119. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
  120. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
  121. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
  122. package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
  123. package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
  124. package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
  125. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
  126. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  127. package/assets/skills/office-presentations/recipes/README.md +71 -0
  128. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  129. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  130. package/assets/skills/office-presentations/recipes/images.md +80 -0
  131. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  132. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  133. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  134. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  135. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  136. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  137. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  138. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  139. package/assets/skills/office-presentations/references/finalization.md +63 -0
  140. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  141. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  142. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  143. package/assets/skills/office-presentations/references/templates.md +76 -0
  144. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  145. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  146. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  147. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  148. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  149. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  150. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  151. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
  152. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
  153. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
  154. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
  155. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
  156. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  157. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
  158. package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
  159. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
  160. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
  161. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
  162. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
  163. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
  164. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
  165. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
  166. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  167. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  168. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  169. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  170. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  171. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  172. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  173. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  174. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  175. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  176. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  177. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  178. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  179. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  180. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  181. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  182. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  183. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  184. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  185. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  186. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  187. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  188. package/assets/skills/shared/annotation-recipes.md +252 -0
  189. package/assets/skills/shared/check_runtime.py +253 -0
  190. package/assets/skills/shared/common-workflow.md +139 -0
  191. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  192. package/dist/client.js +4 -4
  193. package/dist/client.js.map +1 -1
  194. package/dist/index.js +239 -89
  195. package/package.json +1 -1
  196. package/postinstall.mjs +92 -82
  197. package/python/office_cli.py +171 -246
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ """Write values or formulas into specific cells of an existing workbook.
3
+
4
+ Only the named cells change. Values are parsed with `json.loads` when they look
5
+ like JSON, so `42`, `3.5`, `true`, `null`, and `["a","b"]` become numbers and
6
+ structured values, while anything else (`hello`, `=SUM(B2:B9)`, `2026-01-31`) is
7
+ written as text. A leading `=` is a formula string and is stored as a formula.
8
+
9
+ Examples:
10
+ set_cells.py --input book.xlsx --sheet Model --set B3=0.07 --set B4="=B2*(1+B3)"
11
+ set_cells.py --input book.xlsx --spec cells.json
12
+
13
+ cells.json: {"sheet": "Model", "cells": {"B3": 0.07, "D9": "=SUM(D2:D8)"}}
14
+ or: [{"sheet": "Model", "cells": {...}}, {"sheet": "Data", "cells": {...}}]
15
+
16
+ Exit codes:
17
+ 0 cells written (the JSON on stdout lists them)
18
+ 1 refused: unknown sheet or an unparseable assignment
19
+ 2 usage error, missing file, or unreadable workbook
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import json
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ if hasattr(sys.stdout, "reconfigure"):
30
+ sys.stdout.reconfigure(errors="replace")
31
+
32
+
33
+ def parse_args() -> argparse.Namespace:
34
+ parser = argparse.ArgumentParser(
35
+ description="Write values or formulas into specific cells of an .xlsx workbook.",
36
+ )
37
+ parser.add_argument("--input", required=True, help="source workbook")
38
+ parser.add_argument("--out", help="destination workbook (default: <input>-edited.xlsx)")
39
+ parser.add_argument("--sheet", help="target sheet for --set assignments")
40
+ parser.add_argument("--set", action="append", default=[], metavar="A1=VALUE", help="assign one cell")
41
+ parser.add_argument("--spec", help="JSON file with sheet/cells assignments")
42
+ return parser.parse_args()
43
+
44
+
45
+ def _error(message: str, code: int) -> int:
46
+ print(json.dumps({"ok": False, "error": message}), file=sys.stderr)
47
+ return code
48
+
49
+
50
+ def parse_value(raw: str):
51
+ try:
52
+ return json.loads(raw)
53
+ except (TypeError, ValueError):
54
+ return raw
55
+
56
+
57
+ def assignments(args: argparse.Namespace) -> list[tuple[str, dict[str, object]]]:
58
+ """Return [(sheet, {cell: value})] from --spec and --set, in that order."""
59
+ batches: list[tuple[str, dict[str, object]]] = []
60
+ if args.spec:
61
+ payload = json.loads(Path(args.spec).expanduser().read_text(encoding="utf-8-sig"))
62
+ items = payload if isinstance(payload, list) else [payload]
63
+ for item in items:
64
+ if not isinstance(item, dict):
65
+ raise ValueError("--spec items must be objects with sheet and cells")
66
+ sheet = item.get("sheet")
67
+ cells = item.get("cells")
68
+ if not isinstance(sheet, str) or not isinstance(cells, dict):
69
+ raise ValueError("--spec needs {sheet, cells: {A1: value}}")
70
+ batches.append((sheet, dict(cells)))
71
+ if args.set:
72
+ if not args.sheet:
73
+ raise ValueError("--set needs --sheet")
74
+ cells: dict[str, object] = {}
75
+ for pair in args.set:
76
+ if "=" not in pair:
77
+ raise ValueError(f"--set expects A1=VALUE, got: {pair}")
78
+ address, raw = pair.split("=", 1)
79
+ cells[address.strip().upper()] = parse_value(raw)
80
+ batches.append((args.sheet, cells))
81
+ if not batches:
82
+ raise ValueError("nothing to do: pass --set A1=VALUE with --sheet, or --spec")
83
+ return batches
84
+
85
+
86
+ def main() -> int:
87
+ args = parse_args()
88
+ source = Path(args.input).expanduser()
89
+ if not source.is_file():
90
+ return _error(f"input file not found: {source}", 2)
91
+
92
+ try:
93
+ from openpyxl import load_workbook
94
+ except ImportError as error: # pragma: no cover - environment guard
95
+ return _error(f"openpyxl missing: {error}", 2)
96
+
97
+ try:
98
+ batches = assignments(args)
99
+ except (OSError, ValueError, json.JSONDecodeError) as error:
100
+ return _error(str(error), 1)
101
+
102
+ try:
103
+ workbook = load_workbook(str(source))
104
+ except Exception as error: # noqa: BLE001
105
+ return _error(f"cannot read workbook: {error}", 2)
106
+
107
+ written: list[dict[str, object]] = []
108
+ for sheet, cells in batches:
109
+ if sheet not in workbook.sheetnames:
110
+ return _error(f"sheet not found: {sheet}", 1)
111
+ worksheet = workbook[sheet]
112
+ for address, value in cells.items():
113
+ worksheet[str(address)] = value
114
+ written.append({"sheet": sheet, "cell": str(address), "value": value})
115
+
116
+ out = Path(args.out).expanduser() if args.out else source.with_name(
117
+ f"{source.stem}-edited{source.suffix}"
118
+ )
119
+ out.parent.mkdir(parents=True, exist_ok=True)
120
+ workbook.save(str(out))
121
+ workbook.close()
122
+
123
+ print(
124
+ json.dumps(
125
+ {"ok": True, "input": str(source), "output": str(out), "written": written},
126
+ ensure_ascii=False,
127
+ indent=2,
128
+ default=str,
129
+ )
130
+ )
131
+ return 0
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """Add, delete, rename, or reorder worksheets in a workbook.
3
+
4
+ Sheet names are referenced by formulas, charts, and defined names, so renaming
5
+ a sheet rewrites nothing: every reference to the old name breaks. Prefer adding
6
+ a sheet over renaming one that formulas point at.
7
+
8
+ Examples:
9
+ sheet_ops.py --input book.xlsx --add Checks
10
+ sheet_ops.py --input book.xlsx --rename Sheet1=Assumptions
11
+ sheet_ops.py --input book.xlsx --delete Scratch --delete Notes
12
+ sheet_ops.py --input book.xlsx --order "Outputs,Assumptions,Build,Inputs"
13
+
14
+ Exit codes:
15
+ 0 done (the JSON on stdout lists the resulting sheet order)
16
+ 1 refused: unknown sheet, duplicate name, or an attempt to delete the last sheet
17
+ 2 usage error, missing file, or unreadable workbook
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ if hasattr(sys.stdout, "reconfigure"):
28
+ sys.stdout.reconfigure(errors="replace")
29
+
30
+
31
+ def parse_args() -> argparse.Namespace:
32
+ parser = argparse.ArgumentParser(
33
+ description="Add, delete, rename, or reorder worksheets in an .xlsx workbook.",
34
+ )
35
+ parser.add_argument("--input", required=True, help="source workbook")
36
+ parser.add_argument("--out", help="destination workbook (default: <input>-edited.xlsx)")
37
+ parser.add_argument("--add", action="append", default=[], metavar="NAME", help="create sheet NAME")
38
+ parser.add_argument("--delete", action="append", default=[], metavar="NAME", help="remove sheet NAME")
39
+ parser.add_argument("--rename", action="append", default=[], metavar="OLD=NEW", help="rename a sheet")
40
+ parser.add_argument(
41
+ "--order",
42
+ metavar="A,B,C",
43
+ help="move these sheets to the front in this order; unlisted sheets keep their order",
44
+ )
45
+ return parser.parse_args()
46
+
47
+
48
+ def _error(message: str, code: int) -> int:
49
+ print(json.dumps({"ok": False, "error": message}), file=sys.stderr)
50
+ return code
51
+
52
+
53
+ def default_out(source: Path) -> Path:
54
+ return source.with_name(f"{source.stem}-edited{source.suffix}")
55
+
56
+
57
+ def main() -> int:
58
+ args = parse_args()
59
+ source = Path(args.input).expanduser()
60
+ if not source.is_file():
61
+ return _error(f"input file not found: {source}", 2)
62
+
63
+ try:
64
+ from openpyxl import load_workbook
65
+ except ImportError as error: # pragma: no cover - environment guard
66
+ return _error(f"openpyxl missing: {error}", 2)
67
+
68
+ try:
69
+ workbook = load_workbook(str(source))
70
+ except Exception as error: # noqa: BLE001 - any unreadable file is reported the same way
71
+ return _error(f"cannot read workbook: {error}", 2)
72
+
73
+ actions: list[str] = []
74
+ try:
75
+ for name in args.add:
76
+ if name in workbook.sheetnames:
77
+ raise ValueError(f"sheet already exists: {name}")
78
+ workbook.create_sheet(str(name)[:31])
79
+ actions.append(f"added {name}")
80
+
81
+ for pair in args.rename:
82
+ if "=" not in pair:
83
+ raise ValueError(f"--rename expects OLD=NEW, got: {pair}")
84
+ old, new = pair.split("=", 1)
85
+ if old not in workbook.sheetnames:
86
+ raise ValueError(f"sheet not found: {old}")
87
+ if new in workbook.sheetnames:
88
+ raise ValueError(f"sheet already exists: {new}")
89
+ workbook[old].title = str(new)[:31]
90
+ actions.append(f"renamed {old} -> {new}")
91
+
92
+ for name in args.delete:
93
+ if name not in workbook.sheetnames:
94
+ raise ValueError(f"sheet not found: {name}")
95
+ if len(workbook.sheetnames) == 1:
96
+ raise ValueError("refusing to delete the last sheet; a workbook needs at least one")
97
+ workbook.remove(workbook[name])
98
+ actions.append(f"deleted {name}")
99
+
100
+ if args.order:
101
+ wanted = [part.strip() for part in args.order.split(",") if part.strip()]
102
+ missing = [name for name in wanted if name not in workbook.sheetnames]
103
+ if missing:
104
+ raise ValueError(f"unknown sheet in --order: {', '.join(missing)}")
105
+ rest = [name for name in workbook.sheetnames if name not in wanted]
106
+ workbook._sheets = [workbook[name] for name in wanted + rest]
107
+ actions.append("reordered " + ", ".join(wanted + rest))
108
+ except ValueError as error:
109
+ return _error(str(error), 1)
110
+
111
+ out = Path(args.out).expanduser() if args.out else default_out(source)
112
+ out.parent.mkdir(parents=True, exist_ok=True)
113
+ names = list(workbook.sheetnames)
114
+ workbook.save(str(out))
115
+ workbook.close()
116
+
117
+ print(
118
+ json.dumps(
119
+ {
120
+ "ok": True,
121
+ "input": str(source),
122
+ "output": str(out),
123
+ "sheetnames": names,
124
+ "actions": actions,
125
+ },
126
+ ensure_ascii=False,
127
+ indent=2,
128
+ )
129
+ )
130
+ return 0
131
+
132
+
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """Apply number formats, fonts, fills, and alignment to existing cells.
3
+
4
+ The style vocabulary is the helper CLI's, so a style written here behaves exactly
5
+ like one written at create time: `bold`, `italic`, `size`, `color` (hex), `fill`
6
+ (hex), `number_format`, `align` (left/center/right).
7
+
8
+ Examples:
9
+ style_cells.py --input book.xlsx --sheet Model --range B11:B14 \
10
+ --style '{"number_format": "#,##0"}'
11
+ style_cells.py --input book.xlsx --sheet Pipeline --range A1:G1 \
12
+ --style '{"bold": true, "fill": "FF1F3864", "color": "FFFFFFFF", "align": "center"}'
13
+ style_cells.py --input book.xlsx --spec styles.json
14
+
15
+ styles.json: {"sheet": "Model", "styles": {"A11:B11": {"bold": true}, "C11:C14": {"number_format": "0.0%"}}}
16
+
17
+ Only cells covered by the given ranges change; neighbouring cells keep their
18
+ existing formatting.
19
+
20
+ Exit codes:
21
+ 0 styles applied (the JSON on stdout lists the ranges and styles)
22
+ 1 refused: unknown sheet, bad range, or no style given
23
+ 2 usage error, missing file, or unreadable workbook
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ if hasattr(sys.stdout, "reconfigure"):
34
+ sys.stdout.reconfigure(errors="replace")
35
+
36
+ ROOT = Path(__file__).resolve().parents[4]
37
+
38
+
39
+ def load_office_cli():
40
+ """Import the helper CLI's style helpers so both paths format identically."""
41
+ scripts = ROOT / "python"
42
+ if not (scripts / "office_cli.py").is_file():
43
+ raise RuntimeError(f"helper CLI not found next to the skill: {scripts / 'office_cli.py'}")
44
+ if str(scripts) not in sys.path:
45
+ sys.path.insert(0, str(scripts))
46
+ import office_cli
47
+
48
+ return office_cli
49
+
50
+
51
+ def parse_args() -> argparse.Namespace:
52
+ parser = argparse.ArgumentParser(
53
+ description="Apply number formats, fonts, fills, and alignment to cells in an .xlsx workbook.",
54
+ )
55
+ parser.add_argument("--input", required=True, help="source workbook")
56
+ parser.add_argument("--out", help="destination workbook (default: <input>-edited.xlsx)")
57
+ parser.add_argument("--sheet", help="target sheet for --range/--style")
58
+ parser.add_argument("--range", action="append", default=[], metavar="A1:B2", help="cell or range to style")
59
+ parser.add_argument("--style", help="JSON style object applied to every --range")
60
+ parser.add_argument("--spec", help="JSON file with sheet/styles assignments")
61
+ return parser.parse_args()
62
+
63
+
64
+ def _error(message: str, code: int) -> int:
65
+ print(json.dumps({"ok": False, "error": message}), file=sys.stderr)
66
+ return code
67
+
68
+
69
+ def assignments(args: argparse.Namespace) -> list[tuple[str, dict[str, dict[str, object]]]]:
70
+ batches: list[tuple[str, dict[str, dict[str, object]]]] = []
71
+ if args.spec:
72
+ payload = json.loads(Path(args.spec).expanduser().read_text(encoding="utf-8-sig"))
73
+ items = payload if isinstance(payload, list) else [payload]
74
+ for item in items:
75
+ if not isinstance(item, dict) or not isinstance(item.get("styles"), dict):
76
+ raise ValueError("--spec items need {sheet, styles: {range: style}}")
77
+ batches.append((str(item.get("sheet", "")), dict(item["styles"])))
78
+ if args.range:
79
+ if not args.sheet or not args.style:
80
+ raise ValueError("--range needs both --sheet and --style")
81
+ style = json.loads(args.style)
82
+ if not isinstance(style, dict):
83
+ raise ValueError("--style must be a JSON object")
84
+ batches.append((args.sheet, {address: style for address in args.range}))
85
+ if not batches:
86
+ raise ValueError("nothing to do: pass --range with --style, or --spec")
87
+ return batches
88
+
89
+
90
+ def main() -> int:
91
+ args = parse_args()
92
+ source = Path(args.input).expanduser()
93
+ if not source.is_file():
94
+ return _error(f"input file not found: {source}", 2)
95
+
96
+ try:
97
+ office_cli = load_office_cli()
98
+ from openpyxl import load_workbook
99
+ except (ImportError, RuntimeError) as error: # pragma: no cover - environment guard
100
+ return _error(str(error), 2)
101
+
102
+ try:
103
+ batches = assignments(args)
104
+ except (OSError, ValueError, json.JSONDecodeError) as error:
105
+ return _error(str(error), 1)
106
+
107
+ workbook = load_workbook(str(source))
108
+ applied: list[dict[str, object]] = []
109
+ for sheet, styles in batches:
110
+ if sheet not in workbook.sheetnames:
111
+ return _error(f"sheet not found: {sheet}", 1)
112
+ worksheet = workbook[sheet]
113
+ for address, style in styles.items():
114
+ if not isinstance(style, dict):
115
+ return _error(f"style for {address} must be an object", 1)
116
+ cells = list(office_cli._xlsx_cells(worksheet, str(address).upper()))
117
+ for cell in cells:
118
+ office_cli._xlsx_apply_style(cell, style)
119
+ applied.append({"sheet": sheet, "range": str(address).upper(), "cells": len(cells), "style": style})
120
+
121
+ out = Path(args.out).expanduser() if args.out else source.with_name(
122
+ f"{source.stem}-edited{source.suffix}"
123
+ )
124
+ out.parent.mkdir(parents=True, exist_ok=True)
125
+ workbook.save(str(out))
126
+ workbook.close()
127
+
128
+ print(json.dumps({"ok": True, "input": str(source), "output": str(out), "applied": applied}, indent=2))
129
+ return 0
130
+
131
+
132
+ if __name__ == "__main__":
133
+ sys.exit(main())
@@ -0,0 +1,66 @@
1
+ # Native charts
2
+
3
+ A native chart is an object bound to cell ranges: the numbers stay in cells, the
4
+ chart reads them, and editing a cell moves the bar. That is the whole reason to
5
+ add one instead of pasting an image.
6
+
7
+ ```python
8
+ import sys
9
+ from openpyxl import load_workbook
10
+ from openpyxl.chart import BarChart, LineChart, Reference
11
+ from openpyxl.chart.axis import ChartLines
12
+
13
+ wb = load_workbook(sys.argv[1])
14
+ ws = wb["Trend"]
15
+
16
+ chart = LineChart()
17
+ chart.title = "Monthly revenue (USD)"
18
+ chart.height, chart.width = 7, 16
19
+ chart.legend = None
20
+
21
+ data = Reference(ws, min_col=2, min_row=3, max_col=13, max_row=3) # one row
22
+ chart.add_data(data, from_rows=True, titles_from_data=False) # one series, 12 points
23
+ chart.set_categories(Reference(ws, min_col=2, min_row=2, max_row=2))
24
+ chart.y_axis.numFmt = "#,##0"
25
+ chart.y_axis.majorGridlines = ChartLines()
26
+ chart.y_axis.scaling.min = 0 # bars must start at zero
27
+ ws.add_chart(chart, "A6")
28
+ wb.save(sys.argv[2])
29
+ ```
30
+
31
+ ## Orientation is the bug people hit
32
+
33
+ A one-row source is **one series of many points**, not many series of one point.
34
+ `Reference` cannot express that; `add_data(..., from_rows=True)` can. Without it
35
+ a monthly row becomes twelve invisible one-point series and the chart renders
36
+ blank. The mirror case is a one-column source added with `from_rows=True`: you
37
+ get one series per cell row, so only pass it when the data really is row-wise.
38
+
39
+ ## What the helper CLI gives you
40
+
41
+ `xlsx create` accepts `charts[]` with `type`, `title`, `anchor`,
42
+ `height`/`width`, `data {min_col,min_row,max_col,max_row,titles_from_data}` and
43
+ `categories`. `container_tools/add_chart.py` adds the same chart to an existing
44
+ file and handles the row-oriented case. Neither sets axis number formats, series
45
+ colours, legend position, data labels, or gap width — if the chart needs those,
46
+ use a snippet like the one above.
47
+
48
+ ## Checks before you present
49
+
50
+ - Series count matches the intent (one line per series, not one per point).
51
+ - Categories are labels, not values and not an empty axis with `1, 2, 3, …`.
52
+ - Bar, column, and area axes start at zero; line and scatter may narrow the
53
+ range, but the scale must be visible and stated.
54
+ - No default legend on a single series; name each series in `titles_from_data`
55
+ or the legend reads `Series 1`.
56
+ - Axis number formats are set on the chart, not inherited from cells — a source
57
+ column formatted as `0.0%` does not format the axis.
58
+ - The chart sits near its data, over empty cells, and is not clipped by the
59
+ print area when the sheet is printed.
60
+
61
+ ## Editing a chart in an existing file
62
+
63
+ Read the object first (`inspect_workbook.py` lists type, anchor, and series
64
+ refs), change the smallest property, and re-render. Moving a chart a few cells is
65
+ fine; rebuilding it with different styling when the user asked for one number to
66
+ be added is not.
@@ -0,0 +1,69 @@
1
+ # Conditional formats
2
+
3
+ Conditional formatting paints a cell from a rule, not from a value. The rule is
4
+ stored separately from the cell format, so a cell can look red while its stored
5
+ fill stays default — and a reader who copies the value out gets the number, not
6
+ the colour.
7
+
8
+ ```python
9
+ import sys
10
+ from openpyxl import load_workbook
11
+ from openpyxl.formatting.rule import (
12
+ CellIsRule, ColorScaleRule, DataBarRule, FormulaRule,
13
+ )
14
+ from openpyxl.styles import Font, PatternFill
15
+
16
+ wb = load_workbook(sys.argv[1])
17
+ ws = wb["Sensitivity"]
18
+
19
+ ws.conditional_formatting.add(
20
+ "C4:G8",
21
+ ColorScaleRule(start_type="min", start_color="F8696B",
22
+ mid_type="percentile", mid_value=50, mid_color="FFEB84",
23
+ end_type="max", end_color="63BE7B"),
24
+ )
25
+ ws.conditional_formatting.add(
26
+ "D2:D13",
27
+ DataBarRule(start_type="min", end_type="max", color="5B9BD5"),
28
+ )
29
+ ws.conditional_formatting.add(
30
+ "C2:C13",
31
+ CellIsRule(operator="lessThan", formula=["0"],
32
+ fill=PatternFill("solid", fgColor="FFC7CE"),
33
+ font=Font(color="9C0006")),
34
+ )
35
+ ws.conditional_formatting.add(
36
+ "A2:A13",
37
+ FormulaRule(formula=['$F2="Lost"'], fill=PatternFill("solid", fgColor="F2F2F2")),
38
+ )
39
+ wb.save(sys.argv[2])
40
+ ```
41
+
42
+ - `ColorScaleRule`/`DataBarRule` take a range, not a cell; one rule covers the
43
+ rectangle and scales against the values inside it.
44
+ - `CellIsRule` compares one cell: `operator` in `lessThan`, `greaterThan`,
45
+ `between`, `equal`, `notEqual`, …
46
+ - `FormulaRule` evaluates a formula with the range's **top-left** cell, so write
47
+ the reference for that cell (`$F2` with row relative) and Excel fills the rest.
48
+ - A whole-column range (`D:D`) is cheap to write and slow to render; bound the
49
+ range to the current data (`D2:D13`) and extend it when rows are added.
50
+ - Rules stack in priority order; later rules win only when the earlier ones do
51
+ not match. `stopIfTrue=True` ends the chain — use it when a second rule must
52
+ not repaint an already-flagged cell.
53
+ - Do not also set a static fill on the same cells: the static fill shows through
54
+ where no rule matches and the sheet looks half-formatted.
55
+
56
+ ## Editing an existing file
57
+
58
+ Replace a rule, do not add a near-duplicate: iterate
59
+ `ws.conditional_formatting` and rebuild the collection for that range. Two rules
60
+ covering `D2:D13` with slightly different thresholds give a result nobody can
61
+ explain. Keep the rule count per sheet small — a dashboard with forty rules is a
62
+ compatibility minefield in other engines.
63
+
64
+ ## Verify
65
+
66
+ Read the file back with `container_tools/inspect_workbook.py`; it lists each
67
+ range and rule type. Then render and look: a data bar that never appears is
68
+ usually a range that does not overlap the data, and a rule that fires on the
69
+ wrong cells is almost always an absolute/relative mistake in `FormulaRule`.
@@ -0,0 +1,58 @@
1
+ # Data tables (Excel Tables)
2
+
3
+ An Excel Table (`ListObject`) is the object behind `Insert > Table`: a name, a
4
+ structured reference, banded rows, an optional totals row, and header filters.
5
+ It is not a pivot table and not a formatted range. Use it when the user asks for
6
+ a table, when a filter row helps, or when formulas should read
7
+ `Sales[Amount]` instead of `$D$2:$D$900`.
8
+
9
+ ```python
10
+ import sys
11
+ from openpyxl import load_workbook
12
+ from openpyxl.worksheet.table import Table, TableStyleInfo
13
+
14
+ wb = load_workbook(sys.argv[1])
15
+ ws = wb["Pipeline"]
16
+ table = Table(displayName="PipelineTable", ref="A1:G13")
17
+ table.tableStyleInfo = TableStyleInfo(
18
+ name="TableStyleMedium2", showRowStripes=True, showColumnStripes=False
19
+ )
20
+ ws.add_table(table)
21
+ wb.save(sys.argv[2])
22
+ ```
23
+
24
+ Rules that survive contact with the user:
25
+
26
+ - `displayName` has no spaces, no hyphens, and must be unique in the workbook — it
27
+ becomes the structured reference name (`PipelineTable[Amount]`).
28
+ - `ref` must start at the header row and match the real data rectangle. A table
29
+ whose `ref` is off by one row shows a phantom header and breaks the filter.
30
+ - One table per rectangle, one header row, no merged cells inside it.
31
+ - Give it a style from `TableStyleLight*`/`Medium*`/`Dark*`; a bare table with no
32
+ style looks like an unstyled range and the user asks why the stripes went away.
33
+
34
+ ## Appending rows to a table
35
+
36
+ Rows written below the last table row are outside the table until you widen
37
+ `ref`. Widen it in the same pass, or the new records will not filter, will not
38
+ band, and structured references will not see them.
39
+
40
+ ```python
41
+ table = ws.tables["PipelineTable"]
42
+ first_row = int(table.ref.split(":")[1].rstrip("0123456789"))
43
+ new_last = first_row + added
44
+ table.ref = f"A1:G{new_last}" # same columns, one row longer
45
+ ```
46
+
47
+ Copy the format and any formula from the previous row (`ws.cell(r, c)._style =
48
+ ws.cell(r - 1, c)._style`) before widening, so the new row does not appear as a
49
+ plain block inside a banded table.
50
+
51
+ ## Totals row
52
+
53
+ `table.totalsRowCount = 1` declares a totals row, and `totalsRowFunction` on a
54
+ column sets `sum`, `count`, `average`, `min`, `max`, `stdDev`, or `var`. The row
55
+ below the last data row must exist and hold the header labels Excel expects; a
56
+ totals row declared without that row renders oddly in some engines. If the user
57
+ only wants a bold total line under a table, a formula row outside the table is
58
+ simpler and less fragile.
@@ -0,0 +1,41 @@
1
+ # Named ranges
2
+
3
+ A defined name is a saved reference with a readable name: `Q4Growth` instead of
4
+ `Model!$B$6`. Formulas that use names survive row and column insertion, and the
5
+ user can select the name box instead of hunting for a cell.
6
+
7
+ ```python
8
+ import sys
9
+ from openpyxl import load_workbook
10
+ from openpyxl.workbook.defined_name import DefinedName
11
+
12
+ wb = load_workbook(sys.argv[1])
13
+ wb.defined_names["Q4Growth"] = DefinedName("Q4Growth", attr_text="Model!$B$6")
14
+ wb.defined_names["PlanRevenue"] = DefinedName("PlanRevenue", attr_text="Model!$B$11")
15
+ wb.save(sys.argv[2])
16
+ ```
17
+
18
+ - The name must be a legal identifier: letters, digits, underscore, not a cell
19
+ address (`Q4` alone is a column, so it is refused).
20
+ - `attr_text` is a full sheet reference; quote sheet names with spaces:
21
+ `'Case A'!$B$4`.
22
+ - Workbook-level names live in `wb.defined_names`. To scope a name to one sheet,
23
+ put it on the worksheet: `wb["Model"].defined_names["Rate"] = ...` — the same
24
+ name can then mean different cells on different sheets, which confuses readers
25
+ unless the workbook already works that way.
26
+ - Read the existing names with `container_tools/inspect_workbook.py` before
27
+ adding one; a duplicate name is a repair prompt in Excel.
28
+ - Never point a name at a range whose size changes (`A2:A900`) unless the sheet
29
+ really ends there; prefer the exact used range, or a table reference.
30
+
31
+ ## Using names in formulas
32
+
33
+ `=PlanRevenue*Q4Growth` is more readable than `=Model!$B$11*Model!$B$6`, and it
34
+ does not break when a row is inserted. Paste the formula text exactly as the user
35
+ would type it; openpyxl never resolves the name.
36
+
37
+ ## Removing a name
38
+
39
+ Deleting a name leaves every formula that used it as `#NAME?`. Find the users
40
+ first (formula text is visible in `inspect_workbook.py` output and
41
+ `xlsx read`), then delete, then recalculate and check the gate.