athena-python-pptx 0.2.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/API_PARITY_REPORT.md +28 -5
  2. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/CHANGELOG.md +41 -0
  3. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/PKG-INFO +1 -1
  4. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/docs/API_PARITY_EXCEPTIONS.md +496 -2
  5. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/__init__.py +1 -1
  6. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/client.py +36 -1
  7. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/commands.py +1089 -47
  8. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/enum/chart.py +12 -0
  9. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/enum/dml.py +45 -0
  10. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/errors.py +48 -0
  11. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/parts/chart.py +18 -2
  12. athena_python_pptx-0.3.1/pptx/parts/image.py +186 -0
  13. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/presentation.py +141 -12
  14. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/__init__.py +2588 -83
  15. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/slides.py +645 -20
  16. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/text/__init__.py +605 -32
  17. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pyproject.toml +1 -1
  18. athena_python_pptx-0.2.0/pptx/parts/image.py +0 -85
  19. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/.gitignore +0 -0
  20. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/CLAUDE.md +0 -0
  21. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/DEV-GUIDE.md +0 -0
  22. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/PARITY_QUESTIONS.md +0 -0
  23. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/PUBLISHING.md +0 -0
  24. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/README.md +0 -0
  25. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/docs/athena-api.json +0 -0
  26. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/docs/athena-api.md +0 -0
  27. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/_athena_extension.py +0 -0
  28. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/_ptc.py +0 -0
  29. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/_references.py +0 -0
  30. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/action.py +0 -0
  31. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/batching.py +0 -0
  32. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/__init__.py +0 -0
  33. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/axis.py +0 -0
  34. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/category.py +0 -0
  35. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/chart.py +0 -0
  36. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/data.py +0 -0
  37. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/datalabel.py +0 -0
  38. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/legend.py +0 -0
  39. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/marker.py +0 -0
  40. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/plot.py +0 -0
  41. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/point.py +0 -0
  42. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/series.py +0 -0
  43. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/chart/xlsx.py +0 -0
  44. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/decorators.py +0 -0
  45. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/dml/__init__.py +0 -0
  46. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/dml/chtfmt.py +0 -0
  47. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/dml/color.py +0 -0
  48. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/dml/effect.py +0 -0
  49. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/dml/fill.py +0 -0
  50. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/dml/line.py +0 -0
  51. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/docgen.py +0 -0
  52. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/enum/__init__.py +0 -0
  53. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/enum/action.py +0 -0
  54. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/enum/lang.py +0 -0
  55. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/enum/shapes.py +0 -0
  56. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/enum/text.py +0 -0
  57. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/exc.py +0 -0
  58. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/media.py +0 -0
  59. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/package.py +0 -0
  60. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/parts/__init__.py +0 -0
  61. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/parts/_base.py +0 -0
  62. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/parts/coreprops.py +0 -0
  63. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/parts/embeddedpackage.py +0 -0
  64. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/parts/media.py +0 -0
  65. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/parts/presentation.py +0 -0
  66. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/parts/slide.py +0 -0
  67. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/autoshape.py +0 -0
  68. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/base.py +0 -0
  69. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/connector.py +0 -0
  70. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/freeform.py +0 -0
  71. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/graphfrm.py +0 -0
  72. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/group.py +0 -0
  73. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/picture.py +0 -0
  74. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/placeholder.py +0 -0
  75. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shapes/shapetree.py +0 -0
  76. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/shared.py +0 -0
  77. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/slide.py +0 -0
  78. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/spec.py +0 -0
  79. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/table.py +0 -0
  80. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/text/fonts.py +0 -0
  81. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/text/layout.py +0 -0
  82. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/text/text.py +0 -0
  83. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/types.py +0 -0
  84. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/typing.py +0 -0
  85. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/units.py +0 -0
  86. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.1}/pptx/util.py +0 -0
@@ -110,13 +110,36 @@ via Tranches R–W:
110
110
  | `*.element`, `*.part`, `*.ln`, `*.get_or_add_ln` | XML element / OPC package / direct DML XML access — REST SDK has no local XML or packages |
111
111
  | `FillFormat.from_fill_parent()` | Internal XML constructor |
112
112
  | `ColorFormat.from_colorchoice_parent()` | Internal XML constructor |
113
- | `Font.fill` | Font fill format (uncommon) |
114
- | `SlideLayout.background`, `SlideMaster.background` | Not yet exposed at layout / master level |
115
- | `SlideLayout.iter_cloneable_placeholders`, `SlideLayout.used_by_slides` | Placeholder cloning is server-side |
116
- | `SlideShapes.add_movie`, `add_ole_object`, `build_freeform` | Not implemented (uncommon) |
117
- | `Slide.follow_master_background` | XML-level inheritance flag — REST SDK abstracts this |
118
113
  | `Picture.image.blob` | Image bytes live in the studio's asset store — not materialized into Python |
119
114
 
