athena-python-pptx 0.4.0__tar.gz → 0.4.2__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 (86) hide show
  1. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/CHANGELOG.md +25 -0
  2. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/CLAUDE.md +3 -2
  3. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/DEV-GUIDE.md +3 -3
  4. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/PKG-INFO +16 -13
  5. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/README.md +15 -12
  6. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/docs/API_PARITY_EXCEPTIONS.md +47 -19
  7. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/__init__.py +4 -3
  8. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/client.py +1 -0
  9. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/commands.py +60 -54
  10. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/presentation.py +69 -34
  11. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/__init__.py +9 -4
  12. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/text/__init__.py +176 -27
  13. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/typing.py +4 -0
  14. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pyproject.toml +1 -1
  15. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/uv.lock +20 -20
  16. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/.gitignore +0 -0
  17. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/API_PARITY_REPORT.md +0 -0
  18. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/PARITY_QUESTIONS.md +0 -0
  19. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/PUBLISHING.md +0 -0
  20. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/docs/athena-api.json +0 -0
  21. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/docs/athena-api.md +0 -0
  22. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/_athena_extension.py +0 -0
  23. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/_ptc.py +0 -0
  24. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/_references.py +0 -0
  25. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/action.py +0 -0
  26. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/batching.py +0 -0
  27. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/__init__.py +0 -0
  28. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/axis.py +0 -0
  29. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/category.py +0 -0
  30. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/chart.py +0 -0
  31. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/data.py +0 -0
  32. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/datalabel.py +0 -0
  33. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/legend.py +0 -0
  34. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/marker.py +0 -0
  35. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/plot.py +0 -0
  36. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/point.py +0 -0
  37. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/series.py +0 -0
  38. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/chart/xlsx.py +0 -0
  39. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/decorators.py +0 -0
  40. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/dml/__init__.py +0 -0
  41. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/dml/chtfmt.py +0 -0
  42. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/dml/color.py +0 -0
  43. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/dml/effect.py +0 -0
  44. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/dml/fill.py +0 -0
  45. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/dml/line.py +0 -0
  46. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/docgen.py +0 -0
  47. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/enum/__init__.py +0 -0
  48. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/enum/action.py +0 -0
  49. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/enum/chart.py +0 -0
  50. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/enum/dml.py +0 -0
  51. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/enum/lang.py +0 -0
  52. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/enum/shapes.py +0 -0
  53. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/enum/text.py +0 -0
  54. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/errors.py +0 -0
  55. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/exc.py +0 -0
  56. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/media.py +0 -0
  57. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/package.py +0 -0
  58. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/__init__.py +0 -0
  59. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/_base.py +0 -0
  60. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/chart.py +0 -0
  61. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/coreprops.py +0 -0
  62. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/embeddedpackage.py +0 -0
  63. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/image.py +0 -0
  64. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/media.py +0 -0
  65. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/presentation.py +0 -0
  66. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/parts/slide.py +0 -0
  67. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/autoshape.py +0 -0
  68. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/base.py +0 -0
  69. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/connector.py +0 -0
  70. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/freeform.py +0 -0
  71. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/graphfrm.py +0 -0
  72. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/group.py +0 -0
  73. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/picture.py +0 -0
  74. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/placeholder.py +0 -0
  75. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shapes/shapetree.py +0 -0
  76. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/shared.py +0 -0
  77. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/slide.py +0 -0
  78. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/slides.py +0 -0
  79. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/spec.py +0 -0
  80. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/table.py +0 -0
  81. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/text/fonts.py +0 -0
  82. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/text/layout.py +0 -0
  83. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/text/text.py +0 -0
  84. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/types.py +0 -0
  85. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/units.py +0 -0
  86. {athena_python_pptx-0.4.0 → athena_python_pptx-0.4.2}/pptx/util.py +0 -0
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## 0.4.2
6
+
7
+ **Paragraph-level font fidelity — `paragraph.font` now matches stock
8
+ python-pptx `defRPr` semantics (fidelity-pipeline findings batch).**
9
+
10
+ - **`paragraph.font` is a real paragraph-level font.** Previously it
11
+ returned the first run's font ("approximate by targeting the first
12
+ run"), which (a) produced exports with explicit `<a:rPr>` on every run
13
+ where stock python-pptx writes `<a:pPr><a:defRPr/>` (the fidelity
14
+ pipeline's sdk-projection leg counted 229 structural diffs on the
15
+ 25-slide diagnostic deck) and (b) mis-styled multi-run paragraphs (only
16
+ run 0 changed, where stock applies the default to ALL runs). Writes now
17
+ emit `SetParagraphStyle` with a `defaultRunStyle` payload; the studio
18
+ stores it as the paragraph's authored defaults and exports `defRPr`
19
+ exactly like stock.
20
+ - **`run.font.color.brightness` now round-trips.** Previously stored
21
+ locally and silently dropped; now emitted with the theme color and
22
+ serialized as `lumMod`/`lumOff` modifiers on `<a:schemeClr>` using
23
+ python-pptx's mapping.
24
+ - Studio-side (same batch): theme color references (`schemeColor`) now
25
+ survive on SDK-created textbox runs, and new paragraphs added via
26
+ `add_paragraph()`/multi-line `tf.text` no longer inherit the previous
27
+ paragraph's authored level/spacing/alignment (server-side paragraph
28
+ template bleed).
29
+
5
30
  ## 0.3.1
