athena-python-pptx 0.4.3__tar.gz → 0.6.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 (87) hide show
  1. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/CLAUDE.md +9 -0
  2. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/PKG-INFO +2 -1
  3. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/docs/API_PARITY_EXCEPTIONS.md +102 -0
  4. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/__init__.py +1 -1
  5. athena_python_pptx-0.6.0/pptx/_references.py +73 -0
  6. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/commands.py +136 -0
  7. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/presentation.py +22 -0
  8. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/__init__.py +105 -0
  9. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/slides.py +79 -0
  10. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pyproject.toml +6 -1
  11. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/uv.lock +190 -138
  12. athena_python_pptx-0.4.3/pptx/_references.py +0 -165
  13. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/.gitignore +0 -0
  14. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/API_PARITY_REPORT.md +0 -0
  15. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/CHANGELOG.md +0 -0
  16. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/DEV-GUIDE.md +0 -0
  17. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/PARITY_QUESTIONS.md +0 -0
  18. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/PUBLISHING.md +0 -0
  19. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/README.md +0 -0
  20. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/docs/athena-api.json +0 -0
  21. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/docs/athena-api.md +0 -0
  22. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/_athena_extension.py +0 -0
  23. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/_ptc.py +0 -0
  24. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/action.py +0 -0
  25. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/batching.py +0 -0
  26. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/__init__.py +0 -0
  27. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/axis.py +0 -0
  28. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/category.py +0 -0
  29. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/chart.py +0 -0
  30. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/data.py +0 -0
  31. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/datalabel.py +0 -0
  32. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/legend.py +0 -0
  33. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/marker.py +0 -0
  34. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/plot.py +0 -0
  35. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/point.py +0 -0
  36. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/series.py +0 -0
  37. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/chart/xlsx.py +0 -0
  38. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/client.py +0 -0
  39. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/decorators.py +0 -0
  40. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/dml/__init__.py +0 -0
  41. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/dml/chtfmt.py +0 -0
  42. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/dml/color.py +0 -0
  43. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/dml/effect.py +0 -0
  44. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/dml/fill.py +0 -0
  45. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/dml/line.py +0 -0
  46. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/docgen.py +0 -0
  47. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/enum/__init__.py +0 -0
  48. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/enum/action.py +0 -0
  49. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/enum/chart.py +0 -0
  50. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/enum/dml.py +0 -0
  51. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/enum/lang.py +0 -0
  52. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/enum/shapes.py +0 -0
  53. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/enum/text.py +0 -0
  54. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/errors.py +0 -0
  55. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/exc.py +0 -0
  56. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/media.py +0 -0
  57. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/package.py +0 -0
  58. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/__init__.py +0 -0
  59. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/_base.py +0 -0
  60. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/chart.py +0 -0
  61. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/coreprops.py +0 -0
  62. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/embeddedpackage.py +0 -0
  63. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/image.py +0 -0
  64. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/media.py +0 -0
  65. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/presentation.py +0 -0
  66. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/parts/slide.py +0 -0
  67. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/autoshape.py +0 -0
  68. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/base.py +0 -0
  69. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/connector.py +0 -0
  70. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/freeform.py +0 -0
  71. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/graphfrm.py +0 -0
  72. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/group.py +0 -0
  73. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/picture.py +0 -0
  74. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/placeholder.py +0 -0
  75. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shapes/shapetree.py +0 -0
  76. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/shared.py +0 -0
  77. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/slide.py +0 -0
  78. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/spec.py +0 -0
  79. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/table.py +0 -0
  80. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/text/__init__.py +0 -0
  81. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/text/fonts.py +0 -0
  82. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/text/layout.py +0 -0
  83. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/text/text.py +0 -0
  84. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/types.py +0 -0
  85. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/typing.py +0 -0
  86. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/units.py +0 -0
  87. {athena_python_pptx-0.4.3 → athena_python_pptx-0.6.0}/pptx/util.py +0 -0
@@ -69,6 +69,15 @@ local XML / OPC parts:
69
69
 
