athena-python-pptx 0.10.0__tar.gz → 0.13.0__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 (91) hide show
  1. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/CHANGELOG.md +202 -0
  2. athena_python_pptx-0.13.0/CLAUDE.md +5 -0
  3. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/PKG-INFO +2 -2
  4. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/docs/API_PARITY_EXCEPTIONS.md +226 -6
  5. athena_python_pptx-0.13.0/pptx/__init__.py +347 -0
  6. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/batching.py +282 -18
  7. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/data.py +15 -7
  8. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/client.py +70 -8
  9. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/commands.py +14 -3
  10. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/enum/shapes.py +4 -2
  11. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/errors.py +9 -0
  12. athena_python_pptx-0.13.0/pptx/kit.py +1329 -0
  13. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/presentation.py +63 -17
  14. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/__init__.py +319 -148
  15. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/slides.py +558 -76
  16. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/text/__init__.py +64 -8
  17. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/typing.py +5 -0
  18. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pyproject.toml +1 -1
  19. athena_python_pptx-0.10.0/pptx/__init__.py +0 -208
  20. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/.gitignore +0 -0
  21. /athena_python_pptx-0.10.0/CLAUDE.md → /athena_python_pptx-0.13.0/AGENTS.md +0 -0
  22. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/API_PARITY_REPORT.md +0 -0
  23. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/DEV-GUIDE.md +0 -0
  24. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/PARITY_QUESTIONS.md +0 -0
  25. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/PUBLISHING.md +0 -0
  26. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/README.md +0 -0
  27. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/docs/athena-api.json +0 -0
  28. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/docs/athena-api.md +0 -0
  29. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/_athena_extension.py +0 -0
  30. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/_citations.py +0 -0
  31. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/_ptc.py +0 -0
  32. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/_references.py +0 -0
  33. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/action.py +0 -0
  34. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/__init__.py +0 -0
  35. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/axis.py +0 -0
  36. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/category.py +0 -0
  37. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/chart.py +0 -0
  38. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/datalabel.py +0 -0
  39. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/legend.py +0 -0
  40. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/marker.py +0 -0
  41. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/plot.py +0 -0
  42. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/point.py +0 -0
  43. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/series.py +0 -0
  44. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/chart/xlsx.py +0 -0
  45. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/decorators.py +0 -0
  46. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/dml/__init__.py +0 -0
  47. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/dml/chtfmt.py +0 -0
  48. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/dml/color.py +0 -0
  49. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/dml/effect.py +0 -0
  50. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/dml/fill.py +0 -0
  51. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/dml/line.py +0 -0
  52. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/docgen.py +0 -0
  53. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/enum/__init__.py +0 -0
  54. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/enum/action.py +0 -0
  55. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/enum/chart.py +0 -0
  56. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/enum/dml.py +0 -0
  57. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/enum/lang.py +0 -0
  58. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/enum/text.py +0 -0
  59. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/exc.py +0 -0
  60. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/media.py +0 -0
  61. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/oxml/__init__.py +0 -0
  62. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/oxml/ns.py +0 -0
  63. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/package.py +0 -0
  64. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/__init__.py +0 -0
  65. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/_base.py +0 -0
  66. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/chart.py +0 -0
  67. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/coreprops.py +0 -0
  68. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/embeddedpackage.py +0 -0
  69. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/image.py +0 -0
  70. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/media.py +0 -0
  71. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/presentation.py +0 -0
  72. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/parts/slide.py +0 -0
  73. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/autoshape.py +0 -0
  74. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/base.py +0 -0
  75. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/connector.py +0 -0
  76. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/freeform.py +0 -0
  77. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/graphfrm.py +0 -0
  78. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/group.py +0 -0
  79. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/picture.py +0 -0
  80. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/placeholder.py +0 -0
  81. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shapes/shapetree.py +0 -0
  82. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/shared.py +0 -0
  83. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/slide.py +0 -0
  84. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/spec.py +0 -0
  85. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/table.py +0 -0
  86. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/text/fonts.py +0 -0
  87. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/text/layout.py +0 -0
  88. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/text/text.py +0 -0
  89. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/types.py +0 -0
  90. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/units.py +0 -0
  91. {athena_python_pptx-0.10.0 → athena_python_pptx-0.13.0}/pptx/util.py +0 -0