6
31
 
7
32
  **Chart title runs, slide insertion, and shape-delete local state — Insight
@@ -45,8 +45,9 @@ A small number of REST-SDK-specific departures are documented in
45
45
  [`docs/API_PARITY_EXCEPTIONS.md`](docs/API_PARITY_EXCEPTIONS.md):
46
46
 
47
47
  - **`Presentation(filename)` not supported** — REST SDK uses
48
- `Presentation.upload(path)` or `Presentation(deck_id=…)` instead;
49
- there's no local OPC package to open.
48
+ `Presentation.upload(path)` or `Presentation(asset_id=…)` instead
49
+ (`deck_id=` is the accepted legacy alias); there's no local OPC
50
+ package to open.
50
51
  - **`NotesSlide.shapes` / `.placeholders` / `.notes_placeholder`** not yet exposed —
51
52
  use `slide.notes_slide.notes_text_frame` for notes text.
52
53
  - **`XyChartData.add_series(name, values)`** signature drift vs upstream's
@@ -23,9 +23,9 @@ Create `scratch.py` in the `python-sdk/` directory:
23
23
  ```python
24
24
  from pptx import Presentation
25
25
 
26
- # Upload a test file or use an existing deck ID
26
+ # Upload a test file or use an existing asset id
27
27
  prs = Presentation.upload("path/to/test.pptx", name="test")
28
- print(f"Deck: {prs.deck_id}, Slides: {prs.slide_count}")
28
+ print(f"Presentation: {prs.asset_id}, Slides: {prs.slide_count}")
29
29
 
30
30
  slide = prs.slides[0]
31
31
  for shape in slide.shapes:
@@ -100,5 +100,5 @@ The SDK is a **remote proxy** over the PPTX Studio API:
100
100
  | Method | Example |
101
101
  |--------|---------|
102
102
  | Env vars | `ATHENA_PPTX_BASE_URL=http://localhost:4000` |
103
- | Explicit | `Presentation(deck_id, base_url="http://localhost:4000")` |
103
+ | Explicit | `Presentation(asset_id="asset_...", base_url="http://localhost:4000")` (`deck_id=` is the legacy alias) |
104
104
  | API key (optional) | `ATHENA_PPTX_API_KEY=your-key` |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-pptx
3
- Version: 0.4.0
3
+ Version: 0.4.2
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
@@ -202,7 +202,7 @@ export ATHENA_PPTX_API_KEY=your-api-key # Optional
202
202
  Or pass them explicitly:
203
203
 
204
204
  ```python
205
- prs = Presentation(deck_id="deck_123", base_url="...", api_key="...")
205
+ prs = Presentation(asset_id="asset_3a9328bc-9c1c-4498-be8f-bda3883276f5", base_url="...", api_key="...")
206
206
  ```
207
207
 
208
208
  ## Quick Start
@@ -246,24 +246,27 @@ slide.shapes[0].text_frame.text = "Updated Title"
246
246
  prs.save("modified.pptx")
247
247
  ```
248
248
 
249
- ### Connect to an Existing Deck
249
+ ### Connect to an Existing Presentation
250
250
 
251
251
  ```python
252
252
  from pptx import Presentation