70
70
  `Font.language_id` IS now implemented (returns `MSO_LANGUAGE_ID` enum, accepts enum or int LCID) — added in v0.1.68.
71
71
 
72
+ ### Slide-index conventions
73
+
74
+ Slide indexing has TWO bases — never conflate them:
75
+
76
+ - **0-based** — this SDK (python-pptx parity): `prs.slides[0]`, `slide.slide_index`, and every `slide_index=` parameter are 0-based.
77
+ - **1-based** — the Athena citation/anchor system: `SlideAnchor.slideIndex` / `ShapeAnchor.slideIndex` (in `pptx/_references.py`), the citation methods, and anything displayed as "Slide N" are the 1-based display position.
78
+
79
+ The PPTX Studio server applier converts at the SDK→anchor boundary (`+1`). `slideId` (== the SDK's `slide.slide_id`) is the stable, base-agnostic identity — prefer it when available.
80
+
72
81
  ### If you need a deviation
73
82
 
74
83
  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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-pptx
3
- Version: 0.4.3
3
+ Version: 0.6.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
@@ -21,6 +21,7 @@ Classifier: Topic :: Office/Business :: Office Suites
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
+ Requires-Dist: athena-references>=0.1.0
24
25
  Requires-Dist: requests>=2.28.0
25
26
  Provides-Extra: dev
26
27
  Requires-Dist: mypy>=1.0; extra == 'dev'
@@ -1347,3 +1347,105 @@ upstream surface preserved, additions tagged with
1347
1347
 
1348
1348
  Total Tier B closures: 16 implemented, 6 audited (already
1349
1349
  applicable or N/A by REST architecture).
1350
+
1351
+ ---
1352
+
1353
+ ## Athena Source Citations — SDK-native (added post-v0.4.3)
1354
+
1355
+ These methods are **additions** to python-pptx, not deviations — python-pptx
1356
+ has no concept of Athena asset references or citations. The user has approved
1357
+ SDK-native citations. They let an agent attach an Athena source citation to a
1358
+ slide or shape; the studio server applies the citation into the deck's
1359
+ `citations` Y.Array as a unified record that is byte-compatible with agora's
1360
+ `build_deck_source_citation`
1361
+ (`agora/agora/web/api/asset/utils/presentations/citation_operations.py`) and the
1362
+ Olympus `Citation` type (`olympus/src/store/citation.types.ts`). So the deck's
1363
+ existing badge + sidebar rendering surfaces agent-authored citations identically
1364
+ to user-authored ones.
1365
+
1366
+ A citation record is:
1367
+
1368
+ ```jsonc
1369
+ {
1370
+ "id": "citation_<uuid12>",
1371
+ "destinationAnchor": { "type": "slide", "slideId": "...", "slideIndex": 1 },
1372
+ // or { "type": "shape", "slideId": "...", "slideIndex": 1, "shapeIds": ["..."] }
1373
+ "citation_string": "<source Spaces URL>",
1374
+ "displayValue": "Q4 Sales", // optional
1375
+ "active": true,
1376
+ "created_at": "2026-…T…Z",
1377
+ "created_by": "Athena"
1378
+ }
1379
+ ```
1380
+
1381
+ `citation_string` is the serialized source `AssetReference` (whole asset, or
1382
+ asset + sub-anchor) in Spaces-URL form. The SDK leaves it to the server to
1383
+ derive (from `source_ref` + `source_anchor`) so a single canonical serializer
1384
+ produces it.
1385
+
1386
+ ### `Slide.add_citation(source, *, anchor=None, display_value=None) -> str`
1387
+
1388
+ Attach a citation to a whole slide. Emits `AddSlideCitation`; the server
1389
+ resolves `slideId` from `slideIndex` and writes a `slide` destination anchor.
1390
+ Returns the created citation id.
1391
+
1392
+ ```python
1393
+ from pptx._references import AssetReference, SheetRangeAnchor
1394
+
1395
+ cid = slide.add_citation(
1396
+ AssetReference(id="asset_xlsx_abc123"),
1397
+ anchor=SheetRangeAnchor(sheet_id=0, range="A1:F20"), # optional
1398
+ display_value="Q4 Sales · A1:F20", # optional
1399
+ )
1400
+ ```
1401
+
1402
+ ### `Shape.add_citation(source, *, anchor=None, display_value=None) -> str`
1403
+
1404
+ Shape-scoped citation — emits `AddShapeCitation` carrying this shape's id, so
1405
+ the server writes a `shape` destination anchor with `shapeIds`. Returns the
1406
+ created citation id. (Mirrors `Slide.add_citation` / `Cell.add_citation` — the
1407
+ citation verb is `add_citation` on every citable object.)
1408
+
1409
+ ```python
1410
+ shape = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
1411
+ cid = shape.add_citation(AssetReference(id="asset_doc_xyz"))
1412
+ ```
1413
+
1414
+ **Slide-index base shift (citations).** The SDK/command wire `slide_index`
1415
+ (`prs.slides[i]`, `slide.slide_index`) is **0-based** — python-pptx array
1416
+ parity. But the `destinationAnchor.slideIndex` the server writes into the
1417
+ citation record is **1-based** — the "Slide N" display position per the
1418
+ athena-references contract. The server applies the `+1` at the SDK→anchor
1419
+ boundary, so the stored anchor's `slideIndex` is always one greater than the
1420
+ SDK index of the cited slide.
1421
+
1422
+ > ⚠️ If the **cited source** is itself a slide/shape (you pass a
1423
+ > `SlideAnchor` / `ShapeAnchor` as `anchor=`), that anchor's `slide_index` is
1424
+ > the **1-based** display position per the athena-references contract — do
1425
+ > **not** pass the SDK's 0-based `slide.slide_index` into it (add 1).
1426
+
1427
+ ### `Slide.remove_citation(citation_id, *, soft=True)` / `Presentation.remove_citation(citation_id, *, soft=True)`
1428
+
1429
+ Remove a citation from the deck by id. Citations are deck-scoped, so both
1430
+ spellings emit the same `RemoveCitation` command — `Slide.remove_citation` is a
1431
+ convenience when an agent already holds a slide. `soft` (default) flips
1432
+ `active` to `False` in place (reversible, readers skip inactive citations);
1433
+ `soft=False` splices the record out of the array.
1434
+
1435
+ ```python
1436
+ prs.remove_citation(cid) # soft delete (default)
1437
+ slide.remove_citation(cid, soft=False) # hard splice
1438
+ ```
1439
+
1440
+ ### `pptx._references` re-homed on the shared `athena-references` package
1441
+
1442
+ `pptx._references` now re-exports the anchor + version-policy dataclasses from
1443
+ the shared `athena-references` package (the single Python source of truth, kept
1444
+ in lockstep with `packages/references` and `agora/agora/utils/asset_references`).
1445
+ The `to_dict()` wire output of the four anchors the module historically exported
1446
+ (`SheetCellAnchor` / `SheetRangeAnchor` / `SheetTableAnchor` / `SlideAnchor`),
1447
+ plus `VersionPolicyLatest` / `VersionPolicyPinned`, is byte-identical to the
1448
+ prior in-module definitions. `AssetReference` remains a thin in-module wrapper
1449
+ that additionally accepts a plain `dict` `meta` (the historical SDK shape) and
1450
+ serializes it verbatim, preserving byte-identical `to_dict()` output for
1451
+ existing callers.
@@ -133,7 +133,7 @@ def flush_all() -> None:
133
133
  _active_buffers[:] = alive
134
134
 
135
135
 
136
- __version__ = "0.4.3"
136
+ __version__ = "0.6.0"
137
137
 
138
138
  __all__ = [
139
139
  # Main entry point
@@ -0,0 +1,73 @@
1
+ """Thin Python mirror of the @athenaintel/references TypeScript package.
2
+
3
+ Used by the linked-OLE / linked-table / citation SDK methods (Athena
4
+ extensions beyond the python-pptx surface). Mirrors only what the SDK needs to
5
+ construct ``AssetReference`` + ``Anchor`` payloads — full reference
6
+ serialization (URI / URL / Spaces format) is server-side.
7
+
8
+ The anchor + version-policy dataclasses are re-exported from the shared
9
+ ``athena-references`` package (the single Python source of truth, kept in
10
+ lockstep with ``packages/references`` and ``agora/agora/utils/asset_references``).
11
+ ``AssetReference`` is a thin back-compat wrapper that additionally accepts a
12
+ plain ``dict`` ``meta`` (the historical SDK shape) and serializes it verbatim,
13
+ preserving byte-identical ``to_dict()`` output for existing SDK callers.
14
+
15
+ Note (Athena extension):
16
+ This module is NOT part of python-pptx — it exists to construct
17
+ Athena-asset references for the SDK's linked-object / citation methods.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ __athena_extension_module__: bool = True
23
+ __athena_extension_description__: str = (
24
+ "Cross-studio asset reference types (Athena studio-linking, no upstream)."
25
+ )
26
+ __athena_extension_since__: str = "0.1.71"
27
+
28
+ from dataclasses import dataclass
29
+ from typing import Any, Optional
30
+
31
+ from athena_references import (
32
+ Anchor,
33
+ SheetCellAnchor,
34
+ SheetRangeAnchor,
35
+ SheetTableAnchor,
36
+ SlideAnchor,
37
+ VersionPolicy,
38
+ VersionPolicyLatest,
39
+ VersionPolicyPinned,
40
+ )
41
+
42
+
43
+ @dataclass
44
+ class AssetReference:
45
+ """Athena asset reference. ``id`` is the asset UUID.
46
+
47
+ Re-homes the canonical anchor types onto the shared ``athena-references``
48
+ package while preserving the historical SDK contract that ``meta`` may be a
49
+ plain ``dict`` (serialized verbatim) in addition to ``None``.
50
+ """
51
+
52
+ id: str
53
+ meta: Optional[dict[str, Any]] = None
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ """Serialize to the canonical wire shape."""
57
+ out: dict[str, Any] = {"referenced": "asset", "id": self.id}
58
+ if self.meta is not None:
59
+ out["meta"] = dict(self.meta)
60
+ return out
61
+
62
+
63
+ __all__ = [
64
+ "AssetReference",
65
+ "SheetCellAnchor",
66
+ "SheetRangeAnchor",
67
+ "SheetTableAnchor",
68
+ "SlideAnchor",
69
+ "Anchor",
70
+ "VersionPolicyLatest",
71
+ "VersionPolicyPinned",
72
+ "VersionPolicy",
73
+ ]
@@ -772,6 +772,139 @@ class AddLinkedTable(Command):
772
772
  )
773
773
 
774
774
 
775
+ @dataclass
776
+ class AddSlideCitation(Command):
777
+ """Attach an Athena source citation to a whole slide.
778
+
779
+ Records that a deck slide is sourced from another Athena asset. The
780
+ studio server appends a unified citation record to the deck's
781
+ ``citations`` Y.Array, byte-compatible with agora's
782
+ ``build_deck_source_citation`` and the Olympus ``Citation`` type, so the
783
+ deck's existing badge + sidebar rendering surfaces agent-authored links
784
+ identically to user-authored ones.
785
+
786
+ Note (Athena extension):
787
+ This command is NOT part of python-pptx. See
788
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
789
+
790
+ Args:
791
+ slide_index: Zero-based index of the cited slide. This stays 0-based on
792
+ the wire (python-pptx array parity); the studio server applier
793
+ resolves the stable ``slideId`` from it and stores the resulting
794
+ ``destinationAnchor.slideIndex`` as 1-based (display "Slide N") — do
795
+ not read this wire value as a 1-based position.
796
+ source_ref: Source ``AssetReference`` JSON (``referenced='asset'`` +
797
+ ``id``); the asset the slide is sourced from.
798
+ source_anchor: Optional ``Anchor`` JSON within the source (e.g. a
799
+ spreadsheet range); omit to cite the whole asset.
800
+ display_value: Optional human-readable badge label.
801
+ citation_string: Optional pre-serialized source Spaces URL; the
802
+ server derives it from ``source_ref`` + ``source_anchor`` when
803
+ unset.
804
+ client_id: Client-provided ID (optional).
805
+ """
806
+
807
+ slide_index: int
808
+ source_ref: dict
809
+ source_anchor: Optional[dict] = None
810
+ display_value: Optional[str] = None
811
+ citation_string: Optional[str] = None
812
+ client_id: Optional[str] = None
813
+
814
+ @property
815
+ def command_type(self) -> str:
816
+ return "AddSlideCitation"
817
+
818
+ def validate(self) -> None:
819
+ if self.slide_index < 0:
820
+ raise ValidationError("slide_index must be non-negative", "slide_index")
821
+ if not isinstance(self.source_ref, dict) or not self.source_ref.get("id"):
822
+ raise ValidationError(
823
+ "source_ref must be an AssetReference dict with an 'id' field",
824
+ "source_ref",
825
+ )
826
+
827
+
828
+ @dataclass
829
+ class AddShapeCitation(Command):
830
+ """Attach an Athena source citation to one or more shapes on a slide.
831
+
832
+ Shape-scoped analogue of :class:`AddSlideCitation`. The deck citation's
833
+ ``destinationAnchor`` is a ``shape`` anchor carrying ``shapeIds`` so the
834
+ canvas can draw per-shape highlight overlays.
835
+
836
+ Note (Athena extension):
837
+ This command is NOT part of python-pptx. See
838
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
839
+
840
+ Args:
841
+ slide_index: Zero-based index of the slide the shapes live on. This
842
+ stays 0-based on the wire (python-pptx array parity); the studio
843
+ server applier resolves the stable ``slideId`` from it and stores
844
+ the resulting ``destinationAnchor.slideIndex`` as 1-based (display
845
+ "Slide N") — do not read this wire value as a 1-based position.
846
+ shape_ids: Non-empty list of shape ids on that slide to cite.
847
+ source_ref: Source ``AssetReference`` JSON.
848
+ source_anchor: Optional ``Anchor`` JSON within the source.
849
+ display_value: Optional human-readable badge label.
850
+ citation_string: Optional pre-serialized source Spaces URL.
851
+ client_id: Client-provided ID (optional).
852
+ """
853
+
854
+ slide_index: int
855
+ shape_ids: list[str]
856
+ source_ref: dict
857
+ source_anchor: Optional[dict] = None
858
+ display_value: Optional[str] = None
859
+ citation_string: Optional[str] = None
860
+ client_id: Optional[str] = None
861
+
862
+ @property
863
+ def command_type(self) -> str:
864
+ return "AddShapeCitation"
865
+
866
+ def validate(self) -> None:
867
+ if self.slide_index < 0:
868
+ raise ValidationError("slide_index must be non-negative", "slide_index")
869
+ if not self.shape_ids:
870
+ raise ValidationError("shape_ids must be non-empty", "shape_ids")
871
+ if not isinstance(self.source_ref, dict) or not self.source_ref.get("id"):
872
+ raise ValidationError(
873
+ "source_ref must be an AssetReference dict with an 'id' field",
874
+ "source_ref",
875
+ )
876
+
877
+
878
+ @dataclass
879
+ class RemoveCitation(Command):
880
+ """Remove a citation from a deck's ``citations`` Y.Array by id.
881
+
882
+ ``soft`` (default) flips the record's ``active`` flag to ``False`` in
883
+ place (readers skip inactive citations; Keryx rollback can restore it);
884
+ ``soft=False`` splices the record out of the array.
885
+
886
+ Note (Athena extension):
887
+ This command is NOT part of python-pptx. See
888
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
889
+
890
+ Args:
891
+ citation_id: Id of the citation record to remove.
892
+ soft: Soft-delete (flip ``active``) when ``True`` (default); hard
893
+ splice when ``False``.
894
+ """
895
+
896
+ citation_id: str
897
+ soft: bool = True
898
+
899
+ @property
900
+ def command_type(self) -> str:
901
+ return "RemoveCitation"
902
+
903
+ def validate(self) -> None:
904
+ if not self.citation_id:
905
+ raise ValidationError("citation_id is required", "citation_id")
906
+
907
+
775
908
  @dataclass
776
909
  class AddTable(Command):
777
910
  """
@@ -3098,6 +3231,9 @@ AnyCommand = Union[
3098
3231
  AddOleObject,
3099
3232
  AddLinkedOleObject,
3100
3233
  AddLinkedTable,
3234
+ AddSlideCitation,
3235
+ AddShapeCitation,
3236
+ RemoveCitation,
3101
3237
  AddTable,
3102
3238
  SetTableCell,
3103
3239
  SetTableRowCount,
@@ -871,6 +871,28 @@ class Presentation:
871
871
  for i, slide in enumerate(self._slides._slides):
872
872
  slide._slide_index = i
873
873
 
874
+ @athena_extension(
875
+ description=(
876
+ "Presentation.remove_citation — remove an Athena source citation "
877
+ "from the deck by id. Not in python-pptx."
878
+ ),
879
+ )
880
+ def remove_citation(self, citation_id: str, *, soft: bool = True) -> None:
881
+ """Remove an Athena source citation from the deck by id.
882
+
883
+ Citations are deck-scoped records in the deck's ``citations`` Y.Array.
884
+ ``soft`` (default) flips the record's ``active`` flag to ``False`` in
885
+ place (readers skip inactive citations; Keryx rollback can restore it);
886
+ ``soft=False`` splices the record out of the array.
887
+
888
+ Note (Athena extension):
889
+ This method is NOT part of python-pptx. See
890
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
891
+ """
892
+ from .commands import RemoveCitation
893
+
894
+ self._buffer.add(RemoveCitation(citation_id=citation_id, soft=soft))
895
+
874
896
  @athena_extension(
875
897
  issue=1036,
876
898
  since="0.1.81",
@@ -88,6 +88,61 @@ def _is_athena_url(url: str) -> bool:
88
88
  return any(host == suffix or host.endswith("." + suffix) for suffix in _ATHENA_URL_SUFFIXES)
89
89
 
90
90
 
91
+ def _build_citation_wire(
92
+ *,
93
+ source: "AssetReference",
94
+ anchor: Optional["Anchor"],
95
+ ) -> tuple[dict, Optional[dict], None]:
96
+ """Build the ``(source_ref, source_anchor, citation_string)`` wire triple
97
+ for a citation command (Athena extension).
98
+
99
+ Type-checks ``source`` and duck-types the optional ``anchor`` exactly as
100
+ ``add_linked_ole_object`` does, then serializes both to their canonical
101
+ wire dicts. ``citation_string`` is left ``None`` so the studio server
102
+ derives the Spaces URL from ``source_ref`` + ``source_anchor`` — keeping a
103
+ single canonical serializer (matching agora's output) rather than a second
104
+ SDK-side one.
105
+ """
106
+ from .._references import AssetReference
107
+
108
+ if not isinstance(source, AssetReference):
109
+ raise TypeError(
110
+ f"source must be an AssetReference; got {type(source).__name__}",
111
+ )
112
+ source_anchor: Optional[dict] = None
113
+ if anchor is not None:
114
+ if not hasattr(anchor, "to_dict"):
115
+ raise TypeError(
116
+ "anchor must be an Anchor dataclass (SheetRangeAnchor, "
117
+ "SlideAnchor, etc.) — got "
118
+ f"{type(anchor).__name__}",
119
+ )
120
+ source_anchor = anchor.to_dict()
121
+ return source.to_dict(), source_anchor, None
122
+
123
+
124
+ def _add_citation_command(buffer: Optional["CommandBuffer"], cmd: Any) -> str:
125
+ """Enqueue a citation command and return the created citation id.
126
+
127
+ Reads the id back from the command response
128
+ (``created.citationIds[0]`` / ``created.citationId``) when the buffer
129
+ flushes synchronously; falls back to the command's ``client_id`` (or empty
130
+ string) when the response doesn't carry one (e.g. queued in a batch).
131
+ """
132
+ if buffer is None:
133
+ return getattr(cmd, "client_id", "") or ""
134
+ response = buffer.add(cmd)
135
+ if response and response.get("created"):
136
+ created = response["created"]
137
+ citation_ids = created.get("citationIds")
138
+ if citation_ids:
139
+ return citation_ids[0]
140
+ citation_id = created.get("citationId")
141
+ if citation_id:
142
+ return citation_id
143
+ return getattr(cmd, "client_id", "") or ""
144
+
145
+
91
146
  # Default render scale for xlsx-range screenshots embedded in slides.
92
147
  # scale=3 matches xlsx-studio's ``range-as-asset`` default — slide inserts
93
148
  # sit at 6–9″ widths, where scale=2 leaves text visibly soft.
@@ -5899,6 +5954,56 @@ class Shape:
5899
5954
  if isinstance(by_id, dict):
5900
5955
  by_id.pop(self._shape_id, None)
5901
5956
 
5957
+ def add_citation(
5958
+ self,
5959
+ source: "AssetReference",
5960
+ *,
5961
+ anchor: Optional["Anchor"] = None,
5962
+ display_value: Optional[str] = None,
5963
+ ) -> str:
5964
+ """Attach an Athena source citation to this shape.
5965
+
5966
+ Records that this shape is sourced from another Athena asset. The
5967
+ studio server appends a unified citation record to the deck's
5968
+ ``citations`` array (byte-compatible with agora's
5969
+ ``build_deck_source_citation`` and the Olympus ``Citation`` type), so
5970
+ the deck's badge + sidebar rendering surfaces it identically to a
5971
+ user-authored link.
5972
+
5973
+ Args:
5974
+ source: Source ``AssetReference`` from ``pptx._references`` — the
5975
+ asset this shape is sourced from.
5976
+ anchor: Optional ``Anchor`` within the source (e.g.
5977
+ ``SheetRangeAnchor``); omit to cite the whole asset.
5978
+ display_value: Optional human-readable badge label.
5979
+
5980
+ Returns:
5981
+ The created citation id (``citation_<...>``).
5982
+
5983
+ Raises:
5984
+ TypeError: if ``source`` is not an ``AssetReference`` or ``anchor``
5985
+ isn't an Anchor dataclass.
5986
+
5987
+ Note (Athena extension):
5988
+ This method is NOT part of python-pptx. See
5989
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
5990
+ """
5991
+ from ..commands import AddShapeCitation as AddShapeCitationCmd
5992
+
5993
+ source_ref, source_anchor, citation_string = _build_citation_wire(
5994
+ source=source,
5995
+ anchor=anchor,
5996
+ )
5997
+ cmd = AddShapeCitationCmd(
5998
+ slide_index=self._slide.slide_index,
5999
+ shape_ids=[self._shape_id],
6000
+ source_ref=source_ref,
6001
+ source_anchor=source_anchor,
6002
+ display_value=display_value,
6003
+ citation_string=citation_string,
6004
+ )
6005
+ return _add_citation_command(self._buffer, cmd)
6006
+
5902
6007
  # -------------------------------------------------------------------------
5903
6008
  # Fill and line styling
5904
6009
  # -------------------------------------------------------------------------
@@ -21,6 +21,7 @@ from .typing import DeckSnapshot, ElementSnapshot, SlideId, SlideSnapshot
21
21
  if TYPE_CHECKING:
22
22
  from .batching import CommandBuffer
23
23
  from .presentation import Presentation
24
+ from ._references import AssetReference, Anchor
24
25
 
25
26
 
26
27
  def _parse_layout_index(layout_path: Optional[str]) -> Optional[int]:
@@ -1167,6 +1168,84 @@ class Slide:
1167
1168
  )
1168
1169
  return warnings
1169
1170
 
1171
+ @athena_only(
1172
+ description=(
1173
+ "Slide.add_citation — attach an Athena source citation to a whole "
1174
+ "slide. Not in python-pptx."
1175
+ ),
1176
+ )
1177
+ def add_citation(
1178
+ self,
1179
+ source: "AssetReference",
1180
+ *,
1181
+ anchor: Optional["Anchor"] = None,
1182
+ display_value: Optional[str] = None,
1183
+ ) -> str:
1184
+ """Attach an Athena source citation to this whole slide.
1185
+
1186
+ Records that this slide is sourced from another Athena asset. The
1187
+ studio server appends a unified citation record to the deck's
1188
+ ``citations`` array (byte-compatible with agora's
1189
+ ``build_deck_source_citation`` and the Olympus ``Citation`` type), so
1190
+ the deck's badge + sidebar rendering surfaces it identically to a
1191
+ user-authored link.
1192
+
1193
+ Args:
1194
+ source: Source ``AssetReference`` from ``pptx._references`` — the
1195
+ asset this slide is sourced from.
1196
+ anchor: Optional ``Anchor`` within the source (e.g.
1197
+ ``SheetRangeAnchor``); omit to cite the whole asset.
1198
+ display_value: Optional human-readable badge label.
1199
+
1200
+ Returns:
1201
+ The created citation id (``citation_<...>``).
1202
+
1203
+ Note (Athena extension):
1204
+ This method is NOT part of python-pptx. See
1205
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
1206
+ """
1207
+ from .commands import AddSlideCitation
1208
+ from .shapes import _add_citation_command, _build_citation_wire
1209
+
1210
+ source_ref, source_anchor, citation_string = _build_citation_wire(
1211
+ source=source,
1212
+ anchor=anchor,
1213
+ )
1214
+ cmd = AddSlideCitation(
1215
+ slide_index=self._slide_index,
1216
+ source_ref=source_ref,
1217
+ source_anchor=source_anchor,
1218
+ display_value=display_value,
1219
+ citation_string=citation_string,
1220
+ )
1221
+ return _add_citation_command(self._buffer, cmd)
1222
+
1223
+ @athena_only(
1224
+ description=(
1225
+ "Slide.remove_citation — remove a citation from the deck by id. "
1226
+ "Not in python-pptx."
1227
+ ),
1228
+ )
1229
+ def remove_citation(self, citation_id: str, *, soft: bool = True) -> None:
1230
+ """Remove a citation from the deck's ``citations`` array by id.
1231
+
1232
+ ``soft`` (default) flips the record's ``active`` flag to ``False`` in
1233
+ place (readers skip inactive citations; Keryx rollback can restore it);
1234
+ ``soft=False`` splices the record out of the array.
1235
+
1236
+ Citations are deck-scoped, so this is identical to
1237
+ :meth:`Presentation.remove_citation`; it's exposed on ``Slide`` for
1238
+ convenience when an agent is already holding a slide.
1239
+
1240
+ Note (Athena extension):
1241
+ This method is NOT part of python-pptx. See
1242
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
1243
+ """
1244
+ from .commands import RemoveCitation
1245
+
1246
+ if self._buffer is not None:
1247
+ self._buffer.add(RemoveCitation(citation_id=citation_id, soft=soft))
1248
+
1170
1249
  @property
1171
1250
  def name(self) -> Optional[str]:
1172
1251
  """
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "athena-python-pptx"
7
- version = "0.4.3"
7
+ version = "0.6.0"
8
8
  description = "Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -36,6 +36,11 @@ classifiers = [
36
36
  ]
37
37
  dependencies = [
38
38
  "requests>=2.28.0",
39
+ # Canonical Python source of truth for AssetReference + the Anchor union,
40
+ # re-exported by pptx/_references.py. Deploy follow-up: Dockerfile.prod
41
+ # vendors this wheel via `pip download --no-deps athena-references` (the
42
+ # presentation-exec sandbox has no PyPI access at build time).
43
+ "athena-references>=0.1.0",
39
44
  ]
40
45
 
41
46
  [project.optional-dependencies]