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.
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/PKG-INFO +1 -1
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/docs/API_PARITY_EXCEPTIONS.md +79 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/__init__.py +1 -1
- athena_python_pptx-0.2.0/pptx/_athena_extension.py +374 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/_ptc.py +6 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/_references.py +6 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/batching.py +7 -0
- athena_python_pptx-0.2.0/pptx/chart/category.py +200 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/client.py +7 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/commands.py +109 -1
- athena_python_pptx-0.2.0/pptx/decorators.py +136 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/docgen.py +6 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/chart.py +59 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/presentation.py +153 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/__init__.py +643 -8
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/text/__init__.py +51 -1
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/typing.py +15 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pyproject.toml +1 -1
- athena_python_pptx-0.1.80/pptx/chart/category.py +0 -96
- athena_python_pptx-0.1.80/pptx/decorators.py +0 -94
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/.gitignore +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/API_PARITY_REPORT.md +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/CHANGELOG.md +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/CLAUDE.md +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/PUBLISHING.md +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/README.md +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/action.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/chart.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/errors.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/exc.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/media.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/package.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/shared.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/slide.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/slides.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/spec.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/table.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/types.py +0 -0
- {athena_python_pptx-0.1.80 → athena_python_pptx-0.2.0}/pptx/units.py +0 -0
- {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.
|
|
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.
|
|
@@ -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
|