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.
- athena_python_pptx-0.1.56/API_PARITY_REPORT.md +51 -0
- athena_python_pptx-0.1.56/CHANGELOG.md +29 -0
- athena_python_pptx-0.1.56/CLAUDE.md +73 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/PKG-INFO +1 -1
- athena_python_pptx-0.1.56/docs/API_PARITY_EXCEPTIONS.md +110 -0
- {athena_python_pptx-0.1.51 → 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.51 → athena_python_pptx-0.1.56}/pptx/client.py +104 -9
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/commands.py +302 -0
- athena_python_pptx-0.1.56/pptx/dml/color.py +165 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/__init__.py +5 -2
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/chart.py +4 -0
- athena_python_pptx-0.1.56/pptx/enum/shapes.py +574 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/errors.py +3 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/presentation.py +15 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/shapes.py +1191 -387
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/slides.py +149 -48
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/text.py +0 -289
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/typing.py +21 -17
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pyproject.toml +1 -1
- athena_python_pptx-0.1.51/CHANGELOG.md +0 -11
- athena_python_pptx-0.1.51/pptx/chart/data.py +0 -96
- athena_python_pptx-0.1.51/pptx/dml/color.py +0 -1096
- athena_python_pptx-0.1.51/pptx/enum/shapes.py +0 -409
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/.gitignore +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/README.md +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/batching.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.56}/pptx/units.py +0 -0
- {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.
|
|
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).
|
|
@@ -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
|
-
|
|
134
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|