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.
- package/CAPABILITY.md +147 -68
- package/README.md +3 -1
- package/assets/skills/excel-live-control/SKILL.md +234 -0
- package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
- package/assets/skills/excel-live-control/references/office-automation.md +207 -0
- package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
- package/assets/skills/excel-live-control/style_guidelines.md +87 -0
- package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
- package/assets/skills/office-documents/SKILL.md +90 -77
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
- package/assets/skills/office-documents/ooxml/comments.md +40 -0
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
- package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
- package/assets/skills/office-documents/references/docx-structure.md +83 -0
- package/assets/skills/office-documents/references/template-elicitation.md +49 -0
- package/assets/skills/office-documents/references/writing-quality.md +49 -0
- package/assets/skills/office-documents/render_docx.py +92 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
- package/assets/skills/office-documents/scripts/comments_add.py +136 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
- package/assets/skills/office-documents/scripts/content_controls.py +192 -0
- package/assets/skills/office-documents/scripts/core_properties.py +124 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
- package/assets/skills/office-documents/scripts/fields_report.py +59 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
- package/assets/skills/office-documents/scripts/images_audit.py +80 -0
- package/assets/skills/office-documents/scripts/insert_note.py +126 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
- package/assets/skills/office-documents/scripts/replace_text.py +154 -0
- package/assets/skills/office-documents/scripts/section_audit.py +91 -0
- package/assets/skills/office-documents/scripts/set_protection.py +92 -0
- package/assets/skills/office-documents/scripts/style_lint.py +111 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
- package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
- package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
- package/assets/skills/office-documents/tasks/create_edit.md +107 -0
- package/assets/skills/office-documents/tasks/equations.md +59 -0
- package/assets/skills/office-documents/tasks/fields_update.md +44 -0
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
- package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
- package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
- package/assets/skills/office-documents/tasks/images_figures.md +63 -0
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
- package/assets/skills/office-documents/tasks/read_review.md +50 -0
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
- package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
- package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
- package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
- package/assets/skills/office-documents/tasks/verify_render.md +50 -0
- package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
- package/assets/skills/office-pdf/SKILL.md +73 -56
- package/assets/skills/office-pdf/references/form-fields.md +56 -0
- package/assets/skills/office-pdf/references/geometry.md +44 -0
- package/assets/skills/office-pdf/references/layout-quality.md +41 -0
- package/assets/skills/office-pdf/references/limits.md +58 -0
- package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
- package/assets/skills/office-pdf/scripts/forms.py +556 -0
- package/assets/skills/office-pdf/scripts/metadata.py +161 -0
- package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
- package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
- package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
- package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
- package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
- package/assets/skills/office-pdf/tasks/annotations.md +98 -0
- package/assets/skills/office-pdf/tasks/create.md +75 -0
- package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
- package/assets/skills/office-pdf/tasks/forms.md +67 -0
- package/assets/skills/office-pdf/tasks/read.md +62 -0
- package/assets/skills/office-pdf/tasks/redact.md +72 -0
- package/assets/skills/office-pdf/tasks/stamp.md +60 -0
- package/assets/skills/office-presentations/SKILL.md +141 -87
- package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
- package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
- package/assets/skills/office-presentations/recipes/README.md +71 -0
- package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
- package/assets/skills/office-presentations/recipes/connectors.md +83 -0
- package/assets/skills/office-presentations/recipes/images.md +80 -0
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
- package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
- package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
- package/assets/skills/office-presentations/recipes/tables.md +88 -0
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
- package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
- package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
- package/assets/skills/office-presentations/references/finalization.md +63 -0
- package/assets/skills/office-presentations/references/native-bullets.md +78 -0
- package/assets/skills/office-presentations/references/native-evidence.md +67 -0
- package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
- package/assets/skills/office-presentations/references/templates.md +76 -0
- package/assets/skills/office-presentations/routing/google_slides.md +45 -0
- package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
- package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
- package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
- package/assets/skills/office-presentations/tasks/template-following.md +132 -0
- package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
- package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
- package/assets/skills/office-spreadsheets/references/charts.md +63 -0
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
- package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
- package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
- package/assets/skills/shared/annotation-recipes.md +252 -0
- package/assets/skills/shared/check_runtime.py +253 -0
- package/assets/skills/shared/common-workflow.md +139 -0
- package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/index.js +239 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- 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.
|