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.
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/API_PARITY_REPORT.md +0 -1
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/CHANGELOG.md +17 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/PKG-INFO +1 -1
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/docs/API_PARITY_EXCEPTIONS.md +35 -6
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/__init__.py +1 -1
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_unsupported.py +3 -1
- athena_python_pptx-0.18.0/pptx/chart/chart.py +7 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/client.py +48 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/__init__.py +132 -19
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pyproject.toml +1 -1
- athena_python_pptx-0.17.1/pptx/chart/chart.py +0 -7
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/.gitignore +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/AGENTS.md +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/CLAUDE.md +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/README.md +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_athena_extension.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_citations.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_ptc.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/_references.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/action.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/batching.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/category.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/commands.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/errors.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/exc.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/generated/__init__.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/generated/autoshape_types.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/kit.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/media.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/__init__.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/_mirror.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/_tbl.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/_txbody.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/ns.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/presentation.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/__init__.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/connector.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/groupshape.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/picture.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/shared.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/simpletypes.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/table.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/text.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/oxml/xmlchemy.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/package.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/presentation.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/shared.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/slide.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/slides.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/spec.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/table.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/text/__init__.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/types.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/typing.py +0 -0
- {athena_python_pptx-0.17.1 → athena_python_pptx-0.18.0}/pptx/units.py +0 -0
- {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.
|
|
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
|
-
|
|
1559
|
-
`.
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
|
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
|
|
@@ -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
|
|
@@ -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) ->
|
|
16065
|
-
"""The picture's
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16074
|
-
|
|
16075
|
-
|
|
16076
|
-
|
|
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
|
|
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.
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|