253
253
 
254
- # Connect to a deck by ID
255
- prs = Presentation(deck_id="deck_abc123")
254
+ # Connect to a presentation by its Athena asset id
255
+ prs = Presentation(asset_id="asset_3a9328bc-9c1c-4498-be8f-bda3883276f5")
256
256
 
257
257
  # Work with slides
258
258
  for slide in prs.slides:
259
259
  print(f"Slide {slide.slide_index}: {len(slide.shapes)} shapes")
260
260
  ```
261
261
 
262
- > **Important:** When working with an existing deck, you **MUST** include the `deck_id` parameter:
262
+ `deck_id=` is accepted as a legacy alias for `asset_id=` (it is the same
263
+ identifier string); prefer `asset_id=` in new code.
264
+
265
+ > **Important:** When working with an existing presentation, you **MUST** include the `asset_id` parameter:
263
266
  > ```python
264
- > prs = Presentation(deck_id="d822b6e3-0a73-4214-9e71-8f28a3f7c9d9")
267
+ > prs = Presentation(asset_id="asset_d822b6e3-0a73-4214-9e71-8f28a3f7c9d9")
265
268
  > ```
266
- > Without `deck_id`, the SDK cannot connect to PPTX Studio and will fail.
269
+ > Without `asset_id`, the SDK cannot connect to PPTX Studio and will fail.
267
270
 
268
271
  ---
269
272
 
@@ -271,7 +274,7 @@ for slide in prs.slides:
271
274
 
272
275
  ### Presentation
273
276
 
274
- The main entry point for working with a deck.
277
+ The main entry point for working with a presentation.
275
278
 
276
279
  #### Class Methods
277
280
 
@@ -286,14 +289,14 @@ prs = Presentation.open("path/to/file.pptx") # Alias for upload()
286
289
  # Connect from a full URL
287
290
  prs = Presentation.from_url("https://api.example.com/decks/deck_123")
288
291
 
289
- # Connect to an existing deck by ID
290
- prs = Presentation(deck_id="deck_123")
292
+ # Connect to an existing presentation by asset id
293
+ prs = Presentation(asset_id="asset_3a9328bc-9c1c-4498-be8f-bda3883276f5")
291
294
  ```
292
295
 
293
296
  #### Properties
294
297
 
295
298
  ```python
296
- prs.deck_id # str: Deck identifier
299
+ prs.asset_id # str: Athena asset id (prs.deck_id is the legacy alias)
297
300
  prs.slides # Slides: Collection of slides
298
301
  prs.slide_width # Emu: Width of slides
299
302
  prs.slide_height # Emu: Height of slides
@@ -889,7 +892,7 @@ print(info['authToken'])
889
892
 
890
893
  See [`docs/API_PARITY_EXCEPTIONS.md`](docs/API_PARITY_EXCEPTIONS.md) for the full list. Highlights:
891
894
 
892
- - **`Presentation(filename)` not supported** — use `Presentation.upload(path)` or `Presentation(deck_id=…)` instead.
895
+ - **`Presentation(filename)` not supported** — use `Presentation.upload(path)` or `Presentation(asset_id=…)` instead (`deck_id=` is the legacy alias).
893
896
  - **`NotesSlide.shapes` / `.placeholders` / `.notes_placeholder`** not yet exposed — use `slide.notes_slide.notes_text_frame` for text.
894
897
  - **`XyChartData.add_series(name, values)`** signature drift vs upstream's `add_series(name, number_format=None)`.
895
898
  - **`TextFitter.best_fit_font_size()`** returns `max_size` unchanged (auto-fit is server-side).
@@ -162,7 +162,7 @@ export ATHENA_PPTX_API_KEY=your-api-key # Optional
162
162
  Or pass them explicitly:
163
163
 
164
164
  ```python
165
- prs = Presentation(deck_id="deck_123", base_url="...", api_key="...")
165
+ prs = Presentation(asset_id="asset_3a9328bc-9c1c-4498-be8f-bda3883276f5", base_url="...", api_key="...")
166
166
  ```
167
167
 
168
168
  ## Quick Start
@@ -206,24 +206,27 @@ slide.shapes[0].text_frame.text = "Updated Title"
206
206
  prs.save("modified.pptx")
207
207
  ```
208
208
 
