athena-python-pptx 0.15.0__tar.gz → 0.16.1__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.
Files changed (93) hide show
  1. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/AGENTS.md +1 -1
  2. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/CHANGELOG.md +39 -0
  3. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/DEV-GUIDE.md +7 -8
  4. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/PKG-INFO +1 -1
  5. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/docs/API_PARITY_EXCEPTIONS.md +29 -20
  6. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/docs/athena-api.md +6 -2
  7. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/__init__.py +2 -2
  8. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/_ptc.py +1 -3
  9. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/batching.py +2 -1
  10. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/client.py +13 -11
  11. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/presentation.py +113 -49
  12. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/__init__.py +2 -2
  13. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/slides.py +62 -6
  14. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pyproject.toml +1 -1
  15. athena_python_pptx-0.15.0/PUBLISHING.md +0 -177
  16. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/.gitignore +0 -0
  17. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/API_PARITY_REPORT.md +0 -0
  18. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/CLAUDE.md +0 -0
  19. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/PARITY_QUESTIONS.md +0 -0
  20. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/README.md +0 -0
  21. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/docs/athena-api.json +0 -0
  22. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/_athena_extension.py +0 -0
  23. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/_citations.py +0 -0
  24. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/_references.py +0 -0
  25. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/_unsupported.py +0 -0
  26. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/action.py +0 -0
  27. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/__init__.py +0 -0
  28. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/axis.py +0 -0
  29. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/category.py +0 -0
  30. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/chart.py +0 -0
  31. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/data.py +0 -0
  32. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/datalabel.py +0 -0
  33. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/legend.py +0 -0
  34. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/marker.py +0 -0
  35. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/plot.py +0 -0
  36. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/point.py +0 -0
  37. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/series.py +0 -0
  38. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/chart/xlsx.py +0 -0
  39. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/commands.py +0 -0
  40. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/decorators.py +0 -0
  41. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/dml/__init__.py +0 -0
  42. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/dml/chtfmt.py +0 -0
  43. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/dml/color.py +0 -0
  44. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/dml/effect.py +0 -0
  45. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/dml/fill.py +0 -0
  46. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/dml/line.py +0 -0
  47. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/docgen.py +0 -0
  48. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/enum/__init__.py +0 -0
  49. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/enum/action.py +0 -0
  50. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/enum/chart.py +0 -0
  51. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/enum/dml.py +0 -0
  52. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/enum/lang.py +0 -0
  53. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/enum/shapes.py +0 -0
  54. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/enum/text.py +0 -0
  55. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/errors.py +0 -0
  56. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/exc.py +0 -0
  57. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/generated/__init__.py +0 -0
  58. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/generated/autoshape_types.py +0 -0
  59. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/kit.py +0 -0
  60. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/media.py +0 -0
  61. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/oxml/__init__.py +0 -0
  62. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/oxml/ns.py +0 -0
  63. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/package.py +0 -0
  64. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/__init__.py +0 -0
  65. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/_base.py +0 -0
  66. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/chart.py +0 -0
  67. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/coreprops.py +0 -0
  68. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/embeddedpackage.py +0 -0
  69. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/image.py +0 -0
  70. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/media.py +0 -0
  71. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/presentation.py +0 -0
  72. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/parts/slide.py +0 -0
  73. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/autoshape.py +0 -0
  74. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/base.py +0 -0
  75. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/connector.py +0 -0
  76. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/freeform.py +0 -0
  77. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/graphfrm.py +0 -0
  78. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/group.py +0 -0
  79. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/picture.py +0 -0
  80. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/placeholder.py +0 -0
  81. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shapes/shapetree.py +0 -0
  82. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/shared.py +0 -0
  83. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/slide.py +0 -0
  84. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/spec.py +0 -0
  85. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/table.py +0 -0
  86. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/text/__init__.py +0 -0
  87. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/text/fonts.py +0 -0
  88. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/text/layout.py +0 -0
  89. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/text/text.py +0 -0
  90. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/types.py +0 -0
  91. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/typing.py +0 -0
  92. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/pptx/units.py +0 -0
  93. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.1}/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 a Daytona sandbox with `ATHENA_PTC_URL` set. Each method call
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,45 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## 0.16.1 — `Presentation.upload()` and `create(pptx=...)` report the deck they create
6
+
7
+ - `Presentation.upload()` emits the `CreatePresentation` tool-call event with
8
+ the new deck's id, as `Presentation.create()` and the docx and xlsx uploads
9
+ do. It emitted nothing, so a deck uploaded from sandbox code, including one
10
+ seeded from a brand template with `Presentation.create(pptx=...)`, showed no
11
+ creation card and never joined the Athena session's created assets.
12
+
13
+ ## 0.16.0 — `Presentation.create()` makes 13.333" × 7.5" decks; `create(name=, title=)` titles the deck
14
+
15
+ - **Behaviour change:** `Presentation.create()` makes a deck at PowerPoint's
16
+ Widescreen size, 16:9 at 13.333" × 7.5" (12192000 × 6858000 EMU) — the
17
+ size of the blank template, whose master and layouts are laid out for it.
18
+ It used to resize every blank deck to 16" × 9" (14630400 × 8229600 EMU),
19
+ which changed only the size field: layout placeholders sat off-centre, and
20
+ a deck laid out for 13.333" × 7.5" filled only the top-left 83% of each
21
+ slide's width and height, leaving a white band on the right and bottom. `widescreen=True` (the
22
+ default) now sends nothing unless the template has another size.
23
+ `widescreen=False` keeps the template's size, which is the same: it never
24
+ made a 4:3 deck, whatever the docstring said. Decks created earlier keep
25
+ their stored 16" × 9" size.
26
+ - `Presentation.create(name=..., title=...)` with different values names the
27
+ asset `name` and sets the deck's core title (`prs.core_properties.title`)
28
+ to `title`, also with `pptx=`. It used to raise `ValueError`. `title=`
29
+ alone, or the same value for both, still just names the deck.
30
+
31
+ ## 0.16.0 — deleting most of an existing deck needs allow_bulk_delete=True
32
+
33
+ - **Behaviour change:** a run that opens an existing deck of 4 or more slides
34
+ and deletes more than half of them — `slides.keep_only([0])`, a
35
+ `delete_slides()` of most indices, or `slides.delete()` calls adding up —
36
+ raises `ValueError` before anything is sent, unless the call passes
37
+ `allow_bulk_delete=True` (a new keyword on `delete()`, `delete_slides()`
38
+ and `keep_only()`). An agent asked to add one slide had re-run its whole
39
+ build (`keep_only([0])` then rebuild), and stopping it midway left a
40
+ 21-slide deck half-built. Decks the run created with
41
+ `Presentation.create()` or `Presentation.upload()` are not guarded, so
42
+ trimming a fresh template still works.
43
+
5
44
  ## 0.15.0 — uploaded click actions read back; pictures carry python-pptx's descr; layout graphics refuse delete()
