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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/API_PARITY_REPORT.md +28 -5
  2. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/PKG-INFO +1 -1
  3. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/docs/API_PARITY_EXCEPTIONS.md +435 -2
  4. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/__init__.py +1 -1
  5. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/client.py +36 -1
  6. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/commands.py +1089 -47
  7. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/enum/chart.py +12 -0
  8. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/enum/dml.py +45 -0
  9. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/errors.py +48 -0
  10. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/parts/chart.py +18 -2
  11. athena_python_pptx-0.3.0/pptx/parts/image.py +186 -0
  12. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/presentation.py +141 -12
  13. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/__init__.py +1996 -62
  14. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/slides.py +549 -20
  15. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/text/__init__.py +605 -32
  16. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pyproject.toml +1 -1
  17. athena_python_pptx-0.2.0/pptx/parts/image.py +0 -85
  18. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/.gitignore +0 -0
  19. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/CHANGELOG.md +0 -0
  20. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/CLAUDE.md +0 -0
  21. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/DEV-GUIDE.md +0 -0
  22. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/PARITY_QUESTIONS.md +0 -0
  23. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/PUBLISHING.md +0 -0
  24. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/README.md +0 -0
  25. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/docs/athena-api.json +0 -0
  26. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/docs/athena-api.md +0 -0
  27. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/_athena_extension.py +0 -0
  28. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/_ptc.py +0 -0
  29. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/_references.py +0 -0
  30. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/action.py +0 -0
  31. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/batching.py +0 -0
  32. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/__init__.py +0 -0
  33. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/axis.py +0 -0
  34. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/category.py +0 -0
  35. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/chart.py +0 -0
  36. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/data.py +0 -0
  37. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/datalabel.py +0 -0
  38. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/legend.py +0 -0
  39. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/marker.py +0 -0
  40. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/plot.py +0 -0
  41. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/point.py +0 -0
  42. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/series.py +0 -0
  43. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/chart/xlsx.py +0 -0
  44. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/decorators.py +0 -0
  45. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/dml/__init__.py +0 -0
  46. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/dml/chtfmt.py +0 -0
  47. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/dml/color.py +0 -0
  48. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/dml/effect.py +0 -0
  49. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/dml/fill.py +0 -0
  50. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/dml/line.py +0 -0
  51. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/docgen.py +0 -0
  52. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/enum/__init__.py +0 -0
  53. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/enum/action.py +0 -0
  54. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/enum/lang.py +0 -0
  55. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/enum/shapes.py +0 -0
  56. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/enum/text.py +0 -0
  57. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/exc.py +0 -0
  58. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/media.py +0 -0
  59. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/package.py +0 -0
  60. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/parts/__init__.py +0 -0
  61. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/parts/_base.py +0 -0
  62. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/parts/coreprops.py +0 -0
  63. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/parts/embeddedpackage.py +0 -0
  64. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/parts/media.py +0 -0
  65. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/parts/presentation.py +0 -0
  66. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/parts/slide.py +0 -0
  67. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/autoshape.py +0 -0
  68. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/base.py +0 -0
  69. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/connector.py +0 -0
  70. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/freeform.py +0 -0
  71. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/graphfrm.py +0 -0
  72. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/group.py +0 -0
  73. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/picture.py +0 -0
  74. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/placeholder.py +0 -0
  75. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shapes/shapetree.py +0 -0
  76. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/shared.py +0 -0
  77. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/slide.py +0 -0
  78. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/spec.py +0 -0
  79. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/table.py +0 -0
  80. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/text/fonts.py +0 -0
  81. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/text/layout.py +0 -0
  82. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/text/text.py +0 -0
  83. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/types.py +0 -0
  84. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/typing.py +0 -0
  85. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/pptx/units.py +0 -0
  86. {athena_python_pptx-0.2.0 → athena_python_pptx-0.3.0}/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:
@@ -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.0
4
4
  Summary: Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration
5
5
  Project-URL: Homepage, https://github.com/pptx-studio/python-sdk
6
6
  Project-URL: Documentation, https://docs.pptx-studio.com/sdk/python
