athena-python-pptx 0.15.0__tar.gz → 0.16.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.15.0 → athena_python_pptx-0.16.0}/AGENTS.md +1 -1
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/CHANGELOG.md +31 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/DEV-GUIDE.md +7 -8
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/PKG-INFO +1 -1
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/docs/API_PARITY_EXCEPTIONS.md +29 -20
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/docs/athena-api.md +6 -2
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/__init__.py +2 -2
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_ptc.py +1 -3
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/batching.py +2 -1
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/presentation.py +72 -36
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/__init__.py +2 -2
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/slides.py +62 -6
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pyproject.toml +1 -1
- athena_python_pptx-0.15.0/PUBLISHING.md +0 -177
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/.gitignore +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/API_PARITY_REPORT.md +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/CLAUDE.md +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/README.md +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_athena_extension.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_citations.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_references.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_unsupported.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/action.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/category.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/chart.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/client.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/commands.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/errors.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/exc.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/generated/__init__.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/generated/autoshape_types.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/kit.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/media.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/oxml/__init__.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/oxml/ns.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/package.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shared.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/slide.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/spec.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/table.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/text/__init__.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/types.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/typing.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/units.py +0 -0
- {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/util.py +0 -0
|
@@ -124,7 +124,7 @@ This is a **REST API client** that mimics the python-pptx API. Instead of manipu
|
|
|
124
124
|
- State is synchronized via the PPTX Studio collaboration layer (Yjs)
|
|
125
125
|
- **Programmatic Tool Calling (PTC):** every `CommandBuffer.add` and
|
|
126
126
|
`flush` emits begin/end events to agora via `pptx/_ptc.py` when run
|
|
127
|
-
inside
|
|
127
|
+
inside an Athena sandbox with `ATHENA_PTC_URL` set. Each method call
|
|
128
128
|
surfaces as a nested sub-tool-card under the parent
|
|
129
129
|
`run_python_code` tool. See
|
|
130
130
|
[`docs/PROGRAMMATIC_TOOL_CALLING_GUIDE.md`](../../docs/PROGRAMMATIC_TOOL_CALLING_GUIDE.md)
|
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `athena-python-pptx` are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.16.0 — `Presentation.create()` makes 13.333" × 7.5" decks; `create(name=, title=)` titles the deck
|
|
6
|
+
|
|
7
|
+
- **Behaviour change:** `Presentation.create()` makes a deck at PowerPoint's
|
|
8
|
+
Widescreen size, 16:9 at 13.333" × 7.5" (12192000 × 6858000 EMU) — the
|
|
9
|
+
size of the blank template, whose master and layouts are laid out for it.
|
|
10
|
+
It used to resize every blank deck to 16" × 9" (14630400 × 8229600 EMU),
|
|
11
|
+
which changed only the size field: layout placeholders sat off-centre, and
|
|
12
|
+
a deck laid out for 13.333" × 7.5" filled only the top-left 83% of each
|
|
13
|
+
slide's width and height, leaving a white band on the right and bottom. `widescreen=True` (the
|
|
14
|
+
default) now sends nothing unless the template has another size.
|
|
15
|
+
`widescreen=False` keeps the template's size, which is the same: it never
|
|
16
|
+
made a 4:3 deck, whatever the docstring said. Decks created earlier keep
|
|
17
|
+
their stored 16" × 9" size.
|
|
18
|
+
- `Presentation.create(name=..., title=...)` with different values names the
|
|
19
|
+
asset `name` and sets the deck's core title (`prs.core_properties.title`)
|
|
20
|
+
to `title`, also with `pptx=`. It used to raise `ValueError`. `title=`
|
|
21
|
+
alone, or the same value for both, still just names the deck.
|
|
22
|
+
|
|
23
|
+
## 0.16.0 — deleting most of an existing deck needs allow_bulk_delete=True
|
|
24
|
+
|
|
25
|
+
- **Behaviour change:** a run that opens an existing deck of 4 or more slides
|
|
26
|
+
and deletes more than half of them — `slides.keep_only([0])`, a
|
|
27
|
+
`delete_slides()` of most indices, or `slides.delete()` calls adding up —
|
|
28
|
+
raises `ValueError` before anything is sent, unless the call passes
|
|
29
|
+
`allow_bulk_delete=True` (a new keyword on `delete()`, `delete_slides()`
|
|
30
|
+
and `keep_only()`). An agent asked to add one slide had re-run its whole
|
|
31
|
+
build (`keep_only([0])` then rebuild), and stopping it midway left a
|
|
32
|
+
21-slide deck half-built. Decks the run created with
|
|
33
|
+
`Presentation.create()` or `Presentation.upload()` are not guarded, so
|
|
34
|
+
trimming a fresh template still works.
|
|
35
|
+
|
|
5
36
|
## 0.15.0 — uploaded click actions read back; pictures carry python-pptx's descr; layout graphics refuse delete()
|
|
6
37
|
|
|
7
38
|
- `shape.click_action` reads the click actions of an uploaded deck as
|
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
## Quick Setup
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
#
|
|
7
|
-
cd python-sdk
|
|
8
|
-
|
|
6
|
+
# Editable install with the dev group, in the SDK's own .venv
|
|
7
|
+
cd pptx-studio/python-sdk
|
|
8
|
+
uv sync
|
|
9
9
|
|
|
10
10
|
# Point at local API
|
|
11
11
|
export ATHENA_PPTX_BASE_URL=http://localhost:4000
|
|
12
12
|
|
|
13
|
-
# Start the API server (in another terminal)
|
|
14
|
-
|
|
13
|
+
# Start the API server (in another terminal, from pptx-studio/)
|
|
14
|
+
bun run dev
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Testing Changes
|
|
@@ -38,7 +38,7 @@ with prs.batch():
|
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
python scratch.py
|
|
41
|
+
uv run python scratch.py
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
### Run tests
|
|
@@ -68,8 +68,7 @@ CI installs with pip, so a committed lock would be read by nothing.
|
|
|
68
68
|
### Interactive REPL
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
|
|
72
|
-
ATHENA_PPTX_BASE_URL=http://localhost:4000 ipython
|
|
71
|
+
ATHENA_PPTX_BASE_URL=http://localhost:4000 uv run --with ipython ipython
|
|
73
72
|
```
|
|
74
73
|
|
|
75
74
|
### Debug HTTP calls
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: athena-python-pptx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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
|
|
@@ -1192,7 +1192,7 @@ to be world-readable, like any plain image link.
|
|
|
1192
1192
|
download the URL externally and pass `bytes` or a file-like.**
|
|
1193
1193
|
|
|
1194
1194
|
```python
|
|
1195
|
-
# Inside
|
|
1195
|
+
# Inside the presentation-exec sandbox:
|
|
1196
1196
|
img = ws.export_range_as_image_asset("A1:F20", name="Q4 Revenue")
|
|
1197
1197
|
slide.shapes.add_picture(img.url, Inches(1), Inches(1), width=Inches(6))
|
|
1198
1198
|
```
|
|
@@ -1788,29 +1788,37 @@ These additions don't change any python-pptx behaviour — they're new
|
|
|
1788
1788
|
optional surface on top of the existing API. Portable code that needs to
|
|
1789
1789
|
run against stock `python-pptx` should avoid them.
|
|
1790
1790
|
|
|
1791
|
-
### `Presentation.create(title=...)` —
|
|
1791
|
+
### `Presentation.create(title=...)` — names the deck, or titles it next to `name=`
|
|
1792
1792
|
|
|
1793
1793
|
Upstream `python-pptx` has no `Presentation.create()` factory at all;
|
|
1794
|
-
this whole method is an Athena addition.
|
|
1794
|
+
this whole method is an Athena addition. On its own, `title=` is a
|
|
1795
1795
|
convenience alias for `name=` because callers often think of the deck
|
|
1796
|
-
label as a "title" rather than a "name".
|
|
1797
|
-
`
|
|
1796
|
+
label as a "title" rather than a "name". Next to a different `name=`,
|
|
1797
|
+
the asset is named `name` and `title` becomes the deck's core title
|
|
1798
|
+
(`prs.core_properties.title`, the title PowerPoint shows in File > Info):
|
|
1798
1799
|
|
|
1799
1800
|
```python
|
|
1800
1801
|
Presentation.create(name="Q4") # canonical
|
|
1801
1802
|
Presentation.create(title="Q4") # alias — same result
|
|
1802
|
-
Presentation.create(name="Q4", title="Q4") #
|
|
1803
|
-
Presentation.create(name="
|
|
1803
|
+
Presentation.create(name="Q4", title="Q4") # same result
|
|
1804
|
+
Presentation.create(name="q4-review", title="Q4 Review")
|
|
1805
|
+
# asset named "q4-review"; prs.core_properties.title == "Q4 Review"
|
|
1804
1806
|
```
|
|
1805
1807
|
|
|
1806
1808
|
### `Presentation.create(widescreen=True)`
|
|
1807
1809
|
|
|
1808
|
-
Optional kwarg on `Presentation.create()`
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1810
|
+
Optional kwarg on `Presentation.create()`, `True` by default: the deck
|
|
1811
|
+
gets PowerPoint's Widescreen size, 16:9 at 13.333" × 7.5" (12192000 ×
|
|
1812
|
+
6858000 EMU). The blank template new decks start from already has that
|
|
1813
|
+
size and its master and layouts are laid out for it, so nothing is sent
|
|
1814
|
+
unless the template has another size. Athena-only. `widescreen=False`
|
|
1815
|
+
keeps the template's own size, which today is the same 13.333" × 7.5":
|
|
1816
|
+
there is no 4:3 blank template, and resizing a blank deck to 4:3 would
|
|
1817
|
+
leave its master and layout placeholders laid out for 13.333". For any
|
|
1818
|
+
other size call `prs.set_slide_size(width, height)` after `create()`;
|
|
1819
|
+
the master and layouts keep their geometry. Read the size from
|
|
1820
|
+
`prs.slide_width` / `prs.slide_height` (or `prs.slide_size_inches`)
|
|
1821
|
+
rather than assuming it.
|
|
1814
1822
|
|
|
1815
1823
|
### `Presentation(asset_id=...)` / `Presentation.asset_id` — preferred spelling; `deck_id` is the legacy alias
|
|
1816
1824
|
|
|
@@ -1978,8 +1986,8 @@ xlsx-studio's ownership middleware checks that the calling workspace
|
|
|
1978
1986
|
can read the source workbook. Render scale is fixed at 3 (matches
|
|
1979
1987
|
xlsx-studio's `range-as-asset` default — slide inserts sit at 6–9″
|
|
1980
1988
|
widths where scale=2 leaves text soft). When `ATHENA_XLSX_BASE_URL` /
|
|
1981
|
-
`ATHENA_XLSX_API_KEY` aren't set (e.g. running outside
|
|
1982
|
-
|
|
1989
|
+
`ATHENA_XLSX_API_KEY` aren't set (e.g. running outside the
|
|
1990
|
+
presentation-exec sandbox), the call raises `ValueError` rather than
|
|
1983
1991
|
producing a broken picture.
|
|
1984
1992
|
|
|
1985
1993
|
For finer control (custom scale, `include_headers`, off-snapshot
|
|
@@ -2205,11 +2213,12 @@ work in addition to SDK surface and are tracked separately.
|
|
|
2205
2213
|
|
|
2206
2214
|
### Slide- and presentation-level additions
|
|
2207
2215
|
|
|
2208
|
-
- **`Presentation.create(widescreen=True)` is
|
|
2216
|
+
- **`Presentation.create(widescreen=True)` is the default**
|
|
2209
2217
|
(closes python-pptx#1066, 9-comment thread). The blank-template
|
|
2210
|
-
factory creates 16:9
|
|
2211
|
-
|
|
2212
|
-
|
|
2218
|
+
factory creates 16:9 decks at PowerPoint's Widescreen size,
|
|
2219
|
+
13.333" × 7.5" (12192000 × 6858000 EMU) — the size of the blank
|
|
2220
|
+
template itself. `widescreen=False` keeps the template's size, which is
|
|
2221
|
+
the same; there is no 4:3 blank deck.
|
|
2213
2222
|
|
|
2214
2223
|
- **`Presentation.open(deck_id, base_url=None, api_key=None)`**
|
|
2215
2224
|
(closes python-pptx#1018 — "open a PPT in append mode"). Named
|
|
@@ -2624,7 +2633,7 @@ python-pptx. Generated from `pptx._athena_extension.iter_athena_extensions()`.
|
|
|
2624
2633
|
<!-- BEGIN GENERATED: athena-extensions (scripts/generate_parity_tables.py; do not edit by hand) -->
|
|
2625
2634
|
| Surface | Kind | Since | Upstream issue | What it is |
|
|
2626
2635
|
|---|---|---|---|---|
|
|
2627
|
-
| `pptx._ptc` | module | 0.1.0 | | Programmatic Tool Calling (
|
|
2636
|
+
| `pptx._ptc` | module | 0.1.0 | | Programmatic Tool Calling (sandbox event emission). |
|
|
2628
2637
|
| `pptx._references` | module | 0.1.71 | | Cross-studio asset reference types (Athena studio-linking, no upstream). |
|
|
2629
2638
|
| `pptx.action.ActionSetting.action` | property | 0.15.0 | | ActionSetting.action setter: relative slide jumps and end show (read-only in python-pptx). |
|
|
2630
2639
|
| `pptx.action._ActionHyperlink.tooltip` | property | 0.15.0 | | click_action.hyperlink.tooltip: hover text for a shape's click action. |
|
|
@@ -109,17 +109,19 @@ new_slide = slide.clone(target_index=len(prs.slides))
|
|
|
109
109
|
Delete multiple slides from the presentation by their indices.
|
|
110
110
|
|
|
111
111
|
```python
|
|
112
|
-
def delete_slides(indices: list[int]) -> None
|
|
112
|
+
def delete_slides(indices: list[int], *, allow_bulk_delete: bool = False) -> None
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
**Parameters:**
|
|
116
116
|
|
|
117
117
|
- **indices** (`list[int]`): List of zero-based slide indices to delete
|
|
118
|
+
- **allow_bulk_delete** (`bool`): Allow deleting more than half of the slides an existing deck (4+ slides) had when this run opened it.
|
|
118
119
|
|
|
119
120
|
**Raises:**
|
|
120
121
|
|
|
121
122
|
- `ValueError`: If any index is out of range
|
|
122
123
|
- `ValueError`: If duplicate indices are provided
|
|
124
|
+
- `ValueError`: If the run would delete most of an existing deck's slides without `allow_bulk_delete=True`
|
|
123
125
|
|
|
124
126
|
**Example:**
|
|
125
127
|
|
|
@@ -144,18 +146,20 @@ prs.slides.delete_slides([len(prs.slides) - 1, len(prs.slides) - 2])
|
|
|
144
146
|
Keep only the slides at the specified indices, deleting all others.
|
|
145
147
|
|
|
146
148
|
```python
|
|
147
|
-
def keep_only(indices: list[int]) -> None
|
|
149
|
+
def keep_only(indices: list[int], *, allow_bulk_delete: bool = False) -> None
|
|
148
150
|
```
|
|
149
151
|
|
|
150
152
|
**Parameters:**
|
|
151
153
|
|
|
152
154
|
- **indices** (`list[int]`): List of zero-based slide indices to keep
|
|
155
|
+
- **allow_bulk_delete** (`bool`): Allow deleting more than half of the slides an existing deck (4+ slides) had when this run opened it.
|
|
153
156
|
|
|
154
157
|
**Raises:**
|
|
155
158
|
|
|
156
159
|
- `ValueError`: If any index is out of range
|
|
157
160
|
- `ValueError`: If duplicate indices are provided
|
|
158
161
|
- `ValueError`: If indices list is empty
|
|
162
|
+
- `ValueError`: If the run would delete most of an existing deck's slides without `allow_bulk_delete=True`
|
|
159
163
|
|
|
160
164
|
**Example:**
|
|
161
165
|
|
|
@@ -189,7 +189,7 @@ def _flush_failure_message(buffer: "CommandBuffer", exc: BaseException) -> str:
|
|
|
189
189
|
def flush_all(*, strict: bool = False) -> None:
|
|
190
190
|
"""Flush all active presentation command buffers.
|
|
191
191
|
|
|
192
|
-
Called by
|
|
192
|
+
Called by Athena's sandbox executor after user code runs. By default a
|
|
193
193
|
per-buffer flush failure is written to stderr and swallowed (local-script
|
|
194
194
|
back-compat); the sandbox executor calls ``flush_all(strict=True)`` so a
|
|
195
195
|
server rejection of the final buffered edits surfaces as a real tool
|
|
@@ -327,7 +327,7 @@ def validate_all_touched() -> dict[int, list[str]]:
|
|
|
327
327
|
return results
|
|
328
328
|
|
|
329
329
|
|
|
330
|
-
__version__ = "0.
|
|
330
|
+
__version__ = "0.16.0"
|
|
331
331
|
|
|
332
332
|
__all__ = [
|
|
333
333
|
# Main entry point
|
|
@@ -37,9 +37,7 @@ of stdlib code is cheaper than spinning up a 4th release pipeline.
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
39
|
__athena_extension_module__: bool = True
|
|
40
|
-
__athena_extension_description__: str = (
|
|
41
|
-
"Programmatic Tool Calling (Daytona sandbox event emission)."
|
|
42
|
-
)
|
|
40
|
+
__athena_extension_description__: str = "Programmatic Tool Calling (sandbox event emission)."
|
|
43
41
|
__athena_extension_since__: str = "0.1.0"
|
|
44
42
|
|
|
45
43
|
import http.client
|
|
@@ -656,7 +656,8 @@ class CommandBuffer:
|
|
|
656
656
|
|
|
657
657
|
An auto-flush timer (default 2.0s) automatically flushes pending commands
|
|
658
658
|
after an idle period. This ensures commands are not silently lost in
|
|
659
|
-
persistent process environments (e.g.
|
|
659
|
+
persistent process environments (e.g. long-lived sandbox executors) where
|
|
660
|
+
atexit never fires.
|
|
660
661
|
|
|
661
662
|
Usage:
|
|
662
663
|
# Default: commands are buffered
|
|
@@ -33,6 +33,11 @@ from .units import Emu, Length, ensure_emu
|
|
|
33
33
|
# asset and capture its id from ``prs.deck_id``.
|
|
34
34
|
_DECK_ID_PATTERN = re.compile(r"^(asset_|deck_)[A-Za-z0-9_-]+$")
|
|
35
35
|
|
|
36
|
+
# PowerPoint's "Widescreen" slide size (Design > Slide Size), 16:9 at
|
|
37
|
+
# 13.333" x 7.5" — the size of the blank template new decks start from, whose
|
|
38
|
+
# master and layouts are laid out for it.
|
|
39
|
+
_WIDESCREEN_SLIDE_SIZE_EMU = (12192000, 6858000)
|
|
40
|
+
|
|
36
41
|
|
|
37
42
|
# -------------------------------------------------------------------------
|
|
38
43
|
# CoreProperties — python-pptx's ``prs.core_properties``
|
|
@@ -273,6 +278,14 @@ def _open_failure(*, target: str, exc: Exception) -> Exception:
|
|
|
273
278
|
return exc
|
|
274
279
|
|
|
275
280
|
|
|
281
|
+
def _release_opened_slides(prs: Any) -> None:
|
|
282
|
+
"""A deck this run created or uploaded has no earlier slides to protect."""
|
|
283
|
+
prs._protect_slides_pending = False
|
|
284
|
+
slides = getattr(prs, "_slides", None)
|
|
285
|
+
if slides is not None:
|
|
286
|
+
slides._release_opened_slides()
|
|
287
|
+
|
|
288
|
+
|
|
276
289
|
class Presentation:
|
|
277
290
|
"""
|
|
278
291
|
Presentation proxy - the main entry point for working with a deck.
|
|
@@ -406,6 +419,10 @@ class Presentation:
|
|
|
406
419
|
self._atexit_flush = _flush_on_exit
|
|
407
420
|
atexit.register(_flush_on_exit)
|
|
408
421
|
|
|
422
|
+
# The first snapshot adopted (here or a later manual refresh())
|
|
423
|
+
# records the slides the bulk-delete guard protects.
|
|
424
|
+
self._protect_slides_pending = True
|
|
425
|
+
|
|
409
426
|
# PTC: surface deck open as its own sub-tool-card so the
|
|
410
427
|
# frontend can react (e.g., auto-open the asset tab). The
|
|
411
428
|
# paired ``Presentation.create()`` path emits
|
|
@@ -683,9 +700,11 @@ class Presentation:
|
|
|
683
700
|
"""
|
|
684
701
|
Create a new presentation.
|
|
685
702
|
|
|
686
|
-
By default this creates an empty
|
|
687
|
-
that's immediately ready to
|
|
688
|
-
content.
|
|
703
|
+
By default this creates an empty presentation at PowerPoint's
|
|
704
|
+
Widescreen size (16:9, 13.333" x 7.5") that's immediately ready to
|
|
705
|
+
use; you can then add slides and content. Read the size from
|
|
706
|
+
``prs.slide_width`` / ``prs.slide_height`` (or
|
|
707
|
+
``prs.slide_size_inches``) rather than assuming it.
|
|
689
708
|
|
|
690
709
|
When ``pptx`` is provided, the local ``.pptx`` / ``.potx`` file is
|
|
691
710
|
uploaded as the seed instead of starting from a blank template.
|
|
@@ -706,22 +725,27 @@ class Presentation:
|
|
|
706
725
|
file. When provided, the file is uploaded and used as the
|
|
707
726
|
seed for the new presentation (mirrors python-pptx's
|
|
708
727
|
``Presentation(pptx=...)`` constructor kwarg).
|
|
709
|
-
title: Optional
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
728
|
+
title: Optional deck title. On its own it names the deck, as an
|
|
729
|
+
alias for ``name`` (callers often think of the deck label
|
|
730
|
+
as a "title"). With a different ``name``, the deck is
|
|
731
|
+
named ``name`` and ``title`` becomes its core title
|
|
732
|
+
(``prs.core_properties.title``, the title PowerPoint
|
|
733
|
+
shows in File > Info). The same value for both just
|
|
734
|
+
names the deck.
|
|
735
|
+
widescreen: Give the deck PowerPoint's Widescreen size: 16:9,
|
|
736
|
+
13.333" x 7.5" (12192000 x 6858000 EMU). The blank
|
|
737
|
+
template already has that size, so nothing is sent
|
|
738
|
+
unless the template has another one. Defaults to
|
|
739
|
+
``True``. ``widescreen=False`` keeps the template's
|
|
740
|
+
own size, which today is the same 13.333" x 7.5":
|
|
741
|
+
there is no 4:3 blank template, and a 4:3 resize
|
|
742
|
+
would leave the master and layout placeholders laid
|
|
743
|
+
out for 13.333". For another size call
|
|
744
|
+
:meth:`set_slide_size` after ``create()``; the master
|
|
745
|
+
and layouts keep their geometry. Ignored when
|
|
746
|
+
``pptx=`` is provided (the uploaded file defines its
|
|
747
|
+
own slide size). Not in upstream python-pptx, which
|
|
748
|
+
has no factory method here at all.
|
|
725
749
|
|
|
726
750
|
Returns:
|
|
727
751
|
Presentation instance for the new deck
|
|
@@ -741,25 +765,25 @@ class Presentation:
|
|
|
741
765
|
)
|
|
742
766
|
slide = prs.slides.add_slide()
|
|
743
767
|
|
|
744
|
-
#
|
|
745
|
-
prs = Presentation.create(name="
|
|
768
|
+
# Asset named "q3-review", deck titled "Q3 Review"
|
|
769
|
+
prs = Presentation.create(name="q3-review", title="Q3 Review")
|
|
770
|
+
print(prs.slide_size_inches) # (13.333..., 7.5)
|
|
746
771
|
"""
|
|
747
|
-
#
|
|
748
|
-
#
|
|
749
|
-
#
|
|
772
|
+
# ``title`` alone names the deck. Next to a different ``name`` it is
|
|
773
|
+
# the deck's core title, checked here so a title python-pptx would
|
|
774
|
+
# refuse fails before any deck exists.
|
|
775
|
+
core_title: str | None = None
|
|
750
776
|
if title is not None and name is not None and title != name:
|
|
751
|
-
|
|
752
|
-
"Presentation.create(name=..., title=...) — name and title "
|
|
753
|
-
"must match when both are provided. Pass only one."
|
|
754
|
-
)
|
|
777
|
+
core_title = _core_text_value(title)
|
|
755
778
|
effective_name = name if name is not None else title
|
|
756
779
|
|
|
757
780
|
from pptx import _ptc
|
|
758
781
|
|
|
759
782
|
if pptx is not None:
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
783
|
+
uploaded = cls.upload(pptx, base_url=base_url, api_key=api_key, name=effective_name)
|
|
784
|
+
if core_title is not None:
|
|
785
|
+
uploaded.core_properties.title = core_title
|
|
786
|
+
return uploaded
|
|
763
787
|
|
|
764
788
|
# PTC: surface asset creation as its own top-level sub-tool-card.
|
|
765
789
|
# Scoped tightly around ``create_empty_deck`` — the deck is
|
|
@@ -791,12 +815,16 @@ class Presentation:
|
|
|
791
815
|
# The presentation's client takes over this connection.
|
|
792
816
|
with _hand_session_to_next_client(client):
|
|
793
817
|
prs = cls(deck_id=deck_id, base_url=base_url, api_key=api_key)
|
|
818
|
+
_release_opened_slides(prs)
|
|
794
819
|
|
|
795
|
-
# Apply widescreen sizing before slide cleanup so the size command
|
|
796
|
-
# is sent even if the template arrives with no default slides.
|
|
797
820
|
if widescreen:
|
|
798
|
-
|
|
799
|
-
prs.
|
|
821
|
+
width_emu, height_emu = _WIDESCREEN_SLIDE_SIZE_EMU
|
|
822
|
+
snapshot = prs._snapshot
|
|
823
|
+
if snapshot is None or (
|
|
824
|
+
int(snapshot.slide_width_emu),
|
|
825
|
+
int(snapshot.slide_height_emu),
|
|
826
|
+
) != (width_emu, height_emu):
|
|
827
|
+
prs.set_slide_size(Emu(width_emu), Emu(height_emu))
|
|
800
828
|
|
|
801
829
|
# Delete any default slides from the blank template
|
|
802
830
|
# The blank template includes one slide that we need to remove
|
|
@@ -804,6 +832,9 @@ class Presentation:
|
|
|
804
832
|
while len(prs.slides) > 0:
|
|
805
833
|
prs.slides.delete(prs.slides[0])
|
|
806
834
|
|
|
835
|
+
if core_title is not None:
|
|
836
|
+
prs.core_properties.title = core_title
|
|
837
|
+
|
|
807
838
|
return prs
|
|
808
839
|
|
|
809
840
|
@classmethod
|
|
@@ -881,7 +912,9 @@ class Presentation:
|
|
|
881
912
|
# Upload without waiting
|
|
882
913
|
deck_id = client.upload_file_async(str(path), name=name)
|
|
883
914
|
|
|
884
|
-
|
|
915
|
+
prs = cls(deck_id=deck_id, base_url=base_url, api_key=api_key, auto_refresh=wait)
|
|
916
|
+
_release_opened_slides(prs)
|
|
917
|
+
return prs
|
|
885
918
|
|
|
886
919
|
@property
|
|
887
920
|
@athena_extension(
|
|
@@ -1932,6 +1965,9 @@ class Presentation:
|
|
|
1932
1965
|
buffer=self._buffer,
|
|
1933
1966
|
snapshot=snapshot,
|
|
1934
1967
|
)
|
|
1968
|
+
if self.__dict__.get("_protect_slides_pending"):
|
|
1969
|
+
self._protect_slides_pending = False
|
|
1970
|
+
self._slides._protect_opened_slides()
|
|
1935
1971
|
|
|
1936
1972
|
# A rejected shape graph leaves the previously published snapshot and
|
|
1937
1973
|
# held slide proxies intact, including reconciliation aliases for retry.
|
|
@@ -92,7 +92,7 @@ _ATHENA_URL_SUFFIXES: tuple[str, ...] = (
|
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
# Hard cap on bytes read for a URL-supplied image. Olympus's asset proxy
|
|
95
|
-
# limits in practice, but ``add_picture`` runs inside a
|
|
95
|
+
# limits in practice, but ``add_picture`` runs inside a sandbox
|
|
96
96
|
# with finite memory, so we want a deterministic local stop regardless
|
|
97
97
|
# of what the server claims.
|
|
98
98
|
_MAX_URL_IMAGE_BYTES = 50 * 1024 * 1024
|
|
@@ -292,7 +292,7 @@ def _try_convert_to_png(data: bytes, image_format: str) -> Optional[bytes]:
|
|
|
292
292
|
produces a broken-image placeholder on the slide.
|
|
293
293
|
|
|
294
294
|
WebP → PNG: requires ``Pillow`` (transitively included via
|
|
295
|
-
``matplotlib`` in the
|
|
295
|
+
``matplotlib`` in the presentation-exec image, so this
|
|
296
296
|
just works in the standard agent runtime).
|
|
297
297
|
|
|
298
298
|
AVIF → PNG: same Pillow path, but only on Pillow builds with AVIF
|
|
@@ -3696,6 +3696,11 @@ class Slide:
|
|
|
3696
3696
|
return f"<Slide slide_id='{self._slide_id}' index={self.__dict__.get('_slide_index_value')}>"
|
|
3697
3697
|
|
|
3698
3698
|
|
|
3699
|
+
# An existing deck this size or larger refuses losing most of its slides in
|
|
3700
|
+
# one run unless the caller passes allow_bulk_delete=True.
|
|
3701
|
+
_BULK_DELETE_MIN_SLIDES = 4
|
|
3702
|
+
|
|
3703
|
+
|
|
3699
3704
|
class Slides:
|
|
3700
3705
|
"""
|
|
3701
3706
|
Collection of slides in a presentation.
|
|
@@ -3713,6 +3718,9 @@ class Slides:
|
|
|
3713
3718
|
self._buffer = buffer
|
|
3714
3719
|
self._slides: list[Slide] = []
|
|
3715
3720
|
self._slides_by_id: dict[SlideId, Slide] = {}
|
|
3721
|
+
# Slides the deck already had when this run opened it; the
|
|
3722
|
+
# bulk-delete guard counts how many of them are no longer present.
|
|
3723
|
+
self._opened_slide_ids: frozenset[SlideId] = frozenset()
|
|
3716
3724
|
|
|
3717
3725
|
# Build slides from snapshot
|
|
3718
3726
|
if snapshot:
|
|
@@ -4367,11 +4375,44 @@ class Slides:
|
|
|
4367
4375
|
s._slide_index = i
|
|
4368
4376
|
return slide
|
|
4369
4377
|
|
|
4378
|
+
def _protect_opened_slides(self) -> None:
|
|
4379
|
+
"""Record the slides an existing deck had when this run opened it."""
|
|
4380
|
+
self._opened_slide_ids = frozenset(s._slide_id for s in self._slides)
|
|
4381
|
+
|
|
4382
|
+
def _release_opened_slides(self) -> None:
|
|
4383
|
+
"""A deck this run created or uploaded has no prior slides to protect."""
|
|
4384
|
+
self._opened_slide_ids = frozenset()
|
|
4385
|
+
|
|
4386
|
+
def _check_bulk_delete(self, slides: list[Slide], allow_bulk_delete: bool) -> None:
|
|
4387
|
+
"""Refuse a run deleting most of the slides an existing deck had.
|
|
4388
|
+
|
|
4389
|
+
Re-running a whole-deck build (``keep_only([0])`` then rebuild) to
|
|
4390
|
+
change one slide wipes every slide the user did not ask to change,
|
|
4391
|
+
and an interrupted rebuild leaves the deck half-built.
|
|
4392
|
+
"""
|
|
4393
|
+
opened = self._opened_slide_ids
|
|
4394
|
+
if allow_bulk_delete or len(opened) < _BULK_DELETE_MIN_SLIDES:
|
|
4395
|
+
return
|
|
4396
|
+
# Counted from the slides still present, so deletions a failed batch
|
|
4397
|
+
# discarded stop counting once the deck is re-read.
|
|
4398
|
+
remaining = {s._slide_id for s in self._slides} - {s._slide_id for s in slides}
|
|
4399
|
+
deleted = opened - remaining
|
|
4400
|
+
if len(deleted) * 2 <= len(opened):
|
|
4401
|
+
return
|
|
4402
|
+
raise ValueError(
|
|
4403
|
+
f"Refusing to delete {len(deleted)} of the {len(opened)} slides this "
|
|
4404
|
+
f"deck had when it was opened. To change, add or remove a few "
|
|
4405
|
+
f"slides, edit only those: add_slide() then prs.reorder_slides(), "
|
|
4406
|
+
f"edit the slide's shapes, or prs.slides.delete(slide). Do not "
|
|
4407
|
+
f"delete the deck and rebuild it. If the user asked to remove these "
|
|
4408
|
+
f"slides, pass allow_bulk_delete=True."
|
|
4409
|
+
)
|
|
4410
|
+
|
|
4370
4411
|
def index(self, slide: Slide) -> int:
|
|
4371
4412
|
"""Get the index of a slide."""
|
|
4372
4413
|
return self._slides.index(slide)
|
|
4373
4414
|
|
|
4374
|
-
def delete(self, slide) -> None:
|
|
4415
|
+
def delete(self, slide, *, allow_bulk_delete: bool = False) -> None:
|
|
4375
4416
|
"""
|
|
4376
4417
|
Delete a slide from the presentation.
|
|
4377
4418
|
|
|
@@ -4385,15 +4426,20 @@ class Slides:
|
|
|
4385
4426
|
|
|
4386
4427
|
Args:
|
|
4387
4428
|
slide: The slide to delete, or an integer index.
|
|
4429
|
+
allow_bulk_delete: Allow this delete to bring the run's deletions
|
|
4430
|
+
past half of the slides an existing deck had when opened.
|
|
4388
4431
|
|
|
4389
4432
|
Raises:
|
|
4390
4433
|
ValueError: If slide is not in this collection
|
|
4434
|
+
ValueError: If the run would delete most of an existing deck's
|
|
4435
|
+
slides without ``allow_bulk_delete=True``
|
|
4391
4436
|
IndexError: If integer index is out of range
|
|
4392
4437
|
"""
|
|
4393
4438
|
if isinstance(slide, int):
|
|
4394
4439
|
slide = self._slides[slide]
|
|
4395
4440
|
if slide not in self._slides:
|
|
4396
4441
|
raise ValueError("Slide not found in presentation")
|
|
4442
|
+
self._check_bulk_delete([slide], allow_bulk_delete)
|
|
4397
4443
|
|
|
4398
4444
|
slide_index = self._slides.index(slide)
|
|
4399
4445
|
|
|
@@ -4416,20 +4462,20 @@ class Slides:
|
|
|
4416
4462
|
description="Slides.remove_slide — alias for delete() matching the python-pptx#956 verb.",
|
|
4417
4463
|
since="0.1.81",
|
|
4418
4464
|
)
|
|
4419
|
-
def remove_slide(self, slide) -> None:
|
|
4465
|
+
def remove_slide(self, slide, *, allow_bulk_delete: bool = False) -> None:
|
|
4420
4466
|
"""Remove a slide (alias for :py:meth:`delete`).
|
|
4421
4467
|
|
|
4422
4468
|
Closes python-pptx#956 — both ``delete`` and ``remove_slide`` are
|
|
4423
4469
|
spellings the upstream issue thread uses. Accepts a slide object
|
|
4424
4470
|
or an integer index.
|
|
4425
4471
|
"""
|
|
4426
|
-
self.delete(slide)
|
|
4472
|
+
self.delete(slide, allow_bulk_delete=allow_bulk_delete)
|
|
4427
4473
|
|
|
4428
4474
|
@athena_only(
|
|
4429
4475
|
description="Delete multiple slides at once by index",
|
|
4430
4476
|
since="0.1.2",
|
|
4431
4477
|
)
|
|
4432
|
-
def delete_slides(self, indices: list[int]) -> None:
|
|
4478
|
+
def delete_slides(self, indices: list[int], *, allow_bulk_delete: bool = False) -> None:
|
|
4433
4479
|
"""
|
|
4434
4480
|
Delete multiple slides from the presentation by their indices.
|
|
4435
4481
|
|
|
@@ -4439,10 +4485,14 @@ class Slides:
|
|
|
4439
4485
|
|
|
4440
4486
|
Args:
|
|
4441
4487
|
indices: List of zero-based slide indices to delete
|
|
4488
|
+
allow_bulk_delete: Allow deleting more than half of the slides an
|
|
4489
|
+
existing deck (4+ slides) had when this run opened it.
|
|
4442
4490
|
|
|
4443
4491
|
Raises:
|
|
4444
4492
|
ValueError: If any index is out of range
|
|
4445
4493
|
ValueError: If duplicate indices are provided
|
|
4494
|
+
ValueError: If the run would delete most of an existing deck's
|
|
4495
|
+
slides without ``allow_bulk_delete=True``
|
|
4446
4496
|
|
|
4447
4497
|
Example:
|
|
4448
4498
|
# Delete slides at indices 1, 3, and 5
|
|
@@ -4462,6 +4512,8 @@ class Slides:
|
|
|
4462
4512
|
if idx < 0 or idx >= len(self._slides):
|
|
4463
4513
|
raise ValueError(f"Slide index {idx} is out of range (0-{len(self._slides) - 1})")
|
|
4464
4514
|
|
|
4515
|
+
self._check_bulk_delete([self._slides[idx] for idx in indices], allow_bulk_delete)
|
|
4516
|
+
|
|
4465
4517
|
# Sort indices in descending order to avoid index shifting
|
|
4466
4518
|
sorted_indices = sorted(indices, reverse=True)
|
|
4467
4519
|
|
|
@@ -4500,7 +4552,7 @@ class Slides:
|
|
|
4500
4552
|
description="Keep only specified slides, removing all others",
|
|
4501
4553
|
since="0.1.2",
|
|
4502
4554
|
)
|
|
4503
|
-
def keep_only(self, indices: list[int]) -> None:
|
|
4555
|
+
def keep_only(self, indices: list[int], *, allow_bulk_delete: bool = False) -> None:
|
|
4504
4556
|
"""
|
|
4505
4557
|
Keep only the slides at the specified indices, deleting all others.
|
|
4506
4558
|
|
|
@@ -4509,11 +4561,15 @@ class Slides:
|
|
|
4509
4561
|
|
|
4510
4562
|
Args:
|
|
4511
4563
|
indices: List of zero-based slide indices to keep
|
|
4564
|
+
allow_bulk_delete: Allow deleting more than half of the slides an
|
|
4565
|
+
existing deck (4+ slides) had when this run opened it.
|
|
4512
4566
|
|
|
4513
4567
|
Raises:
|
|
4514
4568
|
ValueError: If any index is out of range
|
|
4515
4569
|
ValueError: If duplicate indices are provided
|
|
4516
4570
|
ValueError: If indices list is empty
|
|
4571
|
+
ValueError: If the run would delete most of an existing deck's
|
|
4572
|
+
slides without ``allow_bulk_delete=True``
|
|
4517
4573
|
|
|
4518
4574
|
Example:
|
|
4519
4575
|
# Keep only the first and last slides
|
|
@@ -4545,7 +4601,7 @@ class Slides:
|
|
|
4545
4601
|
|
|
4546
4602
|
# Use delete_slides for the actual deletion
|
|
4547
4603
|
if delete_indices:
|
|
4548
|
-
self.delete_slides(delete_indices)
|
|
4604
|
+
self.delete_slides(delete_indices, allow_bulk_delete=allow_bulk_delete)
|
|
4549
4605
|
|
|
4550
4606
|
def _drop_unsent(self, created_ids: set[str]) -> None:
|
|
4551
4607
|
"""Forget slides and shapes whose creating command was dropped unsent.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "athena-python-pptx"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.16.0"
|
|
8
8
|
description = "Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
# Publishing the Python SDK to PyPI
|
|
2
|
-
|
|
3
|
-
This guide covers how to bump, build, and publish the `athena-python-pptx` package to PyPI.
|
|
4
|
-
|
|
5
|
-
## Quick Start
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
# Bump to version 0.1.26 and publish to PyPI
|
|
9
|
-
./scripts/publish-python-sdk.sh 0.1.26
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Prerequisites
|
|
13
|
-
|
|
14
|
-
The script will check for and install these if missing:
|
|
15
|
-
|
|
16
|
-
- Python 3.10+
|
|
17
|
-
- `build` module (`pip install build`)
|
|
18
|
-
- `twine` (`pip install twine`)
|
|
19
|
-
|
|
20
|
-
## PyPI Credentials
|
|
21
|
-
|
|
22
|
-
You'll need PyPI credentials configured. Options:
|
|
23
|
-
|
|
24
|
-
### Option 1: API Token (Recommended)
|
|
25
|
-
|
|
26
|
-
1. Go to https://pypi.org/manage/account/token/
|
|
27
|
-
2. Create a token scoped to the `athena-python-pptx` project
|
|
28
|
-
3. Configure in `~/.pypirc`:
|
|
29
|
-
|
|
30
|
-
```ini
|
|
31
|
-
[pypi]
|
|
32
|
-
username = __token__
|
|
33
|
-
password = pypi-AgEIcHlwaS5vcmc...
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Option 2: Environment Variables
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
export TWINE_USERNAME=__token__
|
|
40
|
-
export TWINE_PASSWORD=pypi-AgEIcHlwaS5vcmc...
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
### Option 3: Interactive
|
|
44
|
-
|
|
45
|
-
The script will prompt for credentials if not configured.
|
|
46
|
-
|
|
47
|
-
## Usage
|
|
48
|
-
|
|
49
|
-
### Basic Usage
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
# Publish a new version
|
|
53
|
-
./scripts/publish-python-sdk.sh 0.1.26
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Options
|
|
57
|
-
|
|
58
|
-
| Flag | Description |
|
|
59
|
-
|------|-------------|
|
|
60
|
-
| `--dry-run` | Preview changes without executing |
|
|
61
|
-
| `--test-pypi` | Upload to TestPyPI instead of PyPI |
|
|
62
|
-
| `--skip-build` | Skip build step (use existing `dist/`) |
|
|
63
|
-
| `-h, --help` | Show help message |
|
|
64
|
-
|
|
65
|
-
### Examples
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
# Preview what would happen
|
|
69
|
-
./scripts/publish-python-sdk.sh --dry-run 0.1.26
|
|
70
|
-
|
|
71
|
-
# Test on TestPyPI first
|
|
72
|
-
./scripts/publish-python-sdk.sh --test-pypi 0.1.26
|
|
73
|
-
|
|
74
|
-
# Then publish to production PyPI
|
|
75
|
-
./scripts/publish-python-sdk.sh 0.1.26
|
|
76
|
-
|
|
77
|
-
# Re-upload without rebuilding
|
|
78
|
-
./scripts/publish-python-sdk.sh --skip-build 0.1.26
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## What the Script Does
|
|
82
|
-
|
|
83
|
-
1. **Checks prerequisites** - Python, pip, build, twine
|
|
84
|
-
2. **Validates version** - Ensures semver format (X.Y.Z)
|
|
85
|
-
3. **Bumps version** in both files:
|
|
86
|
-
- `pyproject.toml` (line 7)
|
|
87
|
-
- `pptx/__init__.py` (line 98)
|
|
88
|
-
4. **Builds package** - Creates wheel and sdist in `dist/`
|
|
89
|
-
5. **Uploads to PyPI** - Uses twine to publish
|
|
90
|
-
|
|
91
|
-
## Version Locations
|
|
92
|
-
|
|
93
|
-
The version must be updated in two places (the script handles this):
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
python-sdk/
|
|
97
|
-
├── pyproject.toml # version = "X.Y.Z"
|
|
98
|
-
└── pptx/
|
|
99
|
-
└── __init__.py # __version__ = "X.Y.Z"
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## Manual Process
|
|
103
|
-
|
|
104
|
-
If you prefer to do it manually:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
cd python-sdk
|
|
108
|
-
|
|
109
|
-
# 1. Update version in pyproject.toml and pptx/__init__.py
|
|
110
|
-
|
|
111
|
-
# 2. Clean and build
|
|
112
|
-
rm -rf dist/ build/ *.egg-info
|
|
113
|
-
python -m build
|
|
114
|
-
|
|
115
|
-
# 3. Upload to TestPyPI (optional)
|
|
116
|
-
twine upload --repository testpypi dist/*
|
|
117
|
-
|
|
118
|
-
# 4. Upload to PyPI
|
|
119
|
-
twine upload dist/*
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## After Publishing
|
|
123
|
-
|
|
124
|
-
1. **Commit the version bump:**
|
|
125
|
-
```bash
|
|
126
|
-
git add python-sdk/pyproject.toml python-sdk/pptx/__init__.py
|
|
127
|
-
git commit -m "Bump python-sdk to 0.1.26"
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
2. **Create a git tag:**
|
|
131
|
-
```bash
|
|
132
|
-
git tag python-sdk-v0.1.26
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
3. **Push:**
|
|
136
|
-
```bash
|
|
137
|
-
git push && git push --tags
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Testing the Published Package
|
|
141
|
-
|
|
142
|
-
### From TestPyPI
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
pip install -i https://test.pypi.org/simple/ athena-python-pptx==0.1.26
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### From PyPI
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
pip install athena-python-pptx==0.1.26
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Verify Installation
|
|
155
|
-
|
|
156
|
-
```python
|
|
157
|
-
import pptx
|
|
158
|
-
print(pptx.__version__) # Should print: 0.1.26
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Troubleshooting
|
|
162
|
-
|
|
163
|
-
### "File already exists" Error
|
|
164
|
-
|
|
165
|
-
PyPI doesn't allow re-uploading the same version. Bump the version number.
|
|
166
|
-
|
|
167
|
-
### Authentication Failed
|
|
168
|
-
|
|
169
|
-
Check your `~/.pypirc` or environment variables. For tokens, username must be `__token__`.
|
|
170
|
-
|
|
171
|
-
### Build Fails
|
|
172
|
-
|
|
173
|
-
Ensure you're in the `python-sdk/` directory and have the build module installed:
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
pip install build
|
|
177
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|