209
- ### Connect to an Existing Deck
209
+ ### Connect to an Existing Presentation
210
210
 
211
211
  ```python
212
212
  from pptx import Presentation
213
213
 
214
- # Connect to a deck by ID
215
- prs = Presentation(deck_id="deck_abc123")
214
+ # Connect to a presentation by its Athena asset id
215
+ prs = Presentation(asset_id="asset_3a9328bc-9c1c-4498-be8f-bda3883276f5")
216
216
 
217
217
  # Work with slides
218
218
  for slide in prs.slides:
219
219
  print(f"Slide {slide.slide_index}: {len(slide.shapes)} shapes")
220
220
  ```
221
221
 
222
- > **Important:** When working with an existing deck, you **MUST** include the `deck_id` parameter:
222
+ `deck_id=` is accepted as a legacy alias for `asset_id=` (it is the same
223
+ identifier string); prefer `asset_id=` in new code.
224
+
225
+ > **Important:** When working with an existing presentation, you **MUST** include the `asset_id` parameter:
223
226
  > ```python
224
- > prs = Presentation(deck_id="d822b6e3-0a73-4214-9e71-8f28a3f7c9d9")
227
+ > prs = Presentation(asset_id="asset_d822b6e3-0a73-4214-9e71-8f28a3f7c9d9")
225
228
  > ```
226
- > Without `deck_id`, the SDK cannot connect to PPTX Studio and will fail.
229
+ > Without `asset_id`, the SDK cannot connect to PPTX Studio and will fail.
227
230
 
228
231
  ---
229
232
 
@@ -231,7 +234,7 @@ for slide in prs.slides:
231
234
 
232
235
  ### Presentation
233
236
 
234
- The main entry point for working with a deck.
237
+ The main entry point for working with a presentation.
235
238
 
236
239
  #### Class Methods
237
240
 
@@ -246,14 +249,14 @@ prs = Presentation.open("path/to/file.pptx") # Alias for upload()
246
249
  # Connect from a full URL
247
250
  prs = Presentation.from_url("https://api.example.com/decks/deck_123")
248
251
 
249
- # Connect to an existing deck by ID
250
- prs = Presentation(deck_id="deck_123")
252
+ # Connect to an existing presentation by asset id
253
+ prs = Presentation(asset_id="asset_3a9328bc-9c1c-4498-be8f-bda3883276f5")
251
254
  ```
252
255
 
253
256
  #### Properties
254
257
 
255
258
  ```python
256
- prs.deck_id # str: Deck identifier
259
+ prs.asset_id # str: Athena asset id (prs.deck_id is the legacy alias)
257
260
  prs.slides # Slides: Collection of slides
258
261
  prs.slide_width # Emu: Width of slides
259
262
  prs.slide_height # Emu: Height of slides
@@ -849,7 +852,7 @@ print(info['authToken'])
849
852
 
850
853
  See [`docs/API_PARITY_EXCEPTIONS.md`](docs/API_PARITY_EXCEPTIONS.md) for the full list. Highlights:
851
854
 
852
- - **`Presentation(filename)` not supported** — use `Presentation.upload(path)` or `Presentation(deck_id=…)` instead.
855
+ - **`Presentation(filename)` not supported** — use `Presentation.upload(path)` or `Presentation(asset_id=…)` instead (`deck_id=` is the legacy alias).
853
856
  - **`NotesSlide.shapes` / `.placeholders` / `.notes_placeholder`** not yet exposed — use `slide.notes_slide.notes_text_frame` for text.
854
857
  - **`XyChartData.add_series(name, values)`** signature drift vs upstream's `add_series(name, number_format=None)`.
855
858
  - **`TextFitter.best_fit_font_size()`** returns `max_size` unchanged (auto-fit is server-side).
@@ -352,19 +352,30 @@ public reorder API (callers manipulate `slides.sldIdLst` XML directly).
352
352
  prs.reorder_slides([2, 0, 1]) # move slide 2 to the front