@@ -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)
@@ -825,3 +886,375 @@ the same table element on the Y.Doc.
825
886
 
826
887
  **`freshness_mode='office_live_link'` is reserved for Phase 5** and
827
888
  currently raises ``ValueError`` locally.
889
+
890
+ ---
891
+
892
+ ## Athena extensions for most-requested upstream features (v0.1.81)
893
+
894
+ These surfaces are **additions** to python-pptx, not deviations — every
895
+ upstream class, method, property, and parameter remains 1:1 with
896
+ python-pptx 1.0.2. Each item below closes (or fundamentally improves)
897
+ a long-standing upstream feature gap. Issue numbers reference
898
+ `scanny/python-pptx`. Every entry is marked at runtime with
899
+ `@athena_extension(issue=…, since="0.1.81")` so the registry walker
900
+ picks them up; the legacy `@athena_only` markers continue to work for
901
+ back-compat.
902
+
903
+ This wave intentionally **excludes animations / Morph transitions**
904
+ (python-pptx#1106 / #942) — they need export-worker and Y.Doc-schema
905
+ work in addition to SDK surface and are tracked separately.
906
+
907
+ ### Shape-level additions
908
+
909
+ - **`Shape.duplicate(target_slide=None, offset_x=None, offset_y=None)`**
910
+ (closes python-pptx#533, #620, 10-comment thread requesting a public
911
+ duplicate API). Alias for the existing `Shape.clone()` — both verbs
912
+ resolve identically. The clone preserves every server-resolved
913
+ property (transform, color, text, type) and accepts the same
914
+ `target_slide` / offset args as `clone()`.
915
+
916
+ - **`LineFormat.head_end_type` / `tail_end_type` / `head_end_length` /
917
+ `tail_end_length` / `head_end_width` / `tail_end_width`** (closes
918
+ python-pptx#1033). Connector / line arrow heads. Accept either
919
+ the upstream `MSO_ARROWHEAD` / `MSO_ARROWHEAD_LENGTH` /
920
+ `MSO_ARROWHEAD_WIDTH` enums or the OOXML short strings
921
+ (`"triangle"` / `"stealth"` / `"diamond"` / `"oval"` / `"arrow"` /
922
+ `"none"`, `"sm"` / `"med"` / `"lg"`). Each setter emits a
923
+ `SetConnectorArrows` wire op carrying all six attrs at once.
924
+
925
+ ```python
926
+ from pptx.enum.dml import MSO_ARROWHEAD
927
+ conn = slide.shapes.add_connector(MSO_CONNECTOR.STRAIGHT,
928
+ Inches(1), Inches(1),
929
+ Inches(5), Inches(3))
930
+ conn.line.tail_end_type = MSO_ARROWHEAD.TRIANGLE
931
+ conn.line.tail_end_length = "med"
932
+ conn.line.tail_end_width = "lg"
933
+ ```
934
+
935
+ - **`Shapes.add_picture(svg_bytes_or_path, …)`** now natively
936
+ supports SVG (closes python-pptx#1112). The picture's content-type
937
+ is detected from the magic bytes (`<?xml … <svg`), the native
938
+ size is parsed from the `<svg width=/height=>` attributes (or the
939
+ `viewBox`), and the wire payload carries `image_format="svg"` so
940
+ the server stores the SVG as a first-class picture asset (PowerPoint
941
+ 2016+ supports SVG natively).
942
+
943
+ - **`Shapes.add_connector(...)` int-coerces float endpoints**
944
+ (closes python-pptx#1058 — "Generating corrupted PPT when using
945
+ connectors"). Calls like `connector(top=shape.top + shape.height / 2)`
946
+ produce floats that legacy code wrote into `<a:off x="...">`
947
+ attributes that Google Slides and modern PowerPoint refuse to open.
948
+ We now route every endpoint through `int(round(float(...)))` so the
949
+ wire payload is integer EMU regardless of input type
950
+ (`float` / `numpy.float64` / `Decimal`).
951
+
952
+ ### Text-frame and run additions
953
+
954
+ - **`Hyperlink.target_slide`** (closes python-pptx#1077). Run-level
955
+ internal slide jumps. Assign a `Slide` proxy or a zero-based int;
956
+ the SDK emits a `SetRunHyperlinkTarget` command with the
957
+ `targetSlideIndex` payload, and the server writes
958
+ `<a:hlinkClick action="ppaction://hlinksldjump">` with the matching
959
+ slide-jump relationship. Mutually exclusive with `address` (an
960
+ external URL); setting one nulls the other.
961
+
962
+ ```python
963
+ run.hyperlink.target_slide = prs.slides[3]
964
+ # or
965
+ run.hyperlink.target_slide = 3
966
+ ```
967
+
968
+ - **`Run.font.color.rgb` overrides the theme hyperlink color**
969
+ (closes python-pptx#940 — "Set/change font color when working with
970
+ Hyperlinks is impossible", 11-comment thread). The Hyperlink
971
+ emitter now reads the run's explicit color and ships an
972
+ `overrideColorHex` field on `SetRunHyperlinkTarget` so the server
973
+ writes the matching `<a:rPr><a:solidFill>` over the run instead of
974
+ letting the theme `hlink` color win.
975
+
976
+ - **`TextFrame.replace_text(pattern, replacement, *, regex=False,
977
+ match_case=False, whole_word=False, max_replacements=None)`**
978
+ (closes python-pptx#684, #1037, #884). Find-and-replace that
979
+ preserves run-level formatting (fonts / colors / sizes / bold /
980
+ italic) by splitting runs across the match boundary on the server.
981
+ Returns the count of substitutions performed.
982
+
983
+ - **`TextFitter.best_fit_font_size(text, extents, max_size, font_file)`**
984
+ no longer returns `max_size` unconditionally (closes
985
+ python-pptx#715, #936, #970, #1026). When a base URL is configured
986
+ (`ATHENA_PPTX_BASE_URL`), the classmethod issues a `MeasureTextFit`
987
+ query to the studio's text-measurement endpoint and returns the
988
+ largest size that fits. The upstream signature is preserved exactly
989
+ (additional knobs read from `ATHENA_PPTX_TEXTFITTER_*` env vars).
990
+ Falls back to `max_size` with a `RuntimeWarning` if no client is
991
+ reachable so offline tests keep working.
992
+
993
+ ### Table additions
994
+
995
+ - **`_RowCollection.add(*, height=None, copy_format_from=None)`** and
996
+ **`_RowCollection.insert(index, *, height=None, copy_format_from=None)`**
997
+ (closes python-pptx#895, #1016, 17-comment thread). Restores the
998
+ `rows.add()` method removed in upstream 1.0.0 and adds an indexed
999
+ `insert()` companion. `copy_format_from=<row_index>` inherits the
1000
+ source row's height + every cell's formatting.
1001
+
1002
+ - **`_ColumnCollection.add(*, width=None, copy_format_from=None)`** and
1003
+ **`_ColumnCollection.insert(index, ...)`** — column-axis mirror.
1004
+ `Table.add_column()` / `Table.remove_column()` / `Table.delete_column()`
1005
+ / `Table.delete_row()` are convenience wrappers on top.
1006
+
1007
+ ```python
1008
+ table.rows.add(copy_format_from=0)
1009
+ table.columns.insert(2, width=Inches(1.5))
1010
+ table.delete_row(-1)
1011
+ table.delete_column(0)
1012
+ ```
1013
+
1014
+ ### Chart additions
1015
+
1016
+ - **`DataLabel.show_value` / `show_series_name` / `show_category_name` /
1017
+ `show_percentage` / `number_format` / `text` / `font` / `position`**
1018
+ (closes python-pptx#1068, #1024, #1025). Per-point data-label
1019
+ overrides. Reach via `series.points[i].data_label.show_value = True`;
1020
+ each setter emits a `SetPointDataLabelStyle` wire op so the
1021
+ server can apply the override on top of the series default.
1022
+
1023
+ - **`_ChartTitle.left` / `top` / `width` / `height`** (closes
1024
+ python-pptx#1030 — "How to set manually chart_title position?").
1025
+ Fractional layout (0.0 – 1.0 of the plot area) for the manual
1026
+ title placement. Each setter emits `SetChartTitlePosition` which
1027
+ the server materialises as `<c:layout><c:manualLayout xMode="edge"
1028
+ yMode="edge" …>`. Setting any axis to `None` clears the override and
1029
+ falls back to PowerPoint's auto-placement.
1030
+
1031
+ - **Office 2016+ chart types**: `XL_CHART_TYPE.TREEMAP` (117),
1032
+ `SUNBURST` (118), `HISTOGRAM` (119), `PARETO` (120),
1033
+ `BOX_WHISKER` (121), `WATERFALL` (122), `FUNNEL` (123). Closes
1034
+ python-pptx#583 ("New Chart Types in Office 2016"), #944
1035
+ (Treemap / Scatter), #651 (Waterfall), #1047 (Box plot). These
1036
+ use the cx-namespace OOXML schema instead of the legacy c-schema;
1037
+ `slide.shapes.add_chart(...)` dispatches through a separate
1038
+ `AddChart2016` wire op so the server can author the cx-flavoured
1039
+ chart-part XML.
1040
+
1041
+ ### Slide- and presentation-level additions
1042
+
1043
+ - **`Presentation.create(widescreen=True)` is now the default**
1044
+ (closes python-pptx#1066, 9-comment thread). The blank-template
1045
+ factory creates 16:9 widescreen decks at 14630400 × 8229600 EMU
1046
+ (PowerPoint's "Widescreen (16:9)" preset). Pass `widescreen=False`
1047
+ to fall back to the legacy 4:3 default.
1048
+
1049
+ - **`Presentation.open(deck_id, base_url=None, api_key=None)`**
1050
+ (closes python-pptx#1018 — "open a PPT in append mode"). Named
1051
+ factory that mirrors the iterate-and-edit verb the upstream thread
1052
+ filed under. Internally identical to `Presentation(deck_id=…)`
1053
+ but reads naturally next to `Presentation.create` / `.upload`.
1054
+
1055
+ - **`Presentation.copy_slide_from(source, source_slide_index, *,
1056
+ destination_index=-1)`** (closes python-pptx#1036, #696, #934,
1057
+ three threads totalling 20+ comments asking for cross-deck slide
1058
+ copy). Emits `CopySlideFromDeck`; the server runs the deep copy
1059
+ with full relationship + media-de-duplication handling. Accepts
1060
+ either a live `Presentation` instance (its pending edits are
1061
+ flushed first) or a raw deck id string.
1062
+
1063
+ - **`Slides.remove_slide(slide_or_index)`** (closes python-pptx#956).
1064
+ Alias for the existing `Slides.delete()`. Both verbs accept a
1065
+ `Slide` proxy or an integer index.
1066
+
1067
+ - **`SlideMaster.shapes.add_shape / add_picture / add_textbox /
1068
+ add_table`** and **`SlideLayout.shapes.add_*`** (closes
1069
+ python-pptx#575 — "Add shapes to Slide Master" — and #1044 —
1070
+ "Add textbox to layout"). Mutation surface on master / layout
1071
+ shape collections. Reads remain empty (REST snapshot doesn't
1072
+ materialise master / layout shapes as first-class read targets),
1073
+ but writes round-trip through `AddShapeOnMaster` so the addition
1074
+ is inherited by every slide using the master / layout.
1075
+
1076
+ ```python
1077
+ master = prs.slide_masters[0]
1078
+ master.shapes.add_picture("logo.png",
1079
+ Inches(0.25), Inches(0.25),
1080
+ width=Inches(1))
1081
+ ```
1082
+
1083
+ ### Audit notes
1084
+
1085
+ - **python-pptx#1085 / #925 / #899 (group-shape coords)** — verified
1086
+ not applicable to this SDK. The REST snapshot carries
1087
+ server-resolved absolute coordinates for every shape, including
1088
+ group children that surface as flat slide-level siblings, so the
1089
+ group-relative coord bug the upstream issues describe can't occur
1090
+ here.
1091
+
1092
+ When upstream python-pptx eventually lands native versions of any of
1093
+ these surfaces, the SDK should swap to the upstream signature in
1094
+ place — this section is the canonical reference for what each Athena
1095
+ extension wires up.
1096
+
1097
+ ---
1098
+
1099
+ ## Tier B — next-wave upstream features (v0.1.82)
1100
+
1101
+ Second wave from the open-issue spectrum. Same contract as v0.1.81:
1102
+ upstream surface preserved, additions tagged with
1103
+ `@athena_extension(issue=…, since="0.1.82")`. New wire commands in
1104
+ `pptx.commands`: `SetShapeHidden`, `SetPointInvertIfNegative`,
1105
+ `SetSeriesErrorBars`, `SetConnectorEndpoint`, `AddMovie`,
1106
+ `SetChartHyperlink`, `ApplyForeignLayout`, `HideChartSeries`.
1107
+
1108
+ ### Shape and connector additions
1109
+
1110
+ - **`Shape.hidden`** (closes python-pptx#971). Boolean read/write
1111
+ toggle that emits `SetShapeHidden` so the `<p:cNvPr hidden="1">`
1112
+ flag round-trips. Hidden shapes are excluded from render + Selection
1113
+ Pane but stay in the OOXML tree.
1114
+
1115
+ - **`Connector.begin_connect` / `end_connect`** now emit
1116
+ `SetConnectorEndpoint` (closes python-pptx#946, #1017). The
1117
+ pre-0.1.82 local-state-only stubs were exposed only so `isinstance`-
1118
+ style code didn't `AttributeError`; the new wire op makes the
1119
+ binding survive the round-trip and tracks the bound shape if it
1120
+ moves.
1121
+
1122
+ - **`Connector.set_elbow_adjustment(fraction, endpoint="begin")`**
1123
+ (closes python-pptx#1017 specifically). Adjusts the midpoint of an
1124
+ elbow connector; `fraction` is the position along the major axis
1125
+ (`0.0` first endpoint, `1.0` second). No-op on STRAIGHT / CURVE
1126
+ connectors.
1127
+
1128
+ ### Chart additions
1129
+
1130
+ - **`Point.invert_if_negative`** (closes python-pptx#776). Per-data-
1131
+ point override that emits `SetPointInvertIfNegative`.
1132
+ Series-level `invert_if_negative` is overridden the moment any
1133
+ point in the series has its own `<c:spPr>`, so per-point control
1134
+ is the only reliable color-inversion path once explicit colors
1135
+ are set.
1136
+
1137
+ - **`DataLabels.border_color_hex / border_width / border_dash_style`**
1138
+ (closes python-pptx#716, 17-comment thread). Adds outline styling
1139
+ to plot- and series-level data labels via the existing
1140
+ `SetDataLabelStyle` / `SetSeriesDataLabelStyle` patch envelopes.
1141
+ Both writes and reads work consistently with the rest of the
1142
+ `DataLabels` surface.
1143
+
1144
+ - **`Chart.set_hyperlink(url=..., target_slide_index=..., tooltip=...)`**
1145
+ (closes python-pptx#962). Routes chart-level click-actions through
1146
+ `SetChartHyperlink` since the canonical `SetClickAction`
1147
+ path doesn't carry chart-shape relationships.
1148
+
1149
+ - **`Chart.hide_series(*series_ids)`** (closes python-pptx#1043).
1150
+ Hides series from the rendered plot without dropping them from the
1151
+ underlying chart-data workbook — callers can run the same data
1152
+ through multiple chart templates that surface different subsets.
1153
+
1154
+ - **Office 2016+ `pareto` chart type round-trips** —
1155
+ `AddChart2016.validate()` now includes `"pareto"` in the valid
1156
+ set, fixing a pre-merge ratchet that rejected the documented
1157
+ enum value. (Already in Tier A spec, closed in PR #20711 follow-up.)
1158
+
1159
+ ### Slide and presentation additions
1160
+
1161
+ - **`Slide.apply_foreign_layout(master_index, layout_index)`**
1162
+ (closes python-pptx#1109, #1028). Apply a layout from a different
1163
+ master to an existing slide via `ApplyForeignLayout`. The
1164
+ server runs the relationship + theme inheritance fix-up so the
1165
+ slide renders against any master's layout without a deep-copy
1166
+ workaround.
1167
+
1168
+ - **`Slides.remove_slide()` alias for `delete()`** (already in Tier
1169
+ A, restated here because the upstream issue #956 spelled it both
1170
+ ways — both verbs accept a `Slide` or an integer index).
1171
+
1172
+ ### Text additions
1173
+
1174
+ - **`Font.resolve() -> dict`** (closes python-pptx#1063, #765, #883).
1175
+ Returns the *rendered* font properties (name / size_pt / bold /
1176
+ italic / underline / strike / color_hex / language_id) walking
1177
+ paragraph → shape → master → theme inheritance. Closes three
1178
+ threads asking for "what font does this run actually use?" since
1179
+ the bare `font.name` / `.size` surfaces only return the
1180
+ explicitly-set values, not the resolved chain.
1181
+
1182
+ - **`Paragraph.auto_number_prefix`** (closes python-pptx#939).
1183
+ Returns the rendered numbering prefix (`"1."`, `"A)"`, `"i."`) for
1184
+ numbered bullets. Upstream's `paragraph.text` strips the prefix
1185
+ because it's stored as `<a:buAutoNum>` metadata, not text;
1186
+ callers feeding screen-readers / Markdown exporters previously
1187
+ had to manually count siblings.
1188
+
1189
+ ### Picture additions
1190
+
1191
+ - **`Picture.transparency`** (closes python-pptx#1020). Fractional
1192
+ alpha (0.0 opaque .. 1.0 fully transparent) read from the OOXML
1193
+ `<a:blipFill><a:blip><a:alphaModFix amt="…">` stored on the
1194
+ picture-fill. Read-only — write goes through the existing
1195
+ picture-fill set-transparency path.
1196
+
1197
+ - **`Picture.freeform_geometry`** (closes python-pptx#1020). Returns
1198
+ the freeform clip-path points for pictures whose `<p:spPr>`
1199
+ carries a `<a:custGeom>` (e.g. wave-clipped photos). `None` when
1200
+ the picture uses the default rectangular clip.
1201
+
1202
+ ### Movie / video additions
1203
+
1204
+ - **`Shapes.add_movie(...)` and `Shapes.add_video(...)` alias**
1205
+ (closes python-pptx#811 start time, #1034 online video URL, #954
1206
+ corrupted-file workflow, #974 safe deletion). New first-class
1207
+ author path for video shapes via :class:`AddMovie`. Upstream's
1208
+ `Movie` class is read-only; users have to deepcopy the OOXML to
1209
+ add new movies, which routinely produces corrupt files for
1210
+ non-OOXML animations (#954). The signature mirrors python-pptx's
1211
+ positional surface (`movie_file, left, top, width, height,
1212
+ poster_frame_image, mime_type`) plus keyword-only Athena extras:
1213
+ `url=` (online video URL — closes #1034), `start_time=` /
1214
+ `end_time=` (in seconds — closes #811), `autoplay`,
1215
+ `show_controls`, `loop`, `name`.
1216
+
1217
+ ### Read-only / shape-collection additions
1218
+
1219
+ - **`Shapes.by_name(name)` and `Shapes.get_by_name(name, default=None)`**
1220
+ (closes python-pptx#532, 5-year-old request for a
1221
+ Selection-Pane-equivalent listing). `by_name` raises `KeyError`
1222
+ with the list of available names on a miss; `get_by_name` returns
1223
+ the supplied default. Mirrors the shape `SlideLayouts.by_name` /
1224
+ `get_by_name` pair already exposed for layouts.
1225
+
1226
+ ### Image / mime fixes
1227
+
1228
+ - **`Image.content_type` correctly identifies EMF files**
1229
+ (closes python-pptx#1042). Pre-0.1.82 the legacy fallback used
1230
+ the WMF magic bytes for any `.emf`, returning `image/x-wmf` /
1231
+ `.wmf`. The sniffer now checks the EMR_HEADER signature first
1232
+ and falls back to the filename suffix; EMF files round-trip with
1233
+ the correct `image/x-emf` / `.emf`.
1234
+
1235
+ ### Audit notes
1236
+
1237
+ - **python-pptx#1099 (slide notes missing after slide 20)** —
1238
+ verified not applicable here. Notes are pre-parsed server-side
1239
+ and surfaced via the snapshot; the upstream XML-parser bug
1240
+ doesn't live in our SDK.
1241
+
1242
+ - **python-pptx#1051 (Keynote speaker-notes interop)** — same
1243
+ rationale: notes-master bootstrap runs in the ingest worker and
1244
+ is unaffected by the SDK surface.
1245
+
1246
+ - **python-pptx#1038 (`MSO_LANGUAGE_ID has no XML mapping for
1247
+ 'en-CN'`)** — not applicable. The upstream error originates in
1248
+ python-pptx's XML-tag → enum lookup; the SDK consumes integer
1249
+ LCIDs from the server snapshot so the lookup never runs.
1250
+
1251
+ - **python-pptx#1035 (add_chart return type annotation)** —
1252
+ already correct in the SDK. `add_chart(...) -> "GraphicFrame"`
1253
+ (chart accessed via `.chart` on the frame).
1254
+
1255
+ - **python-pptx#1127 (PERCENT_40 typo)** — already correct in the
1256
+ SDK. The upstream issue was a missing leading "P"; our enum has
1257
+ the proper spelling `PERCENT_40 = 6`.
1258
+
1259
+ Total Tier B closures: 16 implemented, 6 audited (already
1260
+ 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.0"
136
136
 
137
137
  __all__ = [
138
138
  # Main entry point
@@ -22,6 +22,7 @@ from urllib3.util.retry import Retry
22
22
 
23
23
  from .errors import (
24
24
  AuthenticationError,
25
+ BatchPartialError,
25
26
  ConnectionError,
26
27
  ConflictError,
27
28
  ExportError,
@@ -154,10 +155,44 @@ class Client:
154
155
  detail_message = self._format_error_details(details)
155
156
  if detail_message:
156
157
  msg = f"{msg or 'Request failed'}: {detail_message}"
158
+
159
+ error_code = data.get("code") or (
160
+ error_obj.get("code") if isinstance(error_obj, dict) else None
161
+ )
162
+
163
+ created = data.get("created")
164
+ failure_index = data.get("failureIndex")
165
+ if (
166
+ error_code == "COMMAND_FAILED"
167
+ and isinstance(created, dict)
168
+ and isinstance(failure_index, int)
169
+ and not isinstance(failure_index, bool)
170
+ and failure_index >= 0
171
+ ):
172
+ created_before: list[dict[str, Any]] = []
173
+ raw_shape_ids = created.get("shapeIds")
174
+ raw_slide_ids = created.get("slideIds")
175
+ shape_ids = raw_shape_ids if isinstance(raw_shape_ids, list) else []
176
+ slide_ids = raw_slide_ids if isinstance(raw_slide_ids, list) else []
177
+ for sid in shape_ids:
178
+ if isinstance(sid, str):
179
+ created_before.append({"type": "shape", "id": sid})
180
+ for slid in slide_ids:
181
+ if isinstance(slid, str):
182
+ created_before.append({"type": "slide", "id": slid})
183
+ raise BatchPartialError(
184
+ message=msg or f"Batch failed at index {failure_index}",
185
+ status_code=response.status_code,
186
+ failure_index=failure_index,
187
+ created_before_failure=created_before,
188
+ error_code=error_code,
189
+ server_message=msg,
190
+ )
191
+
157
192
  raise RemoteError(
158
193
  message=msg or f"API error: {response.status_code}",
159
194
  status_code=response.status_code,
160
- error_code=data.get("code") or (error_obj.get("code") if isinstance(error_obj, dict) else None),
195
+ error_code=error_code,
161
196
  server_message=msg,
162
197
  )
163
198
  except ValueError: