athena-python-pptx 0.1.54__tar.gz → 0.1.56__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.1.56/CHANGELOG.md +29 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/PKG-INFO +1 -1
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/__init__.py +1 -1
- athena_python_pptx-0.1.56/pptx/chart/data.py +117 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/client.py +56 -6
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/commands.py +98 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/shapes.py +333 -46
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pyproject.toml +1 -1
- athena_python_pptx-0.1.54/CHANGELOG.md +0 -15
- athena_python_pptx-0.1.54/pptx/chart/data.py +0 -96
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/.gitignore +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/API_PARITY_REPORT.md +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/CLAUDE.md +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/README.md +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/docs/API_PARITY_EXCEPTIONS.md +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/batching.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/errors.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/presentation.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/slides.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/text.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/typing.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/units.py +0 -0
- {athena_python_pptx-0.1.54 → athena_python_pptx-0.1.56}/pptx/util.py +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `athena-python-pptx` are documented in this file.
|
|
4
|
+
|
|
5
|
+
## 0.1.56
|
|
6
|
+
|
|
7
|
+
- **Charts: end-to-end author + edit support** for column / bar / line / area / pie / doughnut (including all stacked variants).
|
|
8
|
+
- `slide.shapes.add_chart(chart_type, left, top, width, height, chart_data)` now authors a fresh chart from scratch — previously raised `UnsupportedFeatureError`. Returns a `GraphicFrame` whose `.chart` property exposes the resulting `Chart` object. Supported types: `XL_CHART_TYPE.COLUMN_CLUSTERED`, `COLUMN_STACKED`, `COLUMN_STACKED_100`, `BAR_CLUSTERED`, `BAR_STACKED`, `BAR_STACKED_100`, `LINE`, `LINE_MARKERS`, `LINE_STACKED`, `LINE_STACKED_100`, `AREA`, `AREA_STACKED`, `AREA_STACKED_100`, `PIE`, `DOUGHNUT`. 3-D / scatter / bubble / radar / stock / surface / combo still raise `UnsupportedFeatureError` with a clearer message listing the supported set.
|
|
9
|
+
- `placeholder.insert_chart(chart_type, chart_data)` now wires up the same path — previously raised `NotImplementedError`.
|
|
10
|
+
- `Chart.replace_data(chart_data)` now emits `UpdateChartData` patches against an existing chart (ingested or just-authored) — previously raised `UnsupportedFeatureError`. Rewrites series values, series names, and category labels in place; embedded `.xlsx` workbook stays in sync.
|
|
11
|
+
- New `Chart.chart_title` setter and `Chart.has_legend` setter emit `SetChartTitle` and `SetLegendVisible` patches respectively.
|
|
12
|
+
- `CategoryChartData.add_series()` and `XyChartData` / `BubbleChartData` now capture data client-side instead of raising eagerly. (Authoring scatter / bubble charts still raises at `add_chart()` time.)
|
|
13
|
+
- `GraphicFrame` extended to host a `Chart` (in addition to `Table`), so `gf.chart` and `gf.has_chart` work.
|
|
14
|
+
|
|
15
|
+
## 0.1.55
|
|
16
|
+
|
|
17
|
+
- Internal: bumped version coordinated with the `Presentation.create()` server-side fix (#19270). No SDK API change.
|
|
18
|
+
|
|
19
|
+
## 0.1.54
|
|
20
|
+
|
|
21
|
+
- `RemoteError.__str__` now includes the HTTP status code (e.g., `[HTTP 400] Invalid request body: ...`) so the status is visible in tracebacks without unpacking `exc.status_code`.
|
|
22
|
+
|
|
23
|
+
## 0.1.39
|
|
24
|
+
|
|
25
|
+
- Added SDK support for `slide.shapes.add_table(...)` and table creation command wiring.
|
|
26
|
+
- Added `slide.notes_slide.notes_text_frame.text` compatibility adapter for python-pptx style notes access.
|
|
27
|
+
- Added support for auto-shape text frame access (`shape.text_frame`) to match python-pptx behavior.
|
|
28
|
+
- Added smoke/integration tests for table creation/cell updates, notes slide adapter, and shape text-frame regression.
|
|
29
|
+
- Updated README examples for notes slide adapter and auto-shape text support.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: athena-python-pptx
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.56
|
|
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
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chart data classes matching python-pptx.
|
|
3
|
+
|
|
4
|
+
These classes capture series data client-side. The actual server-side
|
|
5
|
+
materialization happens via:
|
|
6
|
+
|
|
7
|
+
* `Shape.chart.replace_data(chart_data)` for ingested charts — emits
|
|
8
|
+
`UpdateSeriesValue` / `UpdateCategoryLabel` patches against the existing
|
|
9
|
+
chart part.
|
|
10
|
+
* `slide.shapes.add_chart(chart_type, ..., chart_data)` for SDK-authored
|
|
11
|
+
charts — sends an `AddChart` command that the export-worker
|
|
12
|
+
materializes via `@pptx/chart-ooxml/export/author.ts`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
from typing import Any, Sequence
|
|
17
|
+
|
|
18
|
+
from ..errors import UnsupportedFeatureError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CategoryChartData:
|
|
22
|
+
"""
|
|
23
|
+
Chart data for category-based charts (column, bar, line, area, pie,
|
|
24
|
+
doughnut). Matches python-pptx's `CategoryChartData` API.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._categories: list[str] = []
|
|
29
|
+
self._series: list[tuple[str, list[float | None]]] = []
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def categories(self) -> list[str]:
|
|
33
|
+
"""Category labels (x-axis tick labels)."""
|
|
34
|
+
return self._categories
|
|
35
|
+
|
|
36
|
+
@categories.setter
|
|
37
|
+
def categories(self, value: Sequence[str]) -> None:
|
|
38
|
+
"""Set category labels."""
|
|
39
|
+
self._categories = [str(v) for v in value]
|
|
40
|
+
|
|
41
|
+
def add_series(
|
|
42
|
+
self,
|
|
43
|
+
name: str,
|
|
44
|
+
values: Sequence[float | None],
|
|
45
|
+
number_format: str = "",
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Add a data series.
|
|
48
|
+
|
|
49
|
+
`number_format` is accepted for python-pptx parity but is not
|
|
50
|
+
currently propagated to the chart spec — the embedded workbook's
|
|
51
|
+
format string controls display formatting.
|
|
52
|
+
"""
|
|
53
|
+
self._series.append((name, [None if v is None else float(v) for v in values]))
|
|
54
|
+
|
|
55
|
+
def to_series_payload(self) -> list[dict[str, Any]]:
|
|
56
|
+
"""Internal: convert to the wire shape used by AddChart / patches."""
|
|
57
|
+
return [{"name": name, "values": list(values)} for name, values in self._series]
|
|
58
|
+
|
|
59
|
+
def __len__(self) -> int:
|
|
60
|
+
return len(self._series)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Alias for compatibility with python-pptx (which exposes both names).
|
|
64
|
+
ChartData = CategoryChartData
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class _XySeries:
|
|
68
|
+
def __init__(self, name: str) -> None:
|
|
69
|
+
self.name = name
|
|
70
|
+
self.points: list[tuple[float, float]] = []
|
|
71
|
+
|
|
72
|
+
def add_data_point(self, x: float, y: float) -> None:
|
|
73
|
+
self.points.append((float(x), float(y)))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class XyChartData:
|
|
77
|
+
"""Chart data for XY (scatter) charts. Captured client-side; authoring
|
|
78
|
+
these via the SDK is gated on the chart-ooxml authoring path adding
|
|
79
|
+
scatter support — currently raises `UnsupportedFeatureError` from
|
|
80
|
+
`add_chart()` for scatter types. The data structure itself is
|
|
81
|
+
populated for parity with python-pptx code."""
|
|
82
|
+
|
|
83
|
+
def __init__(self) -> None:
|
|
84
|
+
self._series: list[_XySeries] = []
|
|
85
|
+
|
|
86
|
+
def add_series(self, name: str, values: Any = None) -> _XySeries:
|
|
87
|
+
series = _XySeries(name)
|
|
88
|
+
if values is not None:
|
|
89
|
+
for point in values:
|
|
90
|
+
if isinstance(point, (tuple, list)) and len(point) == 2:
|
|
91
|
+
series.add_data_point(point[0], point[1])
|
|
92
|
+
self._series.append(series)
|
|
93
|
+
return series
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _BubbleSeries(_XySeries):
|
|
97
|
+
def __init__(self, name: str) -> None:
|
|
98
|
+
super().__init__(name)
|
|
99
|
+
self.sizes: list[float] = []
|
|
100
|
+
|
|
101
|
+
def add_data_point(self, x: float, y: float, size: float = 1.0) -> None: # type: ignore[override]
|
|
102
|
+
super().add_data_point(x, y)
|
|
103
|
+
self.sizes.append(float(size))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class BubbleChartData:
|
|
107
|
+
"""Chart data for bubble charts. Same status as `XyChartData` — captured
|
|
108
|
+
client-side, but `add_chart()` for bubble type is unsupported until the
|
|
109
|
+
chart-ooxml authoring path covers bubble plots."""
|
|
110
|
+
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
self._series: list[_BubbleSeries] = []
|
|
113
|
+
|
|
114
|
+
def add_series(self, name: str, values: Any = None) -> _BubbleSeries:
|
|
115
|
+
series = _BubbleSeries(name)
|
|
116
|
+
self._series.append(series)
|
|
117
|
+
return series
|
|
@@ -201,6 +201,7 @@ class Client:
|
|
|
201
201
|
path: str,
|
|
202
202
|
json: Optional[dict] = None,
|
|
203
203
|
params: Optional[dict] = None,
|
|
204
|
+
timeout: Optional[float] = None,
|
|
204
205
|
) -> Any:
|
|
205
206
|
"""Make an HTTP request."""
|
|
206
207
|
url = urljoin(self.base_url + "/", path.lstrip("/"))
|
|
@@ -218,7 +219,7 @@ class Client:
|
|
|
218
219
|
json=json,
|
|
219
220
|
params=params,
|
|
220
221
|
headers=headers,
|
|
221
|
-
timeout=self.timeout,
|
|
222
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
222
223
|
)
|
|
223
224
|
return self._handle_response(response)
|
|
224
225
|
except requests.ConnectionError as e:
|
|
@@ -260,21 +261,70 @@ class Client:
|
|
|
260
261
|
payload = {"name": name} if name else None
|
|
261
262
|
return self._request("POST", "/decks", json=payload)
|
|
262
263
|
|
|
263
|
-
def create_empty_deck(
|
|
264
|
+
def create_empty_deck(
|
|
265
|
+
self,
|
|
266
|
+
name: Optional[str] = None,
|
|
267
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
268
|
+
max_attempts: int = 240,
|
|
269
|
+
) -> dict[str, Any]:
|
|
264
270
|
"""
|
|
265
|
-
Create a new empty deck
|
|
271
|
+
Create a new empty deck and wait until it is ready.
|
|
266
272
|
|
|
267
273
|
Unlike create_deck(), this creates a deck without requiring an upload.
|
|
268
|
-
The
|
|
274
|
+
The server provisions a blank PPTX template and runs it through the
|
|
275
|
+
ingest pipeline; this method blocks until ingest is finished.
|
|
276
|
+
|
|
277
|
+
The server may return either:
|
|
278
|
+
* 201 with status="ready" if ingest finishes within its synchronous
|
|
279
|
+
poll window, or
|
|
280
|
+
* 202 with status="processing" if ingest is still in progress.
|
|
281
|
+
|
|
282
|
+
In the 202 case we poll GET /decks/:id at ``poll_interval`` until the
|
|
283
|
+
status flips to "ready" (or "error", in which case we raise). The
|
|
284
|
+
initial POST uses an extended timeout so it doesn't race the server's
|
|
285
|
+
own poll loop.
|
|
269
286
|
|
|
270
287
|
Args:
|
|
271
288
|
name: Optional name for the deck
|
|
289
|
+
poll_interval: Seconds between status polls
|
|
290
|
+
max_attempts: Maximum number of poll attempts (default 240 ≈ 2 min)
|
|
272
291
|
|
|
273
292
|
Returns:
|
|
274
|
-
Dictionary with id, name, status, slideCount, createdAt
|
|
293
|
+
Dictionary with id, name, status="ready", slideCount, createdAt
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
UploadError: If ingest reports an error or never reaches "ready".
|
|
275
297
|
"""
|
|
276
298
|
payload = {"name": name} if name else None
|
|
277
|
-
|
|
299
|
+
# Use an extended timeout so we don't race the server's synchronous
|
|
300
|
+
# poll window (the server may hold the request open for up to ~30s
|
|
301
|
+
# waiting for ingest to complete before returning 202).
|
|
302
|
+
result = self._request("POST", "/decks/empty", json=payload, timeout=120)
|
|
303
|
+
|
|
304
|
+
deck_id = result.get("id")
|
|
305
|
+
status = result.get("status")
|
|
306
|
+
if status == "ready":
|
|
307
|
+
return result
|
|
308
|
+
if not deck_id:
|
|
309
|
+
raise UploadError(
|
|
310
|
+
"Server did not return a deck id from POST /decks/empty",
|
|
311
|
+
None,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Poll until ingest finishes.
|
|
315
|
+
for _ in range(max_attempts):
|
|
316
|
+
deck = self.get_deck(deck_id)
|
|
317
|
+
current_status = deck.get("status")
|
|
318
|
+
if current_status == "ready":
|
|
319
|
+
return deck
|
|
320
|
+
if current_status == "error":
|
|
321
|
+
raise UploadError("Empty deck ingest failed", deck_id)
|
|
322
|
+
time.sleep(poll_interval)
|
|
323
|
+
|
|
324
|
+
raise UploadError(
|
|
325
|
+
"Empty deck ingest timed out before reaching 'ready'",
|
|
326
|
+
deck_id,
|
|
327
|
+
)
|
|
278
328
|
|
|
279
329
|
def delete_deck(self, deck_id: str) -> None:
|
|
280
330
|
"""Delete a deck."""
|
|
@@ -1371,6 +1371,101 @@ class SubstitutePlaceholder(Command):
|
|
|
1371
1371
|
raise ValidationError("rows and cols required for table", "rows")
|
|
1372
1372
|
|
|
1373
1373
|
|
|
1374
|
+
@dataclass
|
|
1375
|
+
class UpdateChartData(Command):
|
|
1376
|
+
"""
|
|
1377
|
+
Append `ChartPatch` intents to a chart shape's mutation queue.
|
|
1378
|
+
|
|
1379
|
+
The export-worker drains the queue at export time and applies each
|
|
1380
|
+
patch to the underlying chart-part XML via
|
|
1381
|
+
@pptx/chart-ooxml/export/patcher.ts. Supported patch ops mirror the
|
|
1382
|
+
chart-core ChartPatch discriminated union: UpdateSeriesValue,
|
|
1383
|
+
UpdateSeriesName, UpdateCategoryLabel, SetChartTitle,
|
|
1384
|
+
SetLegendVisible, SetSeriesColor.
|
|
1385
|
+
"""
|
|
1386
|
+
|
|
1387
|
+
shape_id: ShapeId
|
|
1388
|
+
patches: list[dict[str, Any]] = field(default_factory=list)
|
|
1389
|
+
|
|
1390
|
+
@property
|
|
1391
|
+
def command_type(self) -> str:
|
|
1392
|
+
return "UpdateChartData"
|
|
1393
|
+
|
|
1394
|
+
def validate(self) -> None:
|
|
1395
|
+
if not self.shape_id:
|
|
1396
|
+
raise ValidationError("shape_id is required", "shape_id")
|
|
1397
|
+
if not self.patches:
|
|
1398
|
+
raise ValidationError("at least one patch is required", "patches")
|
|
1399
|
+
valid_ops = {
|
|
1400
|
+
"UpdateSeriesValue",
|
|
1401
|
+
"UpdateSeriesName",
|
|
1402
|
+
"UpdateCategoryLabel",
|
|
1403
|
+
"SetChartTitle",
|
|
1404
|
+
"SetLegendVisible",
|
|
1405
|
+
"SetSeriesColor",
|
|
1406
|
+
}
|
|
1407
|
+
for i, p in enumerate(self.patches):
|
|
1408
|
+
if not isinstance(p, dict) or p.get("op") not in valid_ops:
|
|
1409
|
+
raise ValidationError(
|
|
1410
|
+
f"patches[{i}].op must be one of {sorted(valid_ops)}",
|
|
1411
|
+
f"patches[{i}]",
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
@dataclass
|
|
1416
|
+
class AddChart(Command):
|
|
1417
|
+
"""
|
|
1418
|
+
Author a brand-new chart from scratch on a slide.
|
|
1419
|
+
|
|
1420
|
+
The chart element is created with provenance='sdk' and no chartPartRef;
|
|
1421
|
+
the export-worker materializes the chart-part XML, embedded workbook,
|
|
1422
|
+
and slide-level relationships via @pptx/chart-ooxml/export/author.ts
|
|
1423
|
+
on the next export.
|
|
1424
|
+
"""
|
|
1425
|
+
|
|
1426
|
+
slide_index: int
|
|
1427
|
+
chart_type: str # 'column' | 'bar' | 'line' | 'area' | 'pie' | 'doughnut'
|
|
1428
|
+
x_emu: int
|
|
1429
|
+
y_emu: int
|
|
1430
|
+
w_emu: int
|
|
1431
|
+
h_emu: int
|
|
1432
|
+
categories: list[str] = field(default_factory=list)
|
|
1433
|
+
series: list[dict[str, Any]] = field(default_factory=list)
|
|
1434
|
+
title: Optional[str] = None
|
|
1435
|
+
grouping: Optional[str] = None # 'clustered' | 'stacked' | 'percentStacked'
|
|
1436
|
+
name: Optional[str] = None
|
|
1437
|
+
client_id: Optional[str] = None
|
|
1438
|
+
|
|
1439
|
+
@property
|
|
1440
|
+
def command_type(self) -> str:
|
|
1441
|
+
return "AddChart"
|
|
1442
|
+
|
|
1443
|
+
def validate(self) -> None:
|
|
1444
|
+
if self.slide_index < 0:
|
|
1445
|
+
raise ValidationError("slide_index must be >= 0", "slide_index")
|
|
1446
|
+
valid_types = {"column", "bar", "line", "area", "pie", "doughnut"}
|
|
1447
|
+
if self.chart_type not in valid_types:
|
|
1448
|
+
raise ValidationError(
|
|
1449
|
+
f"chart_type must be one of {sorted(valid_types)}",
|
|
1450
|
+
"chart_type",
|
|
1451
|
+
)
|
|
1452
|
+
if self.w_emu <= 0:
|
|
1453
|
+
raise ValidationError("w_emu must be positive", "w_emu")
|
|
1454
|
+
if self.h_emu <= 0:
|
|
1455
|
+
raise ValidationError("h_emu must be positive", "h_emu")
|
|
1456
|
+
if not self.series:
|
|
1457
|
+
raise ValidationError("at least one series is required", "series")
|
|
1458
|
+
if self.grouping is not None and self.grouping not in (
|
|
1459
|
+
"clustered",
|
|
1460
|
+
"stacked",
|
|
1461
|
+
"percentStacked",
|
|
1462
|
+
):
|
|
1463
|
+
raise ValidationError(
|
|
1464
|
+
"grouping must be 'clustered', 'stacked', or 'percentStacked'",
|
|
1465
|
+
"grouping",
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
|
|
1374
1469
|
# Type alias for any command
|
|
1375
1470
|
AnyCommand = Union[
|
|
1376
1471
|
AddTextBox,
|
|
@@ -1411,4 +1506,7 @@ AnyCommand = Union[
|
|
|
1411
1506
|
SetShapeAdjustments,
|
|
1412
1507
|
FitText,
|
|
1413
1508
|
SubstitutePlaceholder,
|
|
1509
|
+
# Charts
|
|
1510
|
+
UpdateChartData,
|
|
1511
|
+
AddChart,
|
|
1414
1512
|
]
|
|
@@ -19,7 +19,9 @@ from .commands import (
|
|
|
19
19
|
SetShapeAdjustments, MergeCells as MergeCellsCmd,
|
|
20
20
|
SplitCell as SplitCellCmd, SetCellMargins, SetColWidth, SetRowHeight,
|
|
21
21
|
SetShapeAdjustments, SubstitutePlaceholder,
|
|
22
|
+
UpdateChartData as UpdateChartDataCmd, AddChart as AddChartCmd,
|
|
22
23
|
)
|
|
24
|
+
from .chart.data import CategoryChartData, ChartData
|
|
23
25
|
from .dml.color import RGBColor, ColorFormat
|
|
24
26
|
from .errors import UnsupportedFeatureError
|
|
25
27
|
from .text import TextFrame
|
|
@@ -35,6 +37,75 @@ if TYPE_CHECKING:
|
|
|
35
37
|
# ``from pptx.shapes import MSO_SHAPE`` and
|
|
36
38
|
# ``from pptx.enum.shapes import MSO_SHAPE`` resolve to the same IntEnum.
|
|
37
39
|
from .enum.shapes import MSO_SHAPE # noqa: F811
|
|
40
|
+
from .enum.chart import XL_CHART_TYPE
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Map XL_CHART_TYPE → (server chart_type, grouping). Only the chart types
|
|
44
|
+
# author.ts supports today are listed here; anything else raises
|
|
45
|
+
# UnsupportedFeatureError. Surface area mirrors python-pptx's accepted enum
|
|
46
|
+
# values so callers can switch between python-pptx and athena-python-pptx
|
|
47
|
+
# without code changes for the supported subset.
|
|
48
|
+
_CHART_TYPE_MAP: dict[int, tuple[str, Optional[str]]] = {
|
|
49
|
+
XL_CHART_TYPE.COLUMN_CLUSTERED: ("column", "clustered"),
|
|
50
|
+
XL_CHART_TYPE.COLUMN_STACKED: ("column", "stacked"),
|
|
51
|
+
XL_CHART_TYPE.COLUMN_STACKED_100: ("column", "percentStacked"),
|
|
52
|
+
XL_CHART_TYPE.BAR_CLUSTERED: ("bar", "clustered"),
|
|
53
|
+
XL_CHART_TYPE.BAR_STACKED: ("bar", "stacked"),
|
|
54
|
+
XL_CHART_TYPE.BAR_STACKED_100: ("bar", "percentStacked"),
|
|
55
|
+
XL_CHART_TYPE.LINE: ("line", "clustered"),
|
|
56
|
+
XL_CHART_TYPE.LINE_MARKERS: ("line", "clustered"),
|
|
57
|
+
XL_CHART_TYPE.LINE_STACKED: ("line", "stacked"),
|
|
58
|
+
XL_CHART_TYPE.LINE_STACKED_100: ("line", "percentStacked"),
|
|
59
|
+
XL_CHART_TYPE.AREA: ("area", "clustered"),
|
|
60
|
+
XL_CHART_TYPE.AREA_STACKED: ("area", "stacked"),
|
|
61
|
+
XL_CHART_TYPE.AREA_STACKED_100: ("area", "percentStacked"),
|
|
62
|
+
XL_CHART_TYPE.PIE: ("pie", None),
|
|
63
|
+
XL_CHART_TYPE.DOUGHNUT: ("doughnut", None),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _split_chart_type(chart_type: Any) -> tuple[str, Optional[str]]:
|
|
68
|
+
"""
|
|
69
|
+
Resolve a `XL_CHART_TYPE` (or matching int / str) into the server's
|
|
70
|
+
`(chart_type, grouping)` pair. Raises `UnsupportedFeatureError` for
|
|
71
|
+
chart types the author.ts path does not yet emit (3-D variants,
|
|
72
|
+
scatter, bubble, radar, stock, combo, surface).
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(chart_type, str):
|
|
75
|
+
normalized = chart_type.lower().strip()
|
|
76
|
+
if normalized in {"column", "bar", "line", "area"}:
|
|
77
|
+
return normalized, "clustered"
|
|
78
|
+
if normalized in {"pie", "doughnut"}:
|
|
79
|
+
return normalized, None
|
|
80
|
+
raise UnsupportedFeatureError(
|
|
81
|
+
"add_chart",
|
|
82
|
+
f"Unsupported chart type string '{chart_type}'. "
|
|
83
|
+
"Use one of: 'column', 'bar', 'line', 'area', 'pie', 'doughnut'.",
|
|
84
|
+
)
|
|
85
|
+
try:
|
|
86
|
+
key = int(chart_type)
|
|
87
|
+
except (TypeError, ValueError):
|
|
88
|
+
raise UnsupportedFeatureError(
|
|
89
|
+
"add_chart",
|
|
90
|
+
f"Unsupported chart_type value: {chart_type!r}",
|
|
91
|
+
)
|
|
92
|
+
mapped = _CHART_TYPE_MAP.get(key)
|
|
93
|
+
if mapped is None:
|
|
94
|
+
raise UnsupportedFeatureError(
|
|
95
|
+
"add_chart",
|
|
96
|
+
f"Chart type {chart_type!r} is not supported by the SDK author "
|
|
97
|
+
"path. Supported XL_CHART_TYPE values: COLUMN_CLUSTERED, "
|
|
98
|
+
"COLUMN_STACKED, COLUMN_STACKED_100, BAR_CLUSTERED, BAR_STACKED, "
|
|
99
|
+
"BAR_STACKED_100, LINE, LINE_MARKERS, LINE_STACKED, "
|
|
100
|
+
"LINE_STACKED_100, AREA, AREA_STACKED, AREA_STACKED_100, PIE, "
|
|
101
|
+
"DOUGHNUT.",
|
|
102
|
+
)
|
|
103
|
+
return mapped
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _coerce_chart_type(chart_type: Any) -> str:
|
|
107
|
+
"""Backward-compatible single-string accessor."""
|
|
108
|
+
return _split_chart_type(chart_type)[0]
|
|
38
109
|
|
|
39
110
|
|
|
40
111
|
class PlaceholderFormat:
|
|
@@ -844,10 +915,13 @@ class ClickAction:
|
|
|
844
915
|
|
|
845
916
|
class Chart:
|
|
846
917
|
"""
|
|
847
|
-
|
|
918
|
+
Chart proxy for python-pptx compatibility.
|
|
848
919
|
|
|
849
|
-
|
|
850
|
-
`
|
|
920
|
+
Wraps a chart Shape and exposes the python-pptx chart edit surface.
|
|
921
|
+
Edits emit `UpdateChartData` commands that append `ChartPatch` intents
|
|
922
|
+
onto the chart element's `chartPatches` queue; the export-worker drains
|
|
923
|
+
the queue and applies each patch via
|
|
924
|
+
`@pptx/chart-ooxml/export/patcher.ts` on the next export.
|
|
851
925
|
"""
|
|
852
926
|
|
|
853
927
|
def __init__(self, shape: "Shape"):
|
|
@@ -858,12 +932,115 @@ class Chart:
|
|
|
858
932
|
"""Best-effort chart type if available in element properties."""
|
|
859
933
|
return self._shape._properties.get("chartType")
|
|
860
934
|
|
|
935
|
+
@property
|
|
936
|
+
def has_title(self) -> bool:
|
|
937
|
+
"""python-pptx parity: True if the underlying chart has a title."""
|
|
938
|
+
return bool(self._shape._properties.get("title"))
|
|
939
|
+
|
|
861
940
|
def replace_data(self, chart_data: Any) -> None:
|
|
862
|
-
"""
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
941
|
+
"""
|
|
942
|
+
Replace this chart's series data with the values in `chart_data`.
|
|
943
|
+
|
|
944
|
+
Mirrors python-pptx semantics: existing series are kept in place
|
|
945
|
+
and their values are overwritten in series order. The category
|
|
946
|
+
labels are also rewritten.
|
|
947
|
+
|
|
948
|
+
Implementation: emits `UpdateSeriesValue` patches (one per data
|
|
949
|
+
point) plus `UpdateSeriesName` and `UpdateCategoryLabel` patches.
|
|
950
|
+
Server queues them on the chart element's `chartPatches`; the
|
|
951
|
+
export-worker applies them to the chart-part XML on export.
|
|
952
|
+
"""
|
|
953
|
+
if not isinstance(chart_data, CategoryChartData):
|
|
954
|
+
raise TypeError(
|
|
955
|
+
"replace_data() requires a CategoryChartData instance "
|
|
956
|
+
f"(got {type(chart_data).__name__})"
|
|
957
|
+
)
|
|
958
|
+
|
|
959
|
+
chart_id = self._shape.shape_id
|
|
960
|
+
existing_spec = self._shape._properties.get("chartSpec") or {}
|
|
961
|
+
existing_plots = existing_spec.get("plots") or []
|
|
962
|
+
existing_series_ids: list[str] = []
|
|
963
|
+
if existing_plots:
|
|
964
|
+
for s in (existing_plots[0].get("series") or []):
|
|
965
|
+
existing_series_ids.append(str(s.get("id") or ""))
|
|
966
|
+
|
|
967
|
+
patches: list[dict[str, Any]] = []
|
|
968
|
+
|
|
969
|
+
for s_idx, (name, values) in enumerate(zip(
|
|
970
|
+
[n for n, _ in chart_data._series],
|
|
971
|
+
[v for _, v in chart_data._series],
|
|
972
|
+
)):
|
|
973
|
+
series_id = (
|
|
974
|
+
existing_series_ids[s_idx]
|
|
975
|
+
if s_idx < len(existing_series_ids)
|
|
976
|
+
else f"series-{s_idx}"
|
|
977
|
+
)
|
|
978
|
+
patches.append({
|
|
979
|
+
"op": "UpdateSeriesName",
|
|
980
|
+
"chartId": chart_id,
|
|
981
|
+
"seriesId": series_id,
|
|
982
|
+
"name": name,
|
|
983
|
+
})
|
|
984
|
+
for i, v in enumerate(values):
|
|
985
|
+
patches.append({
|
|
986
|
+
"op": "UpdateSeriesValue",
|
|
987
|
+
"chartId": chart_id,
|
|
988
|
+
"seriesId": series_id,
|
|
989
|
+
"index": i,
|
|
990
|
+
"value": v,
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
for i, label in enumerate(chart_data.categories):
|
|
994
|
+
patches.append({
|
|
995
|
+
"op": "UpdateCategoryLabel",
|
|
996
|
+
"chartId": chart_id,
|
|
997
|
+
"index": i,
|
|
998
|
+
"label": label,
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
if not patches:
|
|
1002
|
+
return
|
|
1003
|
+
|
|
1004
|
+
if self._shape._buffer is not None:
|
|
1005
|
+
self._shape._buffer.add(UpdateChartDataCmd(
|
|
1006
|
+
shape_id=chart_id,
|
|
1007
|
+
patches=patches,
|
|
1008
|
+
))
|
|
1009
|
+
|
|
1010
|
+
@property
|
|
1011
|
+
def chart_title(self) -> Optional[str]:
|
|
1012
|
+
return self._shape._properties.get("title")
|
|
1013
|
+
|
|
1014
|
+
@chart_title.setter
|
|
1015
|
+
def chart_title(self, value: str) -> None:
|
|
1016
|
+
if self._shape._buffer is None:
|
|
1017
|
+
return
|
|
1018
|
+
self._shape._buffer.add(UpdateChartDataCmd(
|
|
1019
|
+
shape_id=self._shape.shape_id,
|
|
1020
|
+
patches=[{
|
|
1021
|
+
"op": "SetChartTitle",
|
|
1022
|
+
"chartId": self._shape.shape_id,
|
|
1023
|
+
"title": str(value),
|
|
1024
|
+
}],
|
|
1025
|
+
))
|
|
1026
|
+
|
|
1027
|
+
@property
|
|
1028
|
+
def has_legend(self) -> bool:
|
|
1029
|
+
legend = self._shape._properties.get("legend") or {}
|
|
1030
|
+
return bool(legend.get("visible"))
|
|
1031
|
+
|
|
1032
|
+
@has_legend.setter
|
|
1033
|
+
def has_legend(self, value: bool) -> None:
|
|
1034
|
+
if self._shape._buffer is None:
|
|
1035
|
+
return
|
|
1036
|
+
self._shape._buffer.add(UpdateChartDataCmd(
|
|
1037
|
+
shape_id=self._shape.shape_id,
|
|
1038
|
+
patches=[{
|
|
1039
|
+
"op": "SetLegendVisible",
|
|
1040
|
+
"chartId": self._shape.shape_id,
|
|
1041
|
+
"visible": bool(value),
|
|
1042
|
+
}],
|
|
1043
|
+
))
|
|
867
1044
|
|
|
868
1045
|
|
|
869
1046
|
class _ElementParentProxy:
|
|
@@ -2303,21 +2480,53 @@ class Shape:
|
|
|
2303
2480
|
"""
|
|
2304
2481
|
Insert a chart into this placeholder.
|
|
2305
2482
|
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
NotImplementedError: Chart insertion is not yet supported.
|
|
2483
|
+
Authoring path: emits an `AddChart` command targeted at the
|
|
2484
|
+
placeholder's slide and uses the placeholder's anchor as the chart
|
|
2485
|
+
bounding box. The export-worker materializes the chart-part XML
|
|
2486
|
+
and embedded workbook on the next export via
|
|
2487
|
+
`@pptx/chart-ooxml/export/author.ts`.
|
|
2312
2488
|
"""
|
|
2313
2489
|
if not self.is_placeholder:
|
|
2314
2490
|
raise ValueError("insert_chart() is only valid on placeholder shapes")
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2491
|
+
if not isinstance(chart_data, CategoryChartData):
|
|
2492
|
+
raise TypeError(
|
|
2493
|
+
"insert_chart() requires a CategoryChartData instance "
|
|
2494
|
+
f"(got {type(chart_data).__name__})"
|
|
2495
|
+
)
|
|
2496
|
+
|
|
2497
|
+
chart_type_str, grouping = _split_chart_type(chart_type)
|
|
2498
|
+
|
|
2499
|
+
transform = self._transform
|
|
2500
|
+
x_emu = int(ensure_emu(self.left)) if transform else 0
|
|
2501
|
+
y_emu = int(ensure_emu(self.top)) if transform else 0
|
|
2502
|
+
w_emu = int(ensure_emu(self.width)) if transform else 6_858_000
|
|
2503
|
+
h_emu = int(ensure_emu(self.height)) if transform else 4_572_000
|
|
2504
|
+
|
|
2505
|
+
if self._buffer is not None:
|
|
2506
|
+
self._buffer.add(AddChartCmd(
|
|
2507
|
+
slide_index=self._slide.slide_index,
|
|
2508
|
+
chart_type=chart_type_str,
|
|
2509
|
+
x_emu=x_emu, y_emu=y_emu, w_emu=w_emu, h_emu=h_emu,
|
|
2510
|
+
categories=list(chart_data.categories),
|
|
2511
|
+
series=chart_data.to_series_payload(),
|
|
2512
|
+
grouping=grouping,
|
|
2513
|
+
))
|
|
2514
|
+
|
|
2515
|
+
host_shape = Shape(
|
|
2516
|
+
shape_id=self._shape_id,
|
|
2517
|
+
slide=self._slide,
|
|
2518
|
+
buffer=self._buffer,
|
|
2519
|
+
element_type="chart",
|
|
2520
|
+
transform=Transform(x=x_emu, y=y_emu, w=w_emu, h=h_emu),
|
|
2521
|
+
properties={"chartType": chart_type_str},
|
|
2522
|
+
source="sdk",
|
|
2523
|
+
)
|
|
2524
|
+
return GraphicFrame(
|
|
2525
|
+
shape_id=self._shape_id,
|
|
2526
|
+
slide=self._slide,
|
|
2527
|
+
buffer=self._buffer,
|
|
2528
|
+
chart=Chart(host_shape),
|
|
2529
|
+
transform=Transform(x=x_emu, y=y_emu, w=w_emu, h=h_emu),
|
|
2321
2530
|
)
|
|
2322
2531
|
|
|
2323
2532
|
def __repr__(self) -> str:
|
|
@@ -2908,20 +3117,81 @@ class Shapes:
|
|
|
2908
3117
|
width: Length,
|
|
2909
3118
|
height: Length,
|
|
2910
3119
|
chart_data: Any,
|
|
2911
|
-
) ->
|
|
2912
|
-
"""
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
3120
|
+
) -> "GraphicFrame":
|
|
3121
|
+
"""
|
|
3122
|
+
Author a brand-new chart on this slide.
|
|
3123
|
+
|
|
3124
|
+
Mirrors python-pptx's `SlideShapes.add_chart()`. Returns a
|
|
3125
|
+
`GraphicFrame` whose `.chart` property exposes the resulting
|
|
3126
|
+
`Chart` proxy. Edits via `chart.replace_data()` / `chart.chart_title`
|
|
3127
|
+
/ `chart.has_legend` are emitted as `UpdateChartData` patches and
|
|
3128
|
+
applied on export.
|
|
3129
|
+
|
|
3130
|
+
Supported chart types: `XL_CHART_TYPE.COLUMN_CLUSTERED`,
|
|
3131
|
+
`BAR_CLUSTERED`, `BAR_STACKED`, `BAR_STACKED_100`,
|
|
3132
|
+
`COLUMN_STACKED`, `COLUMN_STACKED_100`, `LINE`, `LINE_STACKED`,
|
|
3133
|
+
`LINE_STACKED_100`, `AREA`, `AREA_STACKED`, `AREA_STACKED_100`,
|
|
3134
|
+
`PIE`, `DOUGHNUT`. Other types raise `UnsupportedFeatureError`.
|
|
3135
|
+
"""
|
|
3136
|
+
import uuid
|
|
3137
|
+
|
|
3138
|
+
if not isinstance(chart_data, CategoryChartData):
|
|
3139
|
+
raise TypeError(
|
|
3140
|
+
"add_chart() requires a CategoryChartData instance "
|
|
3141
|
+
f"(got {type(chart_data).__name__})"
|
|
3142
|
+
)
|
|
3143
|
+
|
|
3144
|
+
chart_type_str, grouping = _split_chart_type(chart_type)
|
|
3145
|
+
|
|
3146
|
+
x_emu = int(ensure_emu(left))
|
|
3147
|
+
y_emu = int(ensure_emu(top))
|
|
3148
|
+
w_emu = int(ensure_emu(width))
|
|
3149
|
+
h_emu = int(ensure_emu(height))
|
|
3150
|
+
|
|
3151
|
+
client_id = f"cht_{uuid.uuid4().hex[:8]}"
|
|
3152
|
+
shape_id = client_id
|
|
3153
|
+
|
|
3154
|
+
if self._buffer:
|
|
3155
|
+
cmd = AddChartCmd(
|
|
3156
|
+
slide_index=self._slide.slide_index,
|
|
3157
|
+
chart_type=chart_type_str,
|
|
3158
|
+
x_emu=x_emu, y_emu=y_emu, w_emu=w_emu, h_emu=h_emu,
|
|
3159
|
+
categories=list(chart_data.categories),
|
|
3160
|
+
series=chart_data.to_series_payload(),
|
|
3161
|
+
title=None,
|
|
3162
|
+
grouping=grouping,
|
|
3163
|
+
client_id=client_id,
|
|
3164
|
+
)
|
|
3165
|
+
response = self._buffer.add(cmd)
|
|
3166
|
+
if response and response.get("created"):
|
|
3167
|
+
shape_ids = response["created"].get("shapeIds", [])
|
|
3168
|
+
if shape_ids:
|
|
3169
|
+
shape_id = shape_ids[0]
|
|
3170
|
+
|
|
3171
|
+
transform = Transform(x=x_emu, y=y_emu, w=w_emu, h=h_emu)
|
|
3172
|
+
|
|
3173
|
+
host_shape = Shape(
|
|
3174
|
+
shape_id=shape_id,
|
|
3175
|
+
slide=self._slide,
|
|
3176
|
+
buffer=self._buffer,
|
|
3177
|
+
element_type="chart",
|
|
3178
|
+
transform=transform,
|
|
3179
|
+
properties={"chartType": chart_type_str},
|
|
3180
|
+
source="sdk",
|
|
2924
3181
|
)
|
|
3182
|
+
chart_proxy = Chart(host_shape)
|
|
3183
|
+
|
|
3184
|
+
graphic_frame = GraphicFrame(
|
|
3185
|
+
shape_id=shape_id,
|
|
3186
|
+
slide=self._slide,
|
|
3187
|
+
buffer=self._buffer,
|
|
3188
|
+
chart=chart_proxy,
|
|
3189
|
+
transform=transform,
|
|
3190
|
+
)
|
|
3191
|
+
graphic_frame._source = "sdk"
|
|
3192
|
+
self._shapes.append(graphic_frame)
|
|
3193
|
+
self._shapes_by_id[shape_id] = graphic_frame
|
|
3194
|
+
return graphic_frame
|
|
2925
3195
|
|
|
2926
3196
|
# -------------------------------------------------------------------------
|
|
2927
3197
|
# Phase 5: Grouping
|
|
@@ -4074,11 +4344,12 @@ class _ColumnCollection:
|
|
|
4074
4344
|
|
|
4075
4345
|
class GraphicFrame(Shape):
|
|
4076
4346
|
"""
|
|
4077
|
-
A graphic frame shape that contains a table.
|
|
4347
|
+
A graphic frame shape that contains a table or chart.
|
|
4078
4348
|
|
|
4079
4349
|
Mirrors python-pptx's GraphicFrame class.
|
|
4080
|
-
In python-pptx, SlideShapes.add_table() returns a GraphicFrame
|
|
4081
|
-
|
|
4350
|
+
In python-pptx, SlideShapes.add_table() returns a GraphicFrame and
|
|
4351
|
+
SlideShapes.add_chart() returns a GraphicFrame whose `.chart` exposes
|
|
4352
|
+
the underlying Chart object.
|
|
4082
4353
|
"""
|
|
4083
4354
|
|
|
4084
4355
|
def __init__(
|
|
@@ -4086,25 +4357,27 @@ class GraphicFrame(Shape):
|
|
|
4086
4357
|
shape_id: ShapeId,
|
|
4087
4358
|
slide: "Slide",
|
|
4088
4359
|
buffer: Optional["CommandBuffer"],
|
|
4089
|
-
table: "Table",
|
|
4360
|
+
table: Optional["Table"] = None,
|
|
4361
|
+
chart: Optional["Chart"] = None,
|
|
4090
4362
|
transform: Optional[Transform] = None,
|
|
4091
4363
|
):
|
|
4364
|
+
if table is None and chart is None:
|
|
4365
|
+
raise ValueError("GraphicFrame requires a table or chart")
|
|
4092
4366
|
super().__init__(
|
|
4093
4367
|
shape_id=shape_id,
|
|
4094
4368
|
slide=slide,
|
|
4095
4369
|
buffer=buffer,
|
|
4096
|
-
element_type="table",
|
|
4370
|
+
element_type="chart" if chart is not None else "table",
|
|
4097
4371
|
transform=transform,
|
|
4098
4372
|
)
|
|
4099
4373
|
self._table = table
|
|
4374
|
+
self._chart = chart
|
|
4100
4375
|
|
|
4101
4376
|
@property
|
|
4102
4377
|
def table(self) -> "Table":
|
|
4103
|
-
"""
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
Mirrors python-pptx GraphicFrame.table property.
|
|
4107
|
-
"""
|
|
4378
|
+
"""The Table object contained in this graphic frame."""
|
|
4379
|
+
if self._table is None:
|
|
4380
|
+
raise ValueError("This GraphicFrame does not contain a table")
|
|
4108
4381
|
return self._table
|
|
4109
4382
|
|
|
4110
4383
|
@property
|
|
@@ -4112,22 +4385,36 @@ class GraphicFrame(Shape):
|
|
|
4112
4385
|
"""True if this graphic frame contains a table."""
|
|
4113
4386
|
return self._table is not None
|
|
4114
4387
|
|
|
4388
|
+
@property
|
|
4389
|
+
def chart(self) -> "Chart":
|
|
4390
|
+
"""The Chart object contained in this graphic frame."""
|
|
4391
|
+
if self._chart is None:
|
|
4392
|
+
raise ValueError("This GraphicFrame does not contain a chart")
|
|
4393
|
+
return self._chart
|
|
4394
|
+
|
|
4395
|
+
@property
|
|
4396
|
+
def has_chart(self) -> bool:
|
|
4397
|
+
"""True if this graphic frame contains a chart."""
|
|
4398
|
+
return self._chart is not None
|
|
4399
|
+
|
|
4115
4400
|
# Passthrough methods so agent code like tbl.cell(0,0) works directly
|
|
4116
4401
|
def cell(self, row: int, col: int) -> "TableCell":
|
|
4117
4402
|
"""Shortcut: delegates to self.table.cell(row, col)."""
|
|
4118
|
-
return self.
|
|
4403
|
+
return self.table.cell(row, col)
|
|
4119
4404
|
|
|
4120
4405
|
@property
|
|
4121
4406
|
def rows(self) -> "_RowCollection":
|
|
4122
4407
|
"""Shortcut: delegates to self.table.rows."""
|
|
4123
|
-
return self.
|
|
4408
|
+
return self.table.rows
|
|
4124
4409
|
|
|
4125
4410
|
@property
|
|
4126
4411
|
def cols(self) -> int:
|
|
4127
4412
|
"""Shortcut: delegates to self.table.cols."""
|
|
4128
|
-
return self.
|
|
4413
|
+
return self.table.cols
|
|
4129
4414
|
|
|
4130
4415
|
def __repr__(self) -> str:
|
|
4416
|
+
if self._chart is not None:
|
|
4417
|
+
return f"<GraphicFrame shape_id={self._shape_id} chart>"
|
|
4131
4418
|
return f"<GraphicFrame shape_id={self._shape_id} table={self._table._rows}x{self._table._cols}>"
|
|
4132
4419
|
|
|
4133
4420
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "athena-python-pptx"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.56"
|
|
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,15 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `athena-python-pptx` are documented in this file.
|
|
4
|
-
|
|
5
|
-
## 0.1.54
|
|
6
|
-
|
|
7
|
-
- `RemoteError.__str__` now includes the HTTP status code (e.g., `[HTTP 400] Invalid request body: ...`) so the status is visible in tracebacks without unpacking `exc.status_code`.
|
|
8
|
-
|
|
9
|
-
## 0.1.39
|
|
10
|
-
|
|
11
|
-
- Added SDK support for `slide.shapes.add_table(...)` and table creation command wiring.
|
|
12
|
-
- Added `slide.notes_slide.notes_text_frame.text` compatibility adapter for python-pptx style notes access.
|
|
13
|
-
- Added support for auto-shape text frame access (`shape.text_frame`) to match python-pptx behavior.
|
|
14
|
-
- Added smoke/integration tests for table creation/cell updates, notes slide adapter, and shape text-frame regression.
|
|
15
|
-
- Updated README examples for notes slide adapter and auto-shape text support.
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Chart data classes matching python-pptx.
|
|
3
|
-
|
|
4
|
-
These are stub implementations that raise UnsupportedFeatureError.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
from typing import Any, Sequence
|
|
9
|
-
|
|
10
|
-
from ..errors import UnsupportedFeatureError
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class CategoryChartData:
|
|
14
|
-
"""
|
|
15
|
-
Chart data for category-based charts.
|
|
16
|
-
|
|
17
|
-
This is a stub implementation - charts are not yet supported.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
def __init__(self) -> None:
|
|
21
|
-
self._categories: list[str] = []
|
|
22
|
-
self._series: list[tuple[str, Sequence[float]]] = []
|
|
23
|
-
|
|
24
|
-
@property
|
|
25
|
-
def categories(self) -> list[str]:
|
|
26
|
-
"""Category labels."""
|
|
27
|
-
return self._categories
|
|
28
|
-
|
|
29
|
-
@categories.setter
|
|
30
|
-
def categories(self, value: Sequence[str]) -> None:
|
|
31
|
-
"""Set category labels."""
|
|
32
|
-
self._categories = list(value)
|
|
33
|
-
|
|
34
|
-
def add_series(self, name: str, values: Sequence[float], number_format: str = "") -> None:
|
|
35
|
-
"""
|
|
36
|
-
Add a data series.
|
|
37
|
-
|
|
38
|
-
Note: This method stores data but actual chart creation is not yet supported.
|
|
39
|
-
"""
|
|
40
|
-
self._series.append((name, values))
|
|
41
|
-
|
|
42
|
-
def _raise_not_supported(self) -> None:
|
|
43
|
-
raise UnsupportedFeatureError(
|
|
44
|
-
"CategoryChartData",
|
|
45
|
-
"Charts are not yet supported. Data can be stored but charts cannot be created."
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Alias for compatibility
|
|
50
|
-
ChartData = CategoryChartData
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class XyChartData:
|
|
54
|
-
"""
|
|
55
|
-
Chart data for XY (scatter) charts.
|
|
56
|
-
|
|
57
|
-
This is a stub implementation - charts are not yet supported.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
def __init__(self) -> None:
|
|
61
|
-
self._series: list[Any] = []
|
|
62
|
-
|
|
63
|
-
def add_series(self, name: str, values: Any = None) -> Any:
|
|
64
|
-
"""
|
|
65
|
-
Add a data series.
|
|
66
|
-
|
|
67
|
-
Raises:
|
|
68
|
-
UnsupportedFeatureError: Charts are not yet supported
|
|
69
|
-
"""
|
|
70
|
-
raise UnsupportedFeatureError(
|
|
71
|
-
"XyChartData.add_series",
|
|
72
|
-
"XY charts are not yet supported"
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class BubbleChartData:
|
|
77
|
-
"""
|
|
78
|
-
Chart data for bubble charts.
|
|
79
|
-
|
|
80
|
-
This is a stub implementation - charts are not yet supported.
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
def __init__(self) -> None:
|
|
84
|
-
self._series: list[Any] = []
|
|
85
|
-
|
|
86
|
-
def add_series(self, name: str, values: Any = None) -> Any:
|
|
87
|
-
"""
|
|
88
|
-
Add a data series.
|
|
89
|
-
|
|
90
|
-
Raises:
|
|
91
|
-
UnsupportedFeatureError: Charts are not yet supported
|
|
92
|
-
"""
|
|
93
|
-
raise UnsupportedFeatureError(
|
|
94
|
-
"BubbleChartData.add_series",
|
|
95
|
-
"Bubble charts are not yet supported"
|
|
96
|
-
)
|
|
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
|