athena-python-pptx 0.4.3__tar.gz → 0.5.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.5.0}/PKG-INFO +2 -1
  2. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/docs/API_PARITY_EXCEPTIONS.md +88 -0
  3. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/__init__.py +1 -1
  4. athena_python_pptx-0.5.0/pptx/_references.py +73 -0
  5. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/commands.py +128 -0
  6. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/presentation.py +22 -0
  7. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/__init__.py +105 -0
  8. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/slides.py +79 -0
  9. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pyproject.toml +6 -1
  10. athena_python_pptx-0.4.3/pptx/_references.py +0 -165
  11. athena_python_pptx-0.4.3/uv.lock +0 -1163
  12. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/.gitignore +0 -0
  13. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/API_PARITY_REPORT.md +0 -0
  14. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/CHANGELOG.md +0 -0
  15. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/CLAUDE.md +0 -0
  16. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/DEV-GUIDE.md +0 -0
  17. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/PARITY_QUESTIONS.md +0 -0
  18. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/PUBLISHING.md +0 -0
  19. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/README.md +0 -0
  20. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/docs/athena-api.json +0 -0
  21. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/docs/athena-api.md +0 -0
  22. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/_athena_extension.py +0 -0
  23. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/_ptc.py +0 -0
  24. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/action.py +0 -0
  25. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/batching.py +0 -0
  26. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/__init__.py +0 -0
  27. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/axis.py +0 -0
  28. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/category.py +0 -0
  29. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/chart.py +0 -0
  30. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/data.py +0 -0
  31. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/datalabel.py +0 -0
  32. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/legend.py +0 -0
  33. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/marker.py +0 -0
  34. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/plot.py +0 -0
  35. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/point.py +0 -0
  36. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/series.py +0 -0
  37. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/chart/xlsx.py +0 -0
  38. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/client.py +0 -0
  39. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/decorators.py +0 -0
  40. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/dml/__init__.py +0 -0
  41. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/dml/chtfmt.py +0 -0
  42. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/dml/color.py +0 -0
  43. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/dml/effect.py +0 -0
  44. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/dml/fill.py +0 -0
  45. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/dml/line.py +0 -0
  46. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/docgen.py +0 -0
  47. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/enum/__init__.py +0 -0
  48. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/enum/action.py +0 -0
  49. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/enum/chart.py +0 -0
  50. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/enum/dml.py +0 -0
  51. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/enum/lang.py +0 -0
  52. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/enum/shapes.py +0 -0
  53. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/enum/text.py +0 -0
  54. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/errors.py +0 -0
  55. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/exc.py +0 -0
  56. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/media.py +0 -0
  57. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/package.py +0 -0
  58. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/__init__.py +0 -0
  59. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/_base.py +0 -0
  60. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/chart.py +0 -0
  61. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/coreprops.py +0 -0
  62. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/embeddedpackage.py +0 -0
  63. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/image.py +0 -0
  64. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/media.py +0 -0
  65. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/presentation.py +0 -0
  66. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/parts/slide.py +0 -0
  67. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/autoshape.py +0 -0
  68. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/base.py +0 -0
  69. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/connector.py +0 -0
  70. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/freeform.py +0 -0
  71. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/graphfrm.py +0 -0
  72. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/group.py +0 -0
  73. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/picture.py +0 -0
  74. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/placeholder.py +0 -0
  75. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shapes/shapetree.py +0 -0
  76. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/shared.py +0 -0
  77. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/slide.py +0 -0
  78. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/spec.py +0 -0
  79. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/table.py +0 -0
  80. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/text/__init__.py +0 -0
  81. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/text/fonts.py +0 -0
  82. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/text/layout.py +0 -0
  83. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/text/text.py +0 -0
  84. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/types.py +0 -0
  85. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/typing.py +0 -0
  86. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/units.py +0 -0
  87. {athena_python_pptx-0.4.3 → athena_python_pptx-0.5.0}/pptx/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: athena-python-pptx
