athena-python-pptx 0.4.1__tar.gz → 0.4.3__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.1 → athena_python_pptx-0.4.3}/CHANGELOG.md +25 -0
  2. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/CLAUDE.md +3 -2
  3. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/DEV-GUIDE.md +3 -3
  4. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/PKG-INFO +16 -13
  5. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/README.md +15 -12
  6. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/docs/API_PARITY_EXCEPTIONS.md +23 -6
  7. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/__init__.py +4 -3
  8. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/commands.py +4 -0
  9. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/dml/color.py +44 -0
  10. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/presentation.py +69 -34
  11. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/__init__.py +32 -2
  12. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/text/__init__.py +87 -21
  13. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pyproject.toml +1 -1
  14. athena_python_pptx-0.4.3/uv.lock +1163 -0
  15. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/.gitignore +0 -0
  16. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/API_PARITY_REPORT.md +0 -0
  17. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/PARITY_QUESTIONS.md +0 -0
  18. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/PUBLISHING.md +0 -0
  19. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/docs/athena-api.json +0 -0
  20. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/docs/athena-api.md +0 -0
  21. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/_athena_extension.py +0 -0
  22. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/_ptc.py +0 -0
  23. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/_references.py +0 -0
  24. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/action.py +0 -0
  25. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/batching.py +0 -0
  26. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/__init__.py +0 -0
  27. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/axis.py +0 -0
  28. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/category.py +0 -0
  29. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/chart.py +0 -0
  30. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/data.py +0 -0
  31. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/datalabel.py +0 -0
  32. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/legend.py +0 -0
  33. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/marker.py +0 -0
  34. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/plot.py +0 -0
  35. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/point.py +0 -0
  36. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/series.py +0 -0
  37. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/chart/xlsx.py +0 -0
  38. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/client.py +0 -0
  39. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/decorators.py +0 -0
  40. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/dml/__init__.py +0 -0
  41. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/dml/chtfmt.py +0 -0
  42. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/dml/effect.py +0 -0
  43. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/dml/fill.py +0 -0
  44. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/dml/line.py +0 -0
  45. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/docgen.py +0 -0
  46. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/enum/__init__.py +0 -0
  47. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/enum/action.py +0 -0
  48. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/enum/chart.py +0 -0
  49. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/enum/dml.py +0 -0
  50. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/enum/lang.py +0 -0
  51. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/enum/shapes.py +0 -0
  52. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/enum/text.py +0 -0
  53. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/errors.py +0 -0
  54. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/exc.py +0 -0
  55. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/media.py +0 -0
  56. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/package.py +0 -0
  57. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/__init__.py +0 -0
  58. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/_base.py +0 -0
  59. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/chart.py +0 -0
  60. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/coreprops.py +0 -0
  61. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/embeddedpackage.py +0 -0
  62. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/image.py +0 -0
  63. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/media.py +0 -0
  64. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/presentation.py +0 -0
  65. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/parts/slide.py +0 -0
  66. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/autoshape.py +0 -0
  67. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/base.py +0 -0
  68. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/connector.py +0 -0
  69. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/freeform.py +0 -0
  70. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/graphfrm.py +0 -0
  71. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/group.py +0 -0
  72. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/picture.py +0 -0
  73. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/placeholder.py +0 -0
  74. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shapes/shapetree.py +0 -0
  75. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/shared.py +0 -0
  76. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/slide.py +0 -0
  77. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/slides.py +0 -0
  78. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/spec.py +0 -0
  79. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/table.py +0 -0
  80. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/text/fonts.py +0 -0
  81. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/text/layout.py +0 -0
  82. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/text/text.py +0 -0
  83. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/types.py +0 -0
  84. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/typing.py +0 -0
  85. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/pptx/units.py +0 -0
  86. {athena_python_pptx-0.4.1 → athena_python_pptx-0.4.3}/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.1
3
+ Version: 0.4.3
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).
@@ -746,18 +746,35 @@ Athena-only. Equivalent to calling `prs.set_slide_size(Emu(14630400),
746
746
  Emu(8229600))` immediately after creation. For non-standard sizes call
747
747
  `prs.set_slide_size(width, height)` after `create()`.
