offagent 0.10.0__py3-none-any.whl

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 (39) hide show
  1. offagent/__init__.py +3 -0
  2. offagent/__main__.py +5 -0
  3. offagent/adapters/__init__.py +1 -0
  4. offagent/adapters/docx_adapter.py +1237 -0
  5. offagent/adapters/embedding_provider.py +132 -0
  6. offagent/adapters/pptx_adapter.py +940 -0
  7. offagent/adapters/xlsx_adapter.py +1266 -0
  8. offagent/app/__init__.py +1 -0
  9. offagent/app/progress.py +52 -0
  10. offagent/app/services.py +4267 -0
  11. offagent/config.py +287 -0
  12. offagent/domain/__init__.py +1 -0
  13. offagent/domain/locators.py +444 -0
  14. offagent/domain/models.py +477 -0
  15. offagent/domain/text_fragments.py +136 -0
  16. offagent/errors.py +29 -0
  17. offagent/indexing/__init__.py +1 -0
  18. offagent/indexing/store.py +795 -0
  19. offagent/interfaces/__init__.py +1 -0
  20. offagent/interfaces/cli.py +438 -0
  21. offagent/interfaces/cli_output.py +139 -0
  22. offagent/interfaces/cli_progress.py +120 -0
  23. offagent/interfaces/mcp.py +1145 -0
  24. offagent/interfaces/mcp_converters.py +80 -0
  25. offagent/interfaces/mcp_models.py +923 -0
  26. offagent/objects/__init__.py +3 -0
  27. offagent/objects/base.py +26 -0
  28. offagent/objects/docx_objects.py +951 -0
  29. offagent/objects/pptx_objects.py +895 -0
  30. offagent/objects/xlsx_objects.py +962 -0
  31. offagent/path_policy.py +42 -0
  32. offagent/storage/__init__.py +1 -0
  33. offagent/storage/versioning.py +31 -0
  34. offagent-0.10.0.dist-info/METADATA +546 -0
  35. offagent-0.10.0.dist-info/RECORD +39 -0
  36. offagent-0.10.0.dist-info/WHEEL +5 -0
  37. offagent-0.10.0.dist-info/entry_points.txt +2 -0
  38. offagent-0.10.0.dist-info/licenses/LICENSE +21 -0
  39. offagent-0.10.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,962 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from offagent.adapters import xlsx_adapter