115
+ ## Closed Parity Gaps (added 2026-05-18)
116
+
117
+ The following members are now implemented at the SDK surface — every method is callable and emits the appropriate command (server-side handlers for the new commands ship as a follow-up where noted):
118
+
119
+ | Member | Implementation |
120
+ |--------|----------------|
121
+ | `Font.fill` | Returns ``_FontFillFormat`` proxy; ``fore_color`` delegates to ``Font.color`` |
122
+ | `Image.dpi` | Derived from PIL when blob loaded, ``(72, 72)`` fallback |
123
+ | `Slide.follow_master_background` | Getter + setter; emits ``SetSlideBackground`` with ``follow_master`` |
124
+ | `SlideLayout.background`, `SlideMaster.background` | Returns ``_LayoutBackground`` proxy (read/write local; server-side patch op pending) |
125
+ | `SlideLayout.iter_cloneable_placeholders` | Derived from placeholders, skipping latent types |
126
+ | `SlideLayout.used_by_slides` | Derived from ``prs.slides`` |
127
+ | `SlideLayouts.index(layout)` | List operation |
128
+ | `SlideLayouts.remove(layout)` | Emits ``RemoveLayout`` *(server handler pending)* |
129
+ | `Shapes.clone_placeholder` | Emits ``ClonePlaceholder`` *(server handler pending)* |
130
+ | `Shapes.clone_layout_placeholders` | Loops ``iter_cloneable_placeholders`` + ``clone_placeholder`` |
131
+ | `Shapes.ph_basename` / ``NotesSlideShapes.ph_basename`` | Pure mapping, notes-slide override |
132
+ | `Shapes.turbo_add_enabled` | Local property (REST SDK has no XML batch path) |
133
+ | `NotesSlide.shapes` / ``.placeholders`` / ``.notes_placeholder`` / ``.name`` / ``.background`` / ``.clone_master_placeholders`` | Empty collections + helpers; notes inheritance is automatic in REST |
134
+ | `GraphicFrame.chart_part` | Returns a real ``ChartPart`` with ``.chart`` |
135
+ | `GraphicFrame.ole_format` | Returns ``_OleFormat`` exposing ``prog_id`` / ``show_as_icon`` |
136
+ | `Table.notify_height_changed/width_changed` | Recomputes total + emits ``SetTransform`` |
137
+ | `FillFormat.gradient()` | Emits ``SetGradientFill`` immediately (previously only setters emitted) |
138
+ | `FillFormat.patterned()` + ``pattern`` setter | Emits ``SetPatternFill`` *(server handler pending)* |
139
+ | `FreeformBuilder.convert_to_shape()` | Emits ``AddFreeformShape`` *(server handler pending)* |
140
+
141
+ The 4 new client commands (``RemoveLayout``, ``ClonePlaceholder``, ``SetPatternFill``, ``AddFreeformShape``) ship in the same release; their pptx-studio handlers are tracked as a server-side follow-up.
142
+
120
143
  ## SDK Additions (Documented Deviations)
121
144
 
122
145
  See `docs/API_PARITY_EXCEPTIONS.md` for the full list. Highlights:
