athena-python-pptx 0.1.51__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.
Files changed (40) hide show
  1. athena_python_pptx-0.1.56/API_PARITY_REPORT.md +51 -0
  2. athena_python_pptx-0.1.56/CHANGELOG.md +29 -0
  3. athena_python_pptx-0.1.56/CLAUDE.md +73 -0
  4. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/PKG-INFO +1 -1
  5. athena_python_pptx-0.1.56/docs/API_PARITY_EXCEPTIONS.md +110 -0
  6. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/__init__.py +1 -1
  7. athena_python_pptx-0.1.56/pptx/chart/data.py +117 -0
  8. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/client.py +104 -9
  9. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/commands.py +302 -0
  10. athena_python_pptx-0.1.56/pptx/dml/color.py +165 -0
  11. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/__init__.py +5 -2
  12. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/chart.py +4 -0
  13. athena_python_pptx-0.1.56/pptx/enum/shapes.py +574 -0
  14. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/errors.py +3 -0
  15. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/presentation.py +15 -0
  16. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/shapes.py +1191 -387
  17. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/slides.py +149 -48
  18. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/text.py +0 -289
  19. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/typing.py +21 -17
  20. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pyproject.toml +1 -1
  21. athena_python_pptx-0.1.51/CHANGELOG.md +0 -11
  22. athena_python_pptx-0.1.51/pptx/chart/data.py +0 -96
  23. athena_python_pptx-0.1.51/pptx/dml/color.py +0 -1096
  24. athena_python_pptx-0.1.51/pptx/enum/shapes.py +0 -409
  25. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/.gitignore +0 -0
  26. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/DEV-GUIDE.md +0 -0
  27. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/PUBLISHING.md +0 -0
  28. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/README.md +0 -0
  29. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/docs/athena-api.json +0 -0
  30. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/docs/athena-api.md +0 -0
  31. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/batching.py +0 -0
  32. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/chart/__init__.py +0 -0
  33. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/decorators.py +0 -0
  34. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/dml/__init__.py +0 -0
  35. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/docgen.py +0 -0
  36. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/action.py +0 -0
  37. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/dml.py +0 -0
  38. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/text.py +0 -0
  39. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/units.py +0 -0
  40. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/util.py +0 -0