8
+ from offagent.domain.locators import parse_locator, to_v2_locator
9
+ from offagent.domain.models import Capability, ChildSummary, ObjectPayload
10
+ from offagent.errors import InvalidArgumentsError, TargetNotFoundError
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class _XlsxTarget:
15
+ canonical_locator: str
16
+ object_type: str
17
+ sheet_name: str | None = None
18
+ row_index: int | None = None
19
+ column_index: int | None = None
20
+ coordinate: str | None = None
21
+ cell_range: str | None = None
22
+ name: str | None = None
23
+
24
+
25
+ class XlsxObjectResolver:
26
+ def get_object(self, document_path: Path, locator: str) -> ObjectPayload:
27
+ canonical = to_v2_locator(locator, file_type="xlsx")
28
+ workbook = xlsx_adapter._open_workbook(document_path)
29
+ target = _parse_xlsx_target(canonical)
30
+
31
+ if target.object_type == "workbook":
32
+ return _build_workbook_payload(document_path, workbook)
33
+ if target.object_type == "worksheet":
34
+ return _build_worksheet_payload(document_path, workbook, target)
35
+ if target.object_type == "row":
36
+ return _build_row_payload(document_path, workbook, target)
37
+ if target.object_type == "column":
38
+ return _build_column_payload(document_path, workbook, target)
39
+ if target.object_type == "cell":
40
+ return _build_cell_payload(document_path, workbook, target)
41
+ if target.object_type == "range":
42
+ return _build_range_payload(document_path, workbook, target)
43
+ if target.object_type == "table":
44
+ return _build_table_payload(document_path, workbook, target)
45
+ if target.object_type == "merged_range":
46
+ return _build_merged_range_payload(document_path, workbook, target)
47
+ if target.object_type == "formula_cell":
48
+ return _build_formula_cell_payload(document_path, workbook, target)
49
+ if target.object_type == "named_range":
50
+ return _build_named_range_payload(document_path, workbook, target)
51
+ raise InvalidArgumentsError(f"Unsupported XLSX locator: {locator}")
52
+
53
+ def list_children(
54
+ self,
55
+ document_path: Path,
56
+ locator: str,
57
+ *,
58
+ child_type: str | None = None,
59
+ limit: int | None = None,
60
+ ) -> list[ChildSummary]:
61
+ canonical = to_v2_locator(locator, file_type="xlsx")
62
+ workbook = xlsx_adapter._open_workbook(document_path)
63
+ target = _parse_xlsx_target(canonical)
64
+
65
+ if target.object_type == "workbook":
66
+ children = _workbook_children(workbook, child_type=child_type)
67
+ elif target.object_type == "worksheet":
68
+ children = _worksheet_children(workbook, target, child_type=child_type)
69
+ elif target.object_type == "row":
70
+ children = _row_children(workbook, target, child_type=child_type)
71
+ elif target.object_type == "column":
72
+ children = _column_children(workbook, target, child_type=child_type)
73
+ elif target.object_type == "range":
74
+ children = _range_children(workbook, target, child_type=child_type)
75
+ elif target.object_type == "table":
76
+ children = _table_children(workbook, target, child_type=child_type)
77
+ else:
78
+ children = ()
79
+
80
+ if limit is not None:
81
+ return list(children[:limit])
82
+ return list(children)
83
+
84
+ def resolve_capabilities(
85
+ self, document_path: Path, locator: str
86
+ ) -> frozenset[Capability]:
87
+ workbook = xlsx_adapter._open_workbook(document_path)
88
+ canonical = to_v2_locator(locator, file_type="xlsx")
89
+ target = _parse_xlsx_target(canonical)
90
+ return _capabilities_for(workbook, target)
91
+
92
+
93
+ def write_range(
94
+ document_path: Path,
95
+ locator: str,
96
+ values: list[list[Any]],
97
+ *,
98
+ output_path: Path,
99
+ ) -> tuple[str, str, dict[str, Any]]:
100
+ canonical = to_v2_locator(locator, file_type="xlsx")
101
+ workbook = xlsx_adapter._open_workbook(document_path)
102
+ target = _parse_xlsx_target(canonical)
103
+ if target.object_type != "range":
104
+ raise InvalidArgumentsError("xlsx_write_range requires an XLSX range locator.")
105
+
106
+ worksheet = _resolve_worksheet(workbook, target)
107
+ min_row, min_col, max_row, max_col = _range_bounds(target.cell_range, canonical)
108
+ expected_rows = max_row - min_row + 1
109
+ expected_cols = max_col - min_col + 1
110
+ if len(values) != expected_rows or any(len(row) != expected_cols for row in values):
111
+ raise InvalidArgumentsError(
112
+ "Value array dimensions do not match the target XLSX range."
113
+ )
114
+
115
+ for row_offset, row in enumerate(values):
116
+ for col_offset, value in enumerate(row):
117
+ worksheet.cell(
118
+ row=min_row + row_offset, column=min_col + col_offset
119
+ ).value = _coerce_write_value(value)
120
+
121
+ workbook.save(output_path)
122
+ return (
123
+ canonical,
124
+ f"Wrote {expected_rows}x{expected_cols} values to {canonical}.",
125
+ {"row_count": expected_rows, "column_count": expected_cols},
126
+ )
127
+
128
+
129
+ def insert_rows(
130
+ document_path: Path,
131
+ locator: str,
132
+ row_number: int,
133
+ count: int,
134
+ *,
135
+ output_path: Path,
136
+ ) -> tuple[str, str, dict[str, Any]]:
137
+ if row_number < 1 or count < 1:
138
+ raise InvalidArgumentsError(
139
+ "xlsx_insert_rows requires positive row_number and count."
140
+ )
141
+
142
+ canonical = to_v2_locator(locator, file_type="xlsx")
143
+ workbook = xlsx_adapter._open_workbook(document_path)
144
+ target = _parse_xlsx_target(canonical)
145
+ if target.object_type != "worksheet":
146
+ raise InvalidArgumentsError(
147
+ "xlsx_insert_rows requires an XLSX worksheet locator."
148
+ )
149
+
150
+ worksheet = _resolve_worksheet(workbook, target)
151
+ worksheet.insert_rows(row_number, count)
152
+ workbook.save(output_path)
153
+ return (
154
+ f"xlsx:sheet:{worksheet.title}:row:{row_number}",
155
+ f"Inserted {count} rows at {canonical} row {row_number}.",
156
+ {"worksheet_locator": canonical, "row_number": row_number, "count": count},
157
+ )
158
+
159
+
160
+ def insert_columns(
161
+ document_path: Path,
162
+ locator: str,
163
+ column_index: int,
164
+ count: int,
165
+ *,
166
+ output_path: Path,
167
+ ) -> tuple[str, str, dict[str, Any]]:
168
+ if column_index < 1 or count < 1:
169
+ raise InvalidArgumentsError(
170
+ "xlsx_insert_columns requires positive column_index and count."
171
+ )
172
+
173
+ canonical = to_v2_locator(locator, file_type="xlsx")
174
+ workbook = xlsx_adapter._open_workbook(document_path)
175
+ target = _parse_xlsx_target(canonical)
176
+ if target.object_type != "worksheet":
177
+ raise InvalidArgumentsError(
178
+ "xlsx_insert_columns requires an XLSX worksheet locator."
179
+ )
180
+
181
+ worksheet = _resolve_worksheet(workbook, target)
182
+ worksheet.insert_cols(column_index, count)
183
+ workbook.save(output_path)
184
+ return (
185
+ f"xlsx:sheet:{worksheet.title}:col:{column_index}",
186
+ f"Inserted {count} columns at {canonical} column {column_index}.",
187
+ {"worksheet_locator": canonical, "column_index": column_index, "count": count},
188
+ )
189
+
190
+
191
+ def set_formula(
192
+ document_path: Path,
193
+ locator: str,
194
+ formula: str,
195
+ *,
196
+ output_path: Path,
197
+ ) -> tuple[str, str, dict[str, Any]]:
198
+ canonical = to_v2_locator(locator, file_type="xlsx")
199
+ workbook = xlsx_adapter._open_workbook(document_path)
200
+ target = _parse_xlsx_target(canonical)
201
+ if target.object_type not in {"cell", "formula_cell"}:
202
+ raise InvalidArgumentsError("xlsx_set_formula requires an XLSX cell locator.")
203
+ if not formula.startswith("=") or len(formula.strip()) <= 1:
204
+ raise InvalidArgumentsError(
205
+ "Invalid XLSX formula; formulas must start with '='."
206
+ )
207
+
208
+ worksheet = _resolve_worksheet(workbook, target)
209
+ cell = _resolve_cell(worksheet, target.coordinate, canonical)
210
+ cell.value = formula
211
+ workbook.save(output_path)
212
+ formula_locator = f"xlsx:sheet:{worksheet.title}:formula_cell:{cell.coordinate}"
213
+ return (
214
+ formula_locator,
215
+ f"Set formula on {formula_locator}.",
216
+ {"formula": formula},
217
+ )
218
+
219
+
220
+ def merge_cells(
221
+ document_path: Path,
222
+ locator: str,
223
+ *,
224
+ output_path: Path,
225
+ ) -> tuple[str, str, dict[str, Any]]:
226
+ canonical = to_v2_locator(locator, file_type="xlsx")
227
+ workbook = xlsx_adapter._open_workbook(document_path)
228
+ target = _parse_xlsx_target(canonical)
229
+ if target.object_type != "range":
230
+ raise InvalidArgumentsError("xlsx_merge_cells requires an XLSX range locator.")
231
+
232
+ worksheet = _resolve_worksheet(workbook, target)
233
+ target_bounds = _range_bounds(target.cell_range, canonical)
234
+ for merged in worksheet.merged_cells.ranges:
235
+ if _ranges_overlap(target_bounds, _range_bounds(str(merged), str(merged))):
236
+ raise InvalidArgumentsError(
237
+ f"Range {target.cell_range} overlaps existing merged range {merged}."
238
+ )
239
+
240
+ worksheet.merge_cells(target.cell_range)
241
+ workbook.save(output_path)
242
+ merged_locator = f"xlsx:sheet:{worksheet.title}:merged_range:{target.cell_range}"
243
+ return (
244
+ merged_locator,
245
+ f"Merged cells in {canonical}.",
246
+ {"range": target.cell_range},
247
+ )
248
+
249
+
250
+ def _build_workbook_payload(document_path: Path, workbook) -> ObjectPayload:
251
+ return ObjectPayload(
252
+ document=xlsx_adapter._document_ref(document_path),
253
+ locator="xlsx:workbook",
254
+ object_type="workbook",
255
+ preview=next((worksheet.title for worksheet in workbook.worksheets), ""),
256
+ properties={"sheet_count": len(workbook.worksheets)},
257
+ capabilities=_capability_tuple(
258
+ workbook, _XlsxTarget("xlsx:workbook", "workbook")
259
+ ),
260
+ child_summary=_workbook_children(workbook),
261
+ )
262
+
263
+
264
+ def _build_worksheet_payload(
265
+ document_path: Path, workbook, target: _XlsxTarget
266
+ ) -> ObjectPayload:
267
+ worksheet = _resolve_worksheet(workbook, target)
268
+ used_bounds = xlsx_adapter._used_bounds(worksheet)
269
+ return ObjectPayload(
270
+ document=xlsx_adapter._document_ref(document_path),
271
+ locator=target.canonical_locator,
272
+ object_type="worksheet",
273
+ preview=_worksheet_preview(worksheet),
274
+ properties={
275
+ "sheet_name": worksheet.title,
276
+ "used_range": xlsx_adapter._format_range(used_bounds),
277
+ "row_count": 0
278
+ if used_bounds is None
279
+ else used_bounds[2] - used_bounds[0] + 1,
280
+ "column_count": 0
281
+ if used_bounds is None
282
+ else used_bounds[3] - used_bounds[1] + 1,
283
+ "table_count": len(worksheet.tables),
284
+ "merged_range_count": len(worksheet.merged_cells.ranges),
285
+ },
286
+ capabilities=_capability_tuple(workbook, target),
287
+ parent_locator="xlsx:workbook",
288
+ child_summary=_worksheet_children(workbook, target),
289
+ )
290
+
291
+
292
+ def _build_row_payload(
293
+ document_path: Path, workbook, target: _XlsxTarget
294
+ ) -> ObjectPayload:
295
+ worksheet = _resolve_worksheet(workbook, target)
296
+ assert target.row_index is not None
297
+ used_bounds = xlsx_adapter._used_bounds(worksheet)
298
+ max_col = 0 if used_bounds is None else used_bounds[3]
299
+ row_cells = [
300
+ worksheet.cell(row=target.row_index, column=column_index)
301
+ for column_index in range(1, max_col + 1)
302
+ ]
303
+ return ObjectPayload(
304
+ document=xlsx_adapter._document_ref(document_path),
305
+ locator=target.canonical_locator,
306
+ object_type="row",
307
+ preview=" | ".join(
308
+ xlsx_adapter._display_text(cell)
309
+ for cell in row_cells
310
+ if xlsx_adapter._display_text(cell)
311
+ )[:120],
312
+ properties={
313
+ "sheet_name": worksheet.title,
314
+ "row_index": target.row_index,
315
+ "cell_count": len(row_cells),
316
+ "non_empty_cell_count": sum(
317
+ 1 for cell in row_cells if xlsx_adapter._is_indexable_cell(cell)
318
+ ),
319
+ },
320
+ capabilities=_capability_tuple(workbook, target),
321
+ parent_locator=f"xlsx:sheet:{worksheet.title}",
322
+ child_summary=_row_children(workbook, target),
323
+ )
324
+
325
+
326
+ def _build_column_payload(
327
+ document_path: Path, workbook, target: _XlsxTarget
328
+ ) -> ObjectPayload:
329
+ worksheet = _resolve_worksheet(workbook, target)
330
+ assert target.column_index is not None
331
+ used_bounds = xlsx_adapter._used_bounds(worksheet)
332
+ max_row = 0 if used_bounds is None else used_bounds[2]
333
+ column_cells = [
334
+ worksheet.cell(row=row_index, column=target.column_index)
335
+ for row_index in range(1, max_row + 1)
336
+ ]
337
+ return ObjectPayload(
338
+ document=xlsx_adapter._document_ref(document_path),
339
+ locator=target.canonical_locator,
340
+ object_type="column",
341
+ preview=" | ".join(
342
+ xlsx_adapter._display_text(cell)
343
+ for cell in column_cells
344
+ if xlsx_adapter._display_text(cell)
345
+ )[:120],
346
+ properties={
347
+ "sheet_name": worksheet.title,
348
+ "column_index": target.column_index,
349
+ "column_letter": xlsx_adapter.get_column_letter(target.column_index),
350
+ "cell_count": len(column_cells),
351
+ "non_empty_cell_count": sum(
352
+ 1 for cell in column_cells if xlsx_adapter._is_indexable_cell(cell)
353
+ ),
354
+ },
355
+ capabilities=_capability_tuple(workbook, target),
356
+ parent_locator=f"xlsx:sheet:{worksheet.title}",
357
+ child_summary=_column_children(workbook, target),
358
+ )
359
+
360
+
361
+ def _build_cell_payload(
362
+ document_path: Path, workbook, target: _XlsxTarget
363
+ ) -> ObjectPayload:
364
+ worksheet = _resolve_worksheet(workbook, target)
365
+ cell = _resolve_cell(worksheet, target.coordinate, target.canonical_locator)
366
+ return ObjectPayload(
367
+ document=xlsx_adapter._document_ref(document_path),
368
+ locator=target.canonical_locator,
369
+ object_type="cell",
370
+ preview=xlsx_adapter._display_text(cell)[:120],
371
+ properties={
372
+ "sheet_name": worksheet.title,
373
+ "coordinate": cell.coordinate,
374
+ "value": cell.value,
375
+ "display_value": xlsx_adapter._display_text(cell),
376
+ "formula": xlsx_adapter._formula_text(cell),
377
+ "data_type": cell.data_type,
378
+ },
379
+ capabilities=_capability_tuple(workbook, target),
380
+ parent_locator=f"xlsx:sheet:{worksheet.title}",
381
+ )
382
+
383
+
384
+ def _build_range_payload(
385
+ document_path: Path, workbook, target: _XlsxTarget
386
+ ) -> ObjectPayload:
387
+ worksheet = _resolve_worksheet(workbook, target)
388
+ min_row, min_col, max_row, max_col = _range_bounds(
389
+ target.cell_range, target.canonical_locator
390
+ )
391
+ cells = [
392
+ worksheet.cell(row=row_index, column=column_index)
393
+ for row_index in range(min_row, max_row + 1)
394
+ for column_index in range(min_col, max_col + 1)
395
+ ]
396
+ preview = next(
397
+ (
398
+ xlsx_adapter._display_text(cell)[:120]
399
+ for cell in cells
400
+ if xlsx_adapter._display_text(cell)
401
+ ),
402
+ "",
403
+ )
404
+ return ObjectPayload(
405
+ document=xlsx_adapter._document_ref(document_path),
406
+ locator=target.canonical_locator,
407
+ object_type="range",
408
+ preview=preview,
409
+ properties={
410
+ "sheet_name": worksheet.title,
411
+ "range": target.cell_range,
412
+ "cell_count": len(cells),
413
+ },
414
+ capabilities=_capability_tuple(workbook, target),
415
+ parent_locator=f"xlsx:sheet:{worksheet.title}",
416
+ child_summary=_range_children(workbook, target),
417
+ )
418
+
419
+
420
+ def _build_table_payload(
421
+ document_path: Path, workbook, target: _XlsxTarget
422
+ ) -> ObjectPayload:
423
+ worksheet = _resolve_worksheet(workbook, target)
424
+ table = _resolve_table(worksheet, target.name, target.canonical_locator)
425
+ return ObjectPayload(
426
+ document=xlsx_adapter._document_ref(document_path),
427
+ locator=target.canonical_locator,
428
+ object_type="table",
429
+ preview=table.displayName,
430
+ properties={
431
+ "sheet_name": worksheet.title,
432
+ "table_name": table.displayName,
433
+ "range": table.ref,
434
+ },
435
+ capabilities=_capability_tuple(workbook, target),
436
+ parent_locator=f"xlsx:sheet:{worksheet.title}",
437
+ child_summary=_table_children(workbook, target),
438
+ )
439
+
440
+
441
+ def _build_merged_range_payload(
442
+ document_path: Path, workbook, target: _XlsxTarget
443
+ ) -> ObjectPayload:
444
+ worksheet = _resolve_worksheet(workbook, target)
445
+ merged = _resolve_merged_range(
446
+ worksheet, target.cell_range, target.canonical_locator
447
+ )
448
+ return ObjectPayload(
449
+ document=xlsx_adapter._document_ref(document_path),
450
+ locator=target.canonical_locator,
451
+ object_type="merged_range",
452
+ preview=str(merged),
453
+ properties={"sheet_name": worksheet.title, "range": str(merged)},
454
+ capabilities=_capability_tuple(workbook, target),
455
+ parent_locator=f"xlsx:sheet:{worksheet.title}",
456
+ )
457
+
458
+
459
+ def _build_formula_cell_payload(
460
+ document_path: Path, workbook, target: _XlsxTarget
461
+ ) -> ObjectPayload:
462
+ worksheet = _resolve_worksheet(workbook, target)
463
+ cell = _resolve_cell(worksheet, target.coordinate, target.canonical_locator)
464
+ if xlsx_adapter._formula_text(cell) is None:
465
+ raise TargetNotFoundError(f"Cell {cell.coordinate} does not contain a formula.")
466
+ return ObjectPayload(
467
+ document=xlsx_adapter._document_ref(document_path),
468
+ locator=target.canonical_locator,
469
+ object_type="formula_cell",
470
+ preview=xlsx_adapter._formula_text(cell)[:120],
471
+ properties={
472
+ "sheet_name": worksheet.title,
473
+ "coordinate": cell.coordinate,
474
+ "formula": xlsx_adapter._formula_text(cell),
475
+ "display_value": xlsx_adapter._display_text(cell),
476
+ },
477
+ capabilities=_capability_tuple(workbook, target),
478
+ parent_locator=f"xlsx:sheet:{worksheet.title}",
479
+ )
480
+
481
+
482
+ def _build_named_range_payload(
483
+ document_path: Path, workbook, target: _XlsxTarget
484
+ ) -> ObjectPayload:
485
+ defined_name = _resolve_named_range(workbook, target.name, target.canonical_locator)
486
+ return ObjectPayload(
487
+ document=xlsx_adapter._document_ref(document_path),
488
+ locator=target.canonical_locator,
489
+ object_type="named_range",
490
+ preview=defined_name.name,
491
+ properties={"name": defined_name.name, "reference": defined_name.attr_text},
492
+ capabilities=_capability_tuple(workbook, target),
493
+ parent_locator="xlsx:workbook",
494
+ )
495
+
496
+
497
+ def _workbook_children(
498
+ workbook, *, child_type: str | None = None
499
+ ) -> tuple[ChildSummary, ...]:
500
+ if child_type not in {None, "", "worksheet"}:
501
+ return ()
502
+ return tuple(
503
+ ChildSummary(
504
+ locator=f"xlsx:sheet:{worksheet.title}",
505
+ object_type="worksheet",
506
+ preview=_worksheet_preview(worksheet),
507
+ capabilities=_capability_tuple(
508
+ workbook,
509
+ _XlsxTarget(
510
+ f"xlsx:sheet:{worksheet.title}",
511
+ "worksheet",
512
+ sheet_name=worksheet.title,
513
+ ),
514
+ ),
515
+ )
516
+ for worksheet in workbook.worksheets
517
+ )
518
+
519
+
520
+ def _worksheet_children(
521
+ workbook, target: _XlsxTarget, *, child_type: str | None = None
522
+ ) -> tuple[ChildSummary, ...]:
523
+ worksheet = _resolve_worksheet(workbook, target)
524
+ normalized_child_type = child_type or None
525
+ children: list[ChildSummary] = []
526
+
527
+ used_bounds = xlsx_adapter._used_bounds(worksheet)
528
+ if used_bounds is not None and normalized_child_type in {None, "row"}:
529
+ for row_index in range(used_bounds[0], used_bounds[2] + 1):
530
+ children.append(
531
+ ChildSummary(
532
+ locator=f"xlsx:sheet:{worksheet.title}:row:{row_index}",
533
+ object_type="row",
534
+ preview=_row_preview(worksheet, row_index),
535
+ capabilities=_capability_tuple(
536
+ workbook,
537
+ _XlsxTarget(
538
+ "", "row", sheet_name=worksheet.title, row_index=row_index
539
+ ),
540
+ ),
541
+ )
542
+ )
543
+
544
+ if used_bounds is not None and normalized_child_type in {None, "column"}:
545
+ for column_index in range(used_bounds[1], used_bounds[3] + 1):
546
+ children.append(
547
+ ChildSummary(
548
+ locator=f"xlsx:sheet:{worksheet.title}:col:{column_index}",
549
+ object_type="column",
550
+ preview=_column_preview(worksheet, column_index),
551
+ capabilities=_capability_tuple(
552
+ workbook,
553
+ _XlsxTarget(
554
+ "",
555
+ "column",
556
+ sheet_name=worksheet.title,
557
+ column_index=column_index,
558
+ ),
559
+ ),
560
+ )
561
+ )
562
+
563
+ if normalized_child_type in {None, "table"}:
564
+ for table_name in worksheet.tables:
565
+ children.append(
566
+ ChildSummary(
567
+ locator=f"xlsx:sheet:{worksheet.title}:table:{table_name}",
568
+ object_type="table",
569
+ preview=table_name,
570
+ capabilities=_capability_tuple(
571
+ workbook,
572
+ _XlsxTarget(
573
+ "", "table", sheet_name=worksheet.title, name=table_name
574
+ ),
575
+ ),
576
+ )
577
+ )
578
+
579
+ if normalized_child_type in {None, "merged_range"}:
580
+ for merged in worksheet.merged_cells.ranges:
581
+ children.append(
582
+ ChildSummary(
583
+ locator=f"xlsx:sheet:{worksheet.title}:merged_range:{merged}",
584
+ object_type="merged_range",
585
+ preview=str(merged),
586
+ capabilities=_capability_tuple(
587
+ workbook,
588
+ _XlsxTarget(
589
+ "",
590
+ "merged_range",
591
+ sheet_name=worksheet.title,
592
+ cell_range=str(merged),
593
+ ),
594
+ ),
595
+ )
596
+ )
597
+
598
+ if used_bounds is not None and normalized_child_type in {None, "formula_cell"}:
599
+ for row_index in range(used_bounds[0], used_bounds[2] + 1):
600
+ for column_index in range(used_bounds[1], used_bounds[3] + 1):
601
+ cell = worksheet.cell(row=row_index, column=column_index)
602
+ formula = xlsx_adapter._formula_text(cell)
603
+ if formula is None:
604
+ continue
605
+ children.append(
606
+ ChildSummary(
607
+ locator=f"xlsx:sheet:{worksheet.title}:formula_cell:{cell.coordinate}",
608
+ object_type="formula_cell",
609
+ preview=formula[:120],
610
+ capabilities=_capability_tuple(
611
+ workbook,
612
+ _XlsxTarget(
613
+ "",
614
+ "formula_cell",
615
+ sheet_name=worksheet.title,
616
+ coordinate=cell.coordinate,
617
+ ),
618
+ ),
619
+ )
620
+ )
621
+
622
+ return tuple(children)
623
+
624
+
625
+ def _row_children(
626
+ workbook, target: _XlsxTarget, *, child_type: str | None = None
627
+ ) -> tuple[ChildSummary, ...]:
628
+ if child_type not in {None, "", "cell"}:
629
+ return ()
630
+ worksheet = _resolve_worksheet(workbook, target)
631
+ assert target.row_index is not None
632
+ used_bounds = xlsx_adapter._used_bounds(worksheet)
633
+ if used_bounds is None:
634
+ return ()
635
+ return tuple(
636
+ ChildSummary(
637
+ locator=f"xlsx:sheet:{worksheet.title}!{worksheet.cell(row=target.row_index, column=column_index).coordinate}",
638
+ object_type="cell",
639
+ preview=xlsx_adapter._display_text(
640
+ worksheet.cell(row=target.row_index, column=column_index)
641
+ )[:120],
642
+ capabilities=_capability_tuple(
643
+ workbook,
644
+ _XlsxTarget(
645
+ "",
646
+ "cell",
647
+ sheet_name=worksheet.title,
648
+ coordinate=worksheet.cell(
649
+ row=target.row_index, column=column_index
650
+ ).coordinate,
651
+ ),
652
+ ),
653
+ )
654
+ for column_index in range(used_bounds[1], used_bounds[3] + 1)
655
+ )
656
+
657
+
658
+ def _column_children(
659
+ workbook, target: _XlsxTarget, *, child_type: str | None = None
660
+ ) -> tuple[ChildSummary, ...]:
661
+ if child_type not in {None, "", "cell"}:
662
+ return ()
663
+ worksheet = _resolve_worksheet(workbook, target)
664
+ assert target.column_index is not None
665
+ used_bounds = xlsx_adapter._used_bounds(worksheet)
666
+ if used_bounds is None:
667
+ return ()
668
+ return tuple(
669
+ ChildSummary(
670
+ locator=f"xlsx:sheet:{worksheet.title}!{worksheet.cell(row=row_index, column=target.column_index).coordinate}",
671
+ object_type="cell",
672
+ preview=xlsx_adapter._display_text(
673
+ worksheet.cell(row=row_index, column=target.column_index)
674
+ )[:120],
675
+ capabilities=_capability_tuple(
676
+ workbook,
677
+ _XlsxTarget(
678
+ "",
679
+ "cell",
680
+ sheet_name=worksheet.title,
681
+ coordinate=worksheet.cell(
682
+ row=row_index, column=target.column_index
683
+ ).coordinate,
684
+ ),
685
+ ),
686
+ )
687
+ for row_index in range(used_bounds[0], used_bounds[2] + 1)
688
+ )
689
+
690
+
691
+ def _range_children(
692
+ workbook, target: _XlsxTarget, *, child_type: str | None = None
693
+ ) -> tuple[ChildSummary, ...]:
694
+ if child_type not in {None, "", "cell"}:
695
+ return ()
696
+ worksheet = _resolve_worksheet(workbook, target)
697
+ min_row, min_col, max_row, max_col = _range_bounds(
698
+ target.cell_range, target.canonical_locator
699
+ )
700
+ children: list[ChildSummary] = []
701
+ for row_index in range(min_row, max_row + 1):
702
+ for column_index in range(min_col, max_col + 1):
703
+ cell = worksheet.cell(row=row_index, column=column_index)
704
+ children.append(
705
+ ChildSummary(
706
+ locator=f"xlsx:sheet:{worksheet.title}!{cell.coordinate}",
707
+ object_type="cell",
708
+ preview=xlsx_adapter._display_text(cell)[:120],
709
+ capabilities=_capability_tuple(
710
+ workbook,
711
+ _XlsxTarget(
712
+ "",
713
+ "cell",
714
+ sheet_name=worksheet.title,
715
+ coordinate=cell.coordinate,
716
+ ),
717
+ ),
718
+ )
719
+ )
720
+ return tuple(children)
721
+
722
+
723
+ def _table_children(
724
+ workbook, target: _XlsxTarget, *, child_type: str | None = None
725
+ ) -> tuple[ChildSummary, ...]:
726
+ if child_type not in {None, "", "row"}:
727
+ return ()
728
+ worksheet = _resolve_worksheet(workbook, target)
729
+ table = _resolve_table(worksheet, target.name, target.canonical_locator)
730
+ min_row, _, max_row, _ = _range_bounds(table.ref, target.canonical_locator)
731
+ return tuple(
732
+ ChildSummary(
733
+ locator=f"xlsx:sheet:{worksheet.title}:row:{row_index}",
734
+ object_type="row",
735
+ preview=_row_preview(worksheet, row_index),
736
+ capabilities=_capability_tuple(
737
+ workbook,
738
+ _XlsxTarget("", "row", sheet_name=worksheet.title, row_index=row_index),
739
+ ),
740
+ )
741
+ for row_index in range(min_row, max_row + 1)
742
+ )
743
+
744
+
745
+ def _resolve_worksheet(workbook, target: _XlsxTarget):
746
+ assert target.sheet_name is not None
747
+ return xlsx_adapter._resolve_worksheet(workbook, target.sheet_name)
748
+
749
+
750
+ def _resolve_cell(worksheet, coordinate: str | None, locator: str):
751
+ if coordinate is None:
752
+ raise InvalidArgumentsError(f"Invalid XLSX locator: {locator}")
753
+ return worksheet[xlsx_adapter._normalize_coordinate(coordinate)]
754
+
755
+
756
+ def _resolve_table(worksheet, table_name: str | None, locator: str):
757
+ if table_name is None:
758
+ raise InvalidArgumentsError(f"Invalid XLSX table locator: {locator}")
759
+ try:
760
+ return worksheet.tables[table_name]
761
+ except KeyError as exc:
762
+ raise TargetNotFoundError(
763
+ f"Table {table_name!r} does not exist in worksheet {worksheet.title!r}."
764
+ ) from exc
765
+
766
+
767
+ def _resolve_merged_range(worksheet, cell_range: str | None, locator: str):
768
+ if cell_range is None:
769
+ raise InvalidArgumentsError(f"Invalid merged range locator: {locator}")
770
+ for merged in worksheet.merged_cells.ranges:
771
+ if str(merged) == cell_range:
772
+ return merged
773
+ raise TargetNotFoundError(
774
+ f"Merged range {cell_range!r} does not exist in worksheet {worksheet.title!r}."
775
+ )
776
+
777
+
778
+ def _resolve_named_range(workbook, name: str | None, locator: str):
779
+ if name is None:
780
+ raise InvalidArgumentsError(f"Invalid named range locator: {locator}")
781
+ defined_name = workbook.defined_names.get(name)
782
+ if defined_name is None:
783
+ raise TargetNotFoundError(f"Named range {name!r} does not exist.")
784
+ return defined_name
785
+
786
+
787
+ def _worksheet_preview(worksheet) -> str:
788
+ first_cell = xlsx_adapter._first_indexable_cell(worksheet)
789
+ return "" if first_cell is None else xlsx_adapter._display_text(first_cell)[:120]
790
+
791
+
792
+ def _row_preview(worksheet, row_index: int) -> str:
793
+ used_bounds = xlsx_adapter._used_bounds(worksheet)
794
+ if used_bounds is None:
795
+ return ""
796
+ return " | ".join(
797
+ xlsx_adapter._display_text(worksheet.cell(row=row_index, column=column_index))
798
+ for column_index in range(used_bounds[1], used_bounds[3] + 1)
799
+ if xlsx_adapter._display_text(
800
+ worksheet.cell(row=row_index, column=column_index)
801
+ )
802
+ )[:120]
803
+
804
+
805
+ def _column_preview(worksheet, column_index: int) -> str:
806
+ used_bounds = xlsx_adapter._used_bounds(worksheet)
807
+ if used_bounds is None:
808
+ return ""
809
+ return " | ".join(
810
+ xlsx_adapter._display_text(worksheet.cell(row=row_index, column=column_index))
811
+ for row_index in range(used_bounds[0], used_bounds[2] + 1)
812
+ if xlsx_adapter._display_text(
813
+ worksheet.cell(row=row_index, column=column_index)
814
+ )
815
+ )[:120]
816
+
817
+
818
+ def _range_bounds(cell_range: str | None, locator: str) -> tuple[int, int, int, int]:
819
+ if cell_range is None or xlsx_adapter.range_boundaries is None:
820
+ raise InvalidArgumentsError(f"Invalid XLSX range locator: {locator}")
821
+ min_col, min_row, max_col, max_row = xlsx_adapter.range_boundaries(cell_range)
822
+ return (min_row, min_col, max_row, max_col)
823
+
824
+
825
+ def _ranges_overlap(
826
+ first: tuple[int, int, int, int],
827
+ second: tuple[int, int, int, int],
828
+ ) -> bool:
829
+ return not (
830
+ first[2] < second[0]
831
+ or second[2] < first[0]
832
+ or first[3] < second[1]
833
+ or second[3] < first[1]
834
+ )
835
+
836
+
837
+ def _parse_xlsx_target(locator: str) -> _XlsxTarget:
838
+ parsed = parse_locator(locator)
839
+ components = parsed.components
840
+ if components == ("xlsx", "workbook"):
841
+ return _XlsxTarget(locator, "workbook")
842
+ if len(components) == 3 and components[:2] == ("xlsx", "named_range"):
843
+ return _XlsxTarget(locator, "named_range", name=components[2])
844
+ if len(components) == 3 and components[:2] == ("xlsx", "sheet"):
845
+ return _XlsxTarget(locator, "worksheet", sheet_name=components[2])
846
+ if (
847
+ len(components) == 5
848
+ and components[:2] == ("xlsx", "sheet")
849
+ and components[3] == "row"
850
+ ):
851
+ return _XlsxTarget(
852
+ locator,
853
+ "row",
854
+ sheet_name=components[2],
855
+ row_index=_require_index(components[4], locator),
856
+ )
857
+ if (
858
+ len(components) == 5
859
+ and components[:2] == ("xlsx", "sheet")
860
+ and components[3] == "col"
861
+ ):
862
+ return _XlsxTarget(
863
+ locator,
864
+ "column",
865
+ sheet_name=components[2],
866
+ column_index=_require_index(components[4], locator),
867
+ )
868
+ if len(components) == 4 and components[:2] == ("xlsx", "sheet"):
869
+ coordinate_or_range = components[3]
870
+ if ":" in coordinate_or_range:
871
+ return _XlsxTarget(
872
+ locator,
873
+ "range",
874
+ sheet_name=components[2],
875
+ cell_range=coordinate_or_range,
876
+ )
877
+ return _XlsxTarget(
878
+ locator, "cell", sheet_name=components[2], coordinate=coordinate_or_range
879
+ )
880
+ if (
881
+ len(components) == 5
882
+ and components[:2] == ("xlsx", "sheet")
883
+ and components[3] == "table"
884
+ ):
885
+ return _XlsxTarget(
886
+ locator, "table", sheet_name=components[2], name=components[4]
887
+ )
888
+ if (
889
+ len(components) >= 5
890
+ and components[:2] == ("xlsx", "sheet")
891
+ and components[3] == "merged_range"
892
+ ):
893
+ return _XlsxTarget(
894
+ locator,
895
+ "merged_range",
896
+ sheet_name=components[2],
897
+ cell_range=":".join(components[4:]),
898
+ )
899
+ if (
900
+ len(components) == 5
901
+ and components[:2] == ("xlsx", "sheet")
902
+ and components[3] == "formula_cell"
903
+ ):
904
+ return _XlsxTarget(
905
+ locator, "formula_cell", sheet_name=components[2], coordinate=components[4]
906
+ )
907
+ raise InvalidArgumentsError(f"Unsupported XLSX locator: {locator}")
908
+
909
+
910
+ def _capabilities_for(workbook, target: _XlsxTarget) -> frozenset[Capability]:
911
+ if target.object_type == "workbook":
912
+ return frozenset({Capability.READ, Capability.ADD_CHILD})
913
+ if target.object_type == "worksheet":
914
+ capabilities = {
915
+ Capability.READ,
916
+ Capability.UPDATE,
917
+ Capability.ADD_CHILD,
918
+ Capability.MOVE,
919
+ Capability.COPY,
920
+ }
921
+ if len(workbook.worksheets) > 1:
922
+ capabilities.add(Capability.DELETE)
923
+ return frozenset(capabilities)
924
+ if target.object_type in {"row", "column", "table"}:
925
+ return frozenset(
926
+ {
927
+ Capability.READ,
928
+ Capability.UPDATE,
929
+ Capability.DELETE,
930
+ Capability.ADD_CHILD,
931
+ Capability.MOVE,
932
+ Capability.COPY,
933
+ }
934
+ )
935
+ if target.object_type in {"cell", "range", "formula_cell"}:
936
+ return frozenset({Capability.READ, Capability.UPDATE, Capability.STYLE})
937
+ if target.object_type == "merged_range":
938
+ return frozenset({Capability.READ, Capability.DELETE, Capability.STYLE})
939
+ if target.object_type == "named_range":
940
+ return frozenset({Capability.READ})
941
+ return frozenset({Capability.READ})
942
+
943
+
944
+ def _capability_tuple(workbook, target: _XlsxTarget) -> tuple[Capability, ...]:
945
+ return tuple(
946
+ sorted(
947
+ _capabilities_for(workbook, target), key=lambda capability: capability.value
948
+ )
949
+ )
950
+
951
+
952
+ def _require_index(raw: str, locator: str) -> int:
953
+ try:
954
+ return int(raw)
955
+ except ValueError as exc:
956
+ raise InvalidArgumentsError(f"Invalid XLSX locator: {locator}") from exc
957
+
958
+
959
+ def _coerce_write_value(value: Any) -> Any:
960
+ if isinstance(value, str):
961
+ return xlsx_adapter._coerce_value(value)
962
+ return value