748
748
 
749
- ### `Presentation.asset_id` — alias for `deck_id`
749
+ ### `Presentation(asset_id=...)` / `Presentation.asset_id` — preferred spelling; `deck_id` is the legacy alias
750
750
 
751
751
  The deck id and the Athena asset id are the same string
752
- (`asset_<uuid>`); the property exists because Olympus URLs, GraphQL
753
- schemas, and most agent-facing copy refer to "asset id". Both names
754
- 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:
755
758
 
756
759
  ```python
757
- prs.deck_id # "asset_3a93..."
758
- 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
759
765
  ```
760
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
+
761
778
  ### `Presentation.close()` — alias for `flush()`
762
779
 
763
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.1"
136
+ __version__ = "0.4.3"
136
137
 
137
138
  __all__ = [
138
139
  # Main entry point
@@ -294,6 +294,10 @@ class SetParagraphStyle(Command):
294
294
  space_after_emu: Optional[int] = None
295
295
  margin_left_emu: Optional[int] = None
296
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
297
301
 
298
302
  @property
299
303
  def command_type(self) -> str:
@@ -50,6 +50,50 @@ def theme_color_to_scheme_name(theme_color: "MSO_THEME_COLOR") -> Optional[str]:
50
50
  return _THEME_COLOR_TO_SCHEME_NAME.get(value)
51
51
 
52
52
 
53
+ # Reverse of ``_THEME_COLOR_TO_SCHEME_NAME`` for the read path: map an OOXML
54
+ # ``<a:schemeClr val="...">`` name back to its canonical ``MSO_THEME_COLOR``.
55
+ # The forward map is many-to-one (both DARK_1 and TEXT_1 spell 'tx1'); on the
56
+ # way back python-pptx surfaces the index-13..16 spellings for the bg*/tx*
57
+ # names (TEXT_1/BACKGROUND_1/TEXT_2/BACKGROUND_2) and the dk*/lt* aliases for
58
+ # DARK_1/LIGHT_1/DARK_2/LIGHT_2, which is what we reproduce here.
59
+ _SCHEME_NAME_TO_THEME_COLOR = {
60
+ "tx1": 13, # TEXT_1
61
+ "bg1": 14, # BACKGROUND_1
62
+ "tx2": 16, # TEXT_2
63
+ "bg2": 15, # BACKGROUND_2
64
+ "dk1": 1, # DARK_1
65
+ "lt1": 2, # LIGHT_1
66
+ "dk2": 3, # DARK_2
67
+ "lt2": 4, # LIGHT_2
68
+ "accent1": 5,
69
+ "accent2": 6,
70
+ "accent3": 7,
71
+ "accent4": 8,
72
+ "accent5": 9,
73
+ "accent6": 10,
74
+ "hlink": 11, # HYPERLINK
75
+ "folHlink": 12, # FOLLOWED_HYPERLINK
76
+ }
77
+
78
+
79
+ def scheme_name_to_theme_color(scheme_name: Optional[str]) -> Optional["MSO_THEME_COLOR"]:
80
+ """Convert an OOXML ``<a:schemeClr>`` name back to an ``MSO_THEME_COLOR``.
81
+
82
+ Inverse of :func:`theme_color_to_scheme_name`, used by the read path so a
83
+ re-opened deck exposes ``fill.fore_color.theme_color`` /
84
+ ``line.color.theme_color`` instead of ``None``. Returns ``None`` for
85
+ unknown names (e.g. ``phClr``) so callers fall back to sRGB.
86
+ """
87
+ if not scheme_name:
88
+ return None
89
+ value = _SCHEME_NAME_TO_THEME_COLOR.get(scheme_name)
90
+ if value is None:
91
+ return None
92
+ from ..enum.dml import MSO_THEME_COLOR
93
+
94
+ return MSO_THEME_COLOR(value)
95
+
96
+
53
97
  class RGBColor:
54
98
  """
55
99
  Immutable RGB color value, matching the standard python-pptx API.
@@ -179,7 +179,7 @@ class Presentation:
179
179
  from pptx.units import Inches
180
180
 
181
181
  prs = Presentation(
182
- deck_id="deck_123",
182
+ asset_id="asset_3a9328bc-9c1c-4498-be8f-bda3883276f5",
183
183
  base_url="https://api.pptx-studio.com",
184
184
  api_key="sk_live_..."
185
185
  )
@@ -192,41 +192,73 @@ class Presentation:
192
192
 
193
193
  def __init__(
194
194
  self,
195
- deck_id: DeckId,
195
+ deck_id: Optional[DeckId] = None,
196
196
  base_url: Optional[str] = None,
197
197
  api_key: Optional[str] = None,
198
198
  auto_refresh: bool = True,
199
+ *,
200
+ asset_id: Optional[DeckId] = None,
199
201
  ):
200
202
  """
201
203
  Initialize a Presentation proxy.
202
204
 
203
205
  Args:
204
- deck_id: ID of the deck. MUST be ``asset_<uuid>`` for Athena
205
- assets or ``deck_<id>`` for legacy standalone decks.
206
- Anything else (e.g. a deck TITLE) raises ``ValueError``
207
- before any HTTP request. To mint a new asset, call
208
- ``Presentation.create(name=...)`` and reuse the
209
- returned ``prs.deck_id`` for subsequent opens.
206
+ deck_id: Legacy alias for ``asset_id`` the same identifier
207
+ string. Prefer ``asset_id``. Passing both is allowed
208
+ only when they match; differing values raise
209
+ ``ValueError``.
210
210
  base_url: Base URL of the API. If not provided, uses ATHENA_PPTX_BASE_URL
211
211
  environment variable.
212
212
  api_key: Optional API key for authentication. If not provided, uses
213
213
  ATHENA_PPTX_API_KEY environment variable.
214
214
  auto_refresh: Whether to automatically fetch snapshot on init
215
- """
216
- if not deck_id or not _DECK_ID_PATTERN.match(deck_id):
215
+ asset_id: ID of the presentation asset. MUST be ``asset_<uuid>``
216
+ for Athena assets or ``deck_<id>`` for legacy standalone
217
+ decks. Anything else (e.g. a presentation TITLE) raises
218
+ ``ValueError`` before any HTTP request. To mint a new
219
+ asset, call ``Presentation.create(name=...)`` and reuse
220
+ the returned ``prs.asset_id`` for subsequent opens.
221
+ """
222
+ if deck_id is not None and asset_id is not None and deck_id != asset_id:
217
223
  raise ValueError(
218
- f"Presentation(deck_id={deck_id!r}) is not a valid deck id. "
224
+ "Presentation(asset_id=..., deck_id=...) asset_id and "
225
+ "deck_id must match when both are provided (they are the "
226
+ "same identifier; deck_id is the legacy alias). Pass only "
227
+ "asset_id."
228
+ )
229
+ resolved_id = asset_id if asset_id is not None else deck_id
230
+ if resolved_id is None:
231
+ raise TypeError(
232
+ "Presentation() missing required argument 'asset_id' "
233
+ "(or its legacy alias 'deck_id')."
234
+ )
235
+ id_param = "deck_id" if asset_id is None else "asset_id"
236
+ if not resolved_id or not _DECK_ID_PATTERN.match(resolved_id):
237
+ if id_param == "asset_id":
238
+ raise ValueError(
239
+ f"Presentation(asset_id={resolved_id!r}) is not a valid "
240
+ f"asset id. Asset ids look like 'asset_<uuid>' "
241
+ f"(e.g., 'asset_3a9328bc-9c1c-4498-be8f-bda3883276f5'); "
242
+ f"legacy standalone decks use 'deck_<id>'. To open an "
243
+ f"existing presentation, pass its id — NOT its title. To "
244
+ f"create a new presentation titled {resolved_id!r}, call "
245
+ f"Presentation.create(name={resolved_id!r}) and reuse "
246
+ f"`prs.asset_id` on subsequent calls in the same "
247
+ f"conversation."
248
+ )
249
+ raise ValueError(
250
+ f"Presentation(deck_id={resolved_id!r}) is not a valid deck id. "
219
251
  f"Deck ids look like 'asset_<uuid>' "
220
252
  f"(e.g., 'asset_3a9328bc-9c1c-4498-be8f-bda3883276f5'); "
221
253
  f"legacy standalone decks use 'deck_<id>'. To open an "
222
254
  f"existing deck, pass its id — NOT its title. To create a "
223
- f"new deck titled {deck_id!r}, call "
224
- f"Presentation.create(name={deck_id!r}) and reuse "
255
+ f"new deck titled {resolved_id!r}, call "
256
+ f"Presentation.create(name={resolved_id!r}) and reuse "
225
257
  f"`prs.deck_id` on subsequent calls in the same conversation."
226
258
  )
227
- self._deck_id = deck_id
259
+ self._deck_id = resolved_id
228
260
  self._client = Client(base_url=base_url, api_key=api_key)
229
- self._buffer = CommandBuffer(self._client, deck_id)
261
+ self._buffer = CommandBuffer(self._client, resolved_id)
230
262
  self._snapshot: Optional[DeckSnapshot] = None
231
263
  self._slides: Optional[Slides] = None
232
264
  self._closed = False
@@ -265,7 +297,7 @@ class Presentation:
265
297
  if auto_refresh:
266
298
  from pptx import _ptc
267
299
 
268
- call_id = _ptc.emit_begin("OpenPresentation", {"assetId": deck_id})
300
+ call_id = _ptc.emit_begin("OpenPresentation", {"assetId": resolved_id})
269
301
  try:
270
302
  self.refresh()
271
303
  except AuthenticationError as exc:
@@ -276,10 +308,10 @@ class Presentation:
276
308
  is_error=True,
277
309
  )
