athena-python-pptx 0.6.0__tar.gz → 0.8.1__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 (88) hide show
  1. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/CHANGELOG.md +121 -0
  2. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/CLAUDE.md +4 -2
  3. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/PKG-INFO +1 -1
  4. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/docs/API_PARITY_EXCEPTIONS.md +91 -50
  5. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/__init__.py +1 -1
  6. athena_python_pptx-0.8.1/pptx/_citations.py +54 -0
  7. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/batching.py +178 -19
  8. athena_python_pptx-0.8.1/pptx/chart/data.py +446 -0
  9. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/client.py +87 -10
  10. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/commands.py +104 -22
  11. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/dml/color.py +35 -45
  12. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/enum/dml.py +10 -11
  13. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/enum/shapes.py +23 -1
  14. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/enum/text.py +22 -8
  15. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/media.py +37 -5
  16. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/presentation.py +17 -6
  17. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/__init__.py +364 -77
  18. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/slides.py +95 -45
  19. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/text/__init__.py +217 -78
  20. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/typing.py +1 -0
  21. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/units.py +39 -20
  22. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pyproject.toml +1 -1
  23. athena_python_pptx-0.6.0/pptx/chart/data.py +0 -266
  24. athena_python_pptx-0.6.0/uv.lock +0 -1215
  25. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/.gitignore +0 -0
  26. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/API_PARITY_REPORT.md +0 -0
  27. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/DEV-GUIDE.md +0 -0
  28. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/PARITY_QUESTIONS.md +0 -0
  29. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/PUBLISHING.md +0 -0
  30. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/README.md +0 -0
  31. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/docs/athena-api.json +0 -0
  32. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/docs/athena-api.md +0 -0
  33. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/_athena_extension.py +0 -0
  34. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/_ptc.py +0 -0
  35. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/_references.py +0 -0
  36. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/action.py +0 -0
  37. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/__init__.py +0 -0
  38. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/axis.py +0 -0
  39. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/category.py +0 -0
  40. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/chart.py +0 -0
  41. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/datalabel.py +0 -0
  42. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/legend.py +0 -0
  43. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/marker.py +0 -0
  44. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/plot.py +0 -0
  45. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/point.py +0 -0
  46. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/series.py +0 -0
  47. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/chart/xlsx.py +0 -0
  48. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/decorators.py +0 -0
  49. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/dml/__init__.py +0 -0
  50. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/dml/chtfmt.py +0 -0
  51. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/dml/effect.py +0 -0
  52. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/dml/fill.py +0 -0
  53. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/dml/line.py +0 -0
  54. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/docgen.py +0 -0
  55. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/enum/__init__.py +0 -0
  56. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/enum/action.py +0 -0
  57. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/enum/chart.py +0 -0
  58. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/enum/lang.py +0 -0
  59. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/errors.py +0 -0
  60. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/exc.py +0 -0
  61. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/package.py +0 -0
  62. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/__init__.py +0 -0
  63. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/_base.py +0 -0
  64. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/chart.py +0 -0
  65. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/coreprops.py +0 -0
  66. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/embeddedpackage.py +0 -0
  67. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/image.py +0 -0
  68. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/media.py +0 -0
  69. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/presentation.py +0 -0
  70. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/parts/slide.py +0 -0
  71. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/autoshape.py +0 -0
  72. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/base.py +0 -0
  73. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/connector.py +0 -0
  74. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/freeform.py +0 -0
  75. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/graphfrm.py +0 -0
  76. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/group.py +0 -0
  77. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/picture.py +0 -0
  78. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/placeholder.py +0 -0
  79. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shapes/shapetree.py +0 -0
  80. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/shared.py +0 -0
  81. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/slide.py +0 -0
  82. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/spec.py +0 -0
  83. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/table.py +0 -0
  84. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/text/fonts.py +0 -0
  85. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/text/layout.py +0 -0
  86. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/text/text.py +0 -0
  87. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/types.py +0 -0
  88. {athena_python_pptx-0.6.0 → athena_python_pptx-0.8.1}/pptx/util.py +0 -0
