athena-python-pptx 0.9.0__tar.gz → 0.10.0__tar.gz
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.
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/PKG-INFO +1 -2
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/docs/API_PARITY_EXCEPTIONS.md +28 -5
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/__init__.py +1 -1
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/batching.py +34 -1
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/client.py +17 -1
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/commands.py +98 -0
- athena_python_pptx-0.10.0/pptx/oxml/__init__.py +43 -0
- athena_python_pptx-0.10.0/pptx/oxml/ns.py +91 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/presentation.py +50 -57
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/__init__.py +172 -34
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/slides.py +99 -52
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/typing.py +1 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pyproject.toml +6 -2
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/.gitignore +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/API_PARITY_REPORT.md +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/CHANGELOG.md +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/CLAUDE.md +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/README.md +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/_athena_extension.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/_citations.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/_ptc.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/_references.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/action.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/category.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/chart.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/errors.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/exc.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/media.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/package.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/shared.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/slide.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/spec.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/table.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/text/__init__.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/types.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/units.py +0 -0
- {athena_python_pptx-0.9.0 → athena_python_pptx-0.10.0}/pptx/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-pptx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration
|
|
5
5
|
Project-URL: Homepage, https://github.com/pptx-studio/python-sdk
|
|
6
6
|
Project-URL: Documentation, https://docs.pptx-studio.com/sdk/python
|
|
@@ -31,7 +31,6 @@ Requires-Dist: responses>=0.23; extra == 'dev'
|
|
|
31
31
|
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
32
32
|
Provides-Extra: e2e
|
|
33
33
|
Requires-Dist: pytest>=7.0; extra == 'e2e'
|
|
34
|
-
Requires-Dist: python-pptx>=0.6.21; extra == 'e2e'
|
|
35
34
|
Provides-Extra: visual
|
|
36
35
|
Requires-Dist: numpy>=1.24; extra == 'visual'
|
|
37
36
|
Requires-Dist: pillow>=10.0; extra == 'visual'
|
|
@@ -288,12 +288,18 @@ the iterate-and-rebuild pattern in the v0.1.77 retrospective. None of them
|
|
|
288
288
|
exist in upstream python-pptx — they're Athena additions and code that uses
|
|
289
289
|
them is *not* portable to stock python-pptx.
|
|
290
290
|
|
|
291
|
-
### `slide.clear_shapes() -> int`
|
|
291
|
+
### `slide.clear_shapes(*, delete_unmodeled_source_content=False) -> int`
|
|
292
292
|
|
|
293
|
-
Delete every slide-local shape
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
Delete every slide-local shape with one slide-scoped command. Returns the
|
|
294
|
+
number of modeled shapes removed. Unmodeled visual OOXML from the imported
|
|
295
|
+
slide is preserved by default, because Athena not rendering an object is not
|
|
296
|
+
authorization to delete it. Inherited layout / master placeholders and
|
|
297
|
+
slide-level background/notes are left alone.
|
|
298
|
+
|
|
299
|
+
Pass `delete_unmodeled_source_content=True` only after the user explicitly
|
|
300
|
+
authorizes replacing the complete imported visual layer. That option can
|
|
301
|
+
delete text boxes, SmartArt, controls, and other PowerPoint content that is
|
|
302
|
+
not visible in Athena.
|
|
297
303
|
|
|
298
304
|
**Why we added it:** the previous workaround was a manual
|
|
299
305
|
`for shape in slide.shapes: shape.delete()` loop. Combined with the silent
|
|
@@ -390,6 +396,15 @@ upstream auto-assigns names like `"Rectangle 47"` and only exposes
|
|
|
390
396
|
construction. **Portable code that needs to run against stock python-pptx
|
|
391
397
|
should omit `name=` and assign after the fact.**
|
|
392
398
|
|
|
399
|
+
When `name=` is omitted, Athena's generated name skips any identical name in
|
|
400
|
+
the slide's current SDK snapshot. This matters after clone/delete workflows
|
|
401
|
+
where the simple shape-count candidate can already exist. The selected name is
|
|
402
|
+
also available immediately on the returned proxy, before a snapshot refresh.
|
|
403
|
+
This is snapshot-local rather than a distributed uniqueness guarantee: a
|
|
404
|
+
concurrent collaborator can add the same name after the snapshot was loaded.
|
|
405
|
+
Keep the returned proxy or `shape_id` for cross-client-safe follow-up edits;
|
|
406
|
+
use names as human-readable selectors after refreshing the deck.
|
|
407
|
+
|
|
393
408
|
```python
|
|
394
409
|
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
|
|
395
410
|
Inches(1), Inches(1), Inches(4), Inches(2),
|
|
@@ -1049,6 +1064,14 @@ work in addition to SDK surface and are tracked separately.
|
|
|
1049
1064
|
|
|
1050
1065
|
### Shape-level additions
|
|
1051
1066
|
|
|
1067
|
+
- **`Shape.z_order = "to_back" | "to_front" | "backward" | "forward"`**
|
|
1068
|
+
is a write-only convenience alias for `send_to_back()`,
|
|
1069
|
+
`bring_to_front()`, `send_backward()`, and `bring_forward()`. Assignment
|
|
1070
|
+
emits the same `SetShapeZOrder` command as the method form; unsupported
|
|
1071
|
+
strings raise `ValueError` instead of creating a misleading local
|
|
1072
|
+
attribute. Reading `shape.z_order` raises `AttributeError` because deck
|
|
1073
|
+
snapshots do not expose a stable numeric z-order value.
|
|
1074
|
+
|
|
1052
1075
|
- **`Shape.duplicate(target_slide=None, offset_x=None, offset_y=None)`**
|
|
1053
1076
|
(closes python-pptx#533, #620, 10-comment thread requesting a public
|
|
1054
1077
|
duplicate API). Alias for the existing `Shape.clone()` — both verbs
|
|
@@ -18,9 +18,11 @@ from contextlib import contextmanager
|
|
|
18
18
|
from threading import local
|
|
19
19
|
from typing import TYPE_CHECKING, Any, Generator, Optional
|
|
20
20
|
|
|
21
|
+
import requests
|
|
22
|
+
|
|
21
23
|
from . import _ptc
|
|
22
24
|
from .commands import AnyCommand, validate_command
|
|
23
|
-
from .errors import BatchPartialError
|
|
25
|
+
from .errors import BatchPartialError, ValidationError
|
|
24
26
|
from .typing import CommandsResponse
|
|
25
27
|
|
|
26
28
|
if TYPE_CHECKING:
|
|
@@ -390,6 +392,37 @@ class CommandBuffer:
|
|
|
390
392
|
return_snapshot=False,
|
|
391
393
|
)
|
|
392
394
|
applied += len(chunk)
|
|
395
|
+
except requests.exceptions.InvalidJSONError as exc:
|
|
396
|
+
# The request body could not be serialized, so nothing reached
|
|
397
|
+
# the wire. Restoring the unserializable command(s) would poison
|
|
398
|
+
# the buffer permanently — every later flush would re-fail on the
|
|
399
|
+
# same payload — so drop them from the restore and surface a
|
|
400
|
+
# ValidationError naming what was dropped. (validate_command
|
|
401
|
+
# rejects NaN/Infinity at add() time; this covers commands whose
|
|
402
|
+
# payload became unserializable after they were buffered.)
|
|
403
|
+
unsent = commands[applied:]
|
|
404
|
+
serializable: list[AnyCommand] = []
|
|
405
|
+
poisoned: list[AnyCommand] = []
|
|
406
|
+
for command in unsent:
|
|
407
|
+
try:
|
|
408
|
+
json.dumps(command.to_dict(), allow_nan=False)
|
|
409
|
+
except (TypeError, ValueError):
|
|
410
|
+
poisoned.append(command)
|
|
411
|
+
else:
|
|
412
|
+
serializable.append(command)
|
|
413
|
+
if not poisoned:
|
|
414
|
+
self._restore_unsent(unsent)
|
|
415
|
+
raise
|
|
416
|
+
self._restore_unsent(serializable)
|
|
417
|
+
_ptc_emit_end_batch(commands[:applied], is_error=False)
|
|
418
|
+
_ptc_emit_end_batch(unsent, is_error=True)
|
|
419
|
+
labels = ", ".join(c.context_label() for c in poisoned)
|
|
420
|
+
raise ValidationError(
|
|
421
|
+
f"Dropped {len(poisoned)} buffered command(s) that cannot be "
|
|
422
|
+
f"serialized to JSON ({labels}). The remaining buffered "
|
|
423
|
+
f"commands were kept and will be sent on the next flush. "
|
|
424
|
+
f"Original error: {exc}"
|
|
425
|
+
) from exc
|
|
393
426
|
except BatchPartialError as exc:
|
|
394
427
|
# The failing chunk committed its commands before
|
|
395
428
|
# ``failure_index`` server-side; everything in earlier chunks
|
|
@@ -40,7 +40,23 @@ from .typing import (
|
|
|
40
40
|
from .commands import AnyCommand, validate_command
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
def _resolve_sdk_version() -> str:
|
|
44
|
+
"""Installed package version, matching ``pptx.__version__``.
|
|
45
|
+
|
|
46
|
+
Read from importlib.metadata rather than importing ``pptx`` — client.py
|
|
47
|
+
is imported during ``pptx/__init__`` execution, so importing back would
|
|
48
|
+
be circular. Falls back to "unknown" for source checkouts that were
|
|
49
|
+
never pip-installed.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
from importlib.metadata import version
|
|
53
|
+
|
|
54
|
+
return version("athena-python-pptx")
|
|
55
|
+
except Exception:
|
|
56
|
+
return "unknown"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
SDK_VERSION = _resolve_sdk_version()
|
|
44
60
|
SDK_CLIENT_TYPE = "python-sdk"
|
|
45
61
|
|
|
46
62
|
DEFAULT_TIMEOUT = 30 # seconds
|
|
@@ -13,6 +13,8 @@ __athena_extension_description__: str = (
|
|
|
13
13
|
)
|
|
14
14
|
__athena_extension_since__: str = "0.1.0"
|
|
15
15
|
|
|
16
|
+
import math
|
|
17
|
+
|
|
16
18
|
from dataclasses import dataclass, field, asdict
|
|
17
19
|
from typing import Any, ClassVar, Optional, Union
|
|
18
20
|
from ._citations import normalize_citation_string
|
|
@@ -69,6 +71,23 @@ class Command:
|
|
|
69
71
|
raise NotImplementedError
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
def _find_non_finite(value: Any, path: str) -> Optional[str]:
|
|
75
|
+
"""Return the JSON path of the first NaN/Infinity in ``value``, if any."""
|
|
76
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
77
|
+
return path
|
|
78
|
+
if isinstance(value, dict):
|
|
79
|
+
for key, item in value.items():
|
|
80
|
+
found = _find_non_finite(item, f"{path}.{key}" if path else str(key))
|
|
81
|
+
if found is not None:
|
|
82
|
+
return found
|
|
83
|
+
elif isinstance(value, (list, tuple)):
|
|
84
|
+
for i, item in enumerate(value):
|
|
85
|
+
found = _find_non_finite(item, f"{path}[{i}]")
|
|
86
|
+
if found is not None:
|
|
87
|
+
return found
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
72
91
|
def validate_command(command: "Command") -> None:
|
|
73
92
|
"""Validate ``command``, prefixing any ValidationError with its identity.
|
|
74
93
|
|
|
@@ -77,6 +96,11 @@ def validate_command(command: "Command") -> None:
|
|
|
77
96
|
call site (batched flush, multi-command POST) that alone can't be traced
|
|
78
97
|
back to a shape or slide. Every SDK-internal validation path goes through
|
|
79
98
|
this wrapper so the error always carries the command context.
|
|
99
|
+
|
|
100
|
+
Also rejects NaN/Infinity anywhere in the wire payload: JSON has no
|
|
101
|
+
encoding for them, so they would otherwise pass ``validate()`` and then
|
|
102
|
+
blow up requests' JSON encoder at flush time — long after the offending
|
|
103
|
+
user line — leaving the unserializable command stuck in the buffer.
|
|
80
104
|
"""
|
|
81
105
|
try:
|
|
82
106
|
command.validate()
|
|
@@ -85,6 +109,15 @@ def validate_command(command: "Command") -> None:
|
|
|
85
109
|
f"{command.context_label()}: {exc.message}", exc.field
|
|
86
110
|
) from None
|
|
87
111
|
|
|
112
|
+
non_finite_path = _find_non_finite(command.to_dict(), "")
|
|
113
|
+
if non_finite_path is not None:
|
|
114
|
+
raise ValidationError(
|
|
115
|
+
f"{command.context_label()}: value at '{non_finite_path}' is NaN "
|
|
116
|
+
f"or Infinity, which cannot be sent as JSON. Replace it with a "
|
|
117
|
+
f"finite number, or None to omit the data point.",
|
|
118
|
+
non_finite_path,
|
|
119
|
+
)
|
|
120
|
+
|
|
88
121
|
|
|
89
122
|
@dataclass
|
|
90
123
|
class AddTextBox(Command):
|
|
@@ -269,6 +302,8 @@ class AddSlide(Command):
|
|
|
269
302
|
Add a new slide to the presentation.
|
|
270
303
|
|
|
271
304
|
Args:
|
|
305
|
+
client_id: Correlation ID for commands that reference the new slide in
|
|
306
|
+
the same batch.
|
|
272
307
|
index: Position to insert the slide (optional, defaults to end)
|
|
273
308
|
layout_index: Layout template index (optional, fallback when no
|
|
274
309
|
name/path given)
|
|
@@ -280,6 +315,7 @@ class AddSlide(Command):
|
|
|
280
315
|
``slideLayout{i+1}.xml`` filename numbering).
|
|
281
316
|
"""
|
|
282
317
|
|
|
318
|
+
client_id: Optional[str] = None
|
|
283
319
|
index: Optional[int] = None
|
|
284
320
|
layout_index: Optional[int] = None
|
|
285
321
|
layout_name: Optional[str] = None
|
|
@@ -334,6 +370,22 @@ class DeleteShape(Command):
|
|
|
334
370
|
raise ValidationError("shape_id is required", "shape_id")
|
|
335
371
|
|
|
336
372
|
|
|
373
|
+
@dataclass
|
|
374
|
+
class ClearSlideShapes(Command):
|
|
375
|
+
"""Delete modeled slide-local shapes, preserving source-only OOXML by default."""
|
|
376
|
+
|
|
377
|
+
slide_id: SlideId
|
|
378
|
+
delete_unmodeled_source_content: bool = False
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def command_type(self) -> str:
|
|
382
|
+
return "ClearSlideShapes"
|
|
383
|
+
|
|
384
|
+
def validate(self) -> None:
|
|
385
|
+
if not self.slide_id:
|
|
386
|
+
raise ValidationError("slide_id is required", "slide_id")
|
|
387
|
+
|
|
388
|
+
|
|
337
389
|
@dataclass
|
|
338
390
|
class SetParagraphStyle(Command):
|
|
339
391
|
"""
|
|
@@ -459,14 +511,34 @@ class SetShapeStyle(Command):
|
|
|
459
511
|
line_scheme_color: Optional[str] = None
|
|
460
512
|
line_width_emu: Optional[int] = None
|
|
461
513
|
line_dash: Optional[str] = None
|
|
514
|
+
clear_fill: bool = False
|
|
462
515
|
|
|
463
516
|
@property
|
|
464
517
|
def command_type(self) -> str:
|
|
465
518
|
return "SetShapeStyle"
|
|
466
519
|
|
|
520
|
+
def to_dict(self) -> dict[str, Any]:
|
|
521
|
+
# The wire format distinguishes "clear the fill" (explicit null) from
|
|
522
|
+
# "leave unchanged" (field absent), but base to_dict drops None fields.
|
|
523
|
+
# ``clear_fill`` is a local flag translated into explicit nulls here;
|
|
524
|
+
# it never rides the wire itself.
|
|
525
|
+
result = super().to_dict()
|
|
526
|
+
result.pop("clearFill", None)
|
|
527
|
+
if self.clear_fill:
|
|
528
|
+
result["fillColorHex"] = None
|
|
529
|
+
result["fillSchemeColor"] = None
|
|
530
|
+
return result
|
|
531
|
+
|
|
467
532
|
def validate(self) -> None:
|
|
468
533
|
if not self.shape_id:
|
|
469
534
|
raise ValidationError("shape_id is required", "shape_id")
|
|
535
|
+
if self.clear_fill and (
|
|
536
|
+
self.fill_color_hex is not None or self.fill_scheme_color is not None
|
|
537
|
+
):
|
|
538
|
+
raise ValidationError(
|
|
539
|
+
"clear_fill cannot be combined with a fill color",
|
|
540
|
+
"clear_fill",
|
|
541
|
+
)
|
|
470
542
|
if self.fill_transparency is not None and not (0.0 <= self.fill_transparency <= 1.0):
|
|
471
543
|
raise ValidationError("fill_transparency must be between 0.0 and 1.0", "fill_transparency")
|
|
472
544
|
|
|
@@ -2702,6 +2774,30 @@ class DeleteTableColumn(Command):
|
|
|
2702
2774
|
raise ValidationError("col_index must be >= 0", "col_index")
|
|
2703
2775
|
|
|
2704
2776
|
|
|
2777
|
+
@dataclass
|
|
2778
|
+
class DeleteTableRow(Command):
|
|
2779
|
+
"""Delete the row at ``row_index`` from an existing table.
|
|
2780
|
+
|
|
2781
|
+
Row axis of :class:`DeleteTableColumn`. Unlike ``SetTableRowCount``
|
|
2782
|
+
(which can only truncate rows from the end), this removes a specific
|
|
2783
|
+
row and shifts later rows up, matching ``Table.remove_row(index)``
|
|
2784
|
+
semantics.
|
|
2785
|
+
"""
|
|
2786
|
+
|
|
2787
|
+
shape_id: ShapeId
|
|
2788
|
+
row_index: int
|
|
2789
|
+
|
|
2790
|
+
@property
|
|
2791
|
+
def command_type(self) -> str:
|
|
2792
|
+
return "DeleteTableRow"
|
|
2793
|
+
|
|
2794
|
+
def validate(self) -> None:
|
|
2795
|
+
if not self.shape_id:
|
|
2796
|
+
raise ValidationError("shape_id is required", "shape_id")
|
|
2797
|
+
if self.row_index < 0:
|
|
2798
|
+
raise ValidationError("row_index must be >= 0", "row_index")
|
|
2799
|
+
|
|
2800
|
+
|
|
2705
2801
|
@dataclass
|
|
2706
2802
|
class SetPointDataLabelStyle(Command):
|
|
2707
2803
|
"""Per-point data-label override on a series.
|
|
@@ -3343,6 +3439,7 @@ AnyCommand = Union[
|
|
|
3343
3439
|
AddSlide,
|
|
3344
3440
|
DeleteSlide,
|
|
3345
3441
|
DeleteShape,
|
|
3442
|
+
ClearSlideShapes,
|
|
3346
3443
|
SetParagraphStyle,
|
|
3347
3444
|
AddShape,
|
|
3348
3445
|
SetShapeStyle,
|
|
@@ -3397,6 +3494,7 @@ AnyCommand = Union[
|
|
|
3397
3494
|
InsertTableRow,
|
|
3398
3495
|
InsertTableColumn,
|
|
3399
3496
|
DeleteTableColumn,
|
|
3497
|
+
DeleteTableRow,
|
|
3400
3498
|
SetPointDataLabelStyle,
|
|
3401
3499
|
SetChartTitlePosition,
|
|
3402
3500
|
AddChart2016,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Compatibility shim for python-pptx's ``pptx.oxml`` package.
|
|
2
|
+
|
|
3
|
+
Upstream python-pptx exposes its lxml-backed OOXML layer here. The REST SDK
|
|
4
|
+
has no local XML tree — every mutation is a server command — so the element
|
|
5
|
+
classes and parsers cannot exist. This shim keeps ``import pptx.oxml`` (and
|
|
6
|
+
the very common ``from pptx.oxml.ns import qn``) from crashing an entire
|
|
7
|
+
script at import time: the pure-string helpers in :mod:`pptx.oxml.ns` work
|
|
8
|
+
exactly like upstream, and anything that would need a real XML tree raises
|
|
9
|
+
:class:`~pptx.errors.UnsupportedFeatureError` at the call site with guidance
|
|
10
|
+
instead of ``ModuleNotFoundError`` at the import.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__athena_extension_module__: bool = True
|
|
16
|
+
__athena_extension_description__: str = (
|
|
17
|
+
"pptx.oxml import-compatibility shim (REST-SDK only — no local XML tree)."
|
|
18
|
+
)
|
|
19
|
+
__athena_extension_since__: str = "0.9.2"
|
|
20
|
+
|
|
21
|
+
from typing import Any, NoReturn
|
|
22
|
+
|
|
23
|
+
from ..errors import UnsupportedFeatureError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_xml(xml: Any) -> NoReturn:
|
|
27
|
+
"""Upstream parses an XML fragment into an lxml element tree."""
|
|
28
|
+
raise UnsupportedFeatureError(
|
|
29
|
+
"pptx.oxml.parse_xml",
|
|
30
|
+
"This SDK is a REST client — there is no local XML tree to parse "
|
|
31
|
+
"into. Use the documented python-pptx object API (shapes, "
|
|
32
|
+
"text_frame, table, chart) instead of raw OOXML manipulation.",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def register_element_cls(tag: str, cls: Any) -> NoReturn:
|
|
37
|
+
"""Upstream registers a custom lxml element class for a tag."""
|
|
38
|
+
raise UnsupportedFeatureError(
|
|
39
|
+
"pptx.oxml.register_element_cls",
|
|
40
|
+
"This SDK is a REST client — custom OOXML element classes cannot "
|
|
41
|
+
"be registered because documents are edited through server "
|
|
42
|
+
"commands, not a local XML tree.",
|
|
43
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""OOXML namespace helpers — faithful port of ``pptx.oxml.ns``.
|
|
2
|
+
|
|
3
|
+
These are pure string utilities with no XML-tree dependency, so they behave
|
|
4
|
+
identically to upstream python-pptx. Namespace map copied verbatim from
|
|
5
|
+
python-pptx 1.0.2.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
_nsmap: dict[str, str] = {
|
|
11
|
+
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
|
|
12
|
+
"c": "http://schemas.openxmlformats.org/drawingml/2006/chart",
|
|
13
|
+
"cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
|
|
14
|
+
"ct": "http://schemas.openxmlformats.org/package/2006/content-types",
|
|
15
|
+
"dc": "http://purl.org/dc/elements/1.1/",
|
|
16
|
+
"dcmitype": "http://purl.org/dc/dcmitype/",
|
|
17
|
+
"dcterms": "http://purl.org/dc/terms/",
|
|
18
|
+
"ep": "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties",
|
|
19
|
+
"i": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
20
|
+
"m": "http://schemas.openxmlformats.org/officeDocument/2006/math",
|
|
21
|
+
"mo": "http://schemas.microsoft.com/office/mac/office/2008/main",
|
|
22
|
+
"mv": "urn:schemas-microsoft-com:mac:vml",
|
|
23
|
+
"o": "urn:schemas-microsoft-com:office:office",
|
|
24
|
+
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
|
|
25
|
+
"pd": "http://schemas.openxmlformats.org/drawingml/2006/presentationDrawing",
|
|
26
|
+
"pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
|
|
27
|
+
"pr": "http://schemas.openxmlformats.org/package/2006/relationships",
|
|
28
|
+
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
29
|
+
"sl": "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout",
|
|
30
|
+
"v": "urn:schemas-microsoft-com:vml",
|
|
31
|
+
"ve": "http://schemas.openxmlformats.org/markup-compatibility/2006",
|
|
32
|
+
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
|
33
|
+
"w10": "urn:schemas-microsoft-com:office:word",
|
|
34
|
+
"wne": "http://schemas.microsoft.com/office/word/2006/wordml",
|
|
35
|
+
"wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
|
|
36
|
+
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
nsmap = _nsmap
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NamespacePrefixedTag(str):
|
|
43
|
+
"""Value object holding a namespace-prefixed tag like ``'a:t'``."""
|
|
44
|
+
|
|
45
|
+
def __new__(cls, nstag: str, *args: object) -> "NamespacePrefixedTag":
|
|
46
|
+
return super().__new__(cls, nstag)
|
|
47
|
+
|
|
48
|
+
def __init__(self, nstag: str) -> None:
|
|
49
|
+
self._pfx, self._local_part = nstag.split(":")
|
|
50
|
+
self._ns_uri = _nsmap[self._pfx]
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def clark_name(self) -> str:
|
|
54
|
+
return f"{{{self._ns_uri}}}{self._local_part}"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def local_part(self) -> str:
|
|
58
|
+
return self._local_part
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def nsmap(self) -> dict[str, str]:
|
|
62
|
+
return {self._pfx: self._ns_uri}
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def nspfx(self) -> str:
|
|
66
|
+
return self._pfx
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def nsuri(self) -> str:
|
|
70
|
+
return self._ns_uri
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def namespaces(*prefixes: str) -> dict[str, str]:
|
|
74
|
+
"""Namespace map containing only the specified prefixes."""
|
|
75
|
+
return {pfx: _nsmap[pfx] for pfx in prefixes}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def nsdecls(*prefixes: str) -> str:
|
|
79
|
+
"""Namespace declarations for the specified prefixes, xmlns-attr form."""
|
|
80
|
+
return " ".join(f'xmlns:{pfx}="{_nsmap[pfx]}"' for pfx in prefixes)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def nsuri(nspfx: str) -> str:
|
|
84
|
+
"""Namespace URI corresponding to ``nspfx``."""
|
|
85
|
+
return _nsmap[nspfx]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def qn(namespace_prefixed_tag: str) -> str:
|
|
89
|
+
"""Clark-notation name for a namespace-prefixed tag, e.g. ``qn('a:t')``."""
|
|
90
|
+
prefix, local = namespace_prefixed_tag.split(":")
|
|
91
|
+
return f"{{{_nsmap[prefix]}}}{local}"
|
|
@@ -405,35 +405,48 @@ class Presentation:
|
|
|
405
405
|
@athena_extension(
|
|
406
406
|
issue=1018,
|
|
407
407
|
since="0.1.81",
|
|
408
|
-
description=
|
|
408
|
+
description=(
|
|
409
|
+
"Presentation.open — open an existing deck by asset id, or "
|
|
410
|
+
"upload a local .pptx by path."
|
|
411
|
+
),
|
|
409
412
|
)
|
|
410
413
|
def open(
|
|
411
414
|
cls,
|
|
412
|
-
|
|
415
|
+
source: Optional[Union[str, Path]] = None,
|
|
413
416
|
base_url: Optional[str] = None,
|
|
414
417
|
api_key: Optional[str] = None,
|
|
418
|
+
name: Optional[str] = None,
|
|
419
|
+
*,
|
|
420
|
+
deck_id: Optional[str] = None,
|
|
421
|
+
path: Optional[Union[str, Path]] = None,
|
|
415
422
|
) -> Presentation:
|
|
416
|
-
"""Open an existing
|
|
423
|
+
"""Open an existing deck by asset id, or upload a local file by path.
|
|
424
|
+
|
|
425
|
+
Closes python-pptx#1018 — "open a PPT in append mode". Two forms,
|
|
426
|
+
dispatched on the shape of ``source``:
|
|
427
|
+
|
|
428
|
+
* ``Presentation.open("asset_<uuid>")`` — reattach to the
|
|
429
|
+
server-side asset for iterative edits. Same as
|
|
430
|
+
``Presentation(asset_id=...)`` but named to mirror the
|
|
431
|
+
iterate-and-edit verb, and to read naturally next to
|
|
432
|
+
``Presentation.create`` / ``Presentation.upload``.
|
|
433
|
+
* ``Presentation.open("deck.pptx")`` — upload a local ``.pptx``
|
|
434
|
+
file (alias for :meth:`upload`, matching python-pptx's
|
|
435
|
+
``Presentation(path)`` pattern).
|
|
417
436
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
is reattaching to the server-side asset. This is the same call
|
|
422
|
-
as ``Presentation(deck_id=...)`` but named to mirror the iterate-
|
|
423
|
-
and-edit verb the upstream issue is filed under, and to read
|
|
424
|
-
naturally next to ``Presentation.create`` / ``Presentation.upload``.
|
|
437
|
+
``deck_id=`` and ``path=`` are accepted as explicit keyword
|
|
438
|
+
spellings of the two forms for callers that want to bypass the
|
|
439
|
+
shape-based dispatch.
|
|
425
440
|
|
|
426
441
|
Args:
|
|
427
|
-
|
|
442
|
+
source: Athena asset id (``asset_<uuid>`` / legacy
|
|
443
|
+
``deck_<id>``) or a local file path.
|
|
428
444
|
base_url: Optional API base URL. Defaults to
|
|
429
445
|
``ATHENA_PPTX_BASE_URL`` env var.
|
|
430
446
|
api_key: Optional API key. Defaults to ``ATHENA_PPTX_API_KEY``.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
adds / shape edits stream straight into the live Y.Doc so
|
|
435
|
-
collaborators see them in real time — the iterative-edit
|
|
436
|
-
flow the upstream issue describes.
|
|
447
|
+
name: Optional presentation name — upload form only.
|
|
448
|
+
deck_id: Keyword-only explicit id form.
|
|
449
|
+
path: Keyword-only explicit upload form.
|
|
437
450
|
|
|
438
451
|
Example:
|
|
439
452
|
prs = Presentation.open("asset_3a93...")
|
|
@@ -441,7 +454,26 @@ class Presentation:
|
|
|
441
454
|
... # inspect / edit
|
|
442
455
|
prs.save() # flush pending commands
|
|
443
456
|
"""
|
|
444
|
-
|
|
457
|
+
provided = [v for v in (source, deck_id, path) if v is not None]
|
|
458
|
+
if len(provided) != 1:
|
|
459
|
+
raise TypeError(
|
|
460
|
+
"Presentation.open() takes exactly one source: a positional "
|
|
461
|
+
"asset id or file path, or one of the deck_id=/path= keywords."
|
|
462
|
+
)
|
|
463
|
+
if deck_id is None and path is None:
|
|
464
|
+
if isinstance(source, str) and _DECK_ID_PATTERN.match(source):
|
|
465
|
+
deck_id = source
|
|
466
|
+
else:
|
|
467
|
+
path = source
|
|
468
|
+
if deck_id is not None:
|
|
469
|
+
if name is not None:
|
|
470
|
+
raise TypeError(
|
|
471
|
+
"Presentation.open(name=...) is only valid when uploading "
|
|
472
|
+
"a local file; an existing deck already has a name."
|
|
473
|
+
)
|
|
474
|
+
return cls(deck_id=deck_id, base_url=base_url, api_key=api_key)
|
|
475
|
+
assert path is not None
|
|
476
|
+
return cls.upload(path, base_url=base_url, api_key=api_key, name=name)
|
|
445
477
|
|
|
446
478
|
@classmethod
|
|
447
479
|
@athena_extension(
|
|
@@ -694,45 +726,6 @@ class Presentation:
|
|
|
694
726
|
|
|
695
727
|
return cls(deck_id=deck_id, base_url=base_url, api_key=api_key, auto_refresh=wait)
|
|
696
728
|
|
|
697
|
-
@classmethod
|
|
698
|
-
@athena_extension(
|
|
699
|
-
since="0.1.0",
|
|
700
|
-
description="Presentation.open — alias for upload().",
|
|
701
|
-
)
|
|
702
|
-
def open(
|
|
703
|
-
cls,
|
|
704
|
-
path: Union[str, Path],
|
|
705
|
-
base_url: Optional[str] = None,
|
|
706
|
-
api_key: Optional[str] = None,
|
|
707
|
-
name: Optional[str] = None,
|
|
708
|
-
) -> Presentation:
|
|
709
|
-
"""
|
|
710
|
-
Open a local PPTX file by uploading it to the server.
|
|
711
|
-
|
|
712
|
-
This is an alias for upload() to match python-pptx's Presentation(path) pattern.
|
|
713
|
-
|
|
714
|
-
Args:
|
|
715
|
-
path: Path to the local PPTX file
|
|
716
|
-
base_url: Base URL of the API. If not provided, uses ATHENA_PPTX_BASE_URL
|
|
717
|
-
environment variable.
|
|
718
|
-
api_key: Optional API key. If not provided, uses ATHENA_PPTX_API_KEY
|
|
719
|
-
environment variable.
|
|
720
|
-
name: Optional name for the presentation
|
|
721
|
-
|
|
722
|
-
Returns:
|
|
723
|
-
Presentation instance for the uploaded deck
|
|
724
|
-
|
|
725
|
-
Example:
|
|
726
|
-
from pptx import Presentation
|
|
727
|
-
|
|
728
|
-
# Using environment variables
|
|
729
|
-
prs = Presentation.open("my_presentation.pptx")
|
|
730
|
-
|
|
731
|
-
# Or with explicit parameters
|
|
732
|
-
prs = Presentation.open("my_presentation.pptx", base_url="http://localhost:4000")
|
|
733
|
-
"""
|
|
734
|
-
return cls.upload(path, base_url=base_url, api_key=api_key, name=name)
|
|
735
|
-
|
|
736
729
|
@property
|
|
737
730
|
@athena_extension(
|
|
738
731
|
since="0.1.0",
|