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.
- offagent/__init__.py +3 -0
- offagent/__main__.py +5 -0
- offagent/adapters/__init__.py +1 -0
- offagent/adapters/docx_adapter.py +1237 -0
- offagent/adapters/embedding_provider.py +132 -0
- offagent/adapters/pptx_adapter.py +940 -0
- offagent/adapters/xlsx_adapter.py +1266 -0
- offagent/app/__init__.py +1 -0
- offagent/app/progress.py +52 -0
- offagent/app/services.py +4267 -0
- offagent/config.py +287 -0
- offagent/domain/__init__.py +1 -0
- offagent/domain/locators.py +444 -0
- offagent/domain/models.py +477 -0
- offagent/domain/text_fragments.py +136 -0
- offagent/errors.py +29 -0
- offagent/indexing/__init__.py +1 -0
- offagent/indexing/store.py +795 -0
- offagent/interfaces/__init__.py +1 -0
- offagent/interfaces/cli.py +438 -0
- offagent/interfaces/cli_output.py +139 -0
- offagent/interfaces/cli_progress.py +120 -0
- offagent/interfaces/mcp.py +1145 -0
- offagent/interfaces/mcp_converters.py +80 -0
- offagent/interfaces/mcp_models.py +923 -0
- offagent/objects/__init__.py +3 -0
- offagent/objects/base.py +26 -0
- offagent/objects/docx_objects.py +951 -0
- offagent/objects/pptx_objects.py +895 -0
- offagent/objects/xlsx_objects.py +962 -0
- offagent/path_policy.py +42 -0
- offagent/storage/__init__.py +1 -0
- offagent/storage/versioning.py +31 -0
- offagent-0.10.0.dist-info/METADATA +546 -0
- offagent-0.10.0.dist-info/RECORD +39 -0
- offagent-0.10.0.dist-info/WHEEL +5 -0
- offagent-0.10.0.dist-info/entry_points.txt +2 -0
- offagent-0.10.0.dist-info/licenses/LICENSE +21 -0
- 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
|