278
310
  raise PermissionError(
279
- f"Presentation(deck_id={deck_id!r}) — access denied. "
311
+ f"Presentation({id_param}={resolved_id!r}) — access denied. "
280
312
  f"The current user doesn't have permission to open this "
281
- f"deck, or the id is invalid. Verify the id is correct "
282
- f"and reachable from the current workspace."
313
+ f"presentation, or the id is invalid. Verify the id is "
314
+ f"correct and reachable from the current workspace."
283
315
  ) from exc
284
316
  except RemoteError as exc:
285
317
  _ptc.emit_end(
@@ -290,16 +322,18 @@ class Presentation:
290
322
  )
291
323
  if exc.status_code == 404:
292
324
  raise ValueError(
293
- f"Presentation(deck_id={deck_id!r}) — deck not found. "
294
- f"To create a new deck, call "
295
- f"Presentation.create(name=...) and reuse `prs.deck_id` "
296
- f"for subsequent opens in the same conversation."
325
+ f"Presentation({id_param}={resolved_id!r}) — "
326
+ f"presentation not found. To create a new "
327
+ f"presentation, call Presentation.create(name=...) "
328
+ f"and reuse `prs.asset_id` for subsequent opens in "
329
+ f"the same conversation."
297
330
  ) from exc
298
331
  if exc.status_code in (401, 403):
