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.
Files changed (38) hide show
  1. athena_python_pptx-0.1.54/API_PARITY_REPORT.md +51 -0
  2. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/CHANGELOG.md +4 -0
  3. athena_python_pptx-0.1.54/CLAUDE.md +73 -0
  4. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/PKG-INFO +1 -1
  5. athena_python_pptx-0.1.54/docs/API_PARITY_EXCEPTIONS.md +110 -0
  6. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/__init__.py +1 -1
  7. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/client.py +48 -3
  8. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/commands.py +204 -0
  9. athena_python_pptx-0.1.54/pptx/dml/color.py +165 -0
  10. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/__init__.py +5 -2
  11. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/chart.py +4 -0
  12. athena_python_pptx-0.1.54/pptx/enum/shapes.py +574 -0
  13. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/errors.py +3 -0
  14. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/presentation.py +15 -0
  15. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/shapes.py +894 -377
  16. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/slides.py +149 -48
  17. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/text.py +0 -289
  18. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/typing.py +21 -17
  19. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pyproject.toml +1 -1
  20. athena_python_pptx-0.1.51/pptx/dml/color.py +0 -1096
  21. athena_python_pptx-0.1.51/pptx/enum/shapes.py +0 -409
  22. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/.gitignore +0 -0
  23. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/DEV-GUIDE.md +0 -0
  24. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/PUBLISHING.md +0 -0
  25. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/README.md +0 -0
  26. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/docs/athena-api.json +0 -0
  27. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/docs/athena-api.md +0 -0
  28. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/batching.py +0 -0
  29. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/chart/__init__.py +0 -0
  30. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/chart/data.py +0 -0
  31. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/decorators.py +0 -0
  32. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/dml/__init__.py +0 -0
  33. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/docgen.py +0 -0
  34. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/action.py +0 -0
  35. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/dml.py +0 -0
  36. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/enum/text.py +0 -0
  37. {athena_python_pptx-0.1.51 → athena_python_pptx-0.1.54}/pptx/units.py +0 -0
  38. {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.51
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).
@@ -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.54"
130
130
 
131
131
  __all__ = [
132
132
  # Main entry point
@@ -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,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
  ]