353
353
  ```
354
354
 
355
- ### Strict positive-dimension validation on every `Add*` command
356
-
357
- `AddTextBox`, `AddShape`, `AddPicture`, `AddTable`, and `SetTransform`
358
- now reject **zero** width/height client-side, not just negative values.
359
- This matches the server's Zod schema (`z.number().int().positive()`) so
360
- a stray `Inches(0)` raises a clear, field-named `ValidationError` locally
361
- instead of becoming a generic `commands.N.hEmu: Too small` 400 after the
362
- batch is on the wire. (For `AddPicture`, omit `width=`/`height=` entirely
363
- to fall back to the image's native dimensions — that path is still
364
- supported; the validation only fires when an explicit zero is passed.)
365
-
366
- `SetColWidth` and `SetRowHeight` still allow zero (`min(0)` server-side)
367
- because zero means "auto-fit" for table dimensions.
355
+ ### Non-negative dimension validation on every `Add*` command
356
+
357
+ `AddTextBox`, `AddShape`, `AddPicture`, `AddTable`, `SetTransform`,
358
+ `AddOleObject`, `AddLinkedOleObject`, `AddLinkedTable`, `AddChart`,
359
+ `AddChart2016`, and `AddMovie` reject **negative** width/height
360
+ client-side (the server's Zod schema is `z.number().int().nonnegative()`).
361
+
362
+ **Zero is allowed**, matching python-pptx and OOXML `a:ext` cx/cy are
363
+ `ST_PositiveCoordinate` whose XSD facet is `minInclusive="0"`, so a
364
+ zero-area shape (e.g. a degenerate divider line/connector) is valid and
365
+ python-pptx accepts it without complaint. Earlier SDK versions rejected
366
+ zero too; that was a parity departure that broke the common
367
+ "thin divider" idiom and aborted whole command batches when an agent
368
+ passed `height=0`, so it was reverted. A negative value still raises a
369
+ clear, field-named `ValidationError` locally (and `400` server-side)
370
+ rather than producing an invalid `<a:ext>`.
371
+
372
+ (For `AddPicture`, omit `width=`/`height=` entirely to fall back to the
373
+ image's native dimensions — that path is unchanged.)
374
+
375
+ `SetColWidth` and `SetRowHeight` also allow zero (`min(0)` server-side)
376
+ because zero means "auto-fit" for table dimensions. `SetPresentationSize`
377
+ still requires strictly positive dimensions (a zero-size slide is
378
+ degenerate, not a valid OOXML construct).
368
379
 
369
380
  ### Server fail-fast on partial batch failure
370
381
 
@@ -735,18 +746,35 @@ Athena-only. Equivalent to calling `prs.set_slide_size(Emu(14630400),
735
746
  Emu(8229600))` immediately after creation. For non-standard sizes call
736
747
  `prs.set_slide_size(width, height)` after `create()`.
737
748
 
738
- ### `Presentation.asset_id` — alias for `deck_id`
749
+ ### `Presentation(asset_id=...)` / `Presentation.asset_id` — preferred spelling; `deck_id` is the legacy alias
739
750
 
740
751
  The deck id and the Athena asset id are the same string
741
- (`asset_<uuid>`); the property exists because Olympus URLs, GraphQL
742
- schemas, and most agent-facing copy refer to "asset id". Both names
743
- return `self._deck_id`:
752
+ (`asset_<uuid>`); `asset_id` is the preferred name because Olympus
753
+ URLs, GraphQL schemas, and most agent-facing copy refer to "asset id".
754
+ Upstream `python-pptx` opens a local OPC package via
755
+ `Presentation(pptx_path)` — the REST SDK has no local package, so the
756
+ constructor reattaches to a server-side asset by id instead. Both the
757
+ constructor keyword and the read property exist in both spellings:
744
758
 
745
759
  ```python