@@ -2,6 +2,47 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## 0.3.1
6
+
7
+ **Chart title runs, slide insertion, and shape-delete local state — Insight
8
+ Partners session feedback batch.**
9
+
10
+ Investigated [thread_7b324723-…](https://app.athenaintel.com/dashboard/spaces/?session_id=thread_7b324723-a478-4e99-ac8f-6e7a69766292)
11
+ where an agent built a 15-slide LP pitch deck and tripped over three
12
+ SDK rough edges. Each is addressed below.
13
+
14
+ - **`chart.chart_title.text_frame.paragraphs[0].runs[0].font` now works.**
15
+ The `_ChartTitleTextFrame` previously exposed only `text`; iterating
16
+ `paragraphs` raised `AttributeError`, and the agent fell back to
17
+ unstyled titles. The text frame now carries paragraph and run
18
+ proxies; per-run font writes coalesce into a single
19
+ `SetChartTitleRich` patch on the next flush. The simple
20
+ `text_frame.text = "..."` path still uses the cheaper `SetChartTitle`
21
+ patch — no wire-size regression on the no-formatting case.
22
+
23
+ - **`prs.slides.insert(idx, layout)` added.** Previously, placing a new
24
+ slide between two existing slides required `add_slide()` followed by
25
+ `clone(target_index=...)` to shuffle the list — a workaround that
26
+ cloned 11 shapes onto the new slide and then couldn't fully clear
27
+ them. `insert()` emits a single `AddSlide(index=...)` and refreshes
28
+ the local collection.
29
+
30
+ - **`shape.delete()` updates the local `slide.shapes` view immediately.**
31
+ The buffered delete left `len(slide.shapes)` stale, so the
32
+ "Before: 33, After: 33" symptom appeared even when the delete was
33
+ going through correctly server-side. `delete()` now removes the
34
+ shape from the parent `Shapes` collection in addition to queueing
35
+ the `DeleteShape` command. Layout-inherited shapes
36
+ (`source == "layout"`) no-op rather than queue a guaranteed-to-fail
37
+ delete.
38
+
39
+ - **Toolkit prompt updated** to surface the working chart APIs —
40
+ `series.format.fill.solid()` + `series.format.fill.fore_color.rgb`,
41
+ `chart.legend.position`, doughnut/pie chart enums — and the new
42
+ `slide.clear_shapes()` / `prs.slides.insert()` helpers. The
43
+ Insight Partners agent assumed series styling was unsupported and
44
+ skipped per-series brand colors entirely.
45
+
5
46
  ## 0.1.76
6
47
 
7
48
  **Table cell paragraph alignment now round-trips to OOXML (baseline-parity Gap A2).**
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-pptx
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration
5
5
  Project-URL: Homepage, https://github.com/pptx-studio/python-sdk
6
6
  Project-URL: Documentation, https://docs.pptx-studio.com/sdk/python
@@ -17,7 +17,6 @@ These standard python-pptx members are omitted because a REST SDK has no access
17
17
  | `FillFormat.from_fill_parent()` | Internal constructor |
18
18
  | `ColorFormat.from_colorchoice_parent()` | Internal constructor |
19
19
  | `Font.fill` | Font fill format (uncommon) |
20
- | `Font.language_id` | Implemented in v0.1.68 (returns `MSO_LANGUAGE_ID` enum, accepts enum or int LCID) |
21
20
 
22
21
  ---
23
22
 
@@ -216,6 +215,35 @@ python-pptx's `_Row.height` and `_Column.width` are read/write EMU properties. T
216
215
 
217
216
  In python-pptx, `Table.rows` returns a `_RowCollection` of `_Row` objects (each with `.height`). This SDK matches that. In a prior version, `Table.rows` returned a flat collection of cell lists.
218
217
 
218
+ ### `TextFrame.clear()` — also resets bullet on paragraph 0
219
+
220
+ In upstream python-pptx, `text_frame.clear()` removes every paragraph except the first, and on that first paragraph it removes every child element **except** `<a:pPr>`. Any bullet definition the author wrote into `<a:pPr>` survives the clear, and an inherited bullet from the master / layout's `<a:lstStyle>` is left to be re-resolved by PowerPoint at render time.
221
+
222
+ This SDK additionally emits a `SetParagraphStyle(paragraph_index=0, bullet='none')` after the empty `SetText`, explicitly overriding the bullet on the cleared paragraph.
223
+
224
+ The deviation exists because pptx-studio **pre-bakes** master/layout list-style inheritance into `richContent.paragraphs[i].bullet` at ingest time (and at slide materialization), then preserves that baked field through every subsequent `applySetText` via `preserveParagraphProps`. Without the explicit override, the python-pptx idiom
225
+
226
+ ```python
227
+ tf.clear()
228
+ p = tf.paragraphs[0]
229
+ run = p.add_run()
230
+ run.text = "Title"
231
+ ```
232
+
233
+ renders the title with a stray "•" whenever the placeholder lives on a layout whose master defines a level-1 `<a:buChar/>` (the common case for `<p:bodyStyle>` and any content placeholder). The explicit `bullet='none'` clears the baked inheritance so the literal "no pPr was authored" intent reaches the renderer.
234
+
235
+ To re-enable a bullet after `clear()`, set it back explicitly:
236
+
237
+ ```python
238
+ tf.clear()
239
+ p = tf.paragraphs[0]
240
+ p.bullet = True # restores 'disc' default
241
+ run = p.add_run()
242
+ run.text = "Bullet item"
243
+ ```
244
+
245
+ Only `bullet` is overridden — `alignment`, `level`, `line_spacing`, `space_before`, `space_after`, `margin_left`, `indent`, and `bullet_color` are left alone (they continue to follow upstream `<a:pPr>`-preservation semantics, since baked-inheritance surprises are far less common on those fields).
246
+
219
247
  ---
220
248
 
221
249
  ## Non-Standard Convenience Methods (candidates for future cleanup)
@@ -271,6 +299,35 @@ if warnings:
271
299
  print(w)
272
300
  ```
273
301
 
302
+ ### `add_picture(image_file=...)` accepts `http(s)://` URLs
303
+
304
+ python-pptx upstream treats a `str` `image_file` strictly as a local
305
+ filesystem path. athena-python-pptx **additionally** treats strings
306
+ starting with `http://` or `https://` as URLs and fetches their bytes
307
+ with `requests` before falling through to the standard AddPicture
308
+ pipeline (base64-encode → server-side SHA256 dedup → S3 upload).
309
+
310
+ This is the canonical handoff for "spreadsheet range → image asset →
311
+ slide": athena-openpyxl's `Worksheet.export_range_as_image_asset` returns
312
+ an `ImageAssetRef` whose `.url` is exactly the Olympus-proxied
313
+ `/asset/asset_<uuid>.png` form this branch accepts.
314
+
315
+ Athena asset URLs (any hostname under `athenaintel.com` /
316
+ `athenaintelligence.ai`) are fetched with the sandbox's
317
+ `ATHENA_PPTX_API_KEY` injected as a Bearer token so Olympus's catch-all
318
+ asset proxy can run the workspace ABAC permission check against the
319
+ calling user. Non-Athena URLs are fetched without auth — they're assumed
320
+ to be world-readable, like any plain image link.
321
+
322
+ **Portable code that needs to run against stock python-pptx should
323
+ download the URL externally and pass `bytes` or a file-like.**
324
+
325
+ ```python
326
+ # Inside a Daytona pptx sandbox:
327
+ img = ws.export_range_as_image_asset("A1:F20", name="Q4 Revenue")
328
+ slide.shapes.add_picture(img.url, Inches(1), Inches(1), width=Inches(6))
329
+ ```
330
+
274
331
  ### `name=` kwarg on `add_textbox()` / `add_shape()` / `add_picture()`
275
332
 
276
333
  Optional keyword for setting a stable shape name at creation. python-pptx
@@ -400,7 +457,11 @@ that closed six more setters end-to-end (see the new table below):
400
457
  setters.
401
458
  - 3-D chart authoring (`XL_CHART_TYPE.THREE_D_*` raises
402
459
  `UnsupportedFeatureError`).
403
- - Combo / Stock / Radar variants for `add_chart()` (also raise).
460
+ - Combo / Stock variants for `add_chart()` (also raise). Radar
461
+ (`XL_CHART_TYPE.RADAR` / `RADAR_FILLED` / `RADAR_MARKERS`) now authors
462
+ end-to-end — all three variants map to OOXML `<c:radarChart>` with
463
+ `<c:radarStyle val="standard"/>`; the marker / filled distinction
464
+ is collapsed to "standard" today (follow-up work to extend ChartSpec).
404
465
  - Trendlines and error bars on series.
405
466
 
406
467
  ### Chart Patches Added in the Styling Residuals PR (6 new operations)
@@ -769,6 +830,67 @@ The default `fit=None` preserves stock python-pptx behaviour. The only
769
830
  other accepted value is `"contain"`; passing anything else raises
770
831
  `ValueError` to fail loudly rather than silently no-op.
771
832
 
833
+ ## `Shapes.add_athena_anchor()` — Athena-only cross-asset screenshot insert
834
+
835
+ Athena extension with no python-pptx analogue. Renders an Athena
836
+ `Anchor` (a structured reference to a region of another asset — sheet
837
+ cells, sheet ranges, etc.) to a PNG and inserts it as a regular
838
+ `Picture` shape on the current slide. The result is a one-shot raster
839
+ snapshot, **not** a live-linked binding — use `add_linked_table()` /
840
+ `add_linked_ole_object()` for refreshable bindings.
841
+
842
+ ```python
843
+ from pptx._references import AssetReference, SheetRangeAnchor
844
+
845
+ slide.shapes.add_athena_anchor(
846
+ AssetReference(id="asset_20880ea3-95fb-4577-9a2e-ad11b7773939"),
847
+ SheetRangeAnchor(sheet_id=1, range="A1:F20"),
848
+ Inches(1), Inches(1),
849
+ width=Inches(8),
850
+ )
851
+ ```
852
+
853
+ Signature:
854
+
855
+ ```python
856
+ add_athena_anchor(
857
+ source: AssetReference,
858
+ anchor: Anchor,
859
+ left: Length,
860
+ top: Length,
861
+ width: Length | None = None,
862
+ height: Length | None = None,
863
+ *,
864
+ name: str | None = None,
865
+ fit: str | None = None, # "contain" — same semantics as add_picture
866
+ ) -> Shape
867
+ ```
868
+
869
+ Supported anchor types today:
870
+
871
+ | Anchor type | Behaviour |
872
+ |--------------------|----------------------------------------------------------|
873
+ | `SheetCellAnchor` | Renders the single cell as a 1×1 range PNG. |
874
+ | `SheetRangeAnchor` | Renders the A1 range via xlsx-studio's screenshot.png. |
875
+ | `SheetTableAnchor` | Raises `NotImplementedError` (resolve to A1 + retry). |
876
+ | `SlideAnchor` | Raises `NotImplementedError` (no slide-snapshot route). |
877
+ | anything else | Raises `NotImplementedError`. |
878
+
879
+ Sheet anchors route through `GET {ATHENA_XLSX_BASE_URL}/workbooks/:id/screenshot.png`
880
+ with the sandbox's `ATHENA_XLSX_API_KEY` forwarded as a Bearer token —
881
+ xlsx-studio's ownership middleware checks that the calling workspace
882
+ can read the source workbook. Render scale is fixed at 3 (matches
883
+ xlsx-studio's `range-as-asset` default — slide inserts sit at 6–9″
884
+ widths where scale=2 leaves text soft). When `ATHENA_XLSX_BASE_URL` /
885
+ `ATHENA_XLSX_API_KEY` aren't set (e.g. running outside a Daytona
886
+ presentation_exec sandbox), the call raises `ValueError` rather than
887
+ producing a broken picture.
888
+
889
+ For finer control (custom scale, `include_headers`, off-snapshot
890
+ post-processing), fetch the PNG bytes directly via athena-openpyxl's
891
+ `Worksheet.export_range_sync()` and call `add_picture(bytes, …)`
892
+ yourself.
893
+
772
894
  ---