@@ -2,6 +2,208 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## Unreleased — `pptx.kit` composition kit
6
+
7
+ - New Athena-only module `pptx.kit` (`from pptx.kit import Kit, Palette,
8
+ Fonts, Region`; also re-exported from `pptx`). `Kit(prs)` composes slides
9
+ from regions derived from the slide size: `header` (small-caps kicker,
10
+ action title auto-fit to a two-line band, footer rule + page number +
11
+ `kit.footer_text`), `footer`, `takeaway` (full-width bar one gap above the
12
+ footer band), `content_region`, `columns`, `grid`, `kpi_row` / `kpi_card`
13
+ (surface card, accent stripe, big auto-fit value, auto-fit label), `card`,
14
+ `text` and `bullets`.
15
+ - Text asked to fit is sized to its box by the studio's `/measure-text-fit`
16
+ layouter (same client pattern as `TextFitter.best_fit_font_size`, result
17
+ floored and cached per kit, one failed probe latches the fallback) or, when
18
+ the studio is unreachable, by a conservative greedy wrap on the
19
+ `_estimate_text_width_emu` glyph table at 1.2 line height. It never grows
20
+ past the requested size; when nothing fits even at the minimum size the kit
21
+ uses the minimum and emits a `RuntimeWarning` naming the text; `fit=False`
22
+ keeps the size as given.
23
+ - Regions are clamped to the slide and the footer band is reserved, so
24
+ nothing lands outside the slide or under the footer; a region with nothing
25
+ left raises instead of silently placing nothing. `content_region` keeps the
26
+ takeaway band free by default so a bar added after the body cannot hide it,
27
+ and the `y` / `h` overrides of `columns` / `grid` / `kpi_row` are clamped to
28
+ the content region. Per-slide state is keyed by the stable `slide.slide_id`.
29
+ - Pure python-pptx public API underneath (`add_shape` / `add_textbox` /
30
+ `fill` / `line` / `text_frame` / `font`), emitted through the
31
+ presentation's command buffer — build each slide inside `with prs.batch():`
32
+ for one round-trip. Reaches agents with the next `athena-python-pptx` bump.
33
+ Documented in `docs/API_PARITY_EXCEPTIONS.md`; demo in
34
+ `examples/kit_demo.py`.
35
+
36
+ ## Unreleased — validate() finds overflow and hidden text, drops containment noise
37
+
38
+ Reworks `Slide.validate()` around the defects that actually ship in agent
39
+ decks (verified 2026-09-14 against two 8-slide agent builds: 167 and 127
40
+ warnings, ~75% of them a background card behind its own label). Same
41
+ signature; an empty list is once again reachable by a designed slide.
42
+
43
+ - **Containment overlaps are no longer reported** unless both shapes carry
44
+ text (text on text is always reported). Partial overlaps are reported
45
+ only when at least one shape carries text; hairline overlaps thinner
46
+ than a text-frame inset (0.05in) and overlaps between two text-less
47
+ shapes are ignored. Tables count as text-bearing.
48
+ - **New: text overflow for non-table shapes** — per-paragraph font-metric
49
+ estimate (`_estimate_text_width_emu`, run size → paragraph default →
50
+ shape default → 18pt; word wrap on → `ceil(width / inner_width)` lines,
51
+ off → horizontal check; `lines × font × 1.2 × line_spacing` + spacing vs
52
+ box height minus margins). Frames set to auto-fit are skipped. When the
53
+ presentation has a studio client each hit whose paragraphs share one
54
+ font size and weight is confirmed with one `MeasureTextFit` round-trip
55
+ pinned at that size (`fits: true` drops it; capped at 12 per call; every
56
+ network error falls back to the heuristic verdict); mixed-font frames keep
57
+ the heuristic verdict since the server measures at a single font.
58
+ - **New: hidden text (occlusion)** — a text shape at least half covered by
59
+ a later (higher z-order) shape with a solid, non-transparent fill and no
60
+ text: `"text in 'X' is covered by 'Y'"`. Unknown fills (none, gradient,
61
+ pattern, pictures) count as not opaque; any transparency on the proxy or
62
+ a snapshot `fillTransparency` disables the rule for that shape.
63
+ - Warnings are deduplicated, ordered overflow → occlusion → text-on-text →
64
+ other (off-slide, one-text partial overlaps, empty placeholders), capped
65
+ at 15 per slide with a trailing `"… and N more (M containment overlaps
66
+ ignored)"` line. Rotated shapes near 90°/270° use their swapped visual
67
+ bbox in the pairwise rules.
68
+ - `bring_to_front()` / `send_to_back()` / `bring_forward()` /
69
+ `send_backward()` now mirror the move on the local `slide.shapes` order
70
+ (previously only the server saw it), so `validate()` reads the intended
71
+ stack without a refresh.
72
+ - New module-level `pptx.validate_all_touched() -> dict[int, list[str]]`
73
+ (Athena extension) for the executor postamble: `CommandBuffer` records
74
+ the slide indexes / slide ids / shape ids of every command the server
75
+ applied (`CommandBuffer.take_touched()` returns and clears them);
76
+ `validate_all_touched()` resolves them to slides on every live
77
+ `Presentation`, validates only those, and returns the slides with
78
+ warnings. Recorded indexes are shifted for slide insertions, deletions
79
+ and reorders applied later in the run; destination-bearing commands
80
+ (`CloneShape`, `CopySlideFromDeck`, `ReplaceText`) record their
81
+ destination; `SetPresentationSize` and a deck-wide `ReplaceText` validate
82
+ every slide. Commands that cannot change a layout (notes, names,
83
+ citations, deck metadata) never mark a slide; never raises for
84
+ validation reasons.
85
+
86
+ ## 0.12.0 — snapshot hydration, ReplaceChartData emission, level dirty-flag, discard_all
87
+
88
+ SDK halves of the issue #30178 Cluster D server work (#30204 / #30211 /
89
+ #30212 / #30213).
90
+
91
+ **⚠️ Server ordering:** `Chart.replace_data` now emits a single
92
+ `ReplaceChartData` patch, which servers without #30211 reject (400) — the
93
+ `presentation-exec` snapshot bump to 0.12.0 must wait for the pptx-studio
94
+ deploy that ships the op.
95
+
96
+ - `Shape.shadow` hydrates from the snapshot's `shadow` bag: a partial edit
97
+ like `shadow.blur_radius = Pt(6)` re-emits the ingested shadow's
98
+ `visible=True` / type / distance / direction / color / transparency
99
+ instead of the un-hydrated defaults (`visible=False` used to DELETE the
100
+ ingested shadow server-side). Hydrated shadows read back
101
+ `inherit == False`.
102
+ - `Shape.click_action` hydrates from the snapshot's `clickAction` bag —
103
+ same deletion-bug class (`click_action.tooltip = ...` used to emit
104
+ `action_type='none'` and delete the ingested link).
105
+ - `Presentation.core_properties` hydrates from the snapshot's
106
+ `coreProperties` (server #30204): setting one property no longer wipes
107
+ the deck's other metadata to `None`. `refresh()` re-hydrates.
108
+ - `Chart.replace_data` emits ONE `ReplaceChartData` patch carrying the
109
+ full categories + series matrix (python-pptx semantics: shrink drops
110
+ trailing points, grow adds them, extra series are removed) instead of
111
+ per-index `UpdateSeries*` / `UpdateCategoryLabel` patches that couldn't
112
+ change the data's shape. The local `chartSpec` mirror still updates,
113
+ preserving server series ids by index. A zero-series `chart_data`
114
+ raises `ValidationError` before touching the local mirror (the op
115
+ requires at least one series; silently returning would desync local
116
+ reads from the server).
117
+ - `run.hyperlink.target_slide = n` no longer rides with `address=''` (the
118
+ clear convention) — exactly one of address / targetSlideIndex is sent,
119
+ aligning with server #30212. The SetText re-emit carries
120
+ `hyperlinkTargetSlideIndex`, and snapshot rich content hydrates it back.
121
+ `SetRunHyperlinkTarget.validate()` rejects setting both.
122
+ - `paragraph.level` is sent only when the paragraph is indented or the
123
+ caller explicitly assigned it (explicit `level = 0` still reaches the
124
+ wire); an untouched level-0 paragraph omits the field so unrelated style
125
+ edits don't stomp the server-side outline level (belt-and-suspenders
126
+ with the server-side lvl=0 canonicalization in #30213).
127
+ - New module-level `pptx.discard_all()` (mirrors athena-python-docx):
128
+ drops all buffered, un-flushed commands across open decks without
129
+ posting and returns the dropped count. The agora executor postamble
130
+ calls it when user code raised, so failed scripts discard their
131
+ half-buffered tail instead of posting it.
132
+
133
+ ## 0.11.0 — silent no-op & data-loss fixes (deep-dive remediation)
134
+
135
+ Eliminates a cluster of silent no-ops, read-after-write desyncs, and
136
+ buffer-poisoning bugs found in the 2026-08 presentation-asset deep dive.
137
+ All fixes preserve python-pptx API parity (behavior, not surface, changes).
138
+
139
+ - `paragraph.level = 0` now reaches the wire (was dropped by the None-drop),
140
+ so resetting an indented paragraph works.
141
+ - `font.subscript = False` / `font.superscript = False` now send `baseline: 0`
142
+ (the server's clear signal) instead of omitting the field, so they actually
143
+ un-sub/superscript existing text.
144
+ - A bare `shape.fill.solid()` now emits, matching `gradient()`/`patterned()`.
145
+ - Chart `has_legend` / `has_title` / `chart_title` now update the local
146
+ snapshot on write, so reads after a write return the new value.
147
+ - `rows.insert()` / `columns.insert()` now shift the cached row-height /
148
+ column-width lists, fixing corrupted height/width reads after an insert.
149
+ - `GroupShape.shapes` now resolves child shapes via `childIds` (was hardcoded
150
+ to `[]`).
151
+ - `PicturePlaceholder.insert_picture` converts WebP/SVG/AVIF to PNG (was
152
+ mislabeling them `png`) and honors its `width`/`height` params.
153
+ - `batch()` discards the buffer when its body raises (was committing the
154
+ partial prefix).
155
+ - A server-rejected command in a batch is dropped (not re-queued), ending the
156
+ permanent buffer-poison loop; the rest of the rolled-back chunk is restored.
157
+ - Command chunks carry a stable `txn.id` across retries so an ambiguous-timeout
158
+ resend dedups server-side instead of double-applying.
159
+ - `flush_all(strict=True)` raises `FlushAllError` on a per-buffer flush failure
160
+ (the sandbox executor opts in) instead of silently swallowing it.
161
+ - `Presentation` no longer pins its buffer/client/session via the atexit hook
162
+ (weakref closure); `close()`/`__del__` release the HTTP session.
163
+ - Export 404s fail fast for a deck this client already read (deleted deck),
164
+ instead of burning the ~85s post-upload retry budget.
165
+ - `XyChartData` / `BubbleChartData` `add_series` raise on malformed inline
166
+ points instead of silently dropping them.
167
+ - `mso_shape_to_string` raises on an unmapped autoshape id instead of silently
168
+ authoring a rectangle.
169
+ - `AddMovie` emits `posterBase64` (the key the server reads) and always sends a
170
+ poster (a 1x1 placeholder when none is supplied), so `add_movie`/`add_video`
171
+ no longer fail the whole batch.
172
+
173
+ ## 0.10.1 — unnamed-shape parity, browser UA on image fetches, honest SVG errors
174
+
175
+ Fixes the three `execute_presentation_code` failures observed in the
176
+ 2026-08-12 production deck-build session (thread_d7eb9dd6).
177
+
178
+ - `Shape.name` now always returns a `str` (python-pptx parity —
179
+ `cNvPr@name` is a schema-required attribute upstream). Elements
180
+ persisted before names were stored (e.g. pictures added by pre-0.10
181
+ SDKs) read back as `""` instead of `None`, so idioms like
182
+ `'Picture' in shape.name` no longer raise `TypeError`.
183
+ - `add_picture(url)` fetches now send browser-equivalent
184
+ `User-Agent`/`Accept` headers. Image hosts and proxies behind bot
185
+ rules (e.g. `wsrv.nl`, Cloudflare-fronted CDNs) 403 the default
186
+ `python-requests/x.y` UA while serving the identical URL to a browser
187
+ UA. The Accept header advertises only formats the deck pipeline
188
+ handles (PNG/JPEG native, WebP via Pillow) so format-negotiating CDNs
189
+ don't switch the body to AVIF or SVG.
190
+ - AVIF is now detected explicitly (any `avif`/`avis` brand in the
191
+ ISO-BMFF `ftyp` box — major or compatible, so `mif1`-major HEIF
192
+ containers are covered) instead of falling through the sniffer as
193
+ `png` — it converts via Pillow where the build supports AVIF, and
194
+ otherwise raises the actionable convert-or-fail error rather than
195
+ shipping undecodable bytes that render as a silent broken-image
196
+ placeholder.
197
+ - `Shapes.by_name("")` / `get_by_name("")` no longer match unnamed
198
+ legacy shapes — `""` is the no-name sentinel, not an addressable
199
+ name.
200
+ - The SVG `add_picture` error no longer recommends `pip install svglib`
201
+ (rasterization also needs the `rlPyCairo` backend and the native cairo
202
+ library, so that advice cannot work in a stock sandbox). WebP and SVG
203
+ now get separate, accurate remediation hints. Pairs with the
204
+ presentation-exec v59 snapshot, which bundles `svglib` + `rlPyCairo`
205
+ so SVG→PNG conversion works in-sandbox.
206
+
5
207
  ## 0.9.0 — eager validation, table overflow diagnostics, and text caps
6
208
 
7
209
  Publishes the SDK changes already deployed with PPTX Studio after 0.8.1.
@@ -0,0 +1,5 @@
1
+ @AGENTS.md
2
+
3
+ <!-- AGENTS.md next to this file is the single source of truth for agent
4
+ instructions. Claude Code does not read AGENTS.md natively, so this file
5
+ imports it. Add Claude-Code-only rules below this line. -->
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: athena-python-pptx
3
- Version: 0.10.0
3
+ Version: 0.13.0
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
@@ -158,6 +158,119 @@ very large inline image) cannot be split — it is sent alone and may still
158
158
  be rejected with a teaching `IMAGE_TOO_LARGE` error, but it no longer
159
159
  takes the rest of the queued edits down with it.
160
160
 
161
+ ### `Chart.replace_data()` emits one `ReplaceChartData` op (v0.12.0 — requires a server with ReplaceChartData support)
162
+
163
+ python-pptx's `chart.replace_data(chart_data)` rewrites the chart's whole
164
+ data matrix. Before v0.12.0 the SDK approximated that with per-index
165
+ `UpdateSeries*` / `UpdateCategoryLabel` patches, which could neither shrink
166
+ nor grow the data (stale trailing `c:pt` entries and orphaned series
167
+ corrupted the chart part). As of v0.12.0 each `replace_data` call emits
168
+ exactly **one** `ReplaceChartData` patch carrying the full
169
+ `categories` + `series` matrix, so shrinking drops trailing points, growing
170
+ adds them, and extra series are removed — matching python-pptx semantics.
171
+
172
+ **Deployment note:** a pptx-studio server without the `ReplaceChartData`
173
+ patch op (pre-#30211) rejects the whole command batch with a Zod validation
174
+ error. SDK 0.12.0 must only be rolled into sandbox snapshots after the
175
+ studio deploy that ships the op.
176
+
177
+ ### `pptx.flush_all()` / `pptx.discard_all()` — module-level buffer drains (Athena-only)
178
+
179
+ The sandbox executor postamble ends every `execute_presentation_code` run
180
+ with one of these:
181
+
182
+ - `flush_all(strict=False)` — flush every live presentation's buffered
183
+ commands. `strict=True` (used by the executor) raises `FlushAllError`
184
+ when any buffer's flush is rejected instead of silently swallowing it.
185
+ - `discard_all()` (v0.12.0) — drop all buffered, un-flushed commands
186
+ across open decks **without posting them** and return the dropped count.
187
+ Called when user code raised: posting a dead script's half-buffered tail
188
+ would write content whose prerequisites failed (and duplicate it when
189
+ the agent retries the whole script). Best-effort — a batch already
190
+ handed to an in-flight background flush cannot be recalled.
191
+
192
+ ```python
193
+ import pptx
194
+ dropped = pptx.discard_all() # -> int, 0 when nothing was buffered
195
+ ```
196
+
197
+ Stock python-pptx has no equivalent; local scripts normally never need
198
+ either function (buffers auto-flush on idle and at interpreter exit).
199
+
200
+ ### `pptx.validate_all_touched() -> dict[int, list[str]]` — validate the slides a run edited (Athena-only)
201
+
202
+ The third executor hook, called after `flush_all(strict=True)`. Every
203
+ `CommandBuffer` remembers the slide indexes, slide ids (including the
204
+ `sld_…` client ids of slides added in the run) and shape ids of the commands
205
+ the server **applied** since the last call (`CommandBuffer.take_touched()`);
206
+ `validate_all_touched()` resolves them against each live `Presentation`'s
207
+ slide proxies, runs [`slide.validate()`](#slidevalidate---liststr) on exactly
208
+ those slides, and returns `{slide_index: warnings}` for the slides that had
209
+ warnings — `{}` means every touched slide is clean.
210
+
211
+ - Only flushed commands count — buffered edits are not yet "touched".
212
+ - The touched set is consumed by the call; a second call with no flush in
213
+ between returns `{}`.
214
+ - Commands that cannot change a layout (notes, names, citations, deck
215
+ metadata) never mark a slide. `DeleteSlide` and `ReorderSlides` mark
216
+ nothing but shift the indexes recorded before them, as do slide
217
+ insertions (`AddSlide`, `CloneSlide`, `CopySlideFromDeck`), so an index
218
+ recorded early in a batch still names the same slide at the end.
219
+ - Destination-bearing commands record their destination: `CloneShape`
220
+ marks `target_slide_index` (the source shape only when cloning in place),
221
+ `CopySlideFromDeck` its `destination_index`, `UngroupShapes` the group
222
+ shape's slide, `ReplaceText` its shape or slide. `SetPresentationSize`
223
+ and a deck-wide `ReplaceText` set `all_slides` and validate every slide.
224
+ - Never raises for validation reasons: a slide whose `validate()` fails
225
+ contributes a single `"validate() failed: …"` line.
226
+
227
+ ```python
228
+ import pptx
229
+ pptx.flush_all(strict=True)
230
+ for index, warnings in pptx.validate_all_touched().items():
231
+ print(f"slide {index + 1}:", *warnings, sep="\n ")
232
+ ```
233
+
234
+ Stock python-pptx has no equivalent.
235
+
236
+ ### Snapshot-hydrated full-state setters (v0.12.0)
237
+
238
+ `Shape.shadow`, `Shape.click_action`, and `Presentation.core_properties`
239
+ re-emit their **full** state on every partial write (their wire commands
240
+ have no partial form). As of v0.12.0 they hydrate that state from the
241
+ snapshot at construction, so a partial edit preserves what was ingested:
242
+
243
+ - `shape.shadow.blur_radius = Pt(6)` keeps the ingested shadow's
244
+ `visible=True` / type / distance / direction / color / transparency
245
+ (previously it emitted the un-hydrated `visible=False` default, deleting
246
+ the shadow server-side). A hydrated shadow also reads back
247
+ `shadow.inherit == False`.
248
+ - `shape.click_action.tooltip = "..."` keeps the ingested link's action
249
+ type / URL / slide target (previously it emitted `action_type='none'`,
250
+ deleting the link).
251
+ - `prs.core_properties.author = "..."` keeps the deck's existing title /
252
+ subject / keywords / comments / category (previously they were wiped to
253
+ `None`). `Presentation.refresh()` re-hydrates from the fresh snapshot.
254
+
255
+ ### `paragraph.level` rides the wire only when meaningful (v0.12.0)
256
+
257
+ `SetParagraphStyle` re-emits the paragraph's full style on every setter.
258
+ `level` is now included only when the paragraph is indented (`level > 0`)
259
+ or the caller explicitly assigned `paragraph.level` (including an explicit
260
+ reset to `0`, which must reach the wire). An untouched level-0 paragraph
261
+ omits the field so unrelated style edits (alignment, spacing, …) don't
262
+ stomp the server-side outline level.
263
+
264
+ ### Run-level slide jumps send exactly one of address / targetSlideIndex (v0.12.0)
265
+
266
+ `run.hyperlink.target_slide = n` emits `SetRunHyperlinkTarget` with
267
+ `targetSlideIndex` only — it no longer rides with `address=''` (the
268
+ clear-the-link convention), which servers interpret as "clear". An
269
+ explicit `run.hyperlink.address = None` with no slide target still sends
270
+ `address=''` to clear. The rich-content re-emit carries the jump as
271
+ `hyperlinkTargetSlideIndex` on the run, and snapshot rich content with
272
+ that key hydrates back into `run.hyperlink`.
273
+
161
274
  ---
162
275
 
163
276
  ## Agent-Friendly Additions (not in python-pptx)
@@ -314,12 +427,47 @@ slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(2))
314
427
 
315
428
  ### `slide.validate() -> list[str]`
316
429
 
317
- Report layout issues without rendering. Returns warnings for:
318
-
319
- * Off-slide shapes (bbox extends outside the slide rectangle).
320
- * Overlapping slide-local shapes (>0.01% of slide area overlap).
321
-
322
- Does **not** detect text overflow — for that, render the slide to PNG.
430
+ Report the layout defects a reader would notice, without rendering. An
431
+ empty list means the slide is clean — and a designed slide (cards behind
432
+ their labels, icons inside tiles) reaches it. Warnings come back in this
433
+ order, at most 15 per slide, then a count line
434
+ `"… and N more (M containment overlaps ignored)"`:
435
+
436
+ 1. **Text overflow** — `"text in 'X' likely overflows its box (needs ~N
437
+ lines of ~M at Spt; …)"` for non-table text whose estimated height
438
+ (per-paragraph font-metric width → wrapped lines → lines × font × 1.2 +
439
+ spacing) exceeds the box minus its text-frame margins. Word-wrap-off
440
+ frames are checked horizontally; auto-fit frames are skipped. When the
441
+ presentation has a studio client, each hit whose paragraphs share one
442
+ font size and weight is confirmed with one `POST /measure-text-fit`
443
+ pinned at that size (`fits: true` drops the warning, at most 12
444
+ round-trips per call, network errors keep the heuristic verdict); a
445
+ mixed-font frame (60pt heading over 12pt body) keeps the heuristic
446
+ verdict because the server lays the frame out at a single font. Table
447
+ cells whose single-line text is wider than the column are reported here
448
+ too.
449
+ 2. **Hidden text** — `"text in 'X' is covered by 'Y'"`: a text shape at
450
+ least half covered by a *later* (higher z-order) shape with a solid fill,
451
+ no transparency and no text. A solid fill the snapshot reports without
452
+ transparency data is taken as opaque (PowerPoint's default); any
453
+ transparency set on the proxy or carried as `fillTransparency` disables
454
+ the rule for that shape. `send_to_back()` / `bring_to_front()` now mirror
455
+ the move on `slide.shapes` order so the rule sees the intended stack
456
+ without a refresh.
457
+ 3. **Text on text** — two text-bearing shapes (tables count) whose bboxes
458
+ overlap or nest.
459
+ 4. **Other** — off-slide shapes; partial overlaps where exactly one shape
460
+ carries text (a title running under a card); empty slide-level
461
+ placeholders.
462
+
463
+ Not reported: one shape fully inside another unless both carry text,
464
+ overlaps between two text-less shapes, and hairline overlaps thinner than
465
+ a text-frame inset (0.05in). Rotated shapes near 90°/270° use their swapped
466
+ visual bbox.
467
+
468
+ The overflow estimate uses average glyph widths, so offline it can be off
469
+ by a line either way; render the slide before rebuilding a layout around a
470
+ single warning.
323
471
 
324
472
  ```python
325
473
  warnings = slide.validate()
@@ -1526,3 +1674,75 @@ prior in-module definitions. `AssetReference` remains a thin in-module wrapper
1526
1674
  that additionally accepts a plain `dict` `meta` (the historical SDK shape) and
1527
1675
  serializes it verbatim, preserving byte-identical `to_dict()` output for
1528
1676
  existing callers.
1677
+
1678
+ ---
1679
+
1680
+ ## `pptx.kit` — composition kit (Athena-only, unreleased)
1681
+
1682
+ `from pptx.kit import Kit, Palette, Fonts, Region` (also re-exported from the
1683
+ `pptx` package). No upstream counterpart — python-pptx has no layout layer.
1684
+ The module carries the module-level `__athena_extension_module__` marker and
1685
+ each of the four classes is decorated `@athena_extension`, so the whole
1686
+ surface walks through `iter_athena_extensions`.
1687
+
1688
+ **Why.** Two agent-built 8-slide decks (2026-09-14) each hand-placed 200+
1689
+ shapes with EMU arithmetic and both agents wrote the same six helpers
1690
+ (`rect`, `text`, `header`, `kpi`, `card`, `takeaway`) from scratch. Every
1691
+ visual defect was placement maths: text wrapping in a too-narrow box, a title
1692
+ under the KPI cards, body text hidden under a takeaway bar added later. The
1693
+ model should compose, not compute EMUs.
1694
+
1695
+ **Surface.**
1696
+
1697
+ | Member | Purpose |
1698
+ |--------|---------|
1699
+ | `Kit(prs, palette=None, fonts=None)` | Reads slide size, layouts and the command buffer from `prs`. `kit.footer_text` is the footer text `header()` places on every slide; `kit.reserve_takeaway` (default `True`) keeps the takeaway band free in `content_region()` before a bar exists. |
1700
+ | `Palette(...)` | `RRGGBB` hex fields (`#` / lowercase normalised). Athena defaults: primary `0B1F3A`, accent `00A6A6`, highlight `F2A900`, ink `1F2937`, muted `6B7280`, surface `F4F6FA`, line `D9DEE7`, on_dark `FFFFFF`. Any `color=` / `accent=` argument may name a field (`"accent"`) or pass hex / `RGBColor`. |
1701
+ | `Fonts(heading=None, body=None, …)` | Family names (`None` inherits the deck theme) plus the point-size scale each helper starts from (`title_pt=30`, `title_min_pt=18`, `kicker_pt=11`, `body_pt=14`, `small_pt=11`, `kpi_value_pt=36`, `card_title_pt=14`, `card_body_pt=12`, `takeaway_pt=14`, `footer_pt=9`, `min_pt=8`). Auto-fit only ever shrinks. |
1702
+ | `Region(x, y, w, h)` | Frozen EMU rectangle (accepts `Length`); `.right`, `.bottom`, `.inset(dx, dy=None)`, `.split_h(n_or_weights, gap=None)`, `.split_v(...)`, `.contains`, `.overlaps`, `.intersect`. |
1703
+ | `kit.new_slide(layout_name="Blank") -> Slide` | `slide_layouts.by_name(...)`, falling back to the first layout without body placeholders, then the first layout. |
1704
+ | `kit.header(slide, title, kicker=None, page=None) -> Region` | Small-caps accent kicker, action title auto-fit to a two-line band, footer rule + `footer_text` + page number. Returns the title band. |
1705
+ | `kit.footer(slide, text, page=None) -> Region` | Rule, `text` (left) and `page` (right) inside the reserved footer band; idempotent per slide. |
1706
+ | `kit.takeaway(slide, text) -> Region` | Full-width primary bar with an accent stripe, one gap above the footer band; text auto-fit, `on_dark`. |
1707
+ | `kit.content_region(slide, *, takeaway=None) -> Region` | Between the title band (if a header was placed) and the takeaway band / footer band. |
1708
+ | `kit.columns(slide, n, y=None, h=None, gap=None)` / `kit.grid(slide, rows, cols, y=None, h=None, gap=None)` | Partition the content region; `n` / `rows` / `cols` may be weights. |
1709
+ | `kit.kpi_row(slide, items, y=None, h=None, accents=None)` / `kit.kpi_card(slide, region, value, label, accent=None)` | Surface cards with an accent stripe, a big auto-fit value and an auto-fit label; `kpi_row` returns the card regions. |
1710
+ | `kit.card(slide, region, title=None, body=None, accent=None)` | Card with a bold title and a body (a string = paragraphs, a list = bullets). |
1711
+ | `kit.text(slide, region, text_or_paragraphs, size=None, bold=False, color=None, align=None, anchor=None, fit=True)` / `kit.bullets(slide, region, items, size=None, fit=True)` | A text box filling the region; returns the shape. Bullet items may be `(text, level)` tuples. |
1712
+ | `kit.fit_size(paragraphs, region, max_size, *, min_size=None, bold=False, family=None)` | The fitting size the helpers use, exposed for custom boxes. |
1713
+
1714
+ **Rules the kit enforces.**
1715
+
1716
+ - Every position derives from `prs.slide_width` / `prs.slide_height`: margin
1717
+ 5% of the width, gutter 1.5%, top margin 6% of the height, title band 15%,
1718
+ footer band 6%, takeaway bar 10%, KPI row 20%.
1719
+ - Regions are clamped to the slide and to the canvas above the footer band;
1720
+ a region with nothing left raises `ValueError` rather than placing nothing.
1721
+ The `y` / `h` overrides of `columns`, `grid` and `kpi_row` only ever narrow
1722
+ the content region — they are clamped to it, so they cannot reach the title
1723
+ band or the reserved takeaway band. Header, takeaway and footer bands never
1724
+ overlap each other; footer text always leaves the page-number column free.
1725
+ - With `fit=True` the largest size ≤ the requested size that fits is used.
1726
+ When the presentation's client has a base URL the kit POSTs a
1727
+ `MeasureTextFit` query to `/measure-text-fit` (the same client pattern as
1728
+ `TextFitter.best_fit_font_size`; result floored, cached per kit; one failed
1729
+ probe latches the fallback for the kit's lifetime). Offline it greedy-wraps
1730
+ on the `_estimate_text_width_emu` glyph table at 1.2 line height. When
1731
+ nothing fits even at the minimum size (the server's `fits: false`, or the
1732
+ estimate) the kit uses the minimum and emits a `RuntimeWarning` naming the
1733
+ text, so the author can shorten it or enlarge the region. Fitted boxes also
1734
+ get `auto_size = TEXT_TO_FIT_SHAPE` as a render-time safety net;
1735
+ `fit=False` keeps the requested size and touches no auto-size.
1736
+ - Pure python-pptx public API underneath — `add_shape(MSO_SHAPE.RECTANGLE, …)`,
1737
+ `add_textbox`, `fill.solid()` / `fore_color.rgb`, `line.fill.background()`,
1738
+ `text_frame`, `font` — so the deck is an ordinary deck and every command
1739
+ rides the presentation's `CommandBuffer`; build each slide inside
1740
+ `with prs.batch():` for one round-trip.
1741
+
1742
+ Per-slide state (header bottom, takeaway top, footer parts) is keyed by
1743
+ `slide.slide_id` — the SDK's stable slide identity, so it survives reorders,
1744
+ inserts and deletes — for the life of the `Kit`. Compose a slide through one
1745
+ `Slide` proxy (normally inside one `with prs.batch():`); a batch-created slide
1746
+ re-fetched after a refresh carries its server id and starts with fresh state.
1747
+ Reaches agents with the next `athena-python-pptx` bump. Demo:
1748
+ `examples/kit_demo.py`; tests: `tests/test_kit.py`.