@@ -0,0 +1,51 @@
1
+ # API Parity Report: athena-python-pptx vs python-pptx
2
+
3
+ **Generated:** 2026-03-23
4
+ **athena-python-pptx version:** 0.1.52
5
+ **python-pptx version:** 1.0.2
6
+ **Test script:** `tests/test_python_pptx_api_parity.py`
7
+ **Classes tested:** 8
8
+
9
+ ---
10
+
11
+ ## Summary
12
+
13
+ **0 unexpected missing, 0 unexpected extras, 0 param mismatches across all tested classes.**
14
+
15
+ | Class | Standard Members | Missing | Extra | Status |
16
+ |-------|---------|---------|-------|--------|
17
+ | FillFormat | solid, background, gradient, patterned, fore_color, back_color, pattern, type | 1 (internal) | 2 (gradient props) | PASS |
18
+ | LineFormat | color, dash_style, fill, width | 0 | 2 (is_dashed, is_solid) | PASS |
19
+ | RGBColor | from_string, count, index, __iter__, __len__, __getitem__ | 0 | 0 | PASS |
20
+ | ColorFormat | rgb, theme_color, brightness, type | 1 (internal) | 0 | PASS |
21
+ | Font | bold, italic, underline, size, name, color | 2 (fill, language_id) | 0 | PASS |
22
+ | TextFrame | text, paragraphs, margins, auto_size, word_wrap, add_paragraph, clear, fit_text | 1 (part) | 47 (convenience) | PASS |
23
+ | Paragraph | alignment, font, level, line_spacing, runs, space_after/before, add_run, clear | 1 (part) | 30 (convenience) | PASS |
24
+ | Run | text, font, hyperlink | 1 (part) | 20 (convenience) | PASS |
25
+
26
+ ## Intentionally Omitted
27
+
28
+ | Member | Reason |
29
+ |--------|--------|
30
+ | `*.part` | Package part — REST SDK has no local XML packages |
31
+ | `FillFormat.from_fill_parent()` | Internal XML constructor |
32
+ | `ColorFormat.from_colorchoice_parent()` | Internal XML constructor |
33
+ | `Font.fill` | Font fill format — not commonly used |
34
+ | `Font.language_id` | Not yet implemented |
35
+
36
+ ## Non-Standard Extras (Candidates for Future Cleanup)
37
+
38
+ **Core classes (FillFormat, LineFormat, RGBColor, ColorFormat, Font):** Fully cleaned — no non-standard methods.
39
+
40
+ **TextFrame:** 47 convenience methods (to_html, to_markdown, word_count, capitalize, etc.)
41
+ **Paragraph:** 30 convenience methods (bullet helpers, string ops, to_dict, etc.)
42
+ **Run:** 20 convenience methods (string ops, is_bold, has_hyperlink, etc.)
43
+
44
+ These extras are additive (don't break standard code) but should be removed in a future pass to match the same parity standard as the core classes.
45
+
46
+ ## How to Run
47
+
48
+ ```bash
49
+ pip install python-pptx --target /tmp/pptx-standard
50
+ python -m pytest tests/test_python_pptx_api_parity.py -v -s
51
+ ```
@@ -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.
@@ -0,0 +1,73 @@
1
+ # athena-python-pptx SDK — Claude Instructions
2
+
3
+ ## API Parity Rule (MANDATORY)
4
+
5
+ **This SDK MUST be a 100% exact replica of the standard [python-pptx](https://python-pptx.readthedocs.io/) API.**
6
+
7
+ Every class, method, property, and parameter name must match python-pptx exactly. The goal is that any code written for python-pptx works identically with this SDK — no surprises, no differences.
8
+
9
+ ### What this means in practice
10
+
11
+ - **Do NOT add new methods** that don't exist in python-pptx (e.g., `fill.set_color()`, `line.no_fill()`, `fill.solid_fill()`)
12
+ - **Do NOT add new properties** that don't exist in python-pptx (e.g., `fill.color_hex`, `line.color_hex`, `line.width_pt`)
13
+ - **Do NOT rename parameters** — use the exact same parameter names as python-pptx (e.g., `autoshape_type_id`, not `autoshape_type`)
14
+ - **Do NOT change method signatures** — if python-pptx's `fill.solid()` takes no arguments, ours must also take no arguments
15
+ - **Do NOT change return types** — if python-pptx's `font.color` returns a `ColorFormat`, ours must too (never `None`)
16
+
17
+ ### How to verify parity
18
+
19
+ Before adding or modifying any API surface:
20
+
21
+ 1. Check the [python-pptx documentation](https://python-pptx.readthedocs.io/)
22
+ 2. Check the [python-pptx source code](https://github.com/scanny/python-pptx)
23
+ 3. Confirm the method/property/parameter exists with the same name and signature
24
+ 4. If it doesn't exist in python-pptx, **do not add it** without explicit user approval
25
+
26
+ ### Current status (v0.1.52)
27
+
28
+ **Core classes are at 1:1 parity** with python-pptx: FillFormat, LineFormat, RGBColor, ColorFormat, Font. All legacy convenience methods on these classes have been removed.
29
+
30
+ **TextFrame, Paragraph, and Run** still have non-standard convenience extras (e.g., `to_dict()`, `word_count`, `capitalize()`, string helpers). These are candidates for removal in a future cleanup pass.
31
+
32
+ Run `python -m pytest tests/test_python_pptx_api_parity.py -v -s` to verify parity across 8 classes.
33
+
34
+ ### Intentionally omitted (REST SDK limitations)
35
+
36
+ These standard python-pptx members don't apply to a REST SDK:
37
+ - `Shape.element`, `Shape.part` — XML element access (no local XML)
38
+ - `TextFrame.part`, `Paragraph.part`, `Run.part` — package part access
39
+ - `FillFormat.from_fill_parent()`, `ColorFormat.from_colorchoice_parent()` — internal constructors
40
+ - `Font.fill` — font fill format (not supported)
41
+ - `Font.language_id` — not yet implemented
42
+
43
+ ### If you need a deviation
44
+
45
+ If there is a genuine technical reason why a deviation from python-pptx is necessary (e.g., the SDK is a REST client and cannot replicate XML-level behavior):
46
+
47
+ 1. **Stop and ask the user** before implementing
48
+ 2. Explain what the deviation is and why it's needed
49
+ 3. Get explicit confirmation that the deviation is acceptable
50
+ 4. Document the deviation in the table above
51
+
52
+ **Do NOT silently add convenience methods, aliases, or "improved" APIs.** The value of this SDK is that it's a drop-in replacement — any divergence breaks that contract.
53
+
54
+ ## Development
55
+
56
+ ```bash
57
+ # Install in editable mode
58
+ pip install -e ".[dev]"
59
+
60
+ # Run tests
61
+ python -m pytest tests/ -x -q
62
+
63
+ # Run specific test file
64
+ python -m pytest tests/test_sdk_features.py -x -q
65
+ ```
66
+
67
+ ## Architecture
68
+
69
+ This is a **REST API client** that mimics the python-pptx API. Instead of manipulating XML directly, method calls are translated to commands sent to the PPTX Studio API server. Key differences from python-pptx internals:
70
+
71
+ - No `_element`, `_sp`, `_spTree` XML attributes — these don't exist
72
+ - Operations are batched via `with prs.batch():` context manager
73
+ - State is synchronized via the PPTX Studio collaboration layer (Yjs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-pptx
3
- Version: 0.1.51
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,110 @@
1
+ # athena-python-pptx API Parity Exceptions
2
+
3
+ This document lists all intentional deviations from the standard [python-pptx](https://python-pptx.readthedocs.io/) API in the athena-python-pptx SDK.
4
+
5
+ The SDK's goal is 1:1 API parity with python-pptx. These exceptions exist because the SDK is a REST client (no local XML), or because LLM agents need ergonomic aliases for common patterns.
6
+
7
+ ---
8
+
9
+ ## REST SDK Limitations (python-pptx members that don't apply)
10
+
11
+ These standard python-pptx members are omitted because a REST SDK has no access to the underlying XML or package structure:
12
+
13
+ | Member | Reason |
14
+ |--------|--------|
15
+ | `Shape.element`, `Shape.part` | XML element access (no local XML) |
16
+ | `TextFrame.part`, `Paragraph.part`, `Run.part` | Package part access |
17
+ | `FillFormat.from_fill_parent()` | Internal constructor |
18
+ | `ColorFormat.from_colorchoice_parent()` | Internal constructor |
19
+ | `Font.fill` | Font fill format (not supported) |
20
+ | `Font.language_id` | Not yet implemented |
21
+
22
+ ---
23
+
24
+ ## Agent-Friendly Additions (not in python-pptx)
25
+
26
+ 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.
27
+
28
+ ### `TableCell` — RGB tuple aliases
29
+
30
+ | Property | Type | Maps to | Why |
31
+ |----------|------|---------|-----|
32
+ | `cell.font_bold` | `bool` | `cell.bold` | Agents write `cell.font_bold = True` instead of `cell.bold = True` |
33
+ | `cell.font_color_rgb` | `tuple(R,G,B)` | `cell.font_color_hex` | Agents write `cell.font_color_rgb = (255, 255, 255)` instead of `cell.font_color_hex = "FFFFFF"` |
34
+ | `cell.fill_color_rgb` | `tuple(R,G,B)` | `cell.fill` (hex) | Agents write `cell.fill_color_rgb = (31, 57, 100)` instead of `cell.fill = "1F3964"` |
35
+
36
+ **Example of the agent pattern these support:**
37
+
38
+ ```python
39
+ # LLM agents commonly generate this pattern:
40
+ cell = tbl.cell(0, 0)
41
+ cell.text = "Header"
42
+ cell.font_size_pt = 14
43
+ cell.font_bold = True
44
+ cell.font_color_rgb = (255, 255, 255)
45
+ cell.fill_color_rgb = (31, 57, 100)
46
+ ```
47
+
48
+ ### `GraphicFrame` — Table passthrough methods
49
+
50
+ In python-pptx, `add_table()` returns a `GraphicFrame` and the table is accessed via `.table`. However, agents frequently call `.cell()`, `.rows`, `.cols` directly on the return value. These passthroughs prevent `AttributeError`:
51
+
52
+ | Member | Delegates to |
53
+ |--------|-------------|
54
+ | `graphic_frame.cell(row, col)` | `graphic_frame.table.cell(row, col)` |
55
+ | `graphic_frame.rows` | `graphic_frame.table.rows` |
56
+ | `graphic_frame.cols` | `graphic_frame.table.cols` |
57
+
58
+ **Example:**
59
+
60
+ ```python
61
+ # Both patterns work:
62
+ tbl = slide.shapes.add_table(rows=3, cols=3, ...)
63
+
64
+ # Pattern 1: python-pptx canonical
65
+ tbl.table.cell(0, 0).text = "Header"
66
+
67
+ # Pattern 2: agent shortcut (also works)
68
+ tbl.cell(0, 0).text = "Header"
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Structural Deviations (python-pptx behavior differs)
74
+
75
+ ### `Table.first_row` / `Table.last_row` — Boolean look flags, not cell lists
76
+
77
+ In python-pptx, `Table.first_row` and `Table.last_row` are boolean properties controlling table style look flags (whether the first/last row gets special styling).
78
+
79
+ In a prior version of this SDK, these were cell-list properties returning `list[TableCell]`. They have been changed to match python-pptx (boolean). The old cell-list accessors are available as methods:
80
+
81
+ | Old (removed) | New (python-pptx parity) | Cell-list replacement |
82
+ |---------------|-------------------------|----------------------|
83
+ | `table.first_row` → `list[TableCell]` | `table.first_row` → `bool` | `table.get_first_row_cells()` |
84
+ | `table.last_row` → `list[TableCell]` | `table.last_row` → `bool` | `table.get_last_row_cells()` |
85
+ | `table.first_column` → `list[TableCell]` | (not a python-pptx property) | `table.get_first_column_cells()` |
86
+ | `table.last_column` → `list[TableCell]` | (not a python-pptx property) | `table.get_last_column_cells()` |
87
+
88
+ ### `Shapes.add_table()` — Returns `GraphicFrame`, not `Table`
89
+
90
+ Changed to match python-pptx semantics. `add_table()` returns a `GraphicFrame`. Access the `Table` via `.table`. The passthrough methods above ensure backward compatibility for code that calls `.cell()` directly.
91
+
92
+ ### `_Row` / `_Column` with height/width setters
93
+
94
+ python-pptx's `_Row.height` and `_Column.width` are read/write EMU properties. This SDK matches that API, but setting them also emits SDK commands (`SetRowHeight`, `SetColWidth`) to the server.
95
+
96
+ ### `Table.rows` / `Table.columns` — Return `_RowCollection` / `_ColumnCollection`
97
+
98
+ In python-pptx, `Table.rows` returns a `_RowCollection` of `_Row` objects (each with `.height`). This SDK matches that. In a prior version, `Table.rows` returned a flat collection of cell lists.
99
+
100
+ ---
101
+
102
+ ## Non-Standard Convenience Methods (candidates for future cleanup)
103
+
104
+ These exist on `TextFrame`, `Paragraph`, `Run`, `TableCell`, and `Table` but are NOT in python-pptx:
105
+
106
+ - `to_dict()`, `word_count`, `capitalize()`, `upper()`, `lower()`, `title()`, `strip()`, `is_empty`
107
+ - `Table.to_csv()`, `Table.to_list()`, `Table.to_dict_list()`, `Table.all_text`, `Table.contains_text()`, `Table.find_cells()`
108
+ - `TableCell.address` (A1 notation), `TableCell.copy_from()`, `TableCell.clear()`
109
+
110
+ These are convenience methods for agents and don't conflict with python-pptx API (python-pptx doesn't have them, so there's no behavioral difference).
@@ -126,7 +126,7 @@ def flush_all() -> None:
126
126
  _active_buffers[:] = alive
127
127
 
128
128
 
129
- __version__ = "0.1.51"
129
+ __version__ = "0.1.56"
130
130
 
131
131
  __all__ = [
132
132
  # Main entry point
@@ -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
@@ -108,6 +108,9 @@ class Client:
108
108
  }
109
109
  if self.api_key:
110
110
  headers["Authorization"] = f"Bearer {self.api_key}"
111
+ custom_attr = os.environ.get("ATHENA_PPTX_CUSTOM_ATTRIBUTIONS")
112
+ if custom_attr:
113
+ headers["X-Custom-Attributions"] = custom_attr
111
114
  return headers
112
115
 
113
116
  def _handle_response(self, response: requests.Response) -> Any:
@@ -128,13 +131,17 @@ class Client:
128
131
  # Extract message from top-level or nested error object
129
132
  msg = data.get("message")
130
133
  error_obj = data.get("error")
134
+ details = data.get("details")
131
135
  if not msg and isinstance(error_obj, dict):
132
136
  msg = error_obj.get("message")
133
- details = error_obj.get("details")
134
- if details:
135
- msg = f"{msg}: {details}"
137
+ if not details:
138
+ details = error_obj.get("details")
136
139
  elif not msg and isinstance(error_obj, str):
137
140
  msg = error_obj
141
+
142
+ detail_message = self._format_error_details(details)
143
+ if detail_message:
144
+ msg = f"{msg or 'Request failed'}: {detail_message}"
138
145
  raise RemoteError(
139
146
  message=msg or f"API error: {response.status_code}",
140
147
  status_code=response.status_code,
@@ -152,12 +159,49 @@ class Client:
152
159
 
153
160
  return response.json()
154
161
 
162
+ def _format_error_details(self, details: Any) -> Optional[str]:
163
+ """Convert structured API validation errors into actionable text."""
164
+ if not details:
165
+ return None
166
+
167
+ if isinstance(details, list):
168
+ rendered: list[str] = []
169
+ for item in details:
170
+ if not isinstance(item, dict):
171
+ rendered.append(str(item))
172
+ continue
173
+
174
+ path = ".".join(str(part) for part in item.get("path", []))
175
+ message = item.get("message") or "Invalid value"
176
+ received = item.get("received")
177
+
178
+ if path.endswith("shapeType") and isinstance(received, str):
179
+ suggestion = self._suggest_shape_alias(received)
180
+ if suggestion and suggestion != received:
181
+ message = f"{message} (did you mean '{suggestion}'?)"
182
+
183
+ rendered.append(f"{path}: {message}" if path else message)
184
+
185
+ return "; ".join(rendered)
186
+
187
+ return str(details)
188
+
189
+ def _suggest_shape_alias(self, value: str) -> Optional[str]:
190
+ """Suggest the canonical backend shape name for common aliases."""
191
+ from .enum.shapes import mso_shape_to_string
192
+
193
+ try:
194
+ return mso_shape_to_string(value)
195
+ except Exception:
196
+ return None
197
+
155
198
  def _request(
156
199
  self,
157
200
  method: str,
158
201
  path: str,
159
202
  json: Optional[dict] = None,
160
203
  params: Optional[dict] = None,
204
+ timeout: Optional[float] = None,
161
205
  ) -> Any:
162
206
  """Make an HTTP request."""
163
207
  url = urljoin(self.base_url + "/", path.lstrip("/"))
@@ -175,7 +219,7 @@ class Client:
175
219
  json=json,
176
220
  params=params,
177
221
  headers=headers,
178
- timeout=self.timeout,
222
+ timeout=timeout if timeout is not None else self.timeout,
179
223
  )
180
224
  return self._handle_response(response)
181
225
  except requests.ConnectionError as e:
@@ -217,21 +261,70 @@ class Client:
217
261
  payload = {"name": name} if name else None
218
262
  return self._request("POST", "/decks", json=payload)
219
263
 
220
- def create_empty_deck(self, name: Optional[str] = None) -> dict[str, Any]:
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]:
221
270
  """
222
- Create a new empty deck that's immediately ready.
271
+ Create a new empty deck and wait until it is ready.
223
272
 
224
273
  Unlike create_deck(), this creates a deck without requiring an upload.
225
- The deck is immediately usable with no slides.
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.
226
286
 
227
287
  Args:
228
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)
229
291
 
230
292
  Returns:
231
- 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".
232
297
  """
233
298
  payload = {"name": name} if name else None
234
- return self._request("POST", "/decks/empty", json=payload)
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
+ )
235
328
 
236
329
  def delete_deck(self, deck_id: str) -> None:
237
330
  """Delete a deck."""
@@ -465,7 +558,9 @@ class Client:
465
558
  type=placeholder_data.get("type", "obj"),
466
559
  idx=placeholder_data.get("idx", 0),
467
560
  sz=placeholder_data.get("sz"),
561
+ orient=placeholder_data.get("orient"),
468
562
  has_custom_prompt=placeholder_data.get("hasCustomPrompt"),
563
+ local_transform_override=placeholder_data.get("localTransformOverride"),
469
564
  )
470
565
 
471
566
  elements[elem_id] = ElementSnapshot(