299
332
  raise PermissionError(
300
- f"Presentation(deck_id={deck_id!r}) — access denied "
301
- f"(HTTP {exc.status_code}). Verify the deck id and "
302
- f"that the current user has permission to open it."
333
+ f"Presentation({id_param}={resolved_id!r}) — access "
334
+ f"denied (HTTP {exc.status_code}). Verify the asset "
335
+ f"id and that the current user has permission to "
336
+ f"open it."
303
337
  ) from exc
304
338
  raise
305
339
  except Exception as exc:
@@ -313,7 +347,7 @@ class Presentation:
313
347
  _ptc.emit_end(
314
348
  call_id=call_id,
315
349
  tool_name="OpenPresentation",
316
- result={"ok": True, "assetId": deck_id},
350
+ result={"ok": True, "assetId": resolved_id},
317
351
  is_error=False,
318
352
  )
319
353
 
@@ -708,13 +742,14 @@ class Presentation:
708
742
  description="Presentation.asset_id — Athena asset id alias for deck_id.",
709
743
  )
710
744
  def asset_id(self) -> DeckId:
711
- """Alias for :attr:`deck_id`.
712
-
713
- Athena hosts each presentation as an ``asset_<uuid>`` asset; many
714
- callers think in "asset id" terms (matching the URL pattern in
715
- Olympus and the GraphQL schema) rather than the historical
716
- ``deck_id`` name. Both properties return the same string.
717
- Athena-only ergonomic addition — not in upstream python-pptx.
745
+ """Athena asset id of the presentation — preferred spelling.
746
+
747
+ Athena hosts each presentation as an ``asset_<uuid>`` asset; this
748
+ matches the URL pattern in Olympus, the GraphQL schema, and
749
+ agent-facing vocabulary. :attr:`deck_id` is the legacy alias and
750
+ returns the same string, as does the ``deck_id=`` constructor
751
+ keyword. Athena-only ergonomic addition — not in upstream
752
+ python-pptx.
718
753
  """
719
754
  return self._deck_id
720
755
 
@@ -635,11 +635,27 @@ class FillFormat:
635
635
  def __init__(self, shape: Shape):
636
636
  self._shape = shape
637
637
  self._solid_color: Optional[str] = shape._properties.get("fillColorHex")
638
+ # Scheme (theme) fill read back from the snapshot. Initializing it here
639
+ # (rather than only in ``_on_color_change``) both fixes a latent
640
+ # AttributeError when ``_scheme_color`` is read before any write and
641
+ # lets a re-opened theme-colored fill round-trip on re-export.
642
+ self._scheme_color: Optional[str] = shape._properties.get("fillSchemeColor")
638
643
  self._transparency: float = 0.0
639
- self._type: Optional[str] = 'solid' if self._solid_color else None
644
+ # A scheme fill with no explicit sRGB is still a solid fill.
645
+ self._type: Optional[str] = 'solid' if (self._solid_color or self._scheme_color) else None
640
646
  # Initialize fore_color ColorFormat
641
647
  rgb = RGBColor.from_string(self._solid_color) if self._solid_color else None
642
648
  self._fore_color_format = _FillColorFormat(self, rgb=rgb)
649
+ # Surface ``fore_color.theme_color`` for a scheme fill (no sRGB hex) so
650
+ # the agent "restyle a deck I just opened" workflow reads back the theme
651
+ # color instead of None. Set the attribute directly to avoid triggering
652
+ # the setter's write-emit on a pure read.
653
+ if self._scheme_color and not self._solid_color:
654
+ from ..dml.color import scheme_name_to_theme_color
655
+
656
+ theme = scheme_name_to_theme_color(self._scheme_color)
657
+ if theme is not None:
658
+ self._fore_color_format._theme_color = theme
643
659
 
644
660
  def solid(self) -> None:
645
661
  """