3
- Version: 0.4.3
3
+ Version: 0.5.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,91 @@ 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": 0 },
1372
+ // or { "type": "shape", "slideId": "...", "slideIndex": 0, "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.cite(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.
1407
+
1408
+ ```python
1409
+ shape = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(4), Inches(1))
1410
+ cid = shape.cite(AssetReference(id="asset_doc_xyz"))
1411
+ ```
1412
+
1413
+ ### `Slide.remove_citation(citation_id, *, soft=True)` / `Presentation.remove_citation(citation_id, *, soft=True)`
1414
+
1415
+ Remove a citation from the deck by id. Citations are deck-scoped, so both
1416
+ spellings emit the same `RemoveCitation` command — `Slide.remove_citation` is a
1417
+ convenience when an agent already holds a slide. `soft` (default) flips
1418
+ `active` to `False` in place (reversible, readers skip inactive citations);
1419
+ `soft=False` splices the record out of the array.
1420
+
1421
+ ```python
1422
+ prs.remove_citation(cid) # soft delete (default)
1423
+ slide.remove_citation(cid, soft=False) # hard splice
1424
+ ```
1425
+
1426
+ ### `pptx._references` re-homed on the shared `athena-references` package
1427
+
1428
+ `pptx._references` now re-exports the anchor + version-policy dataclasses from
1429
+ the shared `athena-references` package (the single Python source of truth, kept
1430
+ in lockstep with `packages/references` and `agora/agora/utils/asset_references`).
1431
+ The `to_dict()` wire output of the four anchors the module historically exported
1432
+ (`SheetCellAnchor` / `SheetRangeAnchor` / `SheetTableAnchor` / `SlideAnchor`),
1433
+ plus `VersionPolicyLatest` / `VersionPolicyPinned`, is byte-identical to the
1434
+ prior in-module definitions. `AssetReference` remains a thin in-module wrapper
1435
+ that additionally accepts a plain `dict` `meta` (the historical SDK shape) and
1436
+ serializes it verbatim, preserving byte-identical `to_dict()` output for
1437
+ 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.5.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,131 @@ 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.
792
+ source_ref: Source ``AssetReference`` JSON (``referenced='asset'`` +
793
+ ``id``); the asset the slide is sourced from.
794
+ source_anchor: Optional ``Anchor`` JSON within the source (e.g. a
795
+ spreadsheet range); omit to cite the whole asset.
796
+ display_value: Optional human-readable badge label.
797
+ citation_string: Optional pre-serialized source Spaces URL; the
798
+ server derives it from ``source_ref`` + ``source_anchor`` when
799
+ unset.
800
+ client_id: Client-provided ID (optional).
801
+ """
802
+
803
+ slide_index: int
804
+ source_ref: dict
805
+ source_anchor: Optional[dict] = None
806
+ display_value: Optional[str] = None
807
+ citation_string: Optional[str] = None
808
+ client_id: Optional[str] = None
809
+
810
+ @property
811
+ def command_type(self) -> str:
812
+ return "AddSlideCitation"
813
+
814
+ def validate(self) -> None:
815
+ if self.slide_index < 0:
816
+ raise ValidationError("slide_index must be non-negative", "slide_index")
817
+ if not isinstance(self.source_ref, dict) or not self.source_ref.get("id"):
818
+ raise ValidationError(
819
+ "source_ref must be an AssetReference dict with an 'id' field",
820
+ "source_ref",
821
+ )
822
+
823
+
824
+ @dataclass
825
+ class AddShapeCitation(Command):
826
+ """Attach an Athena source citation to one or more shapes on a slide.
827
+
828
+ Shape-scoped analogue of :class:`AddSlideCitation`. The deck citation's
829
+ ``destinationAnchor`` is a ``shape`` anchor carrying ``shapeIds`` so the
830
+ canvas can draw per-shape highlight overlays.
831
+
832
+ Note (Athena extension):
833
+ This command is NOT part of python-pptx. See
834
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
835
+
836
+ Args:
837
+ slide_index: Zero-based index of the slide the shapes live on.
838
+ shape_ids: Non-empty list of shape ids on that slide to cite.
839
+ source_ref: Source ``AssetReference`` JSON.
840
+ source_anchor: Optional ``Anchor`` JSON within the source.
841
+ display_value: Optional human-readable badge label.
842
+ citation_string: Optional pre-serialized source Spaces URL.
843
+ client_id: Client-provided ID (optional).
844
+ """
845
+
846
+ slide_index: int
847
+ shape_ids: list[str]
848
+ source_ref: dict
849
+ source_anchor: Optional[dict] = None
850
+ display_value: Optional[str] = None
851
+ citation_string: Optional[str] = None
852
+ client_id: Optional[str] = None
853
+
854
+ @property
855
+ def command_type(self) -> str:
856
+ return "AddShapeCitation"
857
+
858
+ def validate(self) -> None:
859
+ if self.slide_index < 0:
860
+ raise ValidationError("slide_index must be non-negative", "slide_index")
861
+ if not self.shape_ids:
862
+ raise ValidationError("shape_ids must be non-empty", "shape_ids")
863
+ if not isinstance(self.source_ref, dict) or not self.source_ref.get("id"):
864
+ raise ValidationError(
865
+ "source_ref must be an AssetReference dict with an 'id' field",
866
+ "source_ref",
867
+ )
868
+
869
+
870
+ @dataclass
871
+ class RemoveCitation(Command):
872
+ """Remove a citation from a deck's ``citations`` Y.Array by id.
873
+
874
+ ``soft`` (default) flips the record's ``active`` flag to ``False`` in
875
+ place (readers skip inactive citations; Keryx rollback can restore it);
876
+ ``soft=False`` splices the record out of the array.
877
+
878
+ Note (Athena extension):
879
+ This command is NOT part of python-pptx. See
880
+ ``docs/API_PARITY_EXCEPTIONS.md`` for the deviation rationale.
881
+
882
+ Args:
883
+ citation_id: Id of the citation record to remove.
884
+ soft: Soft-delete (flip ``active``) when ``True`` (default); hard
885
+ splice when ``False``.
886
+ """
887
+
888
+ citation_id: str
889
+ soft: bool = True
890
+
891
+ @property
892
+ def command_type(self) -> str:
893
+ return "RemoveCitation"
894
+
895
+ def validate(self) -> None:
896
+ if not self.citation_id:
897
+ raise ValidationError("citation_id is required", "citation_id")
898
+
899
+
775
900
  @dataclass
776
901
  class AddTable(Command):
777
902
  """
@@ -3098,6 +3223,9 @@ AnyCommand = Union[
3098
3223
  AddOleObject,
3099
3224
  AddLinkedOleObject,
3100
3225
  AddLinkedTable,
3226
+ AddSlideCitation,
3227
+ AddShapeCitation,
3228
+ RemoveCitation,
3101
3229
  AddTable,
3102
3230
  SetTableCell,
3103
3231
  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 cite(
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.5.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]