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,895 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from offagent.adapters import pptx_adapter
9
+ from offagent.domain.locators import parse_locator, to_v2_locator
10
+ from offagent.domain.models import Capability, ChildSummary, ObjectPayload
11
+ from offagent.errors import InvalidArgumentsError, TargetNotFoundError
12
+
13
+ try:
14
+ from pptx.opc.constants import RELATIONSHIP_TYPE as RT
15
+ except ModuleNotFoundError: # pragma: no cover - exercised through dependency checks
16
+ RT = None
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class _PptxTarget:
21
+ canonical_locator: str
22
+ object_type: str
23
+ slide_number: int | None = None
24
+ shape_id: int | None = None
25
+ paragraph_index: int | None = None
26
+ run_index: int | None = None
27
+ row_index: int | None = None
28
+ column_index: int | None = None
29
+
30
+
31
+ class PptxObjectResolver:
32
+ def get_object(self, document_path: Path, locator: str) -> ObjectPayload:
33
+ canonical = to_v2_locator(locator, file_type="pptx")
34
+ presentation = pptx_adapter._open_presentation(document_path)
35
+ target = _parse_pptx_target(canonical)
36
+
37
+ if target.object_type == "presentation":
38
+ return _build_presentation_payload(document_path, presentation)
39
+ if target.object_type == "slide":
40
+ return _build_slide_payload(document_path, presentation, target)
41
+ if target.object_type == "notes":
42
+ return _build_notes_payload(document_path, presentation, target)
43
+ if target.object_type in {
44
+ "shape",
45
+ "text_shape",
46
+ "image_shape",
47
+ "table",
48
+ "group_shape",
49
+ }:
50
+ return _build_shape_payload(document_path, presentation, target)
51
+ if target.object_type == "paragraph":
52
+ return _build_paragraph_payload(document_path, presentation, target)
53
+ if target.object_type == "run":
54
+ return _build_run_payload(document_path, presentation, target)
55
+ if target.object_type == "table_row":
56
+ return _build_table_row_payload(document_path, presentation, target)
57
+ if target.object_type == "table_cell":
58
+ return _build_table_cell_payload(document_path, presentation, target)
59
+ raise InvalidArgumentsError(f"Unsupported PPTX locator: {locator}")
60
+
61
+ def list_children(
62
+ self,
63
+ document_path: Path,
64
+ locator: str,
65
+ *,
66
+ child_type: str | None = None,
67
+ limit: int | None = None,
68
+ ) -> list[ChildSummary]:
69
+ canonical = to_v2_locator(locator, file_type="pptx")
70
+ presentation = pptx_adapter._open_presentation(document_path)
71
+ target = _parse_pptx_target(canonical)
72
+
73
+ if target.object_type == "presentation":
74
+ children = _presentation_children(presentation, child_type=child_type)
75
+ elif target.object_type == "slide":
76
+ children = _slide_children(presentation, target, child_type=child_type)
77
+ elif target.object_type in {"shape", "text_shape"}:
78
+ children = _text_shape_children(presentation, target, child_type=child_type)
79
+ elif target.object_type == "paragraph":
80
+ children = _paragraph_children(presentation, target, child_type=child_type)
81
+ elif target.object_type == "table":
82
+ children = _table_children(presentation, target, child_type=child_type)
83
+ elif target.object_type == "table_row":
84
+ children = _table_row_children(presentation, target, child_type=child_type)
85
+ else:
86
+ children = ()
87
+
88
+ if limit is not None:
89
+ return list(children[:limit])
90
+ return list(children)
91
+
92
+ def resolve_capabilities(
93
+ self, document_path: Path, locator: str
94
+ ) -> frozenset[Capability]:
95
+ del document_path
96
+ canonical = to_v2_locator(locator, file_type="pptx")
97
+ target = _parse_pptx_target(canonical)
98
+ return _capabilities_for(target.object_type)
99
+
100
+
101
+ def add_slide(
102
+ document_path: Path,
103
+ *,
104
+ layout_index: int | None = None,
105
+ layout_name: str | None = None,
106
+ output_path: Path,
107
+ ) -> tuple[str, str, dict[str, Any]]:
108
+ presentation = pptx_adapter._open_presentation(document_path)
109
+ layout = _resolve_slide_layout(
110
+ presentation, layout_index=layout_index, layout_name=layout_name
111
+ )
112
+ presentation.slides.add_slide(layout)
113
+ slide_number = len(presentation.slides)
114
+ presentation.save(output_path)
115
+ return (
116
+ f"pptx:slide:{slide_number}",
117
+ f"Added slide {slide_number} using layout {layout.name!r}.",
118
+ {
119
+ "layout_name": layout.name,
120
+ "layout_index": _layout_index(presentation, layout),
121
+ },
122
+ )
123
+
124
+
125
+ def duplicate_slide(
126
+ document_path: Path,
127
+ locator: str,
128
+ *,
129
+ position: int | None = None,
130
+ output_path: Path,
131
+ ) -> tuple[str, str, dict[str, Any]]:
132
+ canonical = to_v2_locator(locator, file_type="pptx")
133
+ target = _parse_pptx_target(canonical)
134
+ if target.object_type != "slide":
135
+ raise InvalidArgumentsError("pptx_duplicate_slide requires a slide locator.")
136
+
137
+ presentation = pptx_adapter._open_presentation(document_path)
138
+ copied_position = _duplicate_slide_in_presentation(
139
+ presentation, target.slide_number, position
140
+ )
141
+ presentation.save(output_path)
142
+ return (
143
+ f"pptx:slide:{copied_position}",
144
+ f"Duplicated {canonical} to slide {copied_position}.",
145
+ {"source_locator": canonical, "position": copied_position},
146
+ )
147
+
148
+
149
+ def set_slide_layout(
150
+ document_path: Path,
151
+ locator: str,
152
+ *,
153
+ layout_index: int | None = None,
154
+ layout_name: str | None = None,
155
+ output_path: Path,
156
+ ) -> tuple[str, str, dict[str, Any]]:
157
+ if RT is None:
158
+ raise RuntimeError("python-pptx is required for PPTX operations.")
159
+
160
+ canonical = to_v2_locator(locator, file_type="pptx")
161
+ target = _parse_pptx_target(canonical)
162
+ if target.object_type != "slide":
163
+ raise InvalidArgumentsError("pptx_set_slide_layout requires a slide locator.")
164
+
165
+ presentation = pptx_adapter._open_presentation(document_path)
166
+ slide = _resolve_slide(presentation, target)
167
+ layout = _resolve_slide_layout(
168
+ presentation, layout_index=layout_index, layout_name=layout_name
169
+ )
170
+ old_layout_rids = [
171
+ r_id for r_id, rel in slide.part.rels.items() if rel.reltype == RT.SLIDE_LAYOUT
172
+ ]
173
+ for r_id in old_layout_rids:
174
+ slide.part.drop_rel(r_id)
175
+ slide.part.relate_to(layout.part, RT.SLIDE_LAYOUT)
176
+ presentation.save(output_path)
177
+ return (
178
+ canonical,
179
+ f"Updated {canonical} to layout {layout.name!r}.",
180
+ {
181
+ "layout_name": layout.name,
182
+ "layout_index": _layout_index(presentation, layout),
183
+ },
184
+ )
185
+
186
+
187
+ def add_text_shape(
188
+ document_path: Path,
189
+ locator: str,
190
+ *,
191
+ text: str,
192
+ left: int,
193
+ top: int,
194
+ width: int,
195
+ height: int,
196
+ output_path: Path,
197
+ ) -> tuple[str, str, dict[str, Any]]:
198
+ canonical = to_v2_locator(locator, file_type="pptx")
199
+ target = _parse_pptx_target(canonical)
200
+ if target.object_type != "slide":
201
+ raise InvalidArgumentsError("pptx_add_text_shape requires a slide locator.")
202
+
203
+ presentation = pptx_adapter._open_presentation(document_path)
204
+ slide = _resolve_slide(presentation, target)
205
+ shape = slide.shapes.add_textbox(int(left), int(top), int(width), int(height))
206
+ shape.text_frame.text = text
207
+ locator = _shape_locator(target.slide_number, shape, _shape_object_type(shape))
208
+ presentation.save(output_path)
209
+ return (
210
+ locator,
211
+ f"Added text shape {locator} to {canonical}.",
212
+ {"text": text, "left": left, "top": top, "width": width, "height": height},
213
+ )
214
+
215
+
216
+ def _build_presentation_payload(document_path: Path, presentation) -> ObjectPayload:
217
+ slides = tuple(presentation.slides)
218
+ preview = ""
219
+ if slides:
220
+ preview = _slide_preview(slides[0])
221
+ return ObjectPayload(
222
+ document=pptx_adapter._document_ref(document_path),
223
+ locator="pptx:presentation",
224
+ object_type="presentation",
225
+ preview=preview,
226
+ properties={"slide_count": len(slides)},
227
+ capabilities=_capability_tuple("presentation"),
228
+ child_summary=_presentation_children(presentation),
229
+ )
230
+
231
+
232
+ def _build_slide_payload(
233
+ document_path: Path, presentation, target: _PptxTarget
234
+ ) -> ObjectPayload:
235
+ slide = _resolve_slide(presentation, target)
236
+ bundle = pptx_adapter.get_slide_bundle(document_path, target.slide_number)
237
+ layout_name = getattr(getattr(slide, "slide_layout", None), "name", None)
238
+ return ObjectPayload(
239
+ document=pptx_adapter._document_ref(document_path),
240
+ locator=target.canonical_locator,
241
+ object_type="slide",
242
+ preview=bundle.preview,
243
+ properties={
244
+ "slide_number": target.slide_number,
245
+ "layout_name": layout_name,
246
+ "shape_count": len(slide.shapes),
247
+ "text_block_count": len(bundle.text_blocks),
248
+ "notes_text": bundle.notes_text,
249
+ },
250
+ capabilities=_capability_tuple("slide"),
251
+ parent_locator="pptx:presentation",
252
+ child_summary=_slide_children(presentation, target),
253
+ )
254
+
255
+
256
+ def _build_notes_payload(
257
+ document_path: Path, presentation, target: _PptxTarget
258
+ ) -> ObjectPayload:
259
+ slide = _resolve_slide(presentation, target)
260
+ notes_text = pptx_adapter._notes_text(slide)
261
+ return ObjectPayload(
262
+ document=pptx_adapter._document_ref(document_path),
263
+ locator=target.canonical_locator,
264
+ object_type="notes",
265
+ preview=notes_text[:120],
266
+ properties={"slide_number": target.slide_number, "text": notes_text},
267
+ capabilities=_capability_tuple("notes"),
268
+ parent_locator=f"pptx:slide:{target.slide_number}",
269
+ )
270
+
271
+
272
+ def _build_shape_payload(
273
+ document_path: Path, presentation, target: _PptxTarget
274
+ ) -> ObjectPayload:
275
+ resolved = _resolve_shape_target(presentation, target)
276
+ shape = resolved["shape"]
277
+ actual_object_type = _shape_object_type(shape)
278
+ if target.object_type != "shape" and target.object_type != actual_object_type:
279
+ raise TargetNotFoundError(
280
+ f"Shape {resolved['shape_id']} on slide {resolved['slide_number']} is not a {target.object_type}."
281
+ )
282
+
283
+ properties: dict[str, Any] = {
284
+ "slide_number": resolved["slide_number"],
285
+ "shape_id": resolved["shape_id"],
286
+ "shape_index": resolved["shape_index"],
287
+ "shape_name": getattr(shape, "name", None),
288
+ "shape_type": getattr(
289
+ getattr(shape, "shape_type", None),
290
+ "name",
291
+ str(getattr(shape, "shape_type", "")),
292
+ ),
293
+ "left": int(getattr(shape, "left", 0)),
294
+ "top": int(getattr(shape, "top", 0)),
295
+ "width": int(getattr(shape, "width", 0)),
296
+ "height": int(getattr(shape, "height", 0)),
297
+ "is_placeholder": bool(getattr(shape, "is_placeholder", False)),
298
+ }
299
+ child_summary: tuple[ChildSummary, ...] = ()
300
+ if getattr(shape, "has_text_frame", False):
301
+ properties["text"] = pptx_adapter._text_frame_text(shape.text_frame)
302
+ if getattr(shape, "has_table", False):
303
+ table = shape.table
304
+ properties["row_count"] = len(table.rows)
305
+ properties["column_count"] = len(table.columns)
306
+ child_summary = _table_children(
307
+ presentation,
308
+ _PptxTarget(
309
+ target.canonical_locator, "table", target.slide_number, target.shape_id
310
+ ),
311
+ )
312
+
313
+ return ObjectPayload(
314
+ document=pptx_adapter._document_ref(document_path),
315
+ locator=target.canonical_locator,
316
+ object_type=actual_object_type
317
+ if target.object_type == "shape"
318
+ else target.object_type,
319
+ preview=_shape_preview(shape),
320
+ properties=properties,
321
+ capabilities=_capability_tuple(
322
+ actual_object_type if target.object_type == "shape" else target.object_type
323
+ ),
324
+ parent_locator=f"pptx:slide:{resolved['slide_number']}",
325
+ child_summary=child_summary,
326
+ )
327
+
328
+
329
+ def _build_paragraph_payload(
330
+ document_path: Path, presentation, target: _PptxTarget
331
+ ) -> ObjectPayload:
332
+ resolved = _resolve_paragraph_target(presentation, target)
333
+ paragraph = resolved["paragraph"]
334
+ return ObjectPayload(
335
+ document=pptx_adapter._document_ref(document_path),
336
+ locator=target.canonical_locator,
337
+ object_type="paragraph",
338
+ preview=paragraph.text[:120],
339
+ properties={
340
+ "slide_number": resolved["slide_number"],
341
+ "shape_id": resolved["shape_id"],
342
+ "paragraph_index": resolved["paragraph_index"],
343
+ "text": paragraph.text,
344
+ "run_count": len(paragraph.runs),
345
+ },
346
+ capabilities=_capability_tuple("paragraph"),
347
+ parent_locator=f"pptx:slide:{resolved['slide_number']}:text_shape:{resolved['shape_id']}",
348
+ child_summary=_paragraph_children(presentation, target),
349
+ )
350
+
351
+
352
+ def _build_run_payload(
353
+ document_path: Path, presentation, target: _PptxTarget
354
+ ) -> ObjectPayload:
355
+ resolved = _resolve_run_target(presentation, target)
356
+ run = resolved["run"]
357
+ return ObjectPayload(
358
+ document=pptx_adapter._document_ref(document_path),
359
+ locator=target.canonical_locator,
360
+ object_type="run",
361
+ preview=run.text[:120],
362
+ properties={
363
+ "slide_number": resolved["slide_number"],
364
+ "shape_id": resolved["shape_id"],
365
+ "paragraph_index": resolved["paragraph_index"],
366
+ "run_index": resolved["run_index"],
367
+ "text": run.text,
368
+ },
369
+ capabilities=_capability_tuple("run"),
370
+ parent_locator=f"pptx:slide:{resolved['slide_number']}:text_shape:{resolved['shape_id']}:para:{resolved['paragraph_index']}",
371
+ )
372
+
373
+
374
+ def _build_table_row_payload(
375
+ document_path: Path, presentation, target: _PptxTarget
376
+ ) -> ObjectPayload:
377
+ resolved = _resolve_table_row_target(presentation, target)
378
+ return ObjectPayload(
379
+ document=pptx_adapter._document_ref(document_path),
380
+ locator=target.canonical_locator,
381
+ object_type="table_row",
382
+ preview=" | ".join(cell.text for cell in resolved["row"].cells)[:120],
383
+ properties={
384
+ "slide_number": resolved["slide_number"],
385
+ "shape_id": resolved["shape_id"],
386
+ "row_index": resolved["row_index"],
387
+ "cell_count": len(resolved["row"].cells),
388
+ },
389
+ capabilities=_capability_tuple("table_row"),
390
+ parent_locator=f"pptx:slide:{resolved['slide_number']}:table:{resolved['shape_id']}",
391
+ child_summary=_table_row_children(presentation, target),
392
+ )
393
+
394
+
395
+ def _build_table_cell_payload(
396
+ document_path: Path, presentation, target: _PptxTarget
397
+ ) -> ObjectPayload:
398
+ resolved = _resolve_table_cell_target(presentation, target)
399
+ cell = resolved["cell"]
400
+ return ObjectPayload(
401
+ document=pptx_adapter._document_ref(document_path),
402
+ locator=target.canonical_locator,
403
+ object_type="table_cell",
404
+ preview=cell.text[:120],
405
+ properties={
406
+ "slide_number": resolved["slide_number"],
407
+ "shape_id": resolved["shape_id"],
408
+ "row_index": resolved["row_index"],
409
+ "column_index": resolved["column_index"],
410
+ "text": cell.text,
411
+ },
412
+ capabilities=_capability_tuple("table_cell"),
413
+ parent_locator=f"pptx:slide:{resolved['slide_number']}:table:{resolved['shape_id']}:row:{resolved['row_index']}",
414
+ )
415
+
416
+
417
+ def _presentation_children(
418
+ presentation, *, child_type: str | None = None
419
+ ) -> tuple[ChildSummary, ...]:
420
+ if child_type not in {None, "", "slide"}:
421
+ return ()
422
+ return tuple(
423
+ ChildSummary(
424
+ locator=f"pptx:slide:{slide_number}",
425
+ object_type="slide",
426
+ preview=_slide_preview(slide),
427
+ capabilities=_capability_tuple("slide"),
428
+ )
429
+ for slide_number, slide in enumerate(presentation.slides, start=1)
430
+ )
431
+
432
+
433
+ def _slide_children(
434
+ presentation, target: _PptxTarget, *, child_type: str | None = None
435
+ ) -> tuple[ChildSummary, ...]:
436
+ slide = _resolve_slide(presentation, target)
437
+ normalized_child_type = child_type or None
438
+ children: list[ChildSummary] = []
439
+
440
+ if normalized_child_type in {None, "notes"}:
441
+ children.append(
442
+ ChildSummary(
443
+ locator=f"pptx:slide:{target.slide_number}:notes",
444
+ object_type="notes",
445
+ preview=pptx_adapter._notes_text(slide)[:120],
446
+ capabilities=_capability_tuple("notes"),
447
+ )
448
+ )
449
+
450
+ for shape in slide.shapes:
451
+ object_type = _shape_object_type(shape)
452
+ if normalized_child_type not in {None, "shape", object_type}:
453
+ continue
454
+ children.append(
455
+ ChildSummary(
456
+ locator=_shape_locator(target.slide_number, shape, object_type),
457
+ object_type=object_type,
458
+ preview=_shape_preview(shape),
459
+ capabilities=_capability_tuple(object_type),
460
+ )
461
+ )
462
+ return tuple(children)
463
+
464
+
465
+ def _text_shape_children(
466
+ presentation, target: _PptxTarget, *, child_type: str | None = None
467
+ ) -> tuple[ChildSummary, ...]:
468
+ if child_type not in {None, "", "paragraph"}:
469
+ return ()
470
+ resolved = _resolve_shape_target(presentation, target)
471
+ shape = resolved["shape"]
472
+ if not getattr(shape, "has_text_frame", False):
473
+ return ()
474
+ return tuple(
475
+ ChildSummary(
476
+ locator=f"pptx:slide:{resolved['slide_number']}:text_shape:{resolved['shape_id']}:para:{paragraph_index}",
477
+ object_type="paragraph",
478
+ preview=paragraph.text[:120],
479
+ capabilities=_capability_tuple("paragraph"),
480
+ )
481
+ for paragraph_index, paragraph in enumerate(shape.text_frame.paragraphs)
482
+ )
483
+
484
+
485
+ def _paragraph_children(
486
+ presentation, target: _PptxTarget, *, child_type: str | None = None
487
+ ) -> tuple[ChildSummary, ...]:
488
+ if child_type not in {None, "", "run"}:
489
+ return ()
490
+ resolved = _resolve_paragraph_target(presentation, target)
491
+ return tuple(
492
+ ChildSummary(
493
+ locator=(
494
+ f"pptx:slide:{resolved['slide_number']}:text_shape:{resolved['shape_id']}:para:{resolved['paragraph_index']}:run:{run_index}"
495
+ ),
496
+ object_type="run",
497
+ preview=run.text[:120],
498
+ capabilities=_capability_tuple("run"),
499
+ )
500
+ for run_index, run in enumerate(resolved["paragraph"].runs)
501
+ )
502
+
503
+
504
+ def _table_children(
505
+ presentation, target: _PptxTarget, *, child_type: str | None = None
506
+ ) -> tuple[ChildSummary, ...]:
507
+ resolved = _resolve_shape_target(presentation, target)
508
+ shape = resolved["shape"]
509
+ if not getattr(shape, "has_table", False) or child_type not in {
510
+ None,
511
+ "",
512
+ "table_row",
513
+ }:
514
+ return ()
515
+ return tuple(
516
+ ChildSummary(
517
+ locator=f"pptx:slide:{resolved['slide_number']}:table:{resolved['shape_id']}:row:{row_index}",
518
+ object_type="table_row",
519
+ preview=" | ".join(cell.text for cell in row.cells)[:120],
520
+ capabilities=_capability_tuple("table_row"),
521
+ )
522
+ for row_index, row in enumerate(shape.table.rows)
523
+ )
524
+
525
+
526
+ def _table_row_children(
527
+ presentation, target: _PptxTarget, *, child_type: str | None = None
528
+ ) -> tuple[ChildSummary, ...]:
529
+ resolved = _resolve_table_row_target(presentation, target)
530
+ if child_type not in {None, "", "table_cell"}:
531
+ return ()
532
+ return tuple(
533
+ ChildSummary(
534
+ locator=(
535
+ f"pptx:slide:{resolved['slide_number']}:table:{resolved['shape_id']}:row:{resolved['row_index']}:cell:{column_index}"
536
+ ),
537
+ object_type="table_cell",
538
+ preview=cell.text[:120],
539
+ capabilities=_capability_tuple("table_cell"),
540
+ )
541
+ for column_index, cell in enumerate(resolved["row"].cells)
542
+ )
543
+
544
+
545
+ def _resolve_slide(presentation, target: _PptxTarget):
546
+ assert target.slide_number is not None
547
+ return pptx_adapter._resolve_slide(presentation, target.slide_number)
548
+
549
+
550
+ def _resolve_shape_target(presentation, target: _PptxTarget) -> dict[str, Any]:
551
+ slide = _resolve_slide(presentation, target)
552
+ assert target.shape_id is not None
553
+ for shape_index, shape in enumerate(slide.shapes):
554
+ if shape.shape_id == target.shape_id:
555
+ return {
556
+ "slide_number": target.slide_number,
557
+ "shape_id": target.shape_id,
558
+ "shape_index": shape_index,
559
+ "shape": shape,
560
+ }
561
+ raise TargetNotFoundError(
562
+ f"Shape {target.shape_id} does not exist on slide {target.slide_number}."
563
+ )
564
+
565
+
566
+ def _resolve_paragraph_target(presentation, target: _PptxTarget) -> dict[str, Any]:
567
+ resolved = _resolve_shape_target(presentation, target)
568
+ shape = resolved["shape"]
569
+ if not getattr(shape, "has_text_frame", False):
570
+ raise TargetNotFoundError(f"Shape {resolved['shape_id']} is not a text shape.")
571
+ assert target.paragraph_index is not None
572
+ try:
573
+ paragraph = shape.text_frame.paragraphs[target.paragraph_index]
574
+ except IndexError as exc:
575
+ raise TargetNotFoundError(
576
+ f"Paragraph {target.paragraph_index} does not exist in shape {resolved['shape_id']}."
577
+ ) from exc
578
+ return {
579
+ **resolved,
580
+ "paragraph_index": target.paragraph_index,
581
+ "paragraph": paragraph,
582
+ }
583
+
584
+
585
+ def _resolve_run_target(presentation, target: _PptxTarget) -> dict[str, Any]:
586
+ resolved = _resolve_paragraph_target(presentation, target)
587
+ assert target.run_index is not None
588
+ try:
589
+ run = resolved["paragraph"].runs[target.run_index]
590
+ except IndexError as exc:
591
+ raise TargetNotFoundError(
592
+ f"Run {target.run_index} does not exist in paragraph {resolved['paragraph_index']}."
593
+ ) from exc
594
+ return {**resolved, "run_index": target.run_index, "run": run}
595
+
596
+
597
+ def _resolve_table_row_target(presentation, target: _PptxTarget) -> dict[str, Any]:
598
+ resolved = _resolve_shape_target(presentation, target)
599
+ shape = resolved["shape"]
600
+ if not getattr(shape, "has_table", False):
601
+ raise TargetNotFoundError(f"Shape {resolved['shape_id']} is not a table.")
602
+ assert target.row_index is not None
603
+ try:
604
+ row = shape.table.rows[target.row_index]
605
+ except IndexError as exc:
606
+ raise TargetNotFoundError(
607
+ f"Row {target.row_index} does not exist in table {resolved['shape_id']}."
608
+ ) from exc
609
+ return {**resolved, "row_index": target.row_index, "row": row}
610
+
611
+
612
+ def _resolve_table_cell_target(presentation, target: _PptxTarget) -> dict[str, Any]:
613
+ resolved = _resolve_table_row_target(presentation, target)
614
+ assert target.column_index is not None
615
+ try:
616
+ cell = resolved["row"].cells[target.column_index]
617
+ except IndexError as exc:
618
+ raise TargetNotFoundError(
619
+ f"Cell {target.column_index} does not exist in row {resolved['row_index']}."
620
+ ) from exc
621
+ return {**resolved, "column_index": target.column_index, "cell": cell}
622
+
623
+
624
+ def _shape_object_type(shape) -> str:
625
+ if getattr(shape, "has_table", False):
626
+ return "table"
627
+ shape_type_name = getattr(getattr(shape, "shape_type", None), "name", "")
628
+ if shape_type_name == "PICTURE":
629
+ return "image_shape"
630
+ if shape_type_name == "GROUP":
631
+ return "group_shape"
632
+ if getattr(shape, "has_text_frame", False):
633
+ return "text_shape"
634
+ return "shape"
635
+
636
+
637
+ def _shape_locator(slide_number: int, shape, object_type: str) -> str:
638
+ if object_type == "shape":
639
+ return f"pptx:slide:{slide_number}:shape:{shape.shape_id}"
640
+ return f"pptx:slide:{slide_number}:{object_type}:{shape.shape_id}"
641
+
642
+
643
+ def _shape_preview(shape) -> str:
644
+ if getattr(shape, "has_text_frame", False):
645
+ return pptx_adapter._text_frame_text(shape.text_frame)[:120]
646
+ if getattr(shape, "has_table", False):
647
+ rows = shape.table.rows
648
+ if rows:
649
+ first_row = rows[0]
650
+ return " | ".join(cell.text for cell in first_row.cells)[:120]
651
+ return getattr(shape, "name", "")[:120]
652
+
653
+
654
+ def _slide_preview(slide) -> str:
655
+ text_blocks = pptx_adapter._slide_text_blocks(slide)
656
+ return next((block.text[:120] for block in text_blocks if block.text), "")
657
+
658
+
659
+ def _resolve_slide_layout(
660
+ presentation, *, layout_index: int | None, layout_name: str | None
661
+ ):
662
+ layouts = tuple(presentation.slide_layouts)
663
+ if (layout_index is None) == (layout_name is None):
664
+ raise InvalidArgumentsError(
665
+ "Specify exactly one of layout_index or layout_name."
666
+ )
667
+
668
+ if layout_index is not None:
669
+ if layout_index < 0 or layout_index >= len(layouts):
670
+ raise InvalidArgumentsError(
671
+ f"Unknown PPTX slide layout index: {layout_index}"
672
+ )
673
+ return layouts[layout_index]
674
+
675
+ assert layout_name is not None
676
+ for layout in layouts:
677
+ if layout.name == layout_name:
678
+ return layout
679
+ raise InvalidArgumentsError(f"Unknown PPTX slide layout: {layout_name}")
680
+
681
+
682
+ def _layout_index(presentation, layout) -> int:
683
+ for index, candidate in enumerate(presentation.slide_layouts):
684
+ if candidate == layout:
685
+ return index
686
+ raise RuntimeError("Failed to resolve PPTX slide layout index.")
687
+
688
+
689
+ def _duplicate_slide_in_presentation(
690
+ presentation, slide_number: int | None, position: int | None
691
+ ) -> int:
692
+ if slide_number is None:
693
+ raise InvalidArgumentsError("pptx_duplicate_slide requires a slide locator.")
694
+
695
+ source_slide = pptx_adapter._resolve_slide(presentation, slide_number)
696
+ new_slide = presentation.slides.add_slide(source_slide.slide_layout)
697
+
698
+ for placeholder_shape in list(new_slide.shapes):
699
+ placeholder_shape.element.getparent().remove(placeholder_shape.element)
700
+
701
+ for shape in source_slide.shapes:
702
+ new_slide.shapes._spTree.insert_element_before(
703
+ deepcopy(shape.element), "p:extLst"
704
+ )
705
+
706
+ for rel in source_slide.part.rels.values():
707
+ if rel.reltype.endswith("/notesSlide") or rel.reltype.endswith("/slideLayout"):
708
+ continue
709
+ if rel.is_external:
710
+ new_rid = new_slide.part.relate_to(
711
+ rel.target_ref, rel.reltype, is_external=True
712
+ )
713
+ else:
714
+ new_rid = new_slide.part.relate_to(rel.target_part, rel.reltype)
715
+ _retarget_shape_relationships(new_slide, rel.rId, new_rid)
716
+
717
+ if getattr(source_slide, "notes_slide", None) is not None:
718
+ source_notes = getattr(source_slide.notes_slide, "notes_text_frame", None)
719
+ target_notes = getattr(new_slide.notes_slide, "notes_text_frame", None)
720
+ if source_notes is not None and target_notes is not None:
721
+ target_notes.text = source_notes.text
722
+
723
+ copied_position = len(presentation.slides) if position is None else position
724
+ _move_slide_in_memory(presentation, len(presentation.slides), copied_position)
725
+ return copied_position
726
+
727
+
728
+ def _move_slide_in_memory(presentation, slide_number: int, new_position: int) -> None:
729
+ slide_count = len(presentation.slides)
730
+ if new_position < 1 or new_position > slide_count:
731
+ raise InvalidArgumentsError(f"Invalid target slide position: {new_position}")
732
+ sld_id_list = presentation.slides._sldIdLst
733
+ slide_id = sld_id_list.sldId_lst[slide_number - 1]
734
+ sld_id_list.remove(slide_id)
735
+ sld_id_list.insert(new_position - 1, slide_id)
736
+
737
+
738
+ def _retarget_shape_relationships(slide, source_rid: str, target_rid: str) -> None:
739
+ for shape in slide.shapes:
740
+ for element in shape.element.iter():
741
+ for attr_name, attr_value in list(element.attrib.items()):
742
+ if attr_value == source_rid:
743
+ element.set(attr_name, target_rid)
744
+
745
+
746
+ def _parse_pptx_target(locator: str) -> _PptxTarget:
747
+ parsed = parse_locator(locator)
748
+ components = parsed.components
749
+ if components == ("pptx", "presentation"):
750
+ return _PptxTarget(locator, "presentation")
751
+ if len(components) == 3 and components[:2] == ("pptx", "slide"):
752
+ return _PptxTarget(
753
+ locator, "slide", slide_number=_require_index(components[2], locator)
754
+ )
755
+ if (
756
+ len(components) == 4
757
+ and components[:2] == ("pptx", "slide")
758
+ and components[3] == "notes"
759
+ ):
760
+ return _PptxTarget(
761
+ locator, "notes", slide_number=_require_index(components[2], locator)
762
+ )
763
+ if len(components) == 5 and components[:2] == ("pptx", "slide"):
764
+ return _PptxTarget(
765
+ locator,
766
+ components[3],
767
+ slide_number=_require_index(components[2], locator),
768
+ shape_id=_require_index(components[4], locator),
769
+ )
770
+ if (
771
+ len(components) == 7
772
+ and components[:2] == ("pptx", "slide")
773
+ and components[3] in {"shape", "text_shape"}
774
+ and components[5] == "para"
775
+ ):
776
+ return _PptxTarget(
777
+ locator,
778
+ "paragraph",
779
+ slide_number=_require_index(components[2], locator),
780
+ shape_id=_require_index(components[4], locator),
781
+ paragraph_index=_require_index(components[6], locator),
782
+ )
783
+ if (
784
+ len(components) == 9
785
+ and components[:2] == ("pptx", "slide")
786
+ and components[3] in {"shape", "text_shape"}
787
+ and components[5] == "para"
788
+ and components[7] == "run"
789
+ ):
790
+ return _PptxTarget(
791
+ locator,
792
+ "run",
793
+ slide_number=_require_index(components[2], locator),
794
+ shape_id=_require_index(components[4], locator),
795
+ paragraph_index=_require_index(components[6], locator),
796
+ run_index=_require_index(components[8], locator),
797
+ )
798
+ if (
799
+ len(components) == 7
800
+ and components[:2] == ("pptx", "slide")
801
+ and components[3] == "table"
802
+ and components[5] == "row"
803
+ ):
804
+ return _PptxTarget(
805
+ locator,
806
+ "table_row",
807
+ slide_number=_require_index(components[2], locator),
808
+ shape_id=_require_index(components[4], locator),
809
+ row_index=_require_index(components[6], locator),
810
+ )
811
+ if (
812
+ len(components) == 9
813
+ and components[:2] == ("pptx", "slide")
814
+ and components[3] == "table"
815
+ and components[5] == "row"
816
+ and components[7] == "cell"
817
+ ):
818
+ return _PptxTarget(
819
+ locator,
820
+ "table_cell",
821
+ slide_number=_require_index(components[2], locator),
822
+ shape_id=_require_index(components[4], locator),
823
+ row_index=_require_index(components[6], locator),
824
+ column_index=_require_index(components[8], locator),
825
+ )
826
+ raise InvalidArgumentsError(f"Unsupported PPTX locator: {locator}")
827
+
828
+
829
+ def _capabilities_for(object_type: str) -> frozenset[Capability]:
830
+ if object_type == "presentation":
831
+ return frozenset({Capability.READ, Capability.ADD_CHILD})
832
+ if object_type == "slide":
833
+ return frozenset(
834
+ {
835
+ Capability.READ,
836
+ Capability.UPDATE,
837
+ Capability.DELETE,
838
+ Capability.ADD_CHILD,
839
+ Capability.MOVE,
840
+ Capability.COPY,
841
+ }
842
+ )
843
+ if object_type == "notes":
844
+ return frozenset({Capability.READ, Capability.UPDATE})
845
+ if object_type in {"shape", "text_shape", "image_shape", "group_shape"}:
846
+ return frozenset(
847
+ {
848
+ Capability.READ,
849
+ Capability.UPDATE,
850
+ Capability.DELETE,
851
+ Capability.MOVE,
852
+ Capability.COPY,
853
+ }
854
+ )
855
+ if object_type == "paragraph":
856
+ return frozenset({Capability.READ, Capability.UPDATE, Capability.STYLE})
857
+ if object_type == "run":
858
+ return frozenset({Capability.READ, Capability.UPDATE, Capability.STYLE})
859
+ if object_type == "table":
860
+ return frozenset(
861
+ {
862
+ Capability.READ,
863
+ Capability.UPDATE,
864
+ Capability.DELETE,
865
+ Capability.ADD_CHILD,
866
+ Capability.MOVE,
867
+ Capability.COPY,
868
+ }
869
+ )
870
+ if object_type == "table_row":
871
+ return frozenset(
872
+ {
873
+ Capability.READ,
874
+ Capability.UPDATE,
875
+ Capability.DELETE,
876
+ Capability.MOVE,
877
+ Capability.COPY,
878
+ }
879
+ )
880
+ if object_type == "table_cell":
881
+ return frozenset({Capability.READ, Capability.UPDATE, Capability.STYLE})
882
+ return frozenset({Capability.READ})
883
+
884
+
885
+ def _capability_tuple(object_type: str) -> tuple[Capability, ...]:
886
+ return tuple(
887
+ sorted(_capabilities_for(object_type), key=lambda capability: capability.value)
888
+ )
889
+
890
+
891
+ def _require_index(raw: str, locator: str) -> int:
892
+ try:
893
+ return int(raw)
894
+ except ValueError as exc:
895
+ raise InvalidArgumentsError(f"Invalid PPTX locator: {locator}") from exc