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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
@@ -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.
@@ -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()