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.
Files changed (93) hide show
  1. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/AGENTS.md +1 -1
  2. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/CHANGELOG.md +31 -0
  3. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/DEV-GUIDE.md +7 -8
  4. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/PKG-INFO +1 -1
  5. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/docs/API_PARITY_EXCEPTIONS.md +29 -20
  6. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/docs/athena-api.md +6 -2
  7. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/__init__.py +2 -2
  8. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_ptc.py +1 -3
  9. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/batching.py +2 -1
  10. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/presentation.py +72 -36
  11. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/__init__.py +2 -2
  12. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/slides.py +62 -6
  13. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pyproject.toml +1 -1
  14. athena_python_pptx-0.15.0/PUBLISHING.md +0 -177
  15. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/.gitignore +0 -0
  16. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/API_PARITY_REPORT.md +0 -0
  17. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/CLAUDE.md +0 -0
  18. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/PARITY_QUESTIONS.md +0 -0
  19. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/README.md +0 -0
  20. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/docs/athena-api.json +0 -0
  21. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_athena_extension.py +0 -0
  22. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_citations.py +0 -0
  23. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_references.py +0 -0
  24. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/_unsupported.py +0 -0
  25. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/action.py +0 -0
  26. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/__init__.py +0 -0
  27. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/axis.py +0 -0
  28. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/category.py +0 -0
  29. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/chart.py +0 -0
  30. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/data.py +0 -0
  31. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/datalabel.py +0 -0
  32. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/legend.py +0 -0
  33. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/marker.py +0 -0
  34. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/plot.py +0 -0
  35. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/point.py +0 -0
  36. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/series.py +0 -0
  37. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/chart/xlsx.py +0 -0
  38. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/client.py +0 -0
  39. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/commands.py +0 -0
  40. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/decorators.py +0 -0
  41. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/__init__.py +0 -0
  42. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/chtfmt.py +0 -0
  43. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/color.py +0 -0
  44. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/effect.py +0 -0
  45. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/fill.py +0 -0
  46. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/dml/line.py +0 -0
  47. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/docgen.py +0 -0
  48. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/__init__.py +0 -0
  49. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/action.py +0 -0
  50. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/chart.py +0 -0
  51. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/dml.py +0 -0
  52. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/lang.py +0 -0
  53. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/shapes.py +0 -0
  54. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/enum/text.py +0 -0
  55. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/errors.py +0 -0
  56. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/exc.py +0 -0
  57. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/generated/__init__.py +0 -0
  58. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/generated/autoshape_types.py +0 -0
  59. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/kit.py +0 -0
  60. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/media.py +0 -0
  61. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/oxml/__init__.py +0 -0
  62. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/oxml/ns.py +0 -0
  63. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/package.py +0 -0
  64. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/__init__.py +0 -0
  65. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/_base.py +0 -0
  66. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/chart.py +0 -0
  67. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/coreprops.py +0 -0
  68. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/embeddedpackage.py +0 -0
  69. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/image.py +0 -0
  70. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/media.py +0 -0
  71. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/presentation.py +0 -0
  72. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/parts/slide.py +0 -0
  73. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/autoshape.py +0 -0
  74. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/base.py +0 -0
  75. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/connector.py +0 -0
  76. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/freeform.py +0 -0
  77. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/graphfrm.py +0 -0
  78. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/group.py +0 -0
  79. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/picture.py +0 -0
  80. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/placeholder.py +0 -0
  81. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shapes/shapetree.py +0 -0
  82. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/shared.py +0 -0
  83. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/slide.py +0 -0
  84. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/spec.py +0 -0
  85. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/table.py +0 -0
  86. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/text/__init__.py +0 -0
  87. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/text/fonts.py +0 -0
  88. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/text/layout.py +0 -0
  89. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/text/text.py +0 -0
  90. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/types.py +0 -0
  91. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/typing.py +0 -0
  92. {athena_python_pptx-0.15.0 → athena_python_pptx-0.16.0}/pptx/units.py +0 -0
  93. {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 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,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
- # 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.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 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.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. 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
@@ -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
@@ -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
- return cls(deck_id=deck_id, base_url=base_url, api_key=api_key, auto_refresh=wait)
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 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
@@ -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.15.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
- ```