pykeynote 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.
- pykeynote-0.2.0/.gitignore +6 -0
- pykeynote-0.2.0/LICENSE +21 -0
- pykeynote-0.2.0/PKG-INFO +134 -0
- pykeynote-0.2.0/README.md +110 -0
- pykeynote-0.2.0/pyproject.toml +44 -0
- pykeynote-0.2.0/src/pykeynote/__init__.py +15 -0
- pykeynote-0.2.0/src/pykeynote/_osa.py +56 -0
- pykeynote-0.2.0/src/pykeynote/document.py +420 -0
- pykeynote-0.2.0/src/pykeynote/markdown.py +213 -0
- pykeynote-0.2.0/src/pykeynote/package.py +275 -0
- pykeynote-0.2.0/src/pykeynote/scripting.py +491 -0
- pykeynote-0.2.0/src/pykeynote/slides.py +188 -0
- pykeynote-0.2.0/src/pykeynote/templates/basic-white.key +0 -0
- pykeynote-0.2.0/src/pykeynote/text.py +232 -0
- pykeynote-0.2.0/src/pykeynote/transitions.py +78 -0
- pykeynote-0.2.0/tests/conftest.py +32 -0
- pykeynote-0.2.0/tests/fixtures/blank-basic-black.key +0 -0
- pykeynote-0.2.0/tests/fixtures/blank-basic-white.key +0 -0
- pykeynote-0.2.0/tests/fixtures/blank-white.key +0 -0
- pykeynote-0.2.0/tests/fixtures/layouts-basic-white.key +0 -0
- pykeynote-0.2.0/tests/fixtures/transitions-basic-white.key +0 -0
- pykeynote-0.2.0/tests/test_document.py +107 -0
- pykeynote-0.2.0/tests/test_editing.py +137 -0
- pykeynote-0.2.0/tests/test_markdown.py +44 -0
- pykeynote-0.2.0/tests/test_native_integration.py +138 -0
- pykeynote-0.2.0/tests/test_scripting.py +155 -0
- pykeynote-0.2.0/tests/test_scripting_integration.py +82 -0
- pykeynote-0.2.0/tests/test_slides.py +214 -0
pykeynote-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kevin McAleer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pykeynote-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pykeynote
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Create, read and edit Apple Keynote presentations from Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/kevinmcaleer/pykeynote
|
|
6
|
+
Project-URL: Source, https://github.com/kevinmcaleer/pykeynote
|
|
7
|
+
Project-URL: Issues, https://github.com/kevinmcaleer/pykeynote/issues
|
|
8
|
+
Author: Kevin McAleer
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: apple,iwork,keynote,markdown,presentation,slides
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Multimedia :: Graphics :: Presentation
|
|
18
|
+
Classifier: Topic :: Office/Business
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: keynote-parser<2,>=1.14.5
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# pykeynote
|
|
26
|
+
|
|
27
|
+
Read, and soon write, Apple Keynote presentations from Python. pykeynote
|
|
28
|
+
works on `.key` files directly, so it runs anywhere Python does. You don't
|
|
29
|
+
need Keynote or a Mac.
|
|
30
|
+
|
|
31
|
+
A `.key` file is a zip of Snappy-compressed protobuf archives (`Index/*.iwa`)
|
|
32
|
+
plus images and media in `Data/`. Decoding comes from
|
|
33
|
+
[keynote-parser](https://github.com/psobot/keynote-parser), which supports
|
|
34
|
+
files saved by Keynote up to 14.5.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pip install pykeynote
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For development, clone the repo and run `pip install -e ".[dev]"`.
|
|
43
|
+
|
|
44
|
+
## Reading a deck
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from pykeynote import Presentation
|
|
48
|
+
|
|
49
|
+
deck = Presentation.open("talk.key")
|
|
50
|
+
print(deck.theme, deck.size, deck.layouts)
|
|
51
|
+
|
|
52
|
+
for slide in deck:
|
|
53
|
+
print(slide.index, slide.layout, slide.title)
|
|
54
|
+
for para in slide.paragraphs: # text, indent level, bulleted?
|
|
55
|
+
print(" " * para.level, para.text)
|
|
56
|
+
print(slide.notes, slide.transition.effect, slide.skipped)
|
|
57
|
+
|
|
58
|
+
print(deck.to_markdown())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`title` and `body` are `None` when the slide's layout hides that placeholder.
|
|
62
|
+
|
|
63
|
+
## Making and changing decks
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from pykeynote import Presentation, Paragraph, from_markdown
|
|
67
|
+
|
|
68
|
+
deck = Presentation.new() # bundled Basic White theme, no slides yet
|
|
69
|
+
deck.add_slide("Title", title="Robots!", body="Kevin McAleer")
|
|
70
|
+
slide = deck.add_slide(
|
|
71
|
+
"Title & Bullets",
|
|
72
|
+
title="Why robots?",
|
|
73
|
+
body=[Paragraph("They're fun"), Paragraph("Mostly", level=1), "They're useful"],
|
|
74
|
+
notes="Keep this one short.",
|
|
75
|
+
)
|
|
76
|
+
slide.set_transition("dissolve", 1.5)
|
|
77
|
+
deck.save("talk.key")
|
|
78
|
+
|
|
79
|
+
deck = Presentation.open("talk.key")
|
|
80
|
+
deck[1].title = "Why robots? 🤖"
|
|
81
|
+
deck.duplicate_slide(1)
|
|
82
|
+
deck.move_slide(2, 0)
|
|
83
|
+
deck.delete_slide(-1)
|
|
84
|
+
deck[0].skipped = True
|
|
85
|
+
deck.save() # back to talk.key
|
|
86
|
+
|
|
87
|
+
deck = from_markdown(open("talk.md").read())
|
|
88
|
+
deck.save("from-markdown.key")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`add_slide` copies a slide that already uses the layout, so it keeps the
|
|
92
|
+
theme's look. A deck from `Presentation.new()` has one of every layout to
|
|
93
|
+
copy from, kept out of sight and left out when you save. For a deck you
|
|
94
|
+
opened, at least one slide must already use the layout.
|
|
95
|
+
|
|
96
|
+
## Markdown
|
|
97
|
+
|
|
98
|
+
`to_markdown()` writes one block per slide, separated by `---`. Slide
|
|
99
|
+
settings go in HTML comments, and presenter notes follow a `???` line:
|
|
100
|
+
|
|
101
|
+
```markdown
|
|
102
|
+
<!-- layout: Title & Bullets -->
|
|
103
|
+
<!-- transition: dissolve 1.5s -->
|
|
104
|
+
# Why robots?
|
|
105
|
+
|
|
106
|
+
- They're fun
|
|
107
|
+
- Mostly
|
|
108
|
+
- They're useful
|
|
109
|
+
|
|
110
|
+
???
|
|
111
|
+
Keep this one short.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Roadmap
|
|
115
|
+
|
|
116
|
+
1. Read decks and export Markdown. **Done.**
|
|
117
|
+
2. Edit text and notes, then save. **Done.**
|
|
118
|
+
3. Add, duplicate, delete and reorder slides, set transitions and skipping, and build decks from Markdown. **Done.**
|
|
119
|
+
4. More bundled themes, images, and build animations.
|
|
120
|
+
|
|
121
|
+
## Driving Keynote directly (legacy)
|
|
122
|
+
|
|
123
|
+
`pykeynote.scripting` controls a running copy of Keynote on a Mac through
|
|
124
|
+
AppleScript and JXA. It can already edit decks, but it will be removed once
|
|
125
|
+
the file-based engine catches up.
|
|
126
|
+
|
|
127
|
+
## Tests
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
pytest # everything that runs without Keynote
|
|
131
|
+
PYKEYNOTE_INTEGRATION=1 pytest # also drive real Keynote (macOS)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The sample decks in `tests/fixtures/` were made in Keynote 14.5.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# pykeynote
|
|
2
|
+
|
|
3
|
+
Read, and soon write, Apple Keynote presentations from Python. pykeynote
|
|
4
|
+
works on `.key` files directly, so it runs anywhere Python does. You don't
|
|
5
|
+
need Keynote or a Mac.
|
|
6
|
+
|
|
7
|
+
A `.key` file is a zip of Snappy-compressed protobuf archives (`Index/*.iwa`)
|
|
8
|
+
plus images and media in `Data/`. Decoding comes from
|
|
9
|
+
[keynote-parser](https://github.com/psobot/keynote-parser), which supports
|
|
10
|
+
files saved by Keynote up to 14.5.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pip install pykeynote
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For development, clone the repo and run `pip install -e ".[dev]"`.
|
|
19
|
+
|
|
20
|
+
## Reading a deck
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from pykeynote import Presentation
|
|
24
|
+
|
|
25
|
+
deck = Presentation.open("talk.key")
|
|
26
|
+
print(deck.theme, deck.size, deck.layouts)
|
|
27
|
+
|
|
28
|
+
for slide in deck:
|
|
29
|
+
print(slide.index, slide.layout, slide.title)
|
|
30
|
+
for para in slide.paragraphs: # text, indent level, bulleted?
|
|
31
|
+
print(" " * para.level, para.text)
|
|
32
|
+
print(slide.notes, slide.transition.effect, slide.skipped)
|
|
33
|
+
|
|
34
|
+
print(deck.to_markdown())
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`title` and `body` are `None` when the slide's layout hides that placeholder.
|
|
38
|
+
|
|
39
|
+
## Making and changing decks
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pykeynote import Presentation, Paragraph, from_markdown
|
|
43
|
+
|
|
44
|
+
deck = Presentation.new() # bundled Basic White theme, no slides yet
|
|
45
|
+
deck.add_slide("Title", title="Robots!", body="Kevin McAleer")
|
|
46
|
+
slide = deck.add_slide(
|
|
47
|
+
"Title & Bullets",
|
|
48
|
+
title="Why robots?",
|
|
49
|
+
body=[Paragraph("They're fun"), Paragraph("Mostly", level=1), "They're useful"],
|
|
50
|
+
notes="Keep this one short.",
|
|
51
|
+
)
|
|
52
|
+
slide.set_transition("dissolve", 1.5)
|
|
53
|
+
deck.save("talk.key")
|
|
54
|
+
|
|
55
|
+
deck = Presentation.open("talk.key")
|
|
56
|
+
deck[1].title = "Why robots? 🤖"
|
|
57
|
+
deck.duplicate_slide(1)
|
|
58
|
+
deck.move_slide(2, 0)
|
|
59
|
+
deck.delete_slide(-1)
|
|
60
|
+
deck[0].skipped = True
|
|
61
|
+
deck.save() # back to talk.key
|
|
62
|
+
|
|
63
|
+
deck = from_markdown(open("talk.md").read())
|
|
64
|
+
deck.save("from-markdown.key")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`add_slide` copies a slide that already uses the layout, so it keeps the
|
|
68
|
+
theme's look. A deck from `Presentation.new()` has one of every layout to
|
|
69
|
+
copy from, kept out of sight and left out when you save. For a deck you
|
|
70
|
+
opened, at least one slide must already use the layout.
|
|
71
|
+
|
|
72
|
+
## Markdown
|
|
73
|
+
|
|
74
|
+
`to_markdown()` writes one block per slide, separated by `---`. Slide
|
|
75
|
+
settings go in HTML comments, and presenter notes follow a `???` line:
|
|
76
|
+
|
|
77
|
+
```markdown
|
|
78
|
+
<!-- layout: Title & Bullets -->
|
|
79
|
+
<!-- transition: dissolve 1.5s -->
|
|
80
|
+
# Why robots?
|
|
81
|
+
|
|
82
|
+
- They're fun
|
|
83
|
+
- Mostly
|
|
84
|
+
- They're useful
|
|
85
|
+
|
|
86
|
+
???
|
|
87
|
+
Keep this one short.
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Roadmap
|
|
91
|
+
|
|
92
|
+
1. Read decks and export Markdown. **Done.**
|
|
93
|
+
2. Edit text and notes, then save. **Done.**
|
|
94
|
+
3. Add, duplicate, delete and reorder slides, set transitions and skipping, and build decks from Markdown. **Done.**
|
|
95
|
+
4. More bundled themes, images, and build animations.
|
|
96
|
+
|
|
97
|
+
## Driving Keynote directly (legacy)
|
|
98
|
+
|
|
99
|
+
`pykeynote.scripting` controls a running copy of Keynote on a Mac through
|
|
100
|
+
AppleScript and JXA. It can already edit decks, but it will be removed once
|
|
101
|
+
the file-based engine catches up.
|
|
102
|
+
|
|
103
|
+
## Tests
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
pytest # everything that runs without Keynote
|
|
107
|
+
PYKEYNOTE_INTEGRATION=1 pytest # also drive real Keynote (macOS)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The sample decks in `tests/fixtures/` were made in Keynote 14.5.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pykeynote"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Create, read and edit Apple Keynote presentations from Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
dependencies = ["keynote-parser>=1.14.5,<2"]
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
authors = [{ name = "Kevin McAleer" }]
|
|
15
|
+
keywords = ["keynote", "apple", "presentation", "slides", "markdown", "iwork"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Topic :: Multimedia :: Graphics :: Presentation",
|
|
23
|
+
"Topic :: Office/Business",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/kevinmcaleer/pykeynote"
|
|
28
|
+
Source = "https://github.com/kevinmcaleer/pykeynote"
|
|
29
|
+
Issues = "https://github.com/kevinmcaleer/pykeynote/issues"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.version]
|
|
32
|
+
path = "src/pykeynote/__init__.py"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
include = ["src", "tests", "README.md", "LICENSE"]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = ["pytest>=7"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
markers = [
|
|
43
|
+
"keynote: needs macOS with Keynote installed (set PYKEYNOTE_INTEGRATION=1 to run)",
|
|
44
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Create, read and edit Apple Keynote presentations from Python.
|
|
2
|
+
|
|
3
|
+
:class:`Presentation` reads ``.key`` files directly, so Keynote isn't needed.
|
|
4
|
+
The older :mod:`pykeynote.scripting` module drives a running copy of Keynote
|
|
5
|
+
on a Mac instead, and will go once the file-based engine can do everything
|
|
6
|
+
it does.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._osa import KeynoteError
|
|
10
|
+
from .document import Presentation, Slide, Transition, themes
|
|
11
|
+
from .markdown import from_markdown, parse_markdown
|
|
12
|
+
from .text import Paragraph, UnsupportedTextError
|
|
13
|
+
|
|
14
|
+
__all__ = ["KeynoteError", "Paragraph", "Presentation", "Slide", "Transition", "UnsupportedTextError", "from_markdown", "parse_markdown", "themes"]
|
|
15
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Low-level bridge to Keynote through ``osascript``.
|
|
2
|
+
|
|
3
|
+
Values are never pasted into script source. JXA scripts get their arguments
|
|
4
|
+
as one JSON string in ``argv`` and return JSON. AppleScript scripts get plain
|
|
5
|
+
string arguments through ``on run argv``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import Any, Sequence
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KeynoteError(RuntimeError):
|
|
16
|
+
"""Raised when Keynote or osascript reports an error."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_JXA_WRAPPER = """
|
|
20
|
+
function run(argv) {
|
|
21
|
+
const Keynote = Application("Keynote");
|
|
22
|
+
const args = JSON.parse(argv[0]);
|
|
23
|
+
const result = main(Keynote, args);
|
|
24
|
+
return JSON.stringify(result === undefined ? null : result);
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _execute(language: str, script: str, argv: Sequence[str]) -> str:
|
|
30
|
+
"""Run a script with osascript and return its stdout. Tests replace this."""
|
|
31
|
+
try:
|
|
32
|
+
proc = subprocess.run(
|
|
33
|
+
["osascript", "-l", language, "-", *argv],
|
|
34
|
+
input=script,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
)
|
|
38
|
+
except FileNotFoundError as exc:
|
|
39
|
+
raise KeynoteError(
|
|
40
|
+
"osascript was not found. pykeynote needs macOS with Keynote installed."
|
|
41
|
+
) from exc
|
|
42
|
+
if proc.returncode != 0:
|
|
43
|
+
raise KeynoteError(proc.stderr.strip() or f"osascript exited with {proc.returncode}")
|
|
44
|
+
return proc.stdout
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def jxa(script: str, **args: Any) -> Any:
|
|
48
|
+
"""Run a JXA script that defines ``function main(Keynote, args)``."""
|
|
49
|
+
out = _execute("JavaScript", script + _JXA_WRAPPER, [json.dumps(args)])
|
|
50
|
+
out = out.strip()
|
|
51
|
+
return json.loads(out) if out else None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def applescript(script: str, *args: str) -> str:
|
|
55
|
+
"""Run an AppleScript with an ``on run argv`` handler and return its output."""
|
|
56
|
+
return _execute("AppleScript", script, [str(a) for a in args]).strip()
|