773
895
 
774
896
  ## `Shapes.add_linked_table()` — Athena studio-linking extension (added in Phase 3)
@@ -825,3 +947,375 @@ the same table element on the Y.Doc.
825
947
 
826
948
  **`freshness_mode='office_live_link'` is reserved for Phase 5** and
827
949
  currently raises ``ValueError`` locally.
950
+
951
+ ---
952
+
953
+ ## Athena extensions for most-requested upstream features (v0.1.81)
954
+
955
+ These surfaces are **additions** to python-pptx, not deviations — every
956
+ upstream class, method, property, and parameter remains 1:1 with
957
+ python-pptx 1.0.2. Each item below closes (or fundamentally improves)
958
+ a long-standing upstream feature gap. Issue numbers reference
959
+ `scanny/python-pptx`. Every entry is marked at runtime with
960
+ `@athena_extension(issue=…, since="0.1.81")` so the registry walker
961
+ picks them up; the legacy `@athena_only` markers continue to work for
962
+ back-compat.
963
+
964
+ This wave intentionally **excludes animations / Morph transitions**
965
+ (python-pptx#1106 / #942) — they need export-worker and Y.Doc-schema
966
+ work in addition to SDK surface and are tracked separately.
967
+
968
+ ### Shape-level additions
969
+
970
+ - **`Shape.duplicate(target_slide=None, offset_x=None, offset_y=None)`**
971
+ (closes python-pptx#533, #620, 10-comment thread requesting a public
972
+ duplicate API). Alias for the existing `Shape.clone()` — both verbs
973
+ resolve identically. The clone preserves every server-resolved
974
+ property (transform, color, text, type) and accepts the same
975
+ `target_slide` / offset args as `clone()`.
976
+
977
+ - **`LineFormat.head_end_type` / `tail_end_type` / `head_end_length` /
978
+ `tail_end_length` / `head_end_width` / `tail_end_width`** (closes
979
+ python-pptx#1033). Connector / line arrow heads. Accept either
980
+ the upstream `MSO_ARROWHEAD` / `MSO_ARROWHEAD_LENGTH` /
981
+ `MSO_ARROWHEAD_WIDTH` enums or the OOXML short strings
982
+ (`"triangle"` / `"stealth"` / `"diamond"` / `"oval"` / `"arrow"` /
983
+ `"none"`, `"sm"` / `"med"` / `"lg"`). Each setter emits a
984
+ `SetConnectorArrows` wire op carrying all six attrs at once.
985
+
986
+ ```python
987
+ from pptx.enum.dml import MSO_ARROWHEAD
988
+ conn = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT,
989
+ Inches(1), Inches(1),
990
+ Inches(5), Inches(3))
991
+ conn.line.tail_end_type = MSO_ARROWHEAD.TRIANGLE
992
+ conn.line.tail_end_length = "med"
993
+ conn.line.tail_end_width = "lg"
994
+ ```
995
+
996
+ - **`Shapes.add_picture(svg_bytes_or_path, …)`** now natively
997
+ supports SVG (closes python-pptx#1112). The picture's content-type
998
+ is detected from the magic bytes (`<?xml … <svg`), the native
999
+ size is parsed from the `<svg width=/height=>` attributes (or the
1000
+ `viewBox`), and the wire payload carries `image_format="svg"` so
1001
+ the server stores the SVG as a first-class picture asset (PowerPoint
1002
+ 2016+ supports SVG natively).
1003
+
1004
+ - **`Shapes.add_connector(...)` int-coerces float endpoints**
1005
+ (closes python-pptx#1058 — "Generating corrupted PPT when using
1006
+ connectors"). Calls like `connector(top=shape.top + shape.height / 2)`
1007
+ produce floats that legacy code wrote into `<a:off x="...">`
1008
+ attributes that Google Slides and modern PowerPoint refuse to open.
1009
+ We now route every endpoint through `int(round(float(...)))` so the
1010
+ wire payload is integer EMU regardless of input type
1011
+ (`float` / `numpy.float64` / `Decimal`).
1012
+
1013
+ ### Text-frame and run additions
1014
+
1015
+ - **`Hyperlink.target_slide`** (closes python-pptx#1077). Run-level
1016
+ internal slide jumps. Assign a `Slide` proxy or a zero-based int;
1017
+ the SDK emits a `SetRunHyperlinkTarget` command with the
1018
+ `targetSlideIndex` payload, and the server writes
1019
+ `<a:hlinkClick action="ppaction://hlinksldjump">` with the matching
1020
+ slide-jump relationship. Mutually exclusive with `address` (an
1021
+ external URL); setting one nulls the other.
1022
+
1023
+ ```python
1024
+ run.hyperlink.target_slide = prs.slides[3]
1025
+ # or
1026
+ run.hyperlink.target_slide = 3
1027
+ ```
1028
+
1029
+ - **`Run.font.color.rgb` overrides the theme hyperlink color**
1030
+ (closes python-pptx#940 — "Set/change font color when working with
1031
+ Hyperlinks is impossible", 11-comment thread). The Hyperlink
1032
+ emitter now reads the run's explicit color and ships an
1033
+ `overrideColorHex` field on `SetRunHyperlinkTarget` so the server
1034
+ writes the matching `<a:rPr><a:solidFill>` over the run instead of
1035
+ letting the theme `hlink` color win.
1036
+
1037
+ - **`TextFrame.replace_text(pattern, replacement, *, regex=False,
1038
+ match_case=False, whole_word=False, max_replacements=None)`**
1039
+ (closes python-pptx#684, #1037, #884). Find-and-replace that
1040
+ preserves run-level formatting (fonts / colors / sizes / bold /
1041
+ italic) by splitting runs across the match boundary on the server.
1042
+ Returns the count of substitutions performed.
1043
+
1044
+ - **`TextFitter.best_fit_font_size(text, extents, max_size, font_file)`**
1045
+ no longer returns `max_size` unconditionally (closes
1046
+ python-pptx#715, #936, #970, #1026). When a base URL is configured
1047
+ (`ATHENA_PPTX_BASE_URL`), the classmethod issues a `MeasureTextFit`
1048
+ query to the studio's text-measurement endpoint and returns the
1049
+ largest size that fits. The upstream signature is preserved exactly
1050
+ (additional knobs read from `ATHENA_PPTX_TEXTFITTER_*` env vars).
1051
+ Falls back to `max_size` with a `RuntimeWarning` if no client is
1052
+ reachable so offline tests keep working.
1053
+
1054
+ ### Table additions
1055
+
1056
+ - **`_RowCollection.add(*, height=None, copy_format_from=None)`** and
1057
+ **`_RowCollection.insert(index, *, height=None, copy_format_from=None)`**
1058
+ (closes python-pptx#895, #1016, 17-comment thread). Restores the
1059
+ `rows.add()` method removed in upstream 1.0.0 and adds an indexed
1060
+ `insert()` companion. `copy_format_from=<row_index>` inherits the
1061
+ source row's height + every cell's formatting.
1062
+
1063
+ - **`_ColumnCollection.add(*, width=None, copy_format_from=None)`** and
1064
+ **`_ColumnCollection.insert(index, ...)`** — column-axis mirror.
1065
+ `Table.add_column()` / `Table.remove_column()` / `Table.delete_column()`
1066
+ / `Table.delete_row()` are convenience wrappers on top.
1067
+
1068
+ ```python
1069
+ table.rows.add(copy_format_from=0)
1070
+ table.columns.insert(2, width=Inches(1.5))
1071
+ table.delete_row(-1)
1072
+ table.delete_column(0)
1073
+ ```
1074
+
1075
+ ### Chart additions
1076
+
1077
+ - **`DataLabel.show_value` / `show_series_name` / `show_category_name` /
1078
+ `show_percentage` / `number_format` / `text` / `font` / `position`**
1079
+ (closes python-pptx#1068, #1024, #1025). Per-point data-label
1080
+ overrides. Reach via `series.points[i].data_label.show_value = True`;
1081
+ each setter emits a `SetPointDataLabelStyle` wire op so the
1082
+ server can apply the override on top of the series default.
1083
+
1084
+ - **`_ChartTitle.left` / `top` / `width` / `height`** (closes
1085
+ python-pptx#1030 — "How to set manually chart_title position?").
1086
+ Fractional layout (0.0 – 1.0 of the plot area) for the manual
1087
+ title placement. Each setter emits `SetChartTitlePosition` which
1088
+ the server materialises as `<c:layout><c:manualLayout xMode="edge"
1089
+ yMode="edge" …>`. Setting any axis to `None` clears the override and
1090
+ falls back to PowerPoint's auto-placement.
1091
+
1092
+ - **Office 2016+ chart types**: `XL_CHART_TYPE.TREEMAP` (117),
1093
+ `SUNBURST` (118), `HISTOGRAM` (119), `PARETO` (120),
1094
+ `BOX_WHISKER` (121), `WATERFALL` (122), `FUNNEL` (123). Closes
1095
+ python-pptx#583 ("New Chart Types in Office 2016"), #944
1096
+ (Treemap / Scatter), #651 (Waterfall), #1047 (Box plot). These
1097
+ use the cx-namespace OOXML schema instead of the legacy c-schema;
1098
+ `slide.shapes.add_chart(...)` dispatches through a separate
1099
+ `AddChart2016` wire op so the server can author the cx-flavoured
1100
+ chart-part XML.
1101
+
1102
+ ### Slide- and presentation-level additions
1103
+
1104
+ - **`Presentation.create(widescreen=True)` is now the default**
1105
+ (closes python-pptx#1066, 9-comment thread). The blank-template
1106
+ factory creates 16:9 widescreen decks at 14630400 × 8229600 EMU
1107
+ (PowerPoint's "Widescreen (16:9)" preset). Pass `widescreen=False`
1108
+ to fall back to the legacy 4:3 default.
1109
+
1110
+ - **`Presentation.open(deck_id, base_url=None, api_key=None)`**
1111
+ (closes python-pptx#1018 — "open a PPT in append mode"). Named
1112
+ factory that mirrors the iterate-and-edit verb the upstream thread
1113
+ filed under. Internally identical to `Presentation(deck_id=…)`
1114
+ but reads naturally next to `Presentation.create` / `.upload`.
1115
+
1116
+ - **`Presentation.copy_slide_from(source, source_slide_index, *,
1117
+ destination_index=-1)`** (closes python-pptx#1036, #696, #934,
1118
+ three threads totalling 20+ comments asking for cross-deck slide
1119
+ copy). Emits `CopySlideFromDeck`; the server runs the deep copy
1120
+ with full relationship + media-de-duplication handling. Accepts
1121
+ either a live `Presentation` instance (its pending edits are
1122
+ flushed first) or a raw deck id string.
1123
+
1124
+ - **`Slides.remove_slide(slide_or_index)`** (closes python-pptx#956).
1125
+ Alias for the existing `Slides.delete()`. Both verbs accept a
1126
+ `Slide` proxy or an integer index.
1127
+
1128
+ - **`SlideMaster.shapes.add_shape / add_picture / add_textbox /
1129
+ add_table`** and **`SlideLayout.shapes.add_*`** (closes
1130
+ python-pptx#575 — "Add shapes to Slide Master" — and #1044 —
1131
+ "Add textbox to layout"). Mutation surface on master / layout
1132
+ shape collections. Reads remain empty (REST snapshot doesn't
1133
+ materialise master / layout shapes as first-class read targets),
1134
+ but writes round-trip through `AddShapeOnMaster` so the addition
1135
+ is inherited by every slide using the master / layout.
1136
+
1137
+ ```python
1138
+ master = prs.slide_masters[0]
1139
+ master.shapes.add_picture("logo.png",
1140
+ Inches(0.25), Inches(0.25),
1141
+ width=Inches(1))
1142
+ ```
1143
+
1144
+ ### Audit notes
1145
+
1146
+ - **python-pptx#1085 / #925 / #899 (group-shape coords)** — verified
1147
+ not applicable to this SDK. The REST snapshot carries
1148
+ server-resolved absolute coordinates for every shape, including
1149
+ group children that surface as flat slide-level siblings, so the
1150
+ group-relative coord bug the upstream issues describe can't occur
1151
+ here.
1152
+
1153
+ When upstream python-pptx eventually lands native versions of any of
1154
+ these surfaces, the SDK should swap to the upstream signature in
1155
+ place — this section is the canonical reference for what each Athena
1156
+ extension wires up.
1157
+
1158
+ ---
1159
+
1160
+ ## Tier B — next-wave upstream features (v0.1.82)
1161
+
1162
+ Second wave from the open-issue spectrum. Same contract as v0.1.81:
1163
+ upstream surface preserved, additions tagged with
1164
+ `@athena_extension(issue=…, since="0.1.82")`. New wire commands in
1165
+ `pptx.commands`: `SetShapeHidden`, `SetPointInvertIfNegative`,
1166
+ `SetSeriesErrorBars`, `SetConnectorEndpoint`, `AddMovie`,
1167
+ `SetChartHyperlink`, `ApplyForeignLayout`, `HideChartSeries`.
1168
+
1169
+ ### Shape and connector additions
1170
+
1171
+ - **`Shape.hidden`** (closes python-pptx#971). Boolean read/write
1172
+ toggle that emits `SetShapeHidden` so the `<p:cNvPr hidden="1">`
1173
+ flag round-trips. Hidden shapes are excluded from render + Selection
1174
+ Pane but stay in the OOXML tree.
1175
+
1176
+ - **`Connector.begin_connect` / `end_connect`** now emit
1177
+ `SetConnectorEndpoint` (closes python-pptx#946, #1017). The
1178
+ pre-0.1.82 local-state-only stubs were exposed only so `isinstance`-
1179
+ style code didn't `AttributeError`; the new wire op makes the
1180
+ binding survive the round-trip and tracks the bound shape if it
1181
+ moves.
1182
+
1183
+ - **`Connector.set_elbow_adjustment(fraction, endpoint="begin")`**
1184
+ (closes python-pptx#1017 specifically). Adjusts the midpoint of an
1185
+ elbow connector; `fraction` is the position along the major axis
1186
+ (`0.0` first endpoint, `1.0` second). No-op on STRAIGHT / CURVE
1187
+ connectors.
1188
+
1189
+ ### Chart additions
1190
+
1191
+ - **`Point.invert_if_negative`** (closes python-pptx#776). Per-data-
1192
+ point override that emits `SetPointInvertIfNegative`.
1193
+ Series-level `invert_if_negative` is overridden the moment any
1194
+ point in the series has its own `<c:spPr>`, so per-point control
1195
+ is the only reliable color-inversion path once explicit colors
1196
+ are set.
1197
+
1198
+ - **`DataLabels.border_color_hex / border_width / border_dash_style`**
1199
+ (closes python-pptx#716, 17-comment thread). Adds outline styling
1200
+ to plot- and series-level data labels via the existing
1201
+ `SetDataLabelStyle` / `SetSeriesDataLabelStyle` patch envelopes.
1202
+ Both writes and reads work consistently with the rest of the
1203
+ `DataLabels` surface.
1204
+
1205
+ - **`Chart.set_hyperlink(url=..., target_slide_index=..., tooltip=...)`**
1206
+ (closes python-pptx#962). Routes chart-level click-actions through
1207
+ `SetChartHyperlink` since the canonical `SetClickAction`
1208
+ path doesn't carry chart-shape relationships.
1209
+
1210
+ - **`Chart.hide_series(*series_ids)`** (closes python-pptx#1043).
1211
+ Hides series from the rendered plot without dropping them from the
1212
+ underlying chart-data workbook — callers can run the same data
1213
+ through multiple chart templates that surface different subsets.
1214
+
1215
+ - **Office 2016+ `pareto` chart type round-trips** —
1216
+ `AddChart2016.validate()` now includes `"pareto"` in the valid
1217
+ set, fixing a pre-merge ratchet that rejected the documented
1218
+ enum value. (Already in Tier A spec, closed in PR #20711 follow-up.)
1219
+
1220
+ ### Slide and presentation additions
1221
+
1222
+ - **`Slide.apply_foreign_layout(master_index, layout_index)`**
1223
+ (closes python-pptx#1109, #1028). Apply a layout from a different
1224
+ master to an existing slide via `ApplyForeignLayout`. The
1225
+ server runs the relationship + theme inheritance fix-up so the
1226
+ slide renders against any master's layout without a deep-copy
1227
+ workaround.
1228
+
1229
+ - **`Slides.remove_slide()` alias for `delete()`** (already in Tier
1230
+ A, restated here because the upstream issue #956 spelled it both
1231
+ ways — both verbs accept a `Slide` or an integer index).
1232
+
1233
+ ### Text additions
1234
+
1235
+ - **`Font.resolve() -> dict`** (closes python-pptx#1063, #765, #883).
1236
+ Returns the *rendered* font properties (name / size_pt / bold /
1237
+ italic / underline / strike / color_hex / language_id) walking
1238
+ paragraph → shape → master → theme inheritance. Closes three
1239
+ threads asking for "what font does this run actually use?" since
1240
+ the bare `font.name` / `.size` surfaces only return the
1241
+ explicitly-set values, not the resolved chain.
1242
+
1243
+ - **`Paragraph.auto_number_prefix`** (closes python-pptx#939).
1244
+ Returns the rendered numbering prefix (`"1."`, `"A)"`, `"i."`) for
1245
+ numbered bullets. Upstream's `paragraph.text` strips the prefix
1246
+ because it's stored as `<a:buAutoNum>` metadata, not text;
1247
+ callers feeding screen-readers / Markdown exporters previously
1248
+ had to manually count siblings.
1249
+
1250
+ ### Picture additions
1251
+
1252
+ - **`Picture.transparency`** (closes python-pptx#1020). Fractional
1253
+ alpha (0.0 opaque .. 1.0 fully transparent) read from the OOXML
1254
+ `<a:blipFill><a:blip><a:alphaModFix amt="…">` stored on the
1255
+ picture-fill. Read-only — write goes through the existing
1256
+ picture-fill set-transparency path.
1257
+
1258
+ - **`Picture.freeform_geometry`** (closes python-pptx#1020). Returns
1259
+ the freeform clip-path points for pictures whose `<p:spPr>`
1260
+ carries a `<a:custGeom>` (e.g. wave-clipped photos). `None` when
1261
+ the picture uses the default rectangular clip.
1262
+
1263
+ ### Movie / video additions
1264
+
1265
+ - **`Shapes.add_movie(...)` and `Shapes.add_video(...)` alias**
1266
+ (closes python-pptx#811 start time, #1034 online video URL, #954
1267
+ corrupted-file workflow, #974 safe deletion). New first-class
1268
+ author path for video shapes via :class:`AddMovie`. Upstream's
1269
+ `Movie` class is read-only; users have to deepcopy the OOXML to
1270
+ add new movies, which routinely produces corrupt files for
1271
+ non-OOXML animations (#954). The signature mirrors python-pptx's
1272
+ positional surface (`movie_file, left, top, width, height,
1273
+ poster_frame_image, mime_type`) plus keyword-only Athena extras:
1274
+ `url=` (online video URL — closes #1034), `start_time=` /
1275
+ `end_time=` (in seconds — closes #811), `autoplay`,
1276
+ `show_controls`, `loop`, `name`.
1277
+
1278
+ ### Read-only / shape-collection additions
1279
+
1280
+ - **`Shapes.by_name(name)` and `Shapes.get_by_name(name, default=None)`**
1281
+ (closes python-pptx#532, 5-year-old request for a
1282
+ Selection-Pane-equivalent listing). `by_name` raises `KeyError`
1283
+ with the list of available names on a miss; `get_by_name` returns
1284
+ the supplied default. Mirrors the shape `SlideLayouts.by_name` /
1285
+ `get_by_name` pair already exposed for layouts.
1286
+
1287
+ ### Image / mime fixes
1288
+
1289
+ - **`Image.content_type` correctly identifies EMF files**
1290
+ (closes python-pptx#1042). Pre-0.1.82 the legacy fallback used
1291
+ the WMF magic bytes for any `.emf`, returning `image/x-wmf` /
1292
+ `.wmf`. The sniffer now checks the EMR_HEADER signature first
1293
+ and falls back to the filename suffix; EMF files round-trip with
1294
+ the correct `image/x-emf` / `.emf`.
1295
+
1296
+ ### Audit notes
1297
+
1298
+ - **python-pptx#1099 (slide notes missing after slide 20)** —
1299
+ verified not applicable here. Notes are pre-parsed server-side
1300
+ and surfaced via the snapshot; the upstream XML-parser bug
1301
+ doesn't live in our SDK.
1302
+
1303
+ - **python-pptx#1051 (Keynote speaker-notes interop)** — same
1304
+ rationale: notes-master bootstrap runs in the ingest worker and
1305
+ is unaffected by the SDK surface.
1306
+
1307
+ - **python-pptx#1038 (`MSO_LANGUAGE_ID has no XML mapping for
1308
+ 'en-CN'`)** — not applicable. The upstream error originates in
1309
+ python-pptx's XML-tag → enum lookup; the SDK consumes integer
1310
+ LCIDs from the server snapshot so the lookup never runs.
1311
+
1312
+ - **python-pptx#1035 (add_chart return type annotation)** —
1313
+ already correct in the SDK. `add_chart(...) -> "GraphicFrame"`
1314
+ (chart accessed via `.chart` on the frame).
1315
+
1316
+ - **python-pptx#1127 (PERCENT_40 typo)** — already correct in the
1317
+ SDK. The upstream issue was a missing leading "P"; our enum has
1318
+ the proper spelling `PERCENT_40 = 6`.
1319
+
1320
+ Total Tier B closures: 16 implemented, 6 audited (already
1321
+ applicable or N/A by REST architecture).
@@ -132,7 +132,7 @@ def flush_all() -> None:
132
132
  _active_buffers[:] = alive
133
133
 
134
134
 
135
- __version__ = "0.2.0"
135
+ __version__ = "0.3.1"
136
136
 
137
137
  __all__ = [
138
138
  # Main entry point