@@ -940,11 +956,25 @@ class LineFormat:
940
956
  def __init__(self, shape: Shape):
941
957
  self._shape = shape
942
958
  self._color_hex: Optional[str] = shape._properties.get("strokeColorHex")
959
+ # Scheme (theme) line color; initialized here so it round-trips and so
960
+ # the setters that reference ``_scheme_color`` never hit an
961
+ # AttributeError on a fresh read.
962
+ self._scheme_color: Optional[str] = shape._properties.get("strokeSchemeColor")
943
963
  self._width_emu: int = shape._properties.get("strokeWidthEmu", 12700) # Default 1pt
944
- self._dash_style: str = 'solid'
964
+ # Read the persisted dash style back (defaults to solid) so a re-opened
965
+ # deck exposes ``line.dash_style`` instead of always 'solid'. The Y.Doc
966
+ # and SDK share the same dash vocabulary, so no mapping is needed.
967
+ self._dash_style: str = shape._properties.get("strokeDashStyle") or 'solid'
945
968
  # Initialize color ColorFormat
946
969
  rgb = RGBColor.from_string(self._color_hex) if self._color_hex else None
947
970
  self._color_format = _LineColorFormat(self, rgb=rgb)
971
+ # Surface ``color.theme_color`` for a scheme line color (no sRGB hex).
972
+ if self._scheme_color and not self._color_hex:
973
+ from ..dml.color import scheme_name_to_theme_color
974
+
975
+ theme = scheme_name_to_theme_color(self._scheme_color)
976
+ if theme is not None:
977
+ self._color_format._theme_color = theme
948
978
 
949
979
  @property
950
980
  def color(self) -> _LineColorFormat: