athena-python-pptx 0.17.1__tar.gz → 0.18.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 (108) hide show
  1. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/API_PARITY_REPORT.md +0 -1
  2. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/CHANGELOG.md +17 -0
  3. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/PKG-INFO +1 -1
  4. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/docs/API_PARITY_EXCEPTIONS.md +35 -6
  5. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/__init__.py +1 -1
  6. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_unsupported.py +3 -1
  7. athena_python_pptx-0.18.0/pptx/chart/chart.py +7 -0
  8. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/client.py +48 -0
  9. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/__init__.py +132 -19
  10. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pyproject.toml +1 -1
  11. athena_python_pptx-0.17.1/pptx/chart/chart.py +0 -7
  12. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/.gitignore +0 -0
  13. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/AGENTS.md +0 -0
  14. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/CLAUDE.md +0 -0
  15. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/DEV-GUIDE.md +0 -0
  16. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/PARITY_QUESTIONS.md +0 -0
  17. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/README.md +0 -0
  18. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/docs/athena-api.json +0 -0
  19. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/docs/athena-api.md +0 -0
  20. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_athena_extension.py +0 -0
  21. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_citations.py +0 -0
  22. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_ptc.py +0 -0
  23. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_references.py +0 -0
  24. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/action.py +0 -0
  25. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/batching.py +0 -0
  26. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/__init__.py +0 -0
  27. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/axis.py +0 -0
  28. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/category.py +0 -0
  29. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/data.py +0 -0
  30. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/datalabel.py +0 -0
  31. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/legend.py +0 -0
  32. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/marker.py +0 -0
  33. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/plot.py +0 -0
  34. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/point.py +0 -0
  35. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/series.py +0 -0
  36. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/xlsx.py +0 -0
  37. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/commands.py +0 -0
  38. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/decorators.py +0 -0
  39. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/__init__.py +0 -0
  40. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/chtfmt.py +0 -0
  41. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/color.py +0 -0
  42. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/effect.py +0 -0
  43. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/fill.py +0 -0
  44. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/line.py +0 -0
  45. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/docgen.py +0 -0
  46. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/__init__.py +0 -0
  47. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/action.py +0 -0
  48. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/chart.py +0 -0
  49. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/dml.py +0 -0
  50. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/lang.py +0 -0
  51. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/shapes.py +0 -0
  52. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/text.py +0 -0
  53. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/errors.py +0 -0
  54. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/exc.py +0 -0
  55. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/generated/__init__.py +0 -0
  56. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/generated/autoshape_types.py +0 -0
  57. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/kit.py +0 -0
  58. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/media.py +0 -0
  59. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/__init__.py +0 -0
  60. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/_mirror.py +0 -0
  61. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/_tbl.py +0 -0
  62. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/_txbody.py +0 -0
  63. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/ns.py +0 -0
  64. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/presentation.py +0 -0
  65. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/__init__.py +0 -0
  66. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/autoshape.py +0 -0
  67. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/connector.py +0 -0
  68. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/graphfrm.py +0 -0
  69. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/groupshape.py +0 -0
  70. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/picture.py +0 -0
  71. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/shared.py +0 -0
  72. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/simpletypes.py +0 -0
  73. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/table.py +0 -0
  74. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/text.py +0 -0
  75. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/xmlchemy.py +0 -0
  76. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/package.py +0 -0
  77. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/__init__.py +0 -0
  78. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/_base.py +0 -0
  79. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/chart.py +0 -0
  80. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/coreprops.py +0 -0
  81. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/embeddedpackage.py +0 -0
  82. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/image.py +0 -0
  83. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/media.py +0 -0
  84. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/presentation.py +0 -0
  85. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/slide.py +0 -0
  86. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/presentation.py +0 -0
  87. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/autoshape.py +0 -0
  88. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/base.py +0 -0
  89. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/connector.py +0 -0
  90. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/freeform.py +0 -0
  91. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/graphfrm.py +0 -0
  92. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/group.py +0 -0
  93. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/picture.py +0 -0
  94. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/placeholder.py +0 -0
  95. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/shapetree.py +0 -0
  96. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shared.py +0 -0
  97. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/slide.py +0 -0
  98. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/slides.py +0 -0
  99. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/spec.py +0 -0
  100. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/table.py +0 -0
  101. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/text/__init__.py +0 -0
  102. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/text/fonts.py +0 -0
  103. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/text/layout.py +0 -0
  104. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/text/text.py +0 -0
  105. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/types.py +0 -0
  106. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/typing.py +0 -0
  107. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/units.py +0 -0
  108. {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/util.py +0 -0
@@ -110,7 +110,6 @@ 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
- | `Picture.image.blob` | Image bytes live in the studio's asset store — not materialized into Python |
114
113
 
115
114
  ## Closed Parity Gaps (added 2026-05-18)
116
115
 
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## 0.18.0 — chart.chart_area / chart.plot_area format and add_chart(name=)
6
+
7
+ - `chart.chart_area.format` and `chart.plot_area.format` now round-trip fill
8
+ and line edits through chart specs, OOXML export, import and preview
9
+ rendering. Use `fill.background()` for a transparent chart or plot area.
10
+ `slide.shapes.add_chart(..., name=...)` also accepts an Athena-only shape
11
+ name.
12
+ - `Picture.image` returns python-pptx's `Image`, so `picture.image.blob`,
13
+ `.content_type`, `.ext`, `.sha1`, `.size` and `.filename` work. It returned
14
+ None for every picture, and code reading an existing deck's images had to
15
+ fetch `image_url` by hand with the sandbox key. A picture added in this
16
+ session gives back the bytes it was added from; a picture read from the deck
17
+ fetches its bytes once from the studio's `/media` route with the SDK's own
18
+ credentials (redirects refused, 50 MB cap) and caches them on the shape. A
19
+ picture with no media raises `ValueError("no embedded image")`, as
20
+ python-pptx does.
21
+
5
22
  ## 0.17.1
6
23
 
7
24
  - `Table.table_style` returns the table's style GUID, the text of
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: athena-python-pptx
3
- Version: 0.17.1
3
+ Version: 0.18.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
@@ -1285,6 +1285,24 @@ a later `runs[0].text` edit then wrote the empty run and doubled the text.
1285
1285
 
1286
1286
  These properties/methods were added because LLM agents (Claude, GPT, etc.) frequently generate code using these patterns. Without them, agent-generated table code fails at runtime.
1287
1287
 
1288
+ ### `Chart.chart_area` and `Chart.plot_area` formatting
1289
+
1290
+ python-pptx does not expose chart-area or plot-area fill and line formatting.
1291
+ The Athena SDK carries these edits through the chart spec and export patches,
1292
+ so a transparent chart background does not require editing private XML:
1293
+
1294
+ ```python
1295
+ chart.chart_area.format.fill.background()
1296
+ chart.plot_area.format.fill.background()
1297
+ ```
1298
+
1299
+ Both areas also support solid RGB or theme fills, brightness, line color,
1300
+ width and dash style. Use `chart.chart_area` for the whole chart frame and
1301
+ `chart.plot_area` for the plotting region.
1302
+ These edits apply to classic charts (authored or uploaded); Office 2016
1303
+ chartEx charts, such as waterfall and treemap, refuse them
1304
+ (`chartExEditsRefusal`).
1305
+
1288
1306
  ### `Font.cap` — PowerPoint-native capitalization mode
1289
1307
 
1290
1308
  The SDK exposes PowerPoint's OOXML run capitalization property as
@@ -1555,10 +1573,13 @@ slide.shapes.add_picture(img.url, Inches(1), Inches(1), width=Inches(6))
1555
1573
 
1556
1574
  ### `Picture.image_url` — owner-scoped media URL for cross-deck image copy
1557
1575
 
1558
- python-pptx upstream exposes `Picture.image` returning an `Image` with
1559
- `.blob` bytes. The REST SDK can't materialize bytes locally, so `.image`
1560
- stays `None` (see the omissions list above). **Additionally**, when a
1561
- picture is loaded from a server snapshot it now carries `image_url`: a
1576
+ `Picture.image` returns python-pptx's `Image` (`.blob`, `.content_type`,
1577
+ `.ext`, `.filename`, `.sha1`, `.size`, `.dpi`): a picture added in this
1578
+ session carries the bytes it was added from, and a picture read from the deck
1579
+ fetches its bytes once from the studio's `/media` route with the SDK's own
1580
+ credentials (it raises `ValueError("no embedded image")`, as python-pptx
1581
+ does, for a picture with no media). **Additionally**, when a
1582
+ picture is loaded from a server snapshot it carries `image_url`: a
1562
1583
  stable `{base_url}/media?key=<assetId>` URL that streams the picture's
1563
1584
  bytes. It returns `None` for pictures with no backing media asset (an
1564
1585
  SDK-added picture not yet flushed/refreshed, a placeholder with no image,
@@ -1584,12 +1605,13 @@ dst.save()
1584
1605
  **Portable code that needs to run against stock python-pptx cannot rely
1585
1606
  on `image_url`** — it is an Athena-only accessor.
1586
1607
 
1587
- ### `name=` kwarg on `add_textbox()` / `add_shape()` / `add_picture()`
1608
+ ### `name=` kwarg on `add_textbox()` / `add_shape()` / `add_picture()` / `add_chart()`
1588
1609
 
1589
1610
  Optional keyword for setting a stable shape name at creation. python-pptx
1590
1611
  upstream auto-assigns names like `"Rectangle 47"` and only exposes
1591
1612
  `shape.name = "..."` as a setter; ours additionally accepts `name=` at
1592
- construction. **Portable code that needs to run against stock python-pptx
1613
+ construction on `add_textbox()`, `add_shape()`, `add_picture()` and
1614
+ `add_chart()`. **Portable code that needs to run against stock python-pptx
1593
1615
  should omit `name=` and assign after the fact.**
1594
1616
 
1595
1617
  When `name=` is omitted, Athena's generated name skips any identical name in
@@ -1607,6 +1629,13 @@ card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
1607
1629
  name="kpi_card_revenue")