6
45
 
7
46
  - `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
- # Install in editable mode (changes take effect immediately, no reinstall)
7
- cd python-sdk
8
- pip install -e ".[dev]"
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
- pnpm dev
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
- pip install ipython
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.15.0
3
+ Version: 0.16.1
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 a Daytona pptx sandbox:
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=...)` — alias for `name=`
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. The `title=` keyword is a
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". Passing both at once raises
1797
- `ValueError` unless they agree:
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") # OK, values match
1803
- Presentation.create(name="Q4", title="Q3") # ValueError
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()` that creates the deck at 16:9
1809
- widescreen dimensions (14630400 × 8229600 EMU = 16" × 9") instead of
1810
- the template's default size (typically 4:3 at 12192000 × 6858000 EMU).
1811
- Athena-only. Equivalent to calling `prs.set_slide_size(Emu(14630400),
1812
- Emu(8229600))` immediately after creation. For non-standard sizes call
1813
- `prs.set_slide_size(width, height)` after `create()`.
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 a Daytona
1982
- presentation_exec sandbox), the call raises `ValueError` rather than
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 now the default**
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 widescreen decks at 14630400 × 8229600 EMU
2211
- (PowerPoint's "Widescreen (16:9)" preset). Pass `widescreen=False`
2212
- to fall back to the legacy 4:3 default.
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 (Daytona sandbox event emission). |
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 the Daytona/Talos prelude after user code runs. By default a
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.15.0"
330
+ __version__ = "0.16.1"
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. Daytona) where atexit never fires.
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
@@ -138,6 +138,17 @@ def _poll_delays(poll_interval: float, max_attempts: int) -> Iterator[float]:
138
138
  delay = min(delay * 1.5, poll_interval)
139
139
 
140
140
 
141
+ def _upload_deck_name(*, file_path: str, name: Optional[str]) -> str:
142
+ """The name an upload gives its deck: ``name``, else the file's name
143
+ without a ``.pptx`` / ``.potx`` extension."""
144
+ if name is not None:
145
+ return name
146
+ base = os.path.basename(file_path)
147
+ if base.endswith(".pptx") or base.endswith(".potx"):
148
+ return base[:-5]
149
+ return base
150
+
151
+
141
152
  def _is_read_timeout(exc: BaseException) -> bool:
142
153
  """A response that did not arrive in time. With retries configured,
143
154
  requests reports a read timeout as a ConnectionError wrapping urllib3's
@@ -926,12 +937,7 @@ class Client:
926
937
  if not os.path.exists(file_path):
927
938
  raise FileNotFoundError(f"File not found: {file_path}")
928
939
 
929
- # Use filename as name if not provided
930
- if name is None:
931
- name = os.path.basename(file_path)
932
- # Remove extension
933
- if name.endswith(".pptx") or name.endswith(".potx"):
934
- name = name[:-5]
940
+ name = _upload_deck_name(file_path=file_path, name=name)
935
941
 
936
942
  # Step 1: Create deck to get presigned URL
937
943
  deck_info = self.create_deck(name)
@@ -1007,11 +1013,7 @@ class Client:
1007
1013
  if not os.path.exists(file_path):
1008
1014
  raise FileNotFoundError(f"File not found: {file_path}")
1009
1015
 
1010
- # Use filename as name if not provided
1011
- if name is None:
1012
- name = os.path.basename(file_path)
1013
- if name.endswith(".pptx") or name.endswith(".potx"):
1014
- name = name[:-5]
1016
+ name = _upload_deck_name(file_path=file_path, name=name)
1015
1017
 
1016
1018
  # Step 1: Create deck to get presigned URL
1017
1019
  deck_info = self.create_deck(name)
@@ -17,7 +17,7 @@ import warnings
17
17
 
18
18
  from ._athena_extension import athena_extension
19
19
  from .batching import CommandBuffer, batch_context
20
- from .client import Client, _hand_session_to_next_client
20
+ from .client import Client, _hand_session_to_next_client, _upload_deck_name
21
21
  from .commands import SetCoreProperties, SetPresentationSize, CopySlideFromDeck
22
22
  from .errors import AuthenticationError, RemoteError, UnsupportedFeatureError
23
23
  from .slides import SlideLayouts, SlideMasters, Slides
@@ -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 16:9 widescreen presentation
687
- that's immediately ready to use; you can then add slides and
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 alias for ``name`` — accepted for ergonomic parity
710
- with callers that think of the deck label as a "title".
711
- Passing both ``name`` and ``title`` is allowed only when
712
- they match; differing values raise ``ValueError``.
713
- widescreen: Create the deck at 16:9 widescreen dimensions
714
- (14630400 × 8229600 EMU = 16" × 9"). Defaults to
715
- ``True`` since 0.1.81 — closes python-pptx#1066,
716
- a 9-comment thread asking for a 16:9 default
717
- because PowerPoint itself ships every new deck as
718
- 16:9 and 4:3 is now a legacy aspect ratio.
719
- Pass ``widescreen=False`` to fall back to the
720
- template's default (4:3, 12192000 × 6858000 EMU).
721
- Ignored when ``pptx=`` is provided (the uploaded
722
- file defines its own slide size). Not in upstream
723
- python-pptx, which has no factory method here at
724
- all.
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
- # 16:9 widescreen deck
745
- prs = Presentation.create(name="Quarterly review", widescreen=True)
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
- # Reconcile ``name`` and ``title``: ``title`` is an ergonomic alias
748
- # for ``name``. If both are passed they must agree; otherwise the
749
- # caller almost certainly has a bug (two different intents merged).
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
- raise ValueError(
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
- return cls.upload(
761
- pptx, base_url=base_url, api_key=api_key, name=effective_name,
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
- # 16:9 widescreen — matches PowerPoint's "Widescreen (16:9)" preset.
799
- prs.set_slide_size(Emu(14630400), Emu(8229600))
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
@@ -863,25 +894,55 @@ class Presentation:
863
894
  # Save changes
864
895
  prs.save("modified.pptx")
865
896
  """
897
+ from pptx import _ptc
898
+
866
899
  client = Client(base_url=base_url, api_key=api_key, prefer_user_key=True)
867
900
 
868
901
  # The presentation's client takes over this connection (closed here
869
902
  # when the upload or the open fails first).
870
903
  with _hand_session_to_next_client(client):
871
- if wait:
872
- kwargs: dict[str, Any] = {}
873
- if max_wait_seconds is not None:
874
- # Match the client's poll_interval default (0.5s) so callers
875
- # can think in seconds without remembering attempt math.
876
- from .client import DEFAULT_POLL_INTERVAL
877
-
878
- kwargs["max_attempts"] = max(1, int(max_wait_seconds / DEFAULT_POLL_INTERVAL))
879
- deck_id = client.upload_file(str(path), name=name, **kwargs)
880
- else:
881
- # Upload without waiting
882
- deck_id = client.upload_file_async(str(path), name=name)
904
+ # PTC: the upload creates a deck asset, so it wears the same card
905
+ # as ``create()`` and the docx/xlsx uploads. Agora adds a deck to
906
+ # the session's created assets only from this event. Scoped to the
907
+ # upload as in ``create()``: the open below emits its own
908
+ # ``OpenPresentation``.
909
+ call_id = _ptc.emit_begin(
910
+ "CreatePresentation",
911
+ {"name": _upload_deck_name(file_path=str(path), name=name)},
912
+ )
913
+ try:
914
+ if wait:
915
+ kwargs: dict[str, Any] = {}
916
+ if max_wait_seconds is not None:
917
+ # Match the client's poll_interval default (0.5s) so callers
918
+ # can think in seconds without remembering attempt math.
919
+ from .client import DEFAULT_POLL_INTERVAL
920
+
921
+ kwargs["max_attempts"] = max(
922
+ 1, int(max_wait_seconds / DEFAULT_POLL_INTERVAL)
923
+ )
924
+ deck_id = client.upload_file(str(path), name=name, **kwargs)
925
+ else:
926
+ # Upload without waiting
927
+ deck_id = client.upload_file_async(str(path), name=name)
928
+ except Exception as exc:
929
+ _ptc.emit_end(
930
+ call_id=call_id,
931
+ tool_name="CreatePresentation",
932
+ result={"ok": False, "error": str(exc)},
933
+ is_error=True,
934
+ )
935
+ raise
936
+ _ptc.emit_end(
937
+ call_id=call_id,
938
+ tool_name="CreatePresentation",
939
+ result={"ok": True, "assetId": deck_id},
940
+ is_error=False,
941
+ )
883
942
 
884
- return cls(deck_id=deck_id, base_url=base_url, api_key=api_key, auto_refresh=wait)
943
+ prs = cls(deck_id=deck_id, base_url=base_url, api_key=api_key, auto_refresh=wait)
944
+ _release_opened_slides(prs)
945
+ return prs
885
946
 
886
947
  @property
887
948
  @athena_extension(
@@ -1932,6 +1993,9 @@ class Presentation:
1932
1993
  buffer=self._buffer,
1933
1994
  snapshot=snapshot,
1934
1995
  )
1996
+ if self.__dict__.get("_protect_slides_pending"):
1997
+ self._protect_slides_pending = False
1998
+ self._slides._protect_opened_slides()
1935
1999
 
1936
2000
  # A rejected shape graph leaves the previously published snapshot and
1937
2001
  # 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 Daytona sandbox
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 presentation_exec Daytona snapshot, so this
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