athena-python-pptx 0.1.80__tar.gz → 0.2.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.1.80 → athena_python_pptx-0.2.0}/PKG-INFO +1 -1
  2. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/docs/API_PARITY_EXCEPTIONS.md +79 -0
  3. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/__init__.py +1 -1
  4. athena_python_pptx-0.2.0/pptx/_athena_extension.py +374 -0
  5. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/_ptc.py +6 -0
  6. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/_references.py +6 -0
  7. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/batching.py +7 -0
  8. athena_python_pptx-0.2.0/pptx/chart/category.py +200 -0
  9. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/client.py +7 -0
  10. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/commands.py +109 -1
  11. athena_python_pptx-0.2.0/pptx/decorators.py +136 -0
  12. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/docgen.py +6 -0
  13. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/chart.py +59 -0
  14. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/presentation.py +153 -0
  15. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/__init__.py +643 -8
  16. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/text/__init__.py +51 -1
  17. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/typing.py +15 -0
  18. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pyproject.toml +1 -1
  19. athena_python_pptx-0.1.80/pptx/chart/category.py +0 -96
  20. athena_python_pptx-0.1.80/pptx/decorators.py +0 -94
  21. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/.gitignore +0 -0
  22. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/API_PARITY_REPORT.md +0 -0
  23. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/CHANGELOG.md +0 -0
  24. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/CLAUDE.md +0 -0
  25. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/DEV-GUIDE.md +0 -0
  26. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/PARITY_QUESTIONS.md +0 -0
  27. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/PUBLISHING.md +0 -0
  28. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/README.md +0 -0
  29. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/docs/athena-api.json +0 -0
  30. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/docs/athena-api.md +0 -0
  31. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/action.py +0 -0
  32. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/__init__.py +0 -0
  33. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/axis.py +0 -0
  34. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/chart.py +0 -0
  35. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/data.py +0 -0
  36. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/datalabel.py +0 -0
  37. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/legend.py +0 -0
  38. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/marker.py +0 -0
  39. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/plot.py +0 -0
  40. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/point.py +0 -0
  41. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/series.py +0 -0
  42. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/xlsx.py +0 -0
  43. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/__init__.py +0 -0
  44. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/chtfmt.py +0 -0
  45. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/color.py +0 -0
  46. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/effect.py +0 -0
  47. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/fill.py +0 -0
  48. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/line.py +0 -0
  49. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/__init__.py +0 -0
  50. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/action.py +0 -0
  51. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/dml.py +0 -0
  52. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/lang.py +0 -0
  53. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/shapes.py +0 -0
  54. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/text.py +0 -0
  55. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/errors.py +0 -0
  56. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/exc.py +0 -0
  57. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/media.py +0 -0
  58. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/package.py +0 -0
  59. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/__init__.py +0 -0
  60. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/_base.py +0 -0
  61. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/chart.py +0 -0
  62. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/coreprops.py +0 -0
  63. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/embeddedpackage.py +0 -0
  64. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/image.py +0 -0
  65. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/media.py +0 -0
  66. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/presentation.py +0 -0
  67. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/slide.py +0 -0
  68. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/autoshape.py +0 -0
  69. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/base.py +0 -0
  70. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/connector.py +0 -0
  71. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/freeform.py +0 -0
  72. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/graphfrm.py +0 -0
  73. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/group.py +0 -0
  74. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/picture.py +0 -0
  75. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/placeholder.py +0 -0
  76. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/shapetree.py +0 -0
  77. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shared.py +0 -0
  78. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/slide.py +0 -0
  79. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/slides.py +0 -0
  80. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/spec.py +0 -0
  81. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/table.py +0 -0
  82. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/text/fonts.py +0 -0
  83. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/text/layout.py +0 -0
  84. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/text/text.py +0 -0
  85. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/types.py +0 -0
  86. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/units.py +0 -0
  87. {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.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.1.80
3
+ Version: 0.2.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
@@ -324,6 +324,28 @@ prs.batch():` that fills it (see the skill prompt). When `add_slide()`
324
324
  runs outside the batch it flushes its own command, and a subsequent
325
325
  failed batch leaves the empty slide on the deck.
326
326
 
327
+ ### `<p:style><a:fontRef>` scheme is luminance-derived, not always `lt1`
328
+
329
+ Upstream python-pptx hardcodes `<a:fontRef idx="minor"><a:schemeClr val="lt1"/></a:fontRef>`
330
+ in the `<p:style>` block of every shape it creates. That fontRef is the
331
+ default text color resolved by PowerPoint when a run carries no explicit
332
+ `<a:rPr><a:solidFill>`, and `lt1` is white — so any shape that was filled
333
+ with a light color (white, pale grey, pale red) rendered invisible text
334
+ in PowerPoint. The bug surfaces on every cover-page logo box, agenda
335
+ description, and disclaimer banner that LLM-authored code produces.
336
+
337
+ Starting with the matching export-worker version, the SDK picks the
338
+ fontRef scheme based on the shape's fill luminance: `dk1` (black) when
339
+ the fill is light (sRGB luminance ≥ 0.55), `lt1` (white) otherwise. When
340
+ no `fill_color_hex` is supplied the emitter still falls back to `lt1`
341
+ for byte-stable parity with prior round-trips.
342
+
343
+ This is an export-side behavior change, not an API change — callers who
344
+ were already setting explicit run colors via `run.font.color.rgb` keep
345
+ binary-identical output. The luminance-derived `<p:style>` only matters
346
+ for shapes that rely on the shape preset style for their default text
347
+ color, which is exactly the failure mode the change is intended to fix.
348
+
327
349
  ### Background `CommandBuffer._deferred_error` propagation
328
350
 
329
351
  Not a user-facing API; documented here because it changes the visible
@@ -746,3 +768,60 @@ slide.shapes.add_picture(
746
768
  The default `fit=None` preserves stock python-pptx behaviour. The only
747
769
  other accepted value is `"contain"`; passing anything else raises
748
770
  `ValueError` to fail loudly rather than silently no-op.
771
+
772
+ ---
773
+
774
+ ## `Shapes.add_linked_table()` — Athena studio-linking extension (added in Phase 3)
775
+
776
+ Authors a **native PPT table** (`<a:tbl>`) whose cells are materialized
777
+ from an Athena source asset's anchor range at insert time. Unlike
778
+ ``add_linked_ole_object``, which produces a frozen Excel preview /
779
+ embedding pair, this produces a real PowerPoint table that's
780
+ selectable, editable, and natively styled — but still carries a
781
+ ``SourceBinding`` so refresh can re-pull current values from the
782
+ source spreadsheet.
783
+
784
+ This method has no python-pptx counterpart — python-pptx has no
785
+ concept of Athena asset references. Use ``add_table()`` (which IS in
786
+ python-pptx) for unlinked tables.
787
+
788
+ ```python
789
+ from pptx._references import (
790
+ AssetReference,
791
+ SheetTableAnchor,
792
+ VersionPolicyLatest,
793
+ )
794
+ from pptx.util import Inches
795
+
796
+ slide.shapes.add_linked_table(
797
+ source=AssetReference(id='asset_xlsx_abc123'),
798
+ anchor=SheetTableAnchor(
799
+ sheet_id=0,
800
+ table_name='SalesQ4',
801
+ fallback_range='A1:F50',
802
+ ),
803
+ left=Inches(1),
804
+ top=Inches(1),
805
+ width=Inches(8),
806
+ height=Inches(4),
807
+ freshness_mode='athena_refreshable', # default
808
+ version_policy=VersionPolicyLatest(), # default
809
+ )
810
+ ```
811
+
812
+ Prefer ``SheetTableAnchor`` over ``SheetRangeAnchor`` whenever the
813
+ source is an Excel named table — the table's ``displayName`` survives
814
+ sorts, inserts, and row additions that would shift an A1 range.
815
+
816
+ **Returns:** a ``GraphicFrame`` wrapping a ``Table`` proxy whose
817
+ ``rows`` / ``cols`` start at ``0`` on the client side. The materialized
818
+ dimensions live on the server; fetch the deck snapshot if you need to
819
+ iterate cells programmatically.
820
+
821
+ **Refresh flow:** call the GraphQL ``refreshSourceBinding`` mutation
822
+ (or the Olympus "Refresh from source" UI action). The server
823
+ re-materializes the source range and writes the new cell values into
824
+ the same table element on the Y.Doc.
825
+
826
+ **`freshness_mode='office_live_link'` is reserved for Phase 5** and
827
+ currently raises ``ValueError`` locally.
@@ -132,7 +132,7 @@ def flush_all() -> None:
132
132
  _active_buffers[:] = alive
133
133
 
134
134
 
135
- __version__ = "0.1.80"
135
+ __version__ = "0.2.0"
136
136
 
137
137
  __all__ = [
138
138
  # Main entry point
@@ -0,0 +1,374 @@
1
+ """Marker decorator + audit helpers for Athena extensions beyond
2
+ upstream python-pptx.
3
+
4
+ This SDK aims to be a 100% drop-in replacement for upstream
5
+ [python-pptx](https://python-pptx.readthedocs.io/) — every public
6
+ class, method, property, and parameter name mirrors the upstream API.
7
+ Anything that exists in **athena-python-pptx** but NOT in upstream
8
+ python-pptx is an **Athena extension** and must be:
9
+
10
+ 1. Documented in ``docs/API_PARITY_EXCEPTIONS.md`` for the human-readable
11
+ rationale (motivation, design notes).
12
+ 2. Marked at runtime with :func:`athena_extension` — so introspection
13
+ tooling (parity audits, docs generation, IDE plugins) can identify
14
+ the addition without grepping prose, and so the
15
+ :func:`iter_athena_extensions` walker can enumerate every addition
16
+ in one place.
17
+
18
+ The decorator stamps the wrapped object with introspectable attributes:
19
+
20
+ * ``__athena_extension__`` — always ``True``
21
+ * ``__athena_extension_issue__`` — upstream issue ref, e.g.
22
+ ``"python-pptx#123"`` or ``"Athena"`` for SDK-native additions
23
+ * ``__athena_extension_description__`` — one-line summary
24
+ * ``__athena_extension_since__`` — athena-python-pptx SDK version that
25
+ introduced the surface
26
+
27
+ A pre-existing :func:`pptx.decorators.athena_only` decorator exists for
28
+ back-compat — that decorator is now a thin compat shim around
29
+ :func:`athena_extension` so every call site stays valid.
30
+
31
+ Apply at every Athena-extension layer:
32
+
33
+ * **Class** — decorator on the ``class`` statement::
34
+
35
+ @athena_extension(description="HTTP command buffer (REST-SDK only)")
36
+ class CommandBuffer: ...
37
+
38
+ * **Method / function** — decorator on the ``def`` statement::
39
+
40
+ @athena_extension(description="Presentation.render_slide — PNG render")
41
+ def render_slide(self, slide_index, scale=1.0): ...
42
+
43
+ * **Classmethod** — decorator goes inside ``@classmethod``::
44
+
45
+ @classmethod
46
+ @athena_extension(description="Asset-backed Presentation factory")
47
+ def create(cls, name, ...): ...
48
+
49
+ * **Property** — decorator on the **getter** (before ``@property``)::
50
+
51
+ @property
52
+ @athena_extension(description="Athena asset id alias for deck_id")
53
+ def asset_id(self): ...
54
+
55
+ * **Module-level constant** — for whole modules that are entirely
56
+ Athena extensions, set at top of file::
57
+
58
+ __athena_extension_module__ = True
59
+ __athena_extension_description__ = "HTTP wire layer (REST-SDK only)"
60
+
61
+ The walker picks these up alongside per-member markers.
62
+
63
+ The decorator is import-time-cheap, has no runtime overhead at call
64
+ sites (it returns the original object after stamping), and never
65
+ modifies docstrings or signatures.
66
+
67
+ Mirror of ``docx-studio/python-sdk/docx/_athena_extension.py`` (kept
68
+ in sync across docx-studio / xlsx-studio / pptx-studio SDKs so tooling
69
+ can walk all three the same way).
70
+ """
71
+
72
+ from __future__ import annotations
73
+
74
+ import importlib
75
+ import pkgutil
76
+ from typing import Any, Callable, Iterator, TypeVar
77
+
78
+
79
+ # Attribute names — kept as module constants so audits and tests can
80
+ # reference them without re-typing the string everywhere.
81
+ ATHENA_EXTENSION_ATTR: str = "__athena_extension__"
82
+ ATHENA_EXTENSION_ISSUE_ATTR: str = "__athena_extension_issue__"
83
+ ATHENA_EXTENSION_DESCRIPTION_ATTR: str = "__athena_extension_description__"
84
+ ATHENA_EXTENSION_SINCE_ATTR: str = "__athena_extension_since__"
85
+
86
+ ATHENA_EXTENSION_MODULE_ATTR: str = "__athena_extension_module__"
87
+
88
+ # Default ``since`` value for new additions. Pre-existing extensions
89
+ # carry their original since values explicitly.
90
+ _DEFAULT_SINCE: str = "0.1.80"
91
+
92
+
93
+ T = TypeVar("T")
94
+
95
+
96
+ def athena_extension(
97
+ *,
98
+ issue: int | str | None = None,
99
+ description: str = "",
100
+ since: str = _DEFAULT_SINCE,
101
+ ) -> Callable[[T], T]:
102
+ """Decorator that marks a class / function / method as an Athena
103
+ extension beyond upstream python-pptx.
104
+
105
+ Stamps four introspectable attributes on the wrapped object:
106
+
107
+ - ``__athena_extension__`` — always ``True``
108
+ - ``__athena_extension_issue__`` — ``"python-pptx#<n>"`` when ``issue``
109
+ is an int; passed through verbatim when a string; ``None`` when
110
+ omitted
111
+ - ``__athena_extension_description__`` — ``description``
112
+ - ``__athena_extension_since__`` — ``since``
113
+
114
+ Returns the original object unchanged (apart from the new
115
+ attributes), so call sites pay zero runtime cost.
116
+
117
+ For properties, decorate the underlying getter (and setter) BEFORE
118
+ ``@property`` so the markers stick to ``prop.fget`` / ``prop.fset``::
119
+
120
+ @property
121
+ @athena_extension(description="Athena asset id alias")
122
+ def asset_id(self): ...
123
+ """
124
+ issue_value: str | None
125
+ if issue is None:
126
+ issue_value = None
127
+ elif isinstance(issue, int):
128
+ issue_value = f"python-pptx#{issue}"
129
+ else:
130
+ issue_value = str(issue)
131
+
132
+ def decorator(obj: T) -> T:
133
+ try:
134
+ setattr(obj, ATHENA_EXTENSION_ATTR, True)
135
+ if issue_value is not None:
136
+ setattr(obj, ATHENA_EXTENSION_ISSUE_ATTR, issue_value)
137
+ if description:
138
+ setattr(obj, ATHENA_EXTENSION_DESCRIPTION_ATTR, description)
139
+ setattr(obj, ATHENA_EXTENSION_SINCE_ATTR, since)
140
+ except (AttributeError, TypeError) as exc:
141
+ raise TypeError(
142
+ f"@athena_extension cannot be applied to "
143
+ f"{type(obj).__name__} — built-in descriptor rejects "
144
+ f"setattr. For properties, decorate the underlying "
145
+ f"getter/setter function BEFORE @property:\n\n"
146
+ f" @property\n"
147
+ f" @athena_extension(...)\n"
148
+ f" def my_prop(self): ...\n",
149
+ ) from exc
150
+ return obj
151
+
152
+ return decorator
153
+
154
+
155
+ def is_athena_extension(obj: Any) -> bool:
156
+ """Return ``True`` if ``obj`` is marked as an Athena extension.
157
+
158
+ Handles the property-wrapping case: a ``property`` descriptor
159
+ proxies to its ``fget`` (and ``fset``) — either marker counts.
160
+
161
+ Also recognises the legacy ``@athena_only`` marker
162
+ (``_athena_only`` attribute set by :mod:`pptx.decorators`) so
163
+ surfaces decorated before the migration still show up.
164
+ """
165
+ if getattr(obj, ATHENA_EXTENSION_ATTR, False):
166
+ return True
167
+ if getattr(obj, "_athena_only", False):
168
+ return True
169
+ if isinstance(obj, property):
170
+ if obj.fget is not None and (
171
+ getattr(obj.fget, ATHENA_EXTENSION_ATTR, False)
172
+ or getattr(obj.fget, "_athena_only", False)
173
+ ):
174
+ return True
175
+ if obj.fset is not None and (
176
+ getattr(obj.fset, ATHENA_EXTENSION_ATTR, False)
177
+ or getattr(obj.fset, "_athena_only", False)
178
+ ):
179
+ return True
180
+ return False
181
+
182
+
183
+ def athena_extension_metadata(obj: Any) -> dict[str, Any] | None:
184
+ """Return a dict of marker attributes for ``obj``, or ``None`` if
185
+ it isn't an Athena extension.
186
+
187
+ Surfaces decorated with the legacy ``@athena_only`` marker that
188
+ haven't been migrated to ``@athena_extension`` yet are recognised
189
+ via their ``_athena_only`` / ``_athena_description`` /
190
+ ``_athena_since`` attributes.
191
+ """
192
+ target: Any = obj
193
+ if isinstance(obj, property):
194
+ for candidate in (obj.fget, obj.fset):
195
+ if candidate is None:
196
+ continue
197
+ if (
198
+ getattr(candidate, ATHENA_EXTENSION_ATTR, False)
199
+ or getattr(candidate, "_athena_only", False)
200
+ ):
201
+ target = candidate
202
+ break
203
+ else:
204
+ return None
205
+
206
+ if getattr(target, ATHENA_EXTENSION_ATTR, False):
207
+ return {
208
+ "issue": getattr(target, ATHENA_EXTENSION_ISSUE_ATTR, None),
209
+ "description": getattr(target, ATHENA_EXTENSION_DESCRIPTION_ATTR, ""),
210
+ "since": getattr(target, ATHENA_EXTENSION_SINCE_ATTR, None),
211
+ }
212
+ if getattr(target, "_athena_only", False):
213
+ # Legacy marker from pptx.decorators.athena_only — translate.
214
+ return {
215
+ "issue": None,
216
+ "description": getattr(target, "_athena_description", "") or "",
217
+ "since": getattr(target, "_athena_since", None),
218
+ }
219
+ return None
220
+
221
+
222
+ def iter_athena_extensions(package: str = "pptx") -> Iterator[dict[str, Any]]:
223
+ """Walk every submodule of ``package`` and yield one record per
224
+ Athena extension found.
225
+
226
+ Records have shape::
227
+
228
+ {
229
+ "kind": "module" | "class" | "function" | "method" |
230
+ "property" | "classmethod" | "staticmethod",
231
+ "location": "pptx.presentation.Presentation.create",
232
+ "issue": "python-pptx#123" | "Athena" | None,
233
+ "description": "Asset-backed factory",
234
+ "since": "0.1.80",
235
+ }
236
+
237
+ Recognises both the new ``@athena_extension`` marker AND the
238
+ pre-existing ``@athena_only`` marker so surfaces decorated before
239
+ the migration still surface in the registry.
240
+ """
241
+ try:
242
+ root = importlib.import_module(package)
243
+ except ImportError:
244
+ return
245
+
246
+ if getattr(root, ATHENA_EXTENSION_MODULE_ATTR, False):
247
+ yield {
248
+ "kind": "module",
249
+ "location": package,
250
+ "issue": getattr(root, ATHENA_EXTENSION_ISSUE_ATTR, None),
251
+ "description": getattr(root, ATHENA_EXTENSION_DESCRIPTION_ATTR, ""),
252
+ "since": getattr(root, ATHENA_EXTENSION_SINCE_ATTR, None),
253
+ }
254
+ yield from _iter_module_members(root, package)
255
+
256
+ if not hasattr(root, "__path__"):
257
+ return
258
+
259
+ for _finder, sub_name, _ispkg in pkgutil.walk_packages(
260
+ root.__path__, prefix=f"{package}.",
261
+ ):
262
+ try:
263
+ sub = importlib.import_module(sub_name)
264
+ except Exception: # noqa: BLE001 — broken submodule, skip.
265
+ continue
266
+ if getattr(sub, ATHENA_EXTENSION_MODULE_ATTR, False):
267
+ yield {
268
+ "kind": "module",
269
+ "location": sub_name,
270
+ "issue": getattr(sub, ATHENA_EXTENSION_ISSUE_ATTR, None),
271
+ "description": getattr(sub, ATHENA_EXTENSION_DESCRIPTION_ATTR, ""),
272
+ "since": getattr(sub, ATHENA_EXTENSION_SINCE_ATTR, None),
273
+ }
274
+ yield from _iter_module_members(sub, sub_name)
275
+
276
+
277
+ def _is_marked(obj: Any) -> bool:
278
+ return (
279
+ getattr(obj, ATHENA_EXTENSION_ATTR, False)
280
+ or getattr(obj, "_athena_only", False)
281
+ )
282
+
283
+
284
+ def _iter_module_members(
285
+ module: Any, module_name: str,
286
+ ) -> Iterator[dict[str, Any]]:
287
+ """Walk ``vars(module)`` and yield extension records for top-level
288
+ callables / classes. For each class, recurse into its members.
289
+
290
+ Underscore-prefixed *classes* ARE walked — python-pptx uses
291
+ leading-underscore class names for collection types that are part
292
+ of the public API. Underscore-prefixed *functions* / module attrs
293
+ are treated as private and skipped.
294
+ """
295
+ seen_classes: set[int] = set()
296
+ for attr_name, attr in vars(module).items():
297
+ is_class = isinstance(attr, type)
298
+ if attr_name.startswith("_") and not is_class:
299
+ continue
300
+ if not is_class and _is_marked(attr):
301
+ kind = "command" if attr_name[0:1].isupper() else "function"
302
+ meta = athena_extension_metadata(attr) or {}
303
+ yield {
304
+ "kind": kind,
305
+ "location": f"{module_name}.{attr_name}",
306
+ **meta,
307
+ }
308
+ if is_class:
309
+ qual = getattr(attr, "__qualname__", attr_name)
310
+ mod_of_class = getattr(attr, "__module__", "")
311
+ if mod_of_class and mod_of_class != module_name:
312
+ continue
313
+ if id(attr) in seen_classes:
314
+ continue
315
+ seen_classes.add(id(attr))
316
+ if _is_marked(attr):
317
+ meta = athena_extension_metadata(attr) or {}
318
+ yield {
319
+ "kind": "class",
320
+ "location": f"{module_name}.{qual}",
321
+ **meta,
322
+ }
323
+ yield from _iter_class_members(attr, module_name, qual)
324
+
325
+
326
+ def _iter_class_members(
327
+ cls: type, module_name: str, class_qualname: str,
328
+ ) -> Iterator[dict[str, Any]]:
329
+ """Yield records for marked methods / properties on ``cls``.
330
+
331
+ Handles three wrapped-callable cases the bare attribute lookup
332
+ would miss:
333
+
334
+ * ``property`` — read marker from ``fget`` / ``fset``.
335
+ * ``classmethod`` — marker lives on ``__func__``.
336
+ * ``staticmethod`` — marker lives on ``__func__``.
337
+ """
338
+ for member_name, member in vars(cls).items():
339
+ if member_name.startswith("_"):
340
+ continue
341
+ probe: Any = member
342
+ if isinstance(member, (classmethod, staticmethod)):
343
+ probe = member.__func__
344
+ meta = athena_extension_metadata(probe)
345
+ if meta is None and probe is not member:
346
+ meta = athena_extension_metadata(member)
347
+ if meta is None:
348
+ continue
349
+ if isinstance(member, property):
350
+ kind = "property"
351
+ elif isinstance(member, classmethod):
352
+ kind = "classmethod"
353
+ elif isinstance(member, staticmethod):
354
+ kind = "staticmethod"
355
+ else:
356
+ kind = "method"
357
+ yield {
358
+ "kind": kind,
359
+ "location": f"{module_name}.{class_qualname}.{member_name}",
360
+ **meta,
361
+ }
362
+
363
+
364
+ __all__ = [
365
+ "athena_extension",
366
+ "is_athena_extension",
367
+ "athena_extension_metadata",
368
+ "iter_athena_extensions",
369
+ "ATHENA_EXTENSION_ATTR",
370
+ "ATHENA_EXTENSION_ISSUE_ATTR",
371
+ "ATHENA_EXTENSION_DESCRIPTION_ATTR",
372
+ "ATHENA_EXTENSION_SINCE_ATTR",
373
+ "ATHENA_EXTENSION_MODULE_ATTR",
374
+ ]
@@ -31,6 +31,12 @@ of stdlib code is cheaper than spinning up a 4th release pipeline.
31
31
 
32
32
  from __future__ import annotations
33
33
 
34
+ __athena_extension_module__: bool = True
35
+ __athena_extension_description__: str = (
36
+ "Programmatic Tool Calling (Daytona sandbox event emission)."
37
+ )
38
+ __athena_extension_since__: str = "0.1.0"
39
+
34
40
  import json
35
41
  import os
36
42
  import time
@@ -16,6 +16,12 @@ Note (Athena extension):
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ __athena_extension_module__: bool = True
20
+ __athena_extension_description__: str = (
21
+ "Cross-studio asset reference types (Athena studio-linking, no upstream)."
22
+ )
23
+ __athena_extension_since__: str = "0.1.71"
24
+
19
25
  from dataclasses import dataclass, field
20
26
  from typing import Any, Literal, Optional
21
27
 
@@ -6,6 +6,13 @@ context manager interface for batch operations.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
+
10
+ __athena_extension_module__: bool = True
11
+ __athena_extension_description__: str = (
12
+ "Command buffer + batching helpers (REST-SDK only)."
13
+ )
14
+ __athena_extension_since__: str = "0.1.0"
15
+
9
16
  import threading
10
17
  from contextlib import contextmanager
11
18
  from threading import local