athena-python-pptx 0.1.51__tar.gz → 0.1.54__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.54/API_PARITY_REPORT.md +51 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/CHANGELOG.md +4 -0
- athena_python_pptx-0.1.54/CLAUDE.md +73 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/PKG-INFO +1 -1
- athena_python_pptx-0.1.54/docs/API_PARITY_EXCEPTIONS.md +110 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/__init__.py +1 -1
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/client.py +48 -3
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/commands.py +204 -0
- athena_python_pptx-0.1.54/pptx/dml/color.py +165 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/__init__.py +5 -2
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/chart.py +4 -0
- athena_python_pptx-0.1.54/pptx/enum/shapes.py +574 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/errors.py +3 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/presentation.py +15 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/shapes.py +894 -377
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/slides.py +149 -48
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/text.py +0 -289
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/typing.py +21 -17
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pyproject.toml +1 -1
- 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.54}/.gitignore +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/README.md +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/batching.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/units.py +0 -0
- {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/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
|
+
```
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `athena-python-pptx` are documented in this file.
|
|
4
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
|
+
|
|
5
9
|
## 0.1.39
|
|
6
10
|
|
|
7
11
|
- Added SDK support for `slide.shapes.add_table(...)` and table creation command wiring.
|
|
@@ -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.54
|
|
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).
|
|
@@ -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,6 +159,42 @@ 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,
|
|
@@ -465,7 +508,9 @@ class Client:
|
|
|
465
508
|
type=placeholder_data.get("type", "obj"),
|
|
466
509
|
idx=placeholder_data.get("idx", 0),
|
|
467
510
|
sz=placeholder_data.get("sz"),
|
|
511
|
+
orient=placeholder_data.get("orient"),
|
|
468
512
|
has_custom_prompt=placeholder_data.get("hasCustomPrompt"),
|
|
513
|
+
local_transform_override=placeholder_data.get("localTransformOverride"),
|
|
469
514
|
)
|
|
470
515
|
|
|
471
516
|
elements[elem_id] = ElementSnapshot(
|
|
@@ -464,6 +464,9 @@ class SetTableCell(Command):
|
|
|
464
464
|
col: int
|
|
465
465
|
text: Optional[str] = None
|
|
466
466
|
fill_color_hex: Optional[str] = None
|
|
467
|
+
font_size_centipoints: Optional[int] = None
|
|
468
|
+
bold: Optional[bool] = None
|
|
469
|
+
font_color_hex: Optional[str] = None
|
|
467
470
|
|
|
468
471
|
@property
|
|
469
472
|
def command_type(self) -> str:
|
|
@@ -507,6 +510,167 @@ class SetTableRowCount(Command):
|
|
|
507
510
|
raise ValidationError("row_count must be at most 100", "row_count")
|
|
508
511
|
|
|
509
512
|
|
|
513
|
+
@dataclass
|
|
514
|
+
class MergeCells(Command):
|
|
515
|
+
"""
|
|
516
|
+
Merge a rectangular region of cells in a table.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
shape_id: ID of the table shape
|
|
520
|
+
start_row: Top-left row (0-based)
|
|
521
|
+
start_col: Top-left column (0-based)
|
|
522
|
+
end_row: Bottom-right row (0-based, inclusive)
|
|
523
|
+
end_col: Bottom-right column (0-based, inclusive)
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
shape_id: ShapeId
|
|
527
|
+
start_row: int
|
|
528
|
+
start_col: int
|
|
529
|
+
end_row: int
|
|
530
|
+
end_col: int
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def command_type(self) -> str:
|
|
534
|
+
return "MergeCells"
|
|
535
|
+
|
|
536
|
+
def validate(self) -> None:
|
|
537
|
+
if not self.shape_id:
|
|
538
|
+
raise ValidationError("shape_id is required", "shape_id")
|
|
539
|
+
if self.end_row < self.start_row or self.end_col < self.start_col:
|
|
540
|
+
raise ValidationError("end must be >= start", "end_row")
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@dataclass
|
|
544
|
+
class SplitCell(Command):
|
|
545
|
+
"""
|
|
546
|
+
Unmerge a previously merged cell region.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
shape_id: ID of the table shape
|
|
550
|
+
row: Row of the merge-origin cell (0-based)
|
|
551
|
+
col: Column of the merge-origin cell (0-based)
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
shape_id: ShapeId
|
|
555
|
+
row: int
|
|
556
|
+
col: int
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def command_type(self) -> str:
|
|
560
|
+
return "SplitCell"
|
|
561
|
+
|
|
562
|
+
def validate(self) -> None:
|
|
563
|
+
if not self.shape_id:
|
|
564
|
+
raise ValidationError("shape_id is required", "shape_id")
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@dataclass
|
|
568
|
+
class SetCellMargins(Command):
|
|
569
|
+
"""
|
|
570
|
+
Set margins on a table cell.
|
|
571
|
+
|
|
572
|
+
Emits a SetCellTcPr command with margin values nested in tcPrPatch,
|
|
573
|
+
matching the backend's SetCellTcPrIntentSchema.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
shape_id: ID of the table shape
|
|
577
|
+
row: Row index (0-based)
|
|
578
|
+
col: Column index (0-based)
|
|
579
|
+
mar_l_emu: Left margin in EMU (None to reset to default)
|
|
580
|
+
mar_r_emu: Right margin in EMU (None to reset to default)
|
|
581
|
+
mar_t_emu: Top margin in EMU (None to reset to default)
|
|
582
|
+
mar_b_emu: Bottom margin in EMU (None to reset to default)
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
shape_id: ShapeId
|
|
586
|
+
row: int
|
|
587
|
+
col: int
|
|
588
|
+
mar_l_emu: Optional[int] = None
|
|
589
|
+
mar_r_emu: Optional[int] = None
|
|
590
|
+
mar_t_emu: Optional[int] = None
|
|
591
|
+
mar_b_emu: Optional[int] = None
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def command_type(self) -> str:
|
|
595
|
+
return "SetCellTcPr"
|
|
596
|
+
|
|
597
|
+
def to_dict(self) -> dict[str, Any]:
|
|
598
|
+
"""Serialize with tcPrPatch nesting expected by SetCellTcPrIntentSchema.
|
|
599
|
+
|
|
600
|
+
Always includes all margin keys — None values are serialized as null
|
|
601
|
+
so the backend can distinguish "reset to default" from "not specified".
|
|
602
|
+
"""
|
|
603
|
+
tc_pr_patch: dict[str, Any] = {
|
|
604
|
+
"marL": self.mar_l_emu,
|
|
605
|
+
"marR": self.mar_r_emu,
|
|
606
|
+
"marT": self.mar_t_emu,
|
|
607
|
+
"marB": self.mar_b_emu,
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
"type": self.command_type,
|
|
611
|
+
"shapeId": self.shape_id,
|
|
612
|
+
"row": self.row,
|
|
613
|
+
"col": self.col,
|
|
614
|
+
"tcPrPatch": tc_pr_patch,
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
def validate(self) -> None:
|
|
618
|
+
if not self.shape_id:
|
|
619
|
+
raise ValidationError("shape_id is required", "shape_id")
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@dataclass
|
|
623
|
+
class SetColWidth(Command):
|
|
624
|
+
"""
|
|
625
|
+
Set the width of a table column.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
shape_id: ID of the table shape
|
|
629
|
+
col_index: Column index (0-based)
|
|
630
|
+
width_emu: Width in EMU
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
shape_id: ShapeId
|
|
634
|
+
col_index: int
|
|
635
|
+
width_emu: int
|
|
636
|
+
|
|
637
|
+
@property
|
|
638
|
+
def command_type(self) -> str:
|
|
639
|
+
return "SetColWidth"
|
|
640
|
+
|
|
641
|
+
def validate(self) -> None:
|
|
642
|
+
if not self.shape_id:
|
|
643
|
+
raise ValidationError("shape_id is required", "shape_id")
|
|
644
|
+
if self.width_emu < 0:
|
|
645
|
+
raise ValidationError("width_emu must be non-negative", "width_emu")
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@dataclass
|
|
649
|
+
class SetRowHeight(Command):
|
|
650
|
+
"""
|
|
651
|
+
Set the height of a table row.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
shape_id: ID of the table shape
|
|
655
|
+
row_index: Row index (0-based)
|
|
656
|
+
height_emu: Height in EMU (0 = auto-fit)
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
shape_id: ShapeId
|
|
660
|
+
row_index: int
|
|
661
|
+
height_emu: int
|
|
662
|
+
|
|
663
|
+
@property
|
|
664
|
+
def command_type(self) -> str:
|
|
665
|
+
return "SetRowHeight"
|
|
666
|
+
|
|
667
|
+
def validate(self) -> None:
|
|
668
|
+
if not self.shape_id:
|
|
669
|
+
raise ValidationError("shape_id is required", "shape_id")
|
|
670
|
+
if self.height_emu < 0:
|
|
671
|
+
raise ValidationError("height_emu must be non-negative", "height_emu")
|
|
672
|
+
|
|
673
|
+
|
|
510
674
|
@dataclass
|
|
511
675
|
class SetSlideBackground(Command):
|
|
512
676
|
"""
|
|
@@ -1168,6 +1332,45 @@ class FitText(Command):
|
|
|
1168
1332
|
)
|
|
1169
1333
|
|
|
1170
1334
|
|
|
1335
|
+
@dataclass
|
|
1336
|
+
class SubstitutePlaceholder(Command):
|
|
1337
|
+
"""
|
|
1338
|
+
Substitute a placeholder shape with a picture, table, or chart.
|
|
1339
|
+
|
|
1340
|
+
Preserves the placeholder's semantic linkage (p:ph element in OOXML),
|
|
1341
|
+
enabling correct layout reset and round-trip fidelity with PowerPoint.
|
|
1342
|
+
|
|
1343
|
+
Args:
|
|
1344
|
+
shape_id: ID of the placeholder shape to substitute
|
|
1345
|
+
kind: Kind of content ('picture', 'table', or 'chart')
|
|
1346
|
+
image_base64: Base64-encoded image data (required for picture)
|
|
1347
|
+
image_format: Image format ('png', 'jpeg', etc.) (required for picture)
|
|
1348
|
+
rows: Number of rows (required for table)
|
|
1349
|
+
cols: Number of columns (required for table)
|
|
1350
|
+
"""
|
|
1351
|
+
|
|
1352
|
+
shape_id: ShapeId
|
|
1353
|
+
kind: str # 'picture' | 'table' | 'chart'
|
|
1354
|
+
image_base64: Optional[str] = None
|
|
1355
|
+
image_format: Optional[str] = None
|
|
1356
|
+
rows: Optional[int] = None
|
|
1357
|
+
cols: Optional[int] = None
|
|
1358
|
+
|
|
1359
|
+
@property
|
|
1360
|
+
def command_type(self) -> str:
|
|
1361
|
+
return "SubstitutePlaceholder"
|
|
1362
|
+
|
|
1363
|
+
def validate(self) -> None:
|
|
1364
|
+
if not self.shape_id:
|
|
1365
|
+
raise ValidationError("shape_id is required", "shape_id")
|
|
1366
|
+
if self.kind not in ('picture', 'table', 'chart'):
|
|
1367
|
+
raise ValidationError("kind must be 'picture', 'table', or 'chart'", "kind")
|
|
1368
|
+
if self.kind == 'picture' and (not self.image_base64 or not self.image_format):
|
|
1369
|
+
raise ValidationError("image_base64 and image_format required for picture", "image_base64")
|
|
1370
|
+
if self.kind == 'table' and (not self.rows or not self.cols):
|
|
1371
|
+
raise ValidationError("rows and cols required for table", "rows")
|
|
1372
|
+
|
|
1373
|
+
|
|
1171
1374
|
# Type alias for any command
|
|
1172
1375
|
AnyCommand = Union[
|
|
1173
1376
|
AddTextBox,
|
|
@@ -1207,4 +1410,5 @@ AnyCommand = Union[
|
|
|
1207
1410
|
UngroupShapes,
|
|
1208
1411
|
SetShapeAdjustments,
|
|
1209
1412
|
FitText,
|
|
1413
|
+
SubstitutePlaceholder,
|
|
1210
1414
|
]
|