1608
1630
  ```
1609
1631
 
1632
+ Charts accept the same keyword:
1633
+
1634
+ ```python
1635
+ frame = slide.shapes.add_chart(chart_type, left, top, width, height, data,
1636
+ name="quarterly_revenue")
1637
+ ```
1638
+
1610
1639
  ### `Presentation.reorder_slides(new_order)`
1611
1640
 
1612
1641
  Reorder slides by passing a permutation of indices. Already shipped in
@@ -327,7 +327,7 @@ def validate_all_touched() -> dict[int, list[str]]:
327
327
  return results
328
328
 
329
329
 
330
- __version__ = "0.17.1"
330
+ __version__ = "0.18.0"
331
331
 
332
332
  __all__ = [
333
333
  # Main entry point
@@ -335,7 +335,9 @@ _TEXT_FRAME = (
335
335
  _SHAPE = "Use the Shape API: text_frame, fill, line, shadow, left / top / width / height, rotation, delete()."
336
336
  _CHART = (
337
337
  "Use the Chart API: chart.plots, chart.series, chart.replace_data(chart_data), "
338
- "chart.has_title / chart.chart_title, chart.value_axis, chart.category_axis."
338
+ "chart.has_title / chart.chart_title, chart.value_axis, chart.category_axis. "
339
+ "For the chart's background use chart.chart_area.format / chart.plot_area.format "
340
+ "(fill.background() makes it transparent)."
339
341
  )
340
342
  # python-pptx's private lxml attributes that recipes reach for (``series._element``,
341
343
  # ``chart._chartSpace``, ...) on objects this SDK has no mirror of. Whatever the
@@ -0,0 +1,7 @@
1
+ """Shim re-exporting upstream's ``pptx.chart.chart`` surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..shapes import Chart, ChartArea, ChartTitle, PlotArea
6
+
7
+ __all__ = ["Chart", "ChartArea", "ChartTitle", "PlotArea"]
@@ -808,6 +808,54 @@ class Client:
808
808
  except requests.RequestException as e:
809
809
  raise ConnectionError(f"Failed to download render from {url}: {e}", url=url)
810
810
 
811
+ def _download_media(self, *, key: str, max_bytes: int) -> bytes:
812
+ """Download one deck media object's bytes with this client's credentials.
813
+
814
+ ``/media`` runs the same deck-access check as every deck op, so the
815
+ bearer stays on the configured API host and a redirect is refused
816
+ rather than followed. Reads stop at ``max_bytes``.
817
+ """
818
+ path = "/media"
819
+ url = f"{self.base_url.rstrip('/')}{path}?key={quote(key, safe='')}"
820
+ headers = self._get_headers(path)
821
+ headers.pop("Content-Type", None)
822
+ headers["Accept"] = "*/*"
823
+ try:
824
+ response = self._session.get(
825
+ url,
826
+ headers=headers,
827
+ timeout=self.timeout,
828
+ allow_redirects=False,
829
+ stream=True,
830
+ )
831
+ try:
832
+ if 300 <= response.status_code < 400:
833
+ raise RemoteError(
834
+ message="Media download redirected; refusing to follow it",
835
+ status_code=response.status_code,
836
+ error_code="MEDIA_REDIRECT",
837
+ )
838
+ if not response.ok:
839
+ self._handle_response(response)
840
+ chunks: list[bytes] = []
841
+ received = 0
842
+ for chunk in response.iter_content(chunk_size=65536):
843
+ if not chunk:
844
+ continue
845
+ received += len(chunk)
846
+ if received > max_bytes:
847
+ raise RemoteError(
848
+ message=f"Media object exceeds the {max_bytes}-byte read cap",
849
+ status_code=response.status_code,
850
+ error_code="MEDIA_TOO_LARGE",
851
+ )
852
+ chunks.append(chunk)
853
+ return b"".join(chunks)
854
+ finally:
855
+ response.close()
856
+ except requests.RequestException as e:
857
+ raise ConnectionError(f"Failed to download media from {url}: {e}", url=url) from e
858
+
811
859
  # -------------------------------------------------------------------------
812
860
  # Deck operations
813
861
  # -------------------------------------------------------------------------
@@ -78,7 +78,7 @@ from ..enum.chart import (
78
78
  XL_TICK_LABEL_POSITION,
79
79
  XL_TICK_MARK,
80
80
  )
81
- from ..parts.image import _native_picture_size, _scale_picture_size
81
+ from ..parts.image import Image, _native_picture_size, _scale_picture_size
82
82
 
83
83
  # ``add_picture`` size for a missing dimension of an image whose header it
84
84
  # cannot read (1 inch, the server's own default).
@@ -373,6 +373,16 @@ def _picture_descr(image_file: Any, image_format: str) -> str:
373
373
  return f"image.{'jpg' if image_format == 'jpeg' else image_format}"
374
374
 
375
375
 
376
+ def _loaded_image_filename(image_file: Any) -> Optional[str]:
377
+ """The file name python-pptx's ``Image.filename`` reports for ``image_file``:
378
+ the basename of a filesystem path, None for bytes, streams and URLs."""
379
+ if isinstance(image_file, Path):
380
+ return image_file.name
381
+ if isinstance(image_file, str) and not image_file.startswith(("http://", "https://")):
382
+ return os.path.basename(image_file)
383
+ return None
384
+
385
+
376
386
  def _fetch_image_from_url(url: str) -> bytes:
377
387
  """Fetch image bytes from a URL.
378
388
 
@@ -6683,6 +6693,60 @@ class _ChartElementFormat(ChartFormat):
6683
6693
  return self._line_cached # type: ignore[return-value]
6684
6694
 
6685
6695
 
6696
+ class ChartArea:
6697
+ """Chart-area formatting adapter (Athena extension)."""
6698
+
6699
+ _spec_key = "chartAreaFormat"
6700
+ _patch_op = "SetChartAreaFormat"
6701
+ _label = "chart.chart_area.format"
6702
+
6703
+ def __init__(self, shape: "Shape"):
6704
+ self._shape = shape
6705
+ self._format_cached: Optional[_ChartElementFormat] = None
6706
+
6707
+ @property
6708
+ def format(self) -> ChartFormat:
6709
+ """Fill and line formatting for the chart frame."""
6710
+ if self._format_cached is None:
6711
+ shape = self._shape
6712
+
6713
+ def read() -> Optional[dict[str, Any]]:
6714
+ spec = _chart_spec_dict(shape)
6715
+ element_format = spec.get(self._spec_key) if spec is not None else None
6716
+ return element_format if isinstance(element_format, dict) else None
6717
+
6718
+ def store(element_format: dict[str, Any]) -> None:
6719
+ spec = _chart_spec_dict(shape)
6720
+ if spec is None:
6721
+ return
6722
+ if element_format:
6723
+ spec[self._spec_key] = element_format
6724
+ else:
6725
+ spec.pop(self._spec_key, None)
6726
+
6727
+ def send(edit: dict[str, Any]) -> None:
6728
+ _emit_chart_patches(
6729
+ shape, [{"op": self._patch_op, "chartId": shape.shape_id, **edit}]
6730
+ )
6731
+
6732
+ self._format_cached = _ChartElementFormat(
6733
+ chart_shape=shape,
6734
+ label=self._label,
6735
+ read=read,
6736
+ store=store,
6737
+ send=send,
6738
+ )
6739
+ return self._format_cached
6740
+
6741
+
6742
+ class PlotArea(ChartArea):
6743
+ """Plot-area formatting adapter (Athena extension)."""
6744
+
6745
+ _spec_key = "plotAreaFormat"
6746
+ _patch_op = "SetPlotAreaFormat"
6747
+ _label = "chart.plot_area.format"
6748
+
6749
+
6686
6750
  def _chart_spec_dict(shape: Any) -> Optional[dict[str, Any]]:
6687
6751
  """The chart shape's ``chartSpec`` dict (created empty for a chart that
6688
6752
  has none yet, so a local mirror has somewhere to live)."""
@@ -8024,6 +8088,8 @@ class Chart:
8024
8088
  self._category_axis_cached: Optional[Union[CategoryAxis, ValueAxis]] = None
8025
8089
  self._value_axis_cached: Optional[ValueAxis] = None
8026
8090
  self._legend_cached: Optional[Legend] = None
8091
+ self._chart_area_cached: Optional[ChartArea] = None
8092
+ self._plot_area_cached: Optional[PlotArea] = None
8027
8093
  self._font_cached: Optional[Any] = None
8028
8094
  self._series_cached: Optional[_ChartSeriesCollection] = None
8029
8095
 
@@ -8400,6 +8466,20 @@ class Chart:
8400
8466
  """
8401
8467
  return self._chart_title_obj
8402
8468
 
8469
+ @property
8470
+ def chart_area(self) -> ChartArea:
8471
+ """Fill and line format for the chart frame (Athena extension)."""
8472
+ if self._chart_area_cached is None:
8473
+ self._chart_area_cached = ChartArea(self._shape)
8474
+ return self._chart_area_cached
8475
+
8476
+ @property
8477
+ def plot_area(self) -> PlotArea:
8478
+ """Fill and line format for the plot region (Athena extension)."""
8479
+ if self._plot_area_cached is None:
8480
+ self._plot_area_cached = PlotArea(self._shape)
8481
+ return self._plot_area_cached
8482
+
8403
8483
  @chart_title.setter
8404
8484
  def chart_title(self, value: Any) -> None:
8405
8485
  """Backwards-compat: accept a string and route it through SetChartTitle.
@@ -11401,6 +11481,8 @@ class Shapes:
11401
11481
  properties={"name": name},
11402
11482
  )
11403
11483
  shape._source = "sdk"
11484
+ shape._added_image_blob = image_data
11485
+ shape._added_image_filename = _loaded_image_filename(image_file)
11404
11486
  self._shapes.append(shape)
11405
11487
  self._shapes_by_id[shape_id] = shape
11406
11488
  return shape
@@ -11413,6 +11495,8 @@ class Shapes:
11413
11495
  properties={"name": name},
11414
11496
  )
11415
11497
  shape._source = "sdk"
11498
+ shape._added_image_blob = image_data
11499
+ shape._added_image_filename = _loaded_image_filename(image_file)
11416
11500
  self._shapes.append(shape)
11417
11501
  self._shapes_by_id[client_id] = shape
11418
11502
  return shape
@@ -12595,12 +12679,15 @@ class Shapes:
12595
12679
  cx: Length,
12596
12680
  cy: Length,
12597
12681
  chart_data: Any,
12682
+ *,
12683
+ name: Optional[str] = None,
12598
12684
  ) -> "GraphicFrame":
12599
12685
  """Author a chart. ``x``/``y``/``cx``/``cy`` mirror python-pptx's
12600
12686
  ``SlideShapes.add_chart`` signature (top-left position and size in EMU).
12687
+ ``name`` is an Athena extension for assigning the shape name.
12601
12688
  """
12602
12689
  left, top, width, height = x, y, cx, cy
12603
- return self._add_chart_impl(chart_type, left, top, width, height, chart_data)
12690
+ return self._add_chart_impl(chart_type, left, top, width, height, chart_data, name=name)
12604
12691
 
12605
12692
  def _add_chart_impl(
12606
12693
  self,
@@ -12610,6 +12697,8 @@ class Shapes:
12610
12697
  width: Length,
12611
12698
  height: Length,
12612
12699
  chart_data: Any,
12700
+ *,
12701
+ name: Optional[str] = None,
12613
12702
  ) -> "GraphicFrame":
12614
12703
  """
12615
12704
  Author a brand-new chart on this slide.
@@ -12641,7 +12730,7 @@ class Shapes:
12641
12730
  cx_kind = _is_cx_chart_type(chart_type)
12642
12731
  if cx_kind is not None:
12643
12732
  return self._add_chart_2016_impl(
12644
- cx_kind, left, top, width, height, chart_data,
12733
+ cx_kind, left, top, width, height, chart_data, name=name,
12645
12734
  )
12646
12735
 
12647
12736
  chart_type_str, grouping, markers, categories, series_payload = _sdk_chart_payload(
@@ -12656,7 +12745,7 @@ class Shapes:
12656
12745
  client_id = f"cht_{uuid.uuid4().hex[:8]}"
12657
12746
  shape_id = client_id
12658
12747
  # Mirror python-pptx: "Chart <count_after_add>", as add_table names tables.
12659
- name = self._next_auto_name("Chart")
12748
+ name = name if name is not None else self._next_auto_name("Chart")
12660
12749
 
12661
12750
  if self._buffer:
12662
12751
  cmd = AddChartCmd(
@@ -12718,6 +12807,8 @@ class Shapes:
12718
12807
  width: Length,
12719
12808
  height: Length,
12720
12809
  chart_data: Any,
12810
+ *,
12811
+ name: Optional[str] = None,
12721
12812
  ) -> "GraphicFrame":
12722
12813
  """Author a 2016+ cx-schema chart.
12723
12814
 
@@ -12763,7 +12854,7 @@ class Shapes:
12763
12854
 
12764
12855
  client_id = f"cht_{uuid.uuid4().hex[:8]}"
12765
12856
  shape_id = client_id
12766
- name = self._next_auto_name("Chart")
12857
+ name = name if name is not None else self._next_auto_name("Chart")
12767
12858
 
12768
12859
  if self._buffer:
12769
12860
  cmd = AddChart2016(
@@ -15988,6 +16079,11 @@ class Picture(Shape):
15988
16079
  # picture's bytes, when loaded from a server snapshot. Powers
15989
16080
  # ``image_url`` so images can be copied across decks.
15990
16081
  self._asset_id = asset_id
16082
+ # Bytes handed to add_picture()/insert_picture() in this session, and
16083
+ # the Image ``image`` built (fetched once per proxy).
16084
+ self._added_image_blob: Optional[bytes] = None
16085
+ self._added_image_filename: Optional[str] = None
16086
+ self._image: Optional[Image] = None
15991
16087
 
15992
16088
  @property
15993
16089
  def line(self) -> LineFormat:
@@ -16061,19 +16157,33 @@ class Picture(Shape):
16061
16157
  return None
16062
16158
 
16063
16159
  @property
16064
- def image(self) -> Optional[Any]:
16065
- """The picture's underlying image (python-pptx parity).
16066
-
16067
- Stock python-pptx returns an ``Image`` instance with ``.blob`` /
16068
- ``.content_type`` / ``.filename`` accessors. The REST SDK doesn't
16069
- materialize image bytes back into Python — bytes live in the
16070
- studio's asset store, reachable via the rendered URL — so this
16071
- always returns ``None``. Provided so ``isinstance``-style
16072
- introspection works without ``AttributeError``. To copy this
16073
- picture into another deck, pass :attr:`image_url` to
16074
- ``dst.shapes.add_picture(...)``.
16075
- """
16076
- return None
16160
+ def image(self) -> Image:
16161
+ """The picture's image (python-pptx parity): an :class:`Image` with
16162
+ ``blob``, ``content_type``, ``ext``, ``filename``, ``sha1``, ``size``
16163
+ and ``dpi``.
16164
+
16165
+ A picture this session added carries the bytes it was added from. A
16166
+ picture read from the deck fetches its bytes once from the studio's
16167
+ media store, with the SDK's own credentials and the same deck-access
16168
+ check every deck op runs, and caches them on this proxy. Raises
16169
+ ``ValueError("no embedded image")``, as python-pptx does, when the
16170
+ picture has no image (an empty placeholder, or a source with no media
16171
+ asset such as an OLE or movie poster).
16172
+ """
16173
+ cached = getattr(self, "_image", None)
16174
+ if cached is not None:
16175
+ return cached
16176
+ blob = getattr(self, "_added_image_blob", None)
16177
+ filename = getattr(self, "_added_image_filename", None)
16178
+ if blob is None:
16179
+ asset_id = getattr(self, "_asset_id", None)
16180
+ client = getattr(getattr(self._slide, "_presentation", None), "_client", None)
16181
+ if not asset_id or client is None:
16182
+ raise ValueError("no embedded image")
16183
+ blob = client._download_media(key=asset_id, max_bytes=_MAX_URL_IMAGE_BYTES)
16184
+ image = Image(blob=blob, filename=filename)
16185
+ self._image = image
16186
+ return image
16077
16187
 
16078
16188
  @property
16079
16189
  @athena_extension(
@@ -16083,7 +16193,8 @@ class Picture(Shape):
16083
16193
  def image_url(self) -> Optional[str]:
16084
16194
  """Stable HTTP(S) URL that serves this picture's image bytes.
16085
16195
 
16086
- Athena extension — not in upstream python-pptx. Returns ``None``
16196
+ Athena extension — not in upstream python-pptx; for the bytes
16197
+ themselves read :attr:`image` ``.blob``. Returns ``None``
16087
16198
  when the picture has no backing media asset (an SDK-added picture
16088
16199
  not yet flushed/refreshed, or a placeholder with no image).
16089
16200
  Otherwise returns ``{base_url}/media?key=<assetId>`` where
@@ -16769,6 +16880,8 @@ class PicturePlaceholder(_BaseSlidePlaceholder):
16769
16880
  placeholder=_dc_replace(placeholder, local_transform_override=None) if placeholder else None,
16770
16881
  source=self._source,
16771
16882
  )
16883
+ picture._added_image_blob = image_data
16884
+ picture._added_image_filename = _loaded_image_filename(image_file)
16772
16885
  self._replace_placeholder_with(picture, method="insert_picture")
16773
16886
  if any(crop.values()):
16774
16887
  picture._emit_crop_change(crop)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "athena-python-pptx"
7
- version = "0.17.1"
7
+ version = "0.18.0"
8
8
  description = "Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,7 +0,0 @@
1
- """Shim re-exporting upstream's ``pptx.chart.chart`` surface."""
2
-
3
- from __future__ import annotations
4
-
5
- from ..shapes import Chart, ChartTitle
6
-
7
- __all__ = ["Chart", "ChartTitle"]