746
- prs.deck_id # "asset_3a93..."
747
- prs.asset_id # same value
760
+ prs = Presentation(asset_id="asset_3a93...") # preferred
761
+ prs = Presentation(deck_id="asset_3a93...") # legacy alias — same behaviour
762
+
763
+ prs.asset_id # "asset_3a93..." — preferred
764
+ prs.deck_id # same value — legacy alias
748
765
  ```
749
766
 
767
+ Rules (mirrors the `name`/`title` reconciliation on
768
+ `Presentation.create()`):
769
+
770
+ - `asset_id` and `deck_id` are interchangeable; internally both bind the
771
+ same identifier (`self._deck_id`).
772
+ - Passing **both** is allowed only when the values match; differing
773
+ values raise `ValueError`.
774
+ - Passing **neither** raises `TypeError` (previously `deck_id` was the
775
+ required first positional parameter; positional
776
+ `Presentation("asset_...")` still works unchanged).
777
+
750
778
  ### `Presentation.close()` — alias for `flush()`
751
779
 
752
780
  Stock python-pptx has no flush/close concept (it writes packages
@@ -19,8 +19,9 @@ for real-time collaboration. Use exactly the same code you would with python-ppt
19
19
  # Or upload an existing file
20
20
  prs = Presentation.upload("my_presentation.pptx")
21
21
 
22
- # Or connect to an existing deck
23
- prs = Presentation(deck_id="deck_123")
22
+ # Or connect to an existing presentation asset
23
+ # (deck_id= is accepted as a legacy alias for asset_id=)
24
+ prs = Presentation(asset_id="asset_3a9328bc-9c1c-4498-be8f-bda3883276f5")
24
25
 
25
26
  # Work with slides and shapes (same API as python-pptx)
26
27
  slide = prs.slides[0]
@@ -132,7 +133,7 @@ def flush_all() -> None:
132
133
  _active_buffers[:] = alive
133
134
 
134
135
 
135
- __version__ = "0.4.0"
136
+ __version__ = "0.4.2"
136
137
 
137
138
  __all__ = [
138
139
  # Main entry point
@@ -666,6 +666,7 @@ class Client:
666
666
  flipV=transform_data.get("flipV"),
667
667
  ),
668
668
  preview_text=elem_data.get("previewText"),
669
+ rich_content=elem_data.get("richContent"),
669
670
  placeholder=placeholder,
670
671
  properties=elem_data.get("properties"),
671
672
  source=elem_data.get("source"),
@@ -87,14 +87,14 @@ class AddTextBox(Command):
87
87
  def validate(self) -> None:
88
88
  if self.slide_index < 0:
89
89
  raise ValidationError("slide_index must be non-negative", "slide_index")
90
- # Server Zod schema is z.number().int().positive() → rejects 0 and
91
- # negatives. Reject locally too so a stray Inches(0) raises with a
92
- # clear field name + value instead of becoming a generic
93
- # "commands.N.hEmu: Too small" 400 after the round-trip.
94
- if self.w_emu <= 0:
95
- raise ValidationError(f"width must be > 0 EMU (got {self.w_emu})", "w_emu")
96
- if self.h_emu <= 0:
97
- raise ValidationError(f"height must be > 0 EMU (got {self.h_emu})", "h_emu")
90
+ # OOXML `a:ext` cx/cy are ST_PositiveCoordinate (minInclusive=0), so
91
+ # zero-area shapes are valid (e.g. a degenerate divider/connector) and
92
+ # python-pptx accepts them. Only negatives are rejected, matching the
93
+ # server Zod schema (z.number().int().nonnegative()).
94
+ if self.w_emu < 0:
95
+ raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
96
+ if self.h_emu < 0:
97
+ raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
98
98
 
99
99
 
100
100
  @dataclass
@@ -155,14 +155,15 @@ class SetTransform(Command):
155
155
  def validate(self) -> None:
156
156
  if not self.shape_id:
157
157
  raise ValidationError("shape_id is required", "shape_id")
158
- # Server Zod schema: wEmu / hEmu are z.number().int().positive() —
159
- # rejects 0 and negatives. Mirror locally so a stray Inches(0) raises
160
- # synchronously with the offending field rather than failing
161
- # validation server-side after the round-trip.
162
- if self.w_emu is not None and self.w_emu <= 0:
163
- raise ValidationError(f"width must be > 0 EMU (got {self.w_emu})", "w_emu")
164
- if self.h_emu is not None and self.h_emu <= 0:
165
- raise ValidationError(f"height must be > 0 EMU (got {self.h_emu})", "h_emu")
158
+ # Server Zod schema: wEmu / hEmu are z.number().int().nonnegative() —
159
+ # zero is valid (OOXML a:ext cx/cy are ST_PositiveCoordinate,
160
+ # minInclusive=0; python-pptx accepts zero-area shapes). Only negatives
161
+ # are rejected, mirrored locally so they surface synchronously with the
162
+ # offending field rather than failing server-side after the round-trip.
163
+ if self.w_emu is not None and self.w_emu < 0:
164
+ raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
165
+ if self.h_emu is not None and self.h_emu < 0:
166
+ raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
166
167
 
167
168
 
168
169
  @dataclass
@@ -293,6 +294,10 @@ class SetParagraphStyle(Command):
293
294
  space_after_emu: Optional[int] = None
294
295
  margin_left_emu: Optional[int] = None
295
296
  indent_emu: Optional[int] = None
297
+ # Authored paragraph-level run defaults (python-pptx ``paragraph.font``).
298
+ # Keys are already wire-format camelCase (the Font style payload);
299
+ # the studio exports them as ``<a:pPr><a:defRPr …/></a:pPr>``.
300
+ default_run_style: Optional[dict] = None
296
301
 
297
302
  @property
298
303
  def command_type(self) -> str:
@@ -352,10 +357,10 @@ class AddShape(Command):
352
357
  raise ValidationError("slide_index must be non-negative", "slide_index")
353
358
  if not self.shape_type:
354
359
  raise ValidationError("shape_type is required", "shape_type")
355
- if self.w_emu <= 0:
356
- raise ValidationError(f"width must be > 0 EMU (got {self.w_emu})", "w_emu")
357
- if self.h_emu <= 0:
358
- raise ValidationError(f"height must be > 0 EMU (got {self.h_emu})", "h_emu")
360
+ if self.w_emu < 0:
361
+ raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
362
+ if self.h_emu < 0:
363
+ raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
359
364
 
360
365
 
361
366
  @dataclass
@@ -492,12 +497,13 @@ class AddPicture(Command):
492
497
  raise ValidationError(
493
498
  "image_format must be 'png', 'jpeg', 'gif', 'bmp', or 'tiff'", "image_format"
494
499
  )
495
- # Server schema treats wEmu / hEmu as positive() when present (omit
496
- # them entirely to fall back to the image's native size).
497
- if self.w_emu is not None and self.w_emu <= 0:
498
- raise ValidationError(f"width must be > 0 EMU (got {self.w_emu})", "w_emu")
499
- if self.h_emu is not None and self.h_emu <= 0:
500
- raise ValidationError(f"height must be > 0 EMU (got {self.h_emu})", "h_emu")
500
+ # Server schema treats wEmu / hEmu as nonnegative() when present (zero
501
+ # is allowed; omit them entirely to fall back to the image's native
502
+ # size). Only negatives are rejected.
503
+ if self.w_emu is not None and self.w_emu < 0:
504
+ raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
505
+ if self.h_emu is not None and self.h_emu < 0:
506
+ raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
501
507
 
502
508
 
503
509
  @dataclass
@@ -547,10 +553,10 @@ class AddOleObject(Command):
547
553
  def validate(self) -> None:
548
554
  if self.slide_index < 0:
549
555
  raise ValidationError("slide_index must be non-negative", "slide_index")
550
- if self.w_emu <= 0:
551
- raise ValidationError(f"width must be > 0 EMU (got {self.w_emu})", "w_emu")
552
- if self.h_emu <= 0:
553
- raise ValidationError(f"height must be > 0 EMU (got {self.h_emu})", "h_emu")
556
+ if self.w_emu < 0:
557
+ raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
558
+ if self.h_emu < 0:
559
+ raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
554
560
  if not self.preview_base64:
555
561
  raise ValidationError("preview_base64 is required", "preview_base64")
556
562
  if self.preview_format not in ("png", "jpeg"):
@@ -635,10 +641,10 @@ class AddLinkedOleObject(Command):
635
641
  def validate(self) -> None:
636
642
  if self.slide_index < 0:
637
643
  raise ValidationError("slide_index must be non-negative", "slide_index")
638
- if self.w_emu <= 0:
639
- raise ValidationError(f"width must be > 0 EMU (got {self.w_emu})", "w_emu")
640
- if self.h_emu <= 0:
641
- raise ValidationError(f"height must be > 0 EMU (got {self.h_emu})", "h_emu")
644
+ if self.w_emu < 0:
645
+ raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
646
+ if self.h_emu < 0:
647
+ raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
642
648
  if not isinstance(self.source_ref, dict) or not self.source_ref.get("id"):
643
649
  raise ValidationError(
644
650
  "source_ref must be an AssetReference dict with an 'id' field",
@@ -730,10 +736,10 @@ class AddLinkedTable(Command):
730
736
  def validate(self) -> None:
731
737
  if self.slide_index < 0:
732
738
  raise ValidationError("slide_index must be non-negative", "slide_index")
733
- if self.w_emu <= 0:
734
- raise ValidationError(f"width must be > 0 EMU (got {self.w_emu})", "w_emu")
735
- if self.h_emu <= 0:
736
- raise ValidationError(f"height must be > 0 EMU (got {self.h_emu})", "h_emu")
739
+ if self.w_emu < 0:
740
+ raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
741
+ if self.h_emu < 0:
742
+ raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
737
743
  if not isinstance(self.source_ref, dict) or not self.source_ref.get("id"):
738
744
  raise ValidationError(
739
745
  "source_ref must be an AssetReference dict with an 'id' field",
@@ -803,10 +809,10 @@ class AddTable(Command):
803
809
  raise ValidationError("rows must be between 1 and 100", "rows")
804
810
  if self.cols < 1 or self.cols > 26:
805
811
  raise ValidationError("cols must be between 1 and 26", "cols")
806
- if self.w_emu <= 0:
807
- raise ValidationError(f"width must be > 0 EMU (got {self.w_emu})", "w_emu")
808
- if self.h_emu <= 0:
809
- raise ValidationError(f"height must be > 0 EMU (got {self.h_emu})", "h_emu")
812
+ if self.w_emu < 0:
813
+ raise ValidationError(f"width must be >= 0 EMU (got {self.w_emu})", "w_emu")
814
+ if self.h_emu < 0:
815
+ raise ValidationError(f"height must be >= 0 EMU (got {self.h_emu})", "h_emu")
810
816
 
811
817
 
812
818
  @dataclass
@@ -2260,10 +2266,10 @@ class AddChart(Command):
2260
2266
  f"chart_type must be one of {sorted(valid_types)}",
2261
2267
  "chart_type",
2262
2268
  )
2263
- if self.w_emu <= 0:
2264
- raise ValidationError("w_emu must be positive", "w_emu")
2265
- if self.h_emu <= 0:
2266
- raise ValidationError("h_emu must be positive", "h_emu")
2269
+ if self.w_emu < 0:
2270
+ raise ValidationError("w_emu must be >= 0", "w_emu")
2271
+ if self.h_emu < 0:
2272
+ raise ValidationError("h_emu must be >= 0", "h_emu")
2267
2273
  if not self.series:
2268
2274
  raise ValidationError("at least one series is required", "series")
2269
2275
  if self.grouping is not None and self.grouping not in (
@@ -2576,10 +2582,10 @@ class AddChart2016(Command):
2576
2582
  f"chart_type must be one of {sorted(valid)} (got {self.chart_type!r})",
2577
2583
  "chart_type",
2578
2584
  )
2579
- if self.w_emu <= 0:
2580
- raise ValidationError("w_emu must be positive", "w_emu")
2581
- if self.h_emu <= 0:
2582
- raise ValidationError("h_emu must be positive", "h_emu")
2585
+ if self.w_emu < 0:
2586
+ raise ValidationError("w_emu must be >= 0", "w_emu")
2587
+ if self.h_emu < 0:
2588
+ raise ValidationError("h_emu must be >= 0", "h_emu")
2583
2589
  if not self.series:
2584
2590
  raise ValidationError("at least one series is required", "series")
2585
2591
 
@@ -2952,10 +2958,10 @@ class AddMovie(Command):
2952
2958
  def validate(self) -> None:
2953
2959
  if self.slide_index < 0:
2954
2960
  raise ValidationError("slide_index must be >= 0", "slide_index")
2955
- if self.w_emu <= 0:
2956
- raise ValidationError("w_emu must be positive", "w_emu")
2957
- if self.h_emu <= 0:
2958
- raise ValidationError("h_emu must be positive", "h_emu")
2961
+ if self.w_emu < 0:
2962
+ raise ValidationError("w_emu must be >= 0", "w_emu")
2963
+ if self.h_emu < 0:
2964
+ raise ValidationError("h_emu must be >= 0", "h_emu")
2959
2965
  # Reject empty strings — they look "set" but ship no payload.
2960
2966
  has_embedded = bool(self.movie_base64)
2961
2967
  has_url = bool(self.url)