@qvac/skills 0.0.0 → 0.1.1
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/LICENSE.md +198 -0
- package/README.md +40 -0
- package/index.d.ts +4 -0
- package/index.js +9 -0
- package/package.json +87 -1
- package/skills/apple-notes/SKILL.md +92 -0
- package/skills/apple-notes/append-note.applescript +9 -0
- package/skills/apple-notes/cli.schema.json +32 -0
- package/skills/apple-notes/create-note.applescript +15 -0
- package/skills/apple-notes/delete-note.applescript +10 -0
- package/skills/apple-notes/edit-note.applescript +10 -0
- package/skills/apple-notes/read-note.applescript +28 -0
- package/skills/apple-notes/references/read.md +65 -0
- package/skills/apple-notes/references/write.md +105 -0
- package/skills/apple-notes/search-notes.applescript +21 -0
- package/skills/apple-reminders/SKILL.md +129 -0
- package/skills/apple-reminders/cli.schema.json +201 -0
- package/skills/apple-reminders/references/edit.md +69 -0
- package/skills/apple-reminders/references/view.md +58 -0
- package/skills/asana/SKILL.md +59 -0
- package/skills/diagrams/SKILL.md +107 -0
- package/skills/diagrams/references/class.md +29 -0
- package/skills/diagrams/references/er.md +27 -0
- package/skills/diagrams/references/flowchart.md +33 -0
- package/skills/diagrams/references/gantt.md +38 -0
- package/skills/diagrams/references/mindmap.md +35 -0
- package/skills/diagrams/references/pie.md +27 -0
- package/skills/diagrams/references/sequence.md +32 -0
- package/skills/diagrams/references/state.md +30 -0
- package/skills/diagrams/references/timeline.md +28 -0
- package/skills/excel/SKILL.md +120 -0
- package/skills/excel/references/create.md +374 -0
- package/skills/excel/references/edit.md +353 -0
- package/skills/excel/references/read.md +99 -0
- package/skills/github/SKILL.md +42 -0
- package/skills/gmail/SKILL.md +142 -0
- package/skills/gmail/operations.json +71 -0
- package/skills/google-calendar/SKILL.md +139 -0
- package/skills/google-calendar/operations.json +62 -0
- package/skills/google-docs/SKILL.md +74 -0
- package/skills/google-docs/operations.json +61 -0
- package/skills/google-docs/references/create.md +97 -0
- package/skills/google-docs/references/edit.md +146 -0
- package/skills/google-docs/references/read.md +49 -0
- package/skills/google-drive/SKILL.md +118 -0
- package/skills/google-drive/operations.json +40 -0
- package/skills/google-sheets/SKILL.md +71 -0
- package/skills/google-sheets/operations.json +85 -0
- package/skills/google-sheets/references/create.md +54 -0
- package/skills/google-sheets/references/edit.md +124 -0
- package/skills/google-sheets/references/read.md +74 -0
- package/skills/image-generation/SKILL.md +48 -0
- package/skills/music-generation/SKILL.md +76 -0
- package/skills/notion/SKILL.md +61 -0
- package/skills/notion/operations.json +53 -0
- package/skills/notion/references/comments.md +65 -0
- package/skills/notion/references/databases.md +68 -0
- package/skills/notion/references/pages.md +119 -0
- package/skills/notion/references/tasks.md +28 -0
- package/skills/obsidian/SKILL.md +122 -0
- package/skills/obsidian/cli.schema.json +392 -0
- package/skills/obsidian/references/read.md +79 -0
- package/skills/obsidian/references/write.md +67 -0
- package/skills/pdf/SKILL.md +110 -0
- package/skills/pdf/references/create.md +169 -0
- package/skills/pdf/references/transform.md +270 -0
- package/skills/pdf/scripts/decrypt.py +26 -0
- package/skills/pdf/scripts/encrypt.py +25 -0
- package/skills/pdf/scripts/extract_text.py +25 -0
- package/skills/pdf/scripts/merge.py +21 -0
- package/skills/pdf/scripts/rotate.py +27 -0
- package/skills/presentations/SKILL.md +118 -0
- package/skills/presentations/references/create.md +399 -0
- package/skills/presentations/references/edit.md +314 -0
- package/skills/presentations/references/read.md +127 -0
- package/skills/spotify/SKILL.md +86 -0
- package/skills/weather/SKILL.md +33 -0
- package/skills/word/SKILL.md +141 -0
- package/skills/word/references/create.md +368 -0
- package/skills/word/references/edit.md +704 -0
- package/skills/word/references/read.md +141 -0
- package/skills/word/references/replace.md +86 -0
- package/skills/word/scripts/list_paragraphs.py +19 -0
- package/skills/word/scripts/replace_paragraphs.py +58 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Creating a Workbook (openpyxl)
|
|
2
|
+
|
|
3
|
+
Create a new `.xlsx` from scratch by running Python through the `exec` tool.
|
|
4
|
+
A new workbook needs **no** `inputs` — do not invent attachment ids. **Exactly
|
|
5
|
+
one** `exec` call per user request when that call succeeds.
|
|
6
|
+
|
|
7
|
+
**A workbook that already exists in this chat is never rebuilt here.** "Add a
|
|
8
|
+
row", "change a cell", "extend the sheet" — any request that starts from an
|
|
9
|
+
existing `.xlsx` is an EDIT: load `references/edit.md` and stage the workbook
|
|
10
|
+
by its `attachmentId`. Building a fresh workbook for an edit request throws
|
|
11
|
+
away everything the user already has.
|
|
12
|
+
|
|
13
|
+
## The exec call
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"language": "python",
|
|
18
|
+
"packages": ["openpyxl==3.1.5"],
|
|
19
|
+
"outputs": ["report.xlsx"],
|
|
20
|
+
"command": "..."
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `packages` — `["openpyxl==3.1.5"]` on every call. Pin the version; this exact
|
|
25
|
+
version ships with the app and installs with no network; any other version
|
|
26
|
+
has to be downloaded, which fails on a device that is offline. A workbook that
|
|
27
|
+
embeds an image also needs `"pillow"`, **deliberately unpinned** — the runtime owns
|
|
28
|
+
its version and `"pillow==12.2.0"` matches nothing, which sends openpyxl to PyPI
|
|
29
|
+
with it. `["openpyxl==3.1.5", "pillow"]` ships whole and installs offline.
|
|
30
|
+
- `outputs` — `["report.xlsx"]`. `wb.save("report.xlsx")` must match the
|
|
31
|
+
declared output name. `.xlsx` and `.csv` are allowed; `.xlsm` is not. Never
|
|
32
|
+
pass an absolute path to `save()`.
|
|
33
|
+
- `command` — the multi-line Python source, with real newline characters. Never
|
|
34
|
+
collapse it to one line joined by `;` — a `for`/`if`/`with` after a semicolon
|
|
35
|
+
is a `SyntaxError`.
|
|
36
|
+
- No `inputs` key at all — **except** to embed an image, the one file a create
|
|
37
|
+
call stages. See Embedding an Image.
|
|
38
|
+
|
|
39
|
+
## The Recipe
|
|
40
|
+
|
|
41
|
+
Start from this. It is a complete, working workbook — bold headers, data rows,
|
|
42
|
+
number formats, column widths — saved under the declared output name. Copy it
|
|
43
|
+
and change the content; do not assemble a workbook from memory.
|
|
44
|
+
|
|
45
|
+
**Write the program flat — no `def`, no helper functions, no classes.** Top-level
|
|
46
|
+
statements only, in the order the sample shows. Every real failure of this skill
|
|
47
|
+
has come from a model writing a generator function and then wiring it up wrong: a
|
|
48
|
+
list filled inside a function and read outside it (`NameError: name 'data' is not
|
|
49
|
+
defined`), or a function that was never called, which saves an empty sheet and
|
|
50
|
+
delivers a blank file to the user.
|
|
51
|
+
|
|
52
|
+
**Type the data out as literal rows, however long the series.** Ten years is ten
|
|
53
|
+
tuples — write them. Never build periods with `datetime`/`timedelta` arithmetic
|
|
54
|
+
and never increment a month by hand: `month + 1` past December raises
|
|
55
|
+
`ValueError: month must be in 1..12, not 13`. A year is the plain number `2016`
|
|
56
|
+
and a month is the plain string `"2016-03"` — neither needs a date object.
|
|
57
|
+
|
|
58
|
+
**Give the granularity the user asked for.** "Prices from 2016-2025" is one row
|
|
59
|
+
per year — ten rows. Do not expand it into 120 monthly rows they did not ask for.
|
|
60
|
+
|
|
61
|
+
**Import once, then use that exact name.** After
|
|
62
|
+
`from openpyxl import Workbook` the constructor is `Workbook()` — writing
|
|
63
|
+
`openpyxl.Workbook()` raises `NameError: name 'openpyxl' is not defined`, because
|
|
64
|
+
that import never binds the module. Copy the sample's import block as-is.
|
|
65
|
+
|
|
66
|
+
**Keep every underscore in API names.** `load_workbook`, `create_sheet`,
|
|
67
|
+
`number_format`, `column_dimensions`, `column_letter`, `iter_rows`, `max_row`,
|
|
68
|
+
`merge_cells` — stripping them to `loadworkbook` / `createsheet` fails.
|
|
69
|
+
|
|
70
|
+
**Fill the template in — do not rewrite it.** The block below is the whole
|
|
71
|
+
program. Change only its named slots: `SHEET_TITLE`, `HEADERS`, `ROWS`,
|
|
72
|
+
`MONEY_COLUMNS`, and `OUTPUT` (which must match the declared output). Every other
|
|
73
|
+
line stays character for character. In practice every failed run has come from
|
|
74
|
+
code invented *around* this template — an extra per-row styling loop, a
|
|
75
|
+
hand-built summary row, a second unpack of the same data — not from the template
|
|
76
|
+
itself. If a row needs a number format, put its column letter in
|
|
77
|
+
`MONEY_COLUMNS`; that is the only knob.
|
|
78
|
+
|
|
79
|
+
Nothing goes after `print(...)`, and no comparison, `sorted`, `min`/`max` or
|
|
80
|
+
running total goes between the lines. Values that need working out are worked out
|
|
81
|
+
as you type `ROWS`, and anything the user should notice about the numbers belongs
|
|
82
|
+
in your chat reply, not in more Python. Comparing a year to a label is where the
|
|
83
|
+
last run died: `TypeError: '<=' not supported between instances of 'int' and 'str'`.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from openpyxl import Workbook
|
|
87
|
+
from openpyxl.styles import Font
|
|
88
|
+
|
|
89
|
+
SHEET_TITLE = "Q1 Sales"
|
|
90
|
+
HEADERS = ["Region", "Units", "Unit Price", "Revenue"]
|
|
91
|
+
ROWS = [ # every tuple already complete, one per row
|
|
92
|
+
("North", 120, 9.99, 1198.80),
|
|
93
|
+
("South", 80, 12.50, 1000.00),
|
|
94
|
+
("East", 200, 7.25, 1450.00),
|
|
95
|
+
]
|
|
96
|
+
MONEY_COLUMNS = ["C", "D"] # money-formatted columns; [] for none
|
|
97
|
+
OUTPUT = "report.xlsx" # must match the declared output exactly
|
|
98
|
+
|
|
99
|
+
wb = Workbook()
|
|
100
|
+
ws = wb.active # a new workbook already has one sheet
|
|
101
|
+
ws.title = SHEET_TITLE
|
|
102
|
+
|
|
103
|
+
ws.append(HEADERS)
|
|
104
|
+
for cell in ws[1]: # ws[N] is row NUMBER N — never ws[a_list]
|
|
105
|
+
cell.font = Font(bold=True)
|
|
106
|
+
ws.column_dimensions[cell.column_letter].width = 14
|
|
107
|
+
ws.freeze_panes = "A2" # header stays visible while scrolling
|
|
108
|
+
|
|
109
|
+
for row in ROWS: # append whole rows — never unpack them
|
|
110
|
+
ws.append(list(row))
|
|
111
|
+
|
|
112
|
+
for letter in MONEY_COLUMNS:
|
|
113
|
+
for cell in ws[letter][1:]: # ws["C"] is a column; [1:] skips its header
|
|
114
|
+
cell.number_format = '#,##0.00'
|
|
115
|
+
|
|
116
|
+
assert ws.max_row > 1, "no data rows — fix the code, never deliver an empty sheet"
|
|
117
|
+
wb.save(OUTPUT)
|
|
118
|
+
print(f"{len(wb.sheetnames)} sheets, {ws.max_row} rows")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Compute derived columns while writing `ROWS`, not in a loop over them: the
|
|
122
|
+
revenue above is typed into each tuple. Unpacking rows to build other rows
|
|
123
|
+
(`for a, b in ROWS:`) is what raises
|
|
124
|
+
`ValueError: not enough values to unpack` the moment one tuple is a different
|
|
125
|
+
length.
|
|
126
|
+
|
|
127
|
+
Only when the user wants a **live** total, append this after the loop — nothing
|
|
128
|
+
else changes:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
last_data_row = ws.max_row # capture BEFORE appending the total row
|
|
132
|
+
ws.append(["Total", None, None, f"=SUM(D2:D{last_data_row})"])
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Capture `ws.max_row` **before** appending a row whose formula refers to the data —
|
|
136
|
+
`max_row` grows with every append, so building the range afterwards produces a
|
|
137
|
+
formula that includes its own cell (a circular reference the user sees as an error
|
|
138
|
+
in Excel). Remember this runtime computes nothing: the formula cell looks empty
|
|
139
|
+
in the chat preview, so say in the reply that the total shows up once the file
|
|
140
|
+
is opened in a spreadsheet app.
|
|
141
|
+
|
|
142
|
+
Every data table gets the three lines the sample shows — bold header row,
|
|
143
|
+
`ws.freeze_panes = "A2"`, and column widths. Without them the user opens a wall
|
|
144
|
+
of `####` columns and loses the header the moment they scroll.
|
|
145
|
+
|
|
146
|
+
When the user asks for made-up, sample, or random data, generate it so it holds
|
|
147
|
+
together: a high is above its close, a low is below it, dates run in order,
|
|
148
|
+
percentages sum to about 100. Drawing each cell independently from a random range
|
|
149
|
+
puts a low of 64,000 next to a high of 21,000 in the same row, and the user reads
|
|
150
|
+
that as a broken file rather than as placeholder data.
|
|
151
|
+
|
|
152
|
+
Number formats are strings on the cell: `'#,##0.00'` for money-style decimals,
|
|
153
|
+
`'0.0%'` for percentages (store the fraction, e.g. `0.31`, not `31`), `'yyyy-mm-dd'`
|
|
154
|
+
for dates.
|
|
155
|
+
|
|
156
|
+
## Dates
|
|
157
|
+
|
|
158
|
+
Only when the user needs real date sorting or filtering — a year or a month label
|
|
159
|
+
is a plain number or string, and needs none of this. Copy both lines together;
|
|
160
|
+
the import is the half that gets forgotten:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from datetime import date # NOT `import datetime`, NOT `from datetime import datetime`
|
|
164
|
+
|
|
165
|
+
ws["A2"] = date(2016, 1, 1) # a bare date(...) call — never datetime.date(...)
|
|
166
|
+
ws["A2"].number_format = 'yyyy-mm-dd'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`datetime.date(2016, 1, 1)` after `from datetime import datetime` raises
|
|
170
|
+
`TypeError: descriptor 'date' for 'datetime.datetime' objects doesn't apply to a
|
|
171
|
+
'int' object`, and with no import at all it raises `NameError`. Both mean the same
|
|
172
|
+
thing: use the two lines above exactly as written.
|
|
173
|
+
|
|
174
|
+
## Several Sheets
|
|
175
|
+
|
|
176
|
+
When the user asks for N sheets — "a tab per region", "ten sheets of facts" — write
|
|
177
|
+
**one flat list of rows** and let the code group it. Each row names its own sheet as
|
|
178
|
+
its first value, exactly like `ROWS` above, so there are no nested lists to author:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from openpyxl import Workbook
|
|
182
|
+
from openpyxl.styles import Alignment, Font
|
|
183
|
+
|
|
184
|
+
HEADERS = ["#", "Fact"] # the columns AFTER the sheet name
|
|
185
|
+
WIDTHS = [6, 70] # one width per header column
|
|
186
|
+
ROWS = [ # (sheet, then one value per header column)
|
|
187
|
+
("Vision", 1, "Parrots see ultraviolet light that humans cannot."),
|
|
188
|
+
("Vision", 2, "Their color vision uses four cone types, not three."),
|
|
189
|
+
("Vision", 3, "UV patterns on feathers help them choose mates."),
|
|
190
|
+
("Speech", 1, "Parrots mimic sound with a syrinx, not vocal cords."),
|
|
191
|
+
("Speech", 2, "African greys can attach meaning to words."),
|
|
192
|
+
("Speech", 3, "Wild flocks carry regional dialects."),
|
|
193
|
+
]
|
|
194
|
+
OUTPUT = "facts.xlsx"
|
|
195
|
+
|
|
196
|
+
wb = Workbook()
|
|
197
|
+
spare = wb.active # removed at the end; every sheet is created below
|
|
198
|
+
written = {}
|
|
199
|
+
for row in ROWS:
|
|
200
|
+
title = row[0]
|
|
201
|
+
if title in written:
|
|
202
|
+
ws = wb[title]
|
|
203
|
+
else:
|
|
204
|
+
ws = wb.create_sheet(title)
|
|
205
|
+
ws.append(HEADERS)
|
|
206
|
+
for cell in ws[1]:
|
|
207
|
+
cell.font = Font(bold=True)
|
|
208
|
+
ws.column_dimensions[cell.column_letter].width = WIDTHS[cell.column - 1]
|
|
209
|
+
ws.freeze_panes = "A2"
|
|
210
|
+
written[title] = 0
|
|
211
|
+
ws.append(list(row[1:])) # row[1:] drops the sheet name — never append row
|
|
212
|
+
written[title] += len(row) - 1
|
|
213
|
+
ws.cell(row=ws.max_row, column=len(HEADERS)).alignment = Alignment(wrap_text=True)
|
|
214
|
+
|
|
215
|
+
wb.remove(spare)
|
|
216
|
+
assert written, "no rows were written"
|
|
217
|
+
for title in written:
|
|
218
|
+
rows_written = written[title] // len(HEADERS)
|
|
219
|
+
assert rows_written >= 3, f"sheet {title} holds {rows_written} row(s) — write at least 3"
|
|
220
|
+
wb.save(OUTPUT)
|
|
221
|
+
print(f"{len(wb.sheetnames)} sheets: " + ", ".join(f"{t}={written[t]}v" for t in written))
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Append `list(row[1:])`, never `row` and never `[row]`.** `row` still carries the
|
|
225
|
+
sheet name, and `[row]` puts the whole tuple in one cell — that is what
|
|
226
|
+
`ValueError: Cannot convert (1, 'Parrots see …') to Excel` means. The slice is the
|
|
227
|
+
only unpacking this program does; do not add `for a, b in ROWS` loops of your own,
|
|
228
|
+
which raise `ValueError: too many values to unpack`.
|
|
229
|
+
|
|
230
|
+
A formula that references a sheet whose name contains a space must quote it:
|
|
231
|
+
`=SUM('Raw Data'!B1:B2)`.
|
|
232
|
+
|
|
233
|
+
**Do not invent a nested shape.** A list of `(title, [rows…])` pairs is the single
|
|
234
|
+
most common way this call fails: the inner list reaches a cell and openpyxl refuses
|
|
235
|
+
it. One flat list, sheet name first.
|
|
236
|
+
|
|
237
|
+
The count printed per sheet is **written values**, not rows. A row count proves
|
|
238
|
+
nothing: `ws.max_row` grows when a cell is merely styled, so a sheet that got
|
|
239
|
+
alignment but no values still reports four rows while holding nothing.
|
|
240
|
+
|
|
241
|
+
## Embedding an Image
|
|
242
|
+
|
|
243
|
+
**The image must be staged in `inputs` in the same `exec` call.** The working
|
|
244
|
+
directory starts empty, so a file name that is not in `inputs` does not exist.
|
|
245
|
+
`XLImage("photo.png")` without an `inputs` entry whose `path` is `photo.png` raises
|
|
246
|
+
`FileNotFoundError`, and rerunning the same command fails the same way.
|
|
247
|
+
|
|
248
|
+
**`path` is a name you choose, not a name you derive.** It has nothing to do with
|
|
249
|
+
the attachment id: `photo.png` is a fine `path` for an attachment whose id is
|
|
250
|
+
`9f2c4ab1…`. Two mistakes to never make:
|
|
251
|
+
|
|
252
|
+
- Turning an id into a file name — `XLImage("9f2c4ab1….png")`, or opening the bare
|
|
253
|
+
id. An id goes in `attachmentId`, never in a path and never in the Python.
|
|
254
|
+
- Opening a name that merely appeared in the conversation. A `generate_image` result
|
|
255
|
+
carries a `fileName` — what the picture was called when it was made, not a file on
|
|
256
|
+
disk. Take the `attachmentId` from that result and ignore the rest; nothing is
|
|
257
|
+
readable until an `inputs` entry stages it under a `path` you chose.
|
|
258
|
+
|
|
259
|
+
**A `generate_image` result anywhere in the conversation IS the image the user
|
|
260
|
+
means.** "Add this image" points at the picture this chat already produced, in an
|
|
261
|
+
earlier turn as much as this one; the user's own message carries no attachment and
|
|
262
|
+
does not need to. Stage that id and build. Do not ask for an image the conversation
|
|
263
|
+
already has, and never generate a replacement to embed in its place.
|
|
264
|
+
|
|
265
|
+
An image the user **uploaded** has no id to copy. Stage it with `path` only and no
|
|
266
|
+
`attachmentId` key; the first id-less entry is the first image of their latest
|
|
267
|
+
message.
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"language": "python",
|
|
272
|
+
"packages": ["openpyxl==3.1.5", "pillow"],
|
|
273
|
+
"inputs": [{ "attachmentId": "<id from the generate_image result>", "path": "photo.png" }],
|
|
274
|
+
"outputs": ["report.xlsx"],
|
|
275
|
+
"command": "..."
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The class lives in `openpyxl.drawing.image` and collides with PIL's `Image`, so
|
|
280
|
+
alias it. `add_image` anchors the picture's top-left corner at one cell:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from openpyxl.drawing.image import Image as XLImage
|
|
284
|
+
|
|
285
|
+
img = XLImage("photo.png") # the path from inputs, nothing else
|
|
286
|
+
img.width, img.height = 320, 320 # pixels
|
|
287
|
+
ws.add_image(img, "A1") # A1 is the top-left corner, not a range
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`add_image` is a **worksheet** method — `wb.add_image(...)` raises `AttributeError`.
|
|
291
|
+
The picture floats above the grid rather than filling a cell, so leave the rows it
|
|
292
|
+
covers empty.
|
|
293
|
+
|
|
294
|
+
**One `add_image` call, on one sheet.** "Add this image" means the workbook carries
|
|
295
|
+
the picture once — put it on the first sheet, or on a sheet of its own. Calling
|
|
296
|
+
`add_image` inside the loop that builds the sheets embeds a fresh copy per sheet.
|
|
297
|
+
|
|
298
|
+
**Check the call before sending it.** Every image file name in `command` must also
|
|
299
|
+
be the `path` of an `inputs` entry in that same call, and `packages` must include
|
|
300
|
+
`"pillow"`. The reverse holds too: if `inputs` stages an image, `command` must call
|
|
301
|
+
`add_image`. Never wrap `XLImage` or the import in a `try`/`except` that saves
|
|
302
|
+
anyway, and never draw a substitute with PIL — the image already exists.
|
|
303
|
+
|
|
304
|
+
Merged cells: after `ws.merge_cells("A1:C1")` only the top-left anchor is
|
|
305
|
+
writable — `ws["A1"] = "Title"` works, while writing `B1` or `C1` raises
|
|
306
|
+
`AttributeError: 'MergedCell' object attribute 'value' is read-only`. Write the
|
|
307
|
+
anchor, always.
|
|
308
|
+
|
|
309
|
+
## Errors
|
|
310
|
+
|
|
311
|
+
- Never print the workbook's bytes or base64 — stdout is capped and the file
|
|
312
|
+
travels through `outputs`. A build call prints only a short summary line
|
|
313
|
+
(e.g. `2 sheets, 5 rows`).
|
|
314
|
+
- `SyntaxError: invalid syntax` on a one-line `for`/`if` means the source was
|
|
315
|
+
collapsed — restore multi-line newlines from the sample and rerun. Underscores
|
|
316
|
+
in names (`load_workbook`, not `loadworkbook`) must stay. Do not switch to
|
|
317
|
+
`python -c` or change the package pin.
|
|
318
|
+
- `ModuleNotFoundError: No module named 'openpyxl'` means `packages` was missing
|
|
319
|
+
or wrong — add `["openpyxl==3.1.5"]` and rerun. Never try to install it.
|
|
320
|
+
- `attachment … not found in this chat` means `inputs` listed an id that is not in
|
|
321
|
+
this chat (often a copied placeholder). For a new workbook, omit `inputs`
|
|
322
|
+
entirely and rerun.
|
|
323
|
+
- `ValueError: Cannot convert [...] to Excel` means a list or tuple reached a cell:
|
|
324
|
+
the code appended `row` or `[row]` instead of `list(row[1:])`, or invented a
|
|
325
|
+
nested `(title, [rows…])` shape. Flatten it — see Several Sheets.
|
|
326
|
+
- `ImportError: You must install Pillow to fetch image objects` means an image was
|
|
327
|
+
embedded without `"pillow"` in `packages` — openpyxl does not install it. Add it
|
|
328
|
+
and rerun.
|
|
329
|
+
- `ModuleNotFoundError: No module named 'PIL'` means the code imported PIL itself,
|
|
330
|
+
usually to draw a picture that already exists as an attachment. Stage the
|
|
331
|
+
attachment through `inputs` instead.
|
|
332
|
+
- `AttributeError: 'Workbook' object has no attribute 'add_image'` — `add_image` is
|
|
333
|
+
a worksheet method: `ws.add_image(img, "A1")`.
|
|
334
|
+
- `FileNotFoundError` on a 32-character hex name means an attachment id was opened
|
|
335
|
+
as a path. The id belongs in `attachmentId`; open the `path` you chose.
|
|
336
|
+
- A delivered workbook with no picture in it, when the user asked for one, means the
|
|
337
|
+
image was never staged or `add_image` was never called. Rebuild it in one call.
|
|
338
|
+
- `ImportError: cannot import name 'NumberFormat' from 'openpyxl.styles'` — there
|
|
339
|
+
is no such class. A number format is a plain string on the cell:
|
|
340
|
+
`cell.number_format = '#,##0.00'`. The only names to import from
|
|
341
|
+
`openpyxl.styles` are `Font`, `Alignment`, `PatternFill`, `Border`, `Side`.
|
|
342
|
+
- `TypeError: expected string or bytes-like object, got 'list'` means a worksheet
|
|
343
|
+
was indexed with a list — `ws[row]` where `row` holds the values just appended.
|
|
344
|
+
`ws[...]` takes a row number (`ws[1]`) or a range string (`ws["A1:C1"]`). To
|
|
345
|
+
style the row you just appended, index it by number: `ws[ws.max_row]`.
|
|
346
|
+
- `NameError: name 'openpyxl' is not defined` means the code called
|
|
347
|
+
`openpyxl.Workbook()` after `from openpyxl import Workbook` — call `Workbook()`.
|
|
348
|
+
- `NameError` on any other name means a variable was defined inside a `def` and
|
|
349
|
+
read outside it. Delete the function and inline its body at top level.
|
|
350
|
+
- `ValueError: month must be in 1..12, not 13` means the code did month
|
|
351
|
+
arithmetic. Type the periods out as literal values instead.
|
|
352
|
+
- `AssertionError: sheet <name> holds N row(s) — write at least 3` means a sheet got
|
|
353
|
+
fewer facts than the request implies. Add rows to that sheet and rerun; do not lower
|
|
354
|
+
the assert. **Row counts do not prove content**: `ws.max_row` counts a cell that was
|
|
355
|
+
merely styled, and is 1 on a truly empty sheet and never 0, so a header-only sheet
|
|
356
|
+
can report `4 rows` while holding nothing. Append values before styling anything.
|
|
357
|
+
- A bare `Sheet` among the sheet names is the untouched default sheet, left as a
|
|
358
|
+
blank first tab in front of the data. Either is a failed turn even at `exitCode 0`
|
|
359
|
+
— the user gets a blank or near-blank workbook. Rebuild it with literal rows.
|
|
360
|
+
- On an `AttributeError` from openpyxl the API name is wrong, and on a `TypeError`
|
|
361
|
+
about missing positional arguments a required argument was left out — fix either
|
|
362
|
+
against this file's examples. Do not retry the same call, and do not switch to a
|
|
363
|
+
shell.
|
|
364
|
+
- `'MergedCell' object attribute 'value' is read-only` means the write hit a merged
|
|
365
|
+
non-anchor cell — write the range's top-left cell instead.
|
|
366
|
+
- A formula cell that reads `None` or shows empty in a preview is not a bug —
|
|
367
|
+
this runtime computes nothing; the value appears when the user opens the file.
|
|
368
|
+
Do not rewrite the workbook to "fix" it.
|
|
369
|
+
|
|
370
|
+
## Finish
|
|
371
|
+
|
|
372
|
+
When `exitCode` is `0` and `attachments` lists the `.xlsx`, stop tool use and
|
|
373
|
+
answer with one line: file name + the sheet/row summary from stdout. Exactly one
|
|
374
|
+
successful `exec` per request.
|