@@ -2,6 +2,127 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## 0.8.0 — Surgical run text + faithful style hydration
6
+
7
+ Closes the "agent edited deck text and the styling vanished" class of bugs
8
+ (font size/weight/color jumping to the renderer's 18pt default after a text
9
+ rewrite).
10
+
11
+ **`run.text = …` is now surgical.** The setter (and the run text helpers
12
+ `upper/lower/capitalize/title/strip/clear/replace`) emits the new
13
+ `SetRunText` command — the server updates just that run's text, leaving its
14
+ style, hyperlink, sibling runs, and paragraph properties untouched — instead
15
+ of re-serializing the entire text frame from local state. Setting text on a
16
+ freshly `add_run()`-appended run appends server-side inheriting the previous
17
+ run's style (continue-typing semantics). Requires a pptx-studio deployment
18
+ that knows `SetRunText`; set `ATHENA_PPTX_LEGACY_RUN_TEXT=1` to restore the
19
+ old whole-frame re-emit against older servers.
20
+
21
+ **Snapshot hydration is now faithful.** `Run` hydration previously mapped
22
+ only 8 style keys (bold/italic/underline/fontSizePt/fontFamily/fontRef/
23
+ colorHex/spacingPt) — any full-frame re-emit (`paragraph.text`,
24
+ `add_paragraph()`, pre-0.8 `run.text`) silently stripped everything else
25
+ from every run in the shape. Now:
26
+
27
+ - `schemeColor`/`brightness` hydrate into the color API —
28
+ `run.font.color.theme_color` / `.brightness` return the ingested values.
29
+ - `strike`/`strikethrough`, `subscript`/`superscript`/`baseline`, and
30
+ `languageId` hydrate into their typed fields.
31
+ - Everything else the server sends (`cap`, `highlightColorHex`,
32
+ `highlightSchemeColor`, future keys) is carried verbatim in
33
+ `Font._extra_style` and reproduced by re-serialization.
34
+
35
+ Pairs with the server-side `applySetText` preservation upgrade (same PR):
36
+ unchanged lines keep their exact runs, rewritten lines keep the replaced
37
+ line's explicit style, and new lines continue the surrounding style instead
38
+ of collapsing to the 18pt default.
39
+
40
+ ## 0.7.1 — CommandBuffer thread-safety
41
+
42
+ Fixes a data-loss/duplication race in `CommandBuffer`. `self._commands` was
43
+ shared between the foreground `add`/`flush`/`clear` path and the 2.0s
44
+ auto-flush daemon thread, with only the timer guarded — so a flush that
45
+ snapshotted-then-reset the buffer could drop a command a concurrent `add`
46
+ appended in between (silent slide/shape loss), and a timer flush racing a
47
+ foreground flush could POST the same commands twice (duplication). In
48
+ long-lived Daytona executor processes this silently dropped or duplicated
49
+ slides/shapes with no error. A dedicated `_commands_lock` now guards every
50
+ read/append/reassign of `_commands` (drain atomically under the lock; POST
51
+ outside it). No public API change. (#24278)
52
+
53
+ ## 0.7.0
54
+
55
+ **python-pptx behavioral-parity sweep — read-path return types, enum
56
+ values, and chart-data builder objects now match python-pptx 1.0.2.**
57
+
58
+ Surface parity (every public class/method/signature) was already 100%, but a
59
+ deep-dive against python-pptx 1.0.2 found *behavioral* divergences where
60
+ getters returned raw strings/`None` instead of enum members or typed
61
+ defaults, two enums had wrong integer values, and the chart-data builders
62
+ weren't python-pptx-shaped. All fixes are pure-local (no wire/server change);
63
+ `to_series_payload()` output is byte-identical to before.
64
+
65
+ **Enum value corrections (silent drop-in breakers):**
66
+
67
+ - **`MSO_THEME_COLOR.TEXT_2` / `BACKGROUND_2` were swapped** — now `15` /
68
+ `16` to match upstream (the `<a:schemeClr>` read/write maps were updated in
69
+ lockstep so OOXML output is unchanged). `MSO_THEME_COLOR_INDEX` inherits.
70
+ - **`MSO_TEXT_UNDERLINE_TYPE` had 6 wrong values, 9 missing members, and a
71
+ bogus `DASHED_LINE`** — rewritten to upstream's exact 19 members/values;
72
+ added the `MSO_UNDERLINE` alias (`from pptx.enum.text import MSO_UNDERLINE`).
73
+ - **`PP_MEDIA_TYPE`**: `MOVIE` corrected `2`→`3`, added `OTHER = 1`, and the
74
+ enum is now importable from its canonical `pptx.enum.shapes` location.
75
+ - Added `MSO_SHAPE_TYPE.MIXED = -2` and the `MSO` alias; added the
76
+ upstream-typo `MSO_PATTERN.ERCENT_40` member.
77
+
78
+ **Read-path return-type parity (getters now return enums / typed defaults):**
79
+
80
+ - `FillFormat.type` → `MSO_FILL_TYPE` (was a raw string); `background()` now
81
+ reports `MSO_FILL_TYPE.BACKGROUND`. `gradient_angle`/`gradient_stops`/
82
+ `pattern` raise `TypeError` on a wrong-type fill (parity).
83
+ - `LineFormat.dash_style` → `MSO_LINE_DASH_STYLE` (or `None` when unstyled,
84
+ instead of defaulting to `"solid"`); `LineFormat.width` defaults to
85
+ `Emu(0)` for an unstyled line (a color-/dash-only edit no longer injects a
86
+ synthetic 1pt width).
87
+ - `Chart.chart_type` → `XL_CHART_TYPE` member (was a raw server string);
88
+ `add_chart` now persists `grouping` so stacked/clustered variants read back
89
+ exactly.
90
+ - `TableCell.vertical_anchor` → `MSO_VERTICAL_ANCHOR`; `TableCell.margin_*`
91
+ default to the OOXML defaults (`91440`/`45720` EMU) instead of `None`.
92
+ - `Shape.rotation` is normalized to `[0, 360)` and float-coerced.
93
+ - `_Column.width` / `_Row.height` are populated for `add_table(...)` tables
94
+ (even division, last column/row absorbs the remainder).
95
+ - `RGBColor` is now a real `tuple` subclass (`RGBColor(255,0,0) == (255,0,0)`,
96
+ `isinstance(c, tuple)`).
97
+ - `ColorFormat.theme_color` returns `MSO_THEME_COLOR.NOT_THEME_COLOR` for an
98
+ sRGB color (raises `TypeError` when no color is present) instead of `None`.
99
+ - `TextFrame.auto_size` / `vertical_anchor` return their enums; `margin_*`
100
+ default to the python-pptx `Length` defaults; `Paragraph.line_spacing =
101
+ None` clears the override without crashing; `Font.language_id` returns
102
+ `MSO_LANGUAGE_ID.NONE` on an unset run.
103
+ - `SlideLayouts.get_by_name` is exact/case-sensitive (the fuzzy match moved to
104
+ the Athena-only `by_name`); `Slide.has_notes_slide` flips `True` once
105
+ `notes_slide` is accessed.
106
+ - `Video.ext` returns the bare extension (`'mp4'`, `'vid'` fallback) and
107
+ `Video.filename` synthesizes `'movie.{ext}'`.
108
+
109
+ **`pptx.util` truncation parity:** unit constructors (`Emu`/`Pt`/`Inches`/…)
110
+ truncate toward zero like upstream (`int(Pt(1.9999)) == 25398`) instead of
111
+ rounding; `Length.centipoints` is integer floor-division.
112
+
113
+ **Chart-data builder objects (`pptx.chart.data`) are now python-pptx-shaped:**
114
+
115
+ - `CategoryChartData.add_series(name, values)` returns a real
116
+ `CategorySeriesData` with `.values`, `.add_data_point()`, and `.index`;
117
+ `XyChartData`/`BubbleChartData.add_series` return `XySeriesData`/
118
+ `BubbleSeriesData` whose `add_data_point(...)` returns the data point and
119
+ expose `.x_values` / `.y_values` / `.bubble_sizes`.
120
+ - `CategoryChartData.add_category(label)` and a `Categories`/`Category`
121
+ sequence with `are_dates` / `are_numeric` / `number_format`; category label
122
+ types (dates, numbers) are preserved through `cd.categories[i].label`.
123
+ - The three chart-data containers implement the `Sequence` protocol
124
+ (`for s in chart_data` / `chart_data[0]`).
125
+
5
126
  ## 0.4.2
6
127
 
7
128
  **Paragraph-level font fidelity — `paragraph.font` now matches stock
@@ -52,8 +52,10 @@ A small number of REST-SDK-specific departures are documented in
52
52
  use `slide.notes_slide.notes_text_frame` for notes text.
53
53
  - **`XyChartData.add_series(name, values)`** signature drift vs upstream's
54
54
  `add_series(name, number_format=None)` — caller passes inline tuples.
55
- - **`TextFitter.best_fit_font_size()`** returns `max_size` unchanged
56
- (auto-fit is server-side); emits `RuntimeWarning`.
55
+ - **`TextFitter.best_fit_font_size()`** is server-measured: with a base
56
+ URL configured it queries `POST /measure-text-fit` and returns the
57
+ largest fitting size; only falls back to `max_size` (+ `RuntimeWarning`)
58
+ when no studio client is reachable.
57
59
  - **`_BaseSeriesData.index`** raises `NotImplementedError` (raising
58
60
  rather than silently returning 0).
59
61
  - **`Package.open()`** raises `UnsupportedFeatureError`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-pptx
3
- Version: 0.6.0
3
+ Version: 0.8.1
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
@@ -45,48 +45,51 @@ text = slide.notes_slide.notes_text_frame.text
45
45
  A future server-side notes-master patch op will plumb the full shape
46
46
  tree through. Tracked in `PARITY_QUESTIONS.md`.
47
47
 
48
- ### `XyChartData.add_series()` — different signature
48
+ ### `XyChartData.add_series()` — both the upstream and inline patterns work (v0.7.0)
49
49
 
50
- Upstream signature: `add_series(name, number_format=None)` — caller adds
51
- data points via the returned series object.
52
-
53
- **Our signature:** `add_series(name, values)` — caller passes the
54
- (x, y) tuples up front.
55
-
56
- **Migration:**
50
+ As of v0.7.0 `add_series` returns a real `XySeriesData` (resp.
51
+ `BubbleSeriesData`), so the **upstream pattern works**:
57
52
 
58
53
  ```python
59
- # python-pptx pattern (NOT supported by athena-python-pptx):
54
+ # python-pptx pattern (now supported):
60
55
  series = data.add_series("Series 1")
61
- series.add_data_point(1.0, 2.0)
56
+ series.add_data_point(1.0, 2.0) # returns the XyDataPoint
62
57
  series.add_data_point(3.0, 4.0)
58
+ series.x_values # [1.0, 3.0]; series.y_values -> [2.0, 4.0]
63
59
 
64
- # athena-python-pptx pattern (use this):
60
+ # athena-python-pptx inline shorthand (still supported):
65
61
  data.add_series("Series 1", [(1.0, 2.0), (3.0, 4.0)])
66
62
  ```
67
63
 
68
- Same applies to `BubbleChartData.add_series`. Fixing this is a larger
69
- refactor (the SDK's chart-authoring path expects values inline);
70
- tracked in `PARITY_QUESTIONS.md`.
64
+ `add_series` additionally accepts an optional `number_format=` for
65
+ signature parity. The same applies to `BubbleChartData.add_series`
66
+ (inline triples are `(x, y, size)`).
71
67
 
72
- ### `TextFitter.best_fit_font_size()` — returns `max_size` unchanged
68
+ ### `TextFitter.best_fit_font_size()` — server-measured (needs a base URL)
73
69
 
74
- Upstream binary-searches font sizes locally to fit text within bounds.
75
- Our stub always returns `max_size` and emits a `RuntimeWarning`.
76
- Auto-fit happens server-side at render time. **Don't rely on this for
77
- local layout decisions**; use the auto-fit text frame property instead:
70
+ Upstream binary-searches font sizes against locally-installed font
71
+ metrics, which a REST SDK can't read. When a base URL is configured
72
+ (`ATHENA_PPTX_BASE_URL` or an explicit `client`) the classmethod issues
73
+ a `MeasureTextFit` query to the studio's `POST /measure-text-fit`
74
+ endpoint and returns the largest integer size that fits — measured
75
+ against the studio's bundled fonts via the same layouter the renderer
76
+ uses (see the extension entry below). It falls back to `max_size` with a
77
+ `RuntimeWarning` **only** when no studio client is reachable (offline /
78
+ import-time tests). For full-deck layout, prefer letting the text frame
79
+ auto-fit at render time:
78
80
 
79
81
  ```python
80
82
  from pptx.enum.text import MSO_AUTO_SIZE
81
83
  text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
82
84
  ```
83
85
 
84
- ### `_BaseSeriesData.index` — raises `NotImplementedError`
86
+ ### `_BaseSeriesData.index` — resolves against the parent chart-data (v0.7.0)
85
87
 
86
- Upstream's `CategorySeriesData.index` returns the series's offset
87
- within its parent chart-data. Our stub raises rather than silently
88
- returning `0` (which would corrupt any OOXML `<c:idx>` builder that
89
- trusts the value). If you need the series index, track it externally.
88
+ As of v0.7.0, a series created via `chart_data.add_series(...)` carries a
89
+ back-reference to its parent, so `series.index` returns the series's
90
+ offset within the chart-data exactly like upstream. A *detached* series
91
+ constructed standalone (no parent) still raises `NotImplementedError`
92
+ rather than lying about the offset.
90
93
 
91
94
  ### `Package.open(pkg_file)` — raises `UnsupportedFeatureError`
92
95
 
@@ -137,6 +140,24 @@ prs.save() # flush only (Athena-only)
137
140
  prs.save("out.pptx") # flush + export + write to local file (parity)
138
141
  ```
139
142
 
143
+ ### Oversized flushes are split into multiple atomic sub-transactions
144
+
145
+ The SDK serializes every buffered command into one JSON body and POSTs it
146
+ to `/decks/:id/commands`. pptx-studio's apps/api (Fastify) caps that body
147
+ at 32 MiB, and `AddPicture` / `SubstitutePlaceholder(picture)` carry inline
148
+ base64 image bytes (~4/3 expansion), so a few large images can blow past
149
+ the ceiling. For batches that exceed the limit, `CommandBuffer.flush`
150
+ splits them into multiple atomic sub-transactions (still validating the
151
+ whole batch up front, so an invalid command aborts before anything is
152
+ applied). The all-or-nothing guarantee then holds *per sub-transaction*
153
+ rather than end-to-end — a server-side failure mid-sequence can leave
154
+ earlier chunks committed. The split threshold is tunable via
155
+ `ATHENA_PPTX_MAX_REQUEST_BYTES`; batches within the limit ship as one
156
+ atomic request, unchanged. A single command larger than the budget (one
157
+ very large inline image) cannot be split — it is sent alone and may still
158
+ be rejected with a teaching `IMAGE_TOO_LARGE` error, but it no longer
159
+ takes the rest of the queued edits down with it.
160
+
140
161
  ---
141
162
 
142
163
  ## Agent-Friendly Additions (not in python-pptx)
@@ -215,34 +236,23 @@ python-pptx's `_Row.height` and `_Column.width` are read/write EMU properties. T
215
236
 
216
237
  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.
217
238
 
218
- ### `TextFrame.clear()` — also resets bullet on paragraph 0
219
-
220
- In upstream python-pptx, `text_frame.clear()` removes every paragraph except the first, and on that first paragraph it removes every child element **except** `<a:pPr>`. Any bullet definition the author wrote into `<a:pPr>` survives the clear, and an inherited bullet from the master / layout's `<a:lstStyle>` is left to be re-resolved by PowerPoint at render time.
221
-
222
- This SDK additionally emits a `SetParagraphStyle(paragraph_index=0, bullet='none')` after the empty `SetText`, explicitly overriding the bullet on the cleared paragraph.
223
-
224
- The deviation exists because pptx-studio **pre-bakes** master/layout list-style inheritance into `richContent.paragraphs[i].bullet` at ingest time (and at slide materialization), then preserves that baked field through every subsequent `applySetText` via `preserveParagraphProps`. Without the explicit override, the python-pptx idiom
225
-
226
- ```python
227
- tf.clear()
228
- p = tf.paragraphs[0]
229
- run = p.add_run()
230
- run.text = "Title"
231
- ```
232
-
233
- renders the title with a stray "•" whenever the placeholder lives on a layout whose master defines a level-1 `<a:buChar/>` (the common case for `<p:bodyStyle>` and any content placeholder). The explicit `bullet='none'` clears the baked inheritance so the literal "no pPr was authored" intent reaches the renderer.
239
+ ### `TextFrame.clear()` — bullet inheritance (now matches upstream)
234
240
 
235
- To re-enable a bullet after `clear()`, set it back explicitly:
241
+ `text_frame.clear()` matches upstream python-pptx: it removes the runs but
242
+ preserves the cleared paragraph's inherited list style. A content/body
243
+ placeholder therefore keeps its master `<p:bodyStyle>` bullet, and a title
244
+ keeps its `<p:titleStyle>` `<a:buNone/>` (no bullet), exactly as PowerPoint
245
+ re-resolves them at render time.
236
246
 
237
- ```python
238
- tf.clear()
239
- p = tf.paragraphs[0]
240
- p.bullet = True # restores 'disc' default
241
- run = p.add_run()
242
- run.text = "Bullet item"
243
- ```
244
-
245
- Only `bullet` is overridden — `alignment`, `level`, `line_spacing`, `space_before`, `space_after`, `margin_left`, `indent`, and `bullet_color` are left alone (they continue to follow upstream `<a:pPr>`-preservation semantics, since baked-inheritance surprises are far less common on those fields).
247
+ > **Historical note:** earlier versions emitted a
248
+ > `SetParagraphStyle(paragraph_index=0, bullet='none')` after the empty
249
+ > `SetText` to mask a *stray title bullet*. The real cause was incorrect
250
+ > per-type bullet baking — empty layout/master prompt paragraphs were baked
251
+ > with `bullet:'none'` instead of their master `<p:txStyles>` value, and a
252
+ > blanket override then stripped bullets from **content** placeholders too
253
+ > (where bullets are expected). That baking is now correct per placeholder
254
+ > type (`titleStyle` → no bullet, `bodyStyle` → its `<a:buChar/>`), so the
255
+ > override was removed and `clear()` is faithful to upstream.
246
256
 
247
257
  ---
248
258
 
@@ -328,6 +338,37 @@ img = ws.export_range_as_image_asset("A1:F20", name="Q4 Revenue")
328
338
  slide.shapes.add_picture(img.url, Inches(1), Inches(1), width=Inches(6))
329
339
  ```
330
340
 
341
+ ### `Picture.image_url` — owner-scoped media URL for cross-deck image copy
342
+
343
+ python-pptx upstream exposes `Picture.image` returning an `Image` with
344
+ `.blob` bytes. The REST SDK can't materialize bytes locally, so `.image`
345
+ stays `None` (see the omissions list above). **Additionally**, when a
346
+ picture is loaded from a server snapshot it now carries `image_url`: a
347
+ stable `{base_url}/media?key=<assetId>` URL that streams the picture's
348
+ bytes. It returns `None` for pictures with no backing media asset (an
349
+ SDK-added picture not yet flushed/refreshed, a placeholder with no image,
350
+ or exotic sources like OLE/movie posters that lack a top-level asset id).
351
+
352
+ The URL is owner/EDIT-scoped (the `/media` route runs the same deck-access
353
+ check as every other deck op), and `add_picture` auto-attaches the
354
+ sandbox bearer for Athena hosts — so it composes directly into the
355
+ canonical "copy every image from deck A into deck B" recipe:
356
+
357
+ ```python
358
+ src = Presentation(asset_id="asset_source")
359
+ dst = Presentation(asset_id="asset_dest")
360
+ for i, slide in enumerate(src.slides):
361
+ for pic in slide.shapes.get_images():
362
+ if pic.image_url:
363
+ dst.slides[i].shapes.add_picture(
364
+ pic.image_url, pic.left, pic.top, pic.width, pic.height
365
+ )
366
+ dst.save()
367
+ ```
368
+
369
+ **Portable code that needs to run against stock python-pptx cannot rely
370
+ on `image_url`** — it is an Athena-only accessor.
371
+
331
372
  ### `name=` kwarg on `add_textbox()` / `add_shape()` / `add_picture()`
332
373
 
333
374
  Optional keyword for setting a stable shape name at creation. python-pptx
@@ -133,7 +133,7 @@ def flush_all() -> None:
133
133
  _active_buffers[:] = alive
134
134
 
135
135
 
136
- __version__ = "0.6.0"
136
+ __version__ = "0.8.1"
137
137
 
138
138
  __all__ = [
139
139
  # Main entry point
@@ -0,0 +1,54 @@
1
+ """Helpers for SDK-native Athena citations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import parse_qs, urlparse
6
+
7
+ _ATHENA_SPACES_HOST_SUFFIXES: tuple[str, ...] = (
8
+ "athenaintel.com",
9
+ "athenaintelligence.ai",
10
+ )
11
+
12
+
13
+ def _is_athena_spaces_host(hostname: str | None) -> bool:
14
+ host = (hostname or "").lower()
15
+ return any(
16
+ host == suffix or host.endswith(f".{suffix}")
17
+ for suffix in _ATHENA_SPACES_HOST_SUFFIXES
18
+ )
19
+
20
+
21
+ def normalize_citation_string(citation_string: str) -> str:
22
+ value = citation_string.strip()
23
+ if not value:
24
+ raise ValueError("citation_string must not be empty")
25
+
26
+ parsed = urlparse(value)
27
+ if parsed.scheme in ("http", "https") and parsed.netloc:
28
+ if parsed.path.rstrip("/") == "/dashboard/spaces":
29
+ if not _is_athena_spaces_host(parsed.hostname):
30
+ raise ValueError(
31
+ "absolute Athena Spaces citation URLs must use an Athena host"
32
+ )
33
+ asset_ids = parse_qs(parsed.query).get("asset_ids")
34
+ if not asset_ids or not asset_ids[0]:
35
+ raise ValueError("Athena Spaces citation URLs must include asset_ids")
36
+ return value
37
+
38
+ if not parsed.scheme and value.startswith("/"):
39
+ relative = urlparse(value)
40
+ if relative.netloc:
41
+ raise ValueError(
42
+ "protocol-relative Athena Spaces citation URLs are not allowed; "
43
+ "use an absolute Athena host"
44
+ )
45
+ if relative.path.rstrip("/") == "/dashboard/spaces":
46
+ asset_ids = parse_qs(relative.query).get("asset_ids")
47
+ if asset_ids and asset_ids[0]:
48
+ return value
49
+ raise ValueError("Athena Spaces citation URLs must include asset_ids")
50
+
51
+ raise ValueError(
52
+ "citation_string must be a serialized citation URL, such as "
53
+ "'/dashboard/spaces/?asset_ids=...' or an 'https://...' URL"
54
+ )