shortcutkit 0.7.2__tar.gz → 1.0.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.
- shortcutkit-1.0.0/PKG-INFO +153 -0
- shortcutkit-1.0.0/README.md +132 -0
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/pyproject.toml +2 -2
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/src/shortcutkit/__init__.py +100 -28
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/src/shortcutkit/actions.py +363 -182
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/src/shortcutkit/data/builtin-actions.json +1956 -88
- shortcutkit-1.0.0/src/shortcutkit/data/provenance.json +15 -0
- shortcutkit-1.0.0/src/shortcutkit.egg-info/PKG-INFO +153 -0
- shortcutkit-0.7.2/PKG-INFO +0 -194
- shortcutkit-0.7.2/README.md +0 -173
- shortcutkit-0.7.2/src/shortcutkit/data/provenance.json +0 -15
- shortcutkit-0.7.2/src/shortcutkit.egg-info/PKG-INFO +0 -194
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/LICENSE +0 -0
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/setup.cfg +0 -0
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/src/shortcutkit/__main__.py +0 -0
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/src/shortcutkit.egg-info/SOURCES.txt +0 -0
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/src/shortcutkit.egg-info/dependency_links.txt +0 -0
- {shortcutkit-0.7.2 → shortcutkit-1.0.0}/src/shortcutkit.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shortcutkit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Build, validate, and sign Apple Shortcuts files from Python. All 434 built-in actions and 1,449 Apple app actions, extracted from the Shortcuts engine.
|
|
5
|
+
Author: frontboat
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/frontboat/shortcutkit
|
|
8
|
+
Project-URL: Documentation, https://github.com/frontboat/shortcutkit/blob/main/docs/shortcut-file-format.md
|
|
9
|
+
Project-URL: Repository, https://github.com/frontboat/shortcutkit.git
|
|
10
|
+
Project-URL: Changelog, https://github.com/frontboat/shortcutkit/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: apple,apple-shortcuts,shortcuts,shortcuts-app,workflowkit,workflow,automation,macos,macos-automation,ios,plist
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# shortcutkit
|
|
23
|
+
|
|
24
|
+
Build, validate, and sign Apple Shortcuts (`.shortcut`) files from Python.
|
|
25
|
+
|
|
26
|
+
[](https://github.com/frontboat/shortcutkit/blob/main/LICENSE)
|
|
27
|
+
[](https://github.com/frontboat/shortcutkit/actions)
|
|
28
|
+
|
|
29
|
+
A shortcut that asks your name and says hello:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from shortcutkit import Shortcut, actions, ref, text
|
|
33
|
+
|
|
34
|
+
s = Shortcut("Greeting")
|
|
35
|
+
name = s.action(actions.ASK, WFAskActionPrompt="What's your name?")
|
|
36
|
+
s.action(actions.SHOWRESULT, Text=text("Hello, ", ref(name), "!"))
|
|
37
|
+
|
|
38
|
+
s.write() # Greeting.shortcut
|
|
39
|
+
Shortcut.sign("Greeting.shortcut", "Greeting-signed.shortcut")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Open `Greeting-signed.shortcut` and Shortcuts adds it to your library. Shortcuts only imports
|
|
43
|
+
signed files, and signing needs a Mac with an iCloud login; everything else runs on any OS.
|
|
44
|
+
|
|
45
|
+
## Why
|
|
46
|
+
|
|
47
|
+
The action catalog is extracted from the Shortcuts app's own frameworks, not written by hand,
|
|
48
|
+
and one command refreshes it after a macOS update. Earlier libraries kept hand-written lists and
|
|
49
|
+
are archived ([python-shortcuts](https://github.com/alexander-akhmetov/python-shortcuts),
|
|
50
|
+
[shortcuts-js](https://github.com/joshfarrant/shortcuts-js));
|
|
51
|
+
[Cherri](https://github.com/electrikmilk/cherri) is a separate language with its own compiler.
|
|
52
|
+
|
|
53
|
+
- All 434 built-in actions as constants, so a typo is an `AttributeError`.
|
|
54
|
+
- 1,449 actions from Apple's apps: Reminders, Notes, Messages, Settings, and 73 more.
|
|
55
|
+
- Third-party app actions, given their descriptor.
|
|
56
|
+
- If, Repeat, Repeat with Each, and Choose from Menu blocks.
|
|
57
|
+
- A wrong key or value raises `ValueError` before anything is written.
|
|
58
|
+
- Pure Python, no dependencies.
|
|
59
|
+
|
|
60
|
+
A [TypeScript package](https://www.npmjs.com/package/shortcutkit) with the same name and data
|
|
61
|
+
adds compile-time checks.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install shortcutkit # Python 3.9+
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
These examples build on the one at the top.
|
|
72
|
+
|
|
73
|
+
### Share sheet
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from shortcutkit import shortcut_input
|
|
77
|
+
|
|
78
|
+
s = Shortcut("Page Links", types=["ActionExtension"], input_classes=["WFSafariWebPageContentItem"])
|
|
79
|
+
links = s.action(actions.RUNJAVASCRIPTONWEBPAGE, WFInput=shortcut_input(), WFJavaScript="completion(document.links.length)")
|
|
80
|
+
s.action(actions.SHOWRESULT, Text=text("Links: ", ref(links)))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Control flow
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
gid = s.if_(ref(name), "has_any_value")
|
|
87
|
+
s.action(actions.SHOWRESULT, Text=text("Hello, ", ref(name)))
|
|
88
|
+
s.otherwise(gid)
|
|
89
|
+
s.action(actions.SHOWRESULT, Text=text("Hello, stranger"))
|
|
90
|
+
s.end_if(gid)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Apple's apps
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
r = s.action(actions.REMINDERS_CREATE_REMINDER, title=text("Buy milk"), priorityLevel="high")
|
|
97
|
+
s.action(actions.NOTES_CREATE_NOTE, name="Shopping", content=text("Added ", ref(r)))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Other apps
|
|
101
|
+
|
|
102
|
+
Pass the identifier and descriptor yourself; the parameters are not checked.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
s.action(
|
|
106
|
+
"com.example.app.CreateNoteIntent",
|
|
107
|
+
AppIntentDescriptor={
|
|
108
|
+
"BundleIdentifier": "com.example.app", "Name": "Example", "TeamIdentifier": "ABCDE12345",
|
|
109
|
+
"AppIntentIdentifier": "CreateNoteIntent", "ActionRequiresAppInstallation": True,
|
|
110
|
+
},
|
|
111
|
+
title=text("Hello"),
|
|
112
|
+
body=ref(name),
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API
|
|
117
|
+
|
|
118
|
+
| Export | What it does |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `Shortcut(name, color=, glyph=, types=, input_classes=)` | A shortcut. |
|
|
121
|
+
| `s.action(id, **params)` | Appends an action and returns it. |
|
|
122
|
+
| `s.if_(subject, condition, value=None)`, `s.otherwise(gid)`, `s.end_if(gid)` | An If block. |
|
|
123
|
+
| `s.repeat_each(items)`, `s.end_repeat_each(gid)` | A Repeat with Each block. |
|
|
124
|
+
| `s.repeat_count(n)`, `s.end_repeat_count(gid)` | A Repeat block. |
|
|
125
|
+
| `s.choose_from_menu(prompt, items)`, `s.menu_item(gid, title)`, `s.end_menu(gid)` | A Choose from Menu block. |
|
|
126
|
+
| `s.write(path=None)` | Writes the unsigned file and returns its path. |
|
|
127
|
+
| `Shortcut.sign(src, dst, mode="anyone")` | Signs a file with `shortcuts sign`. |
|
|
128
|
+
| `s.to_plist()` | The shortcut as a plist dictionary. |
|
|
129
|
+
| `ref(action)` | An action's output. |
|
|
130
|
+
| `text(*parts)` | A string with references in it. |
|
|
131
|
+
| `variable(name)`, `repeat_item()`, `repeat_index()` | Named and repeat variables. |
|
|
132
|
+
| `shortcut_input()`, `clipboard()`, `current_date()`, `ask()`, `device_details()`, `current_app()` | Special variables. |
|
|
133
|
+
| `actions`, `ACTIONS`, `get_action(id)` | Action identifiers and their catalog entries. |
|
|
134
|
+
| `PARAM_KINDS`, `PARAM_CHOICES`, `PARAM_VARIABLE_TYPES` | What each parameter accepts. |
|
|
135
|
+
| `CONDITION`, `ICON_COLORS`, `PROVENANCE` | If conditions, icon colors, and the build the data came from. |
|
|
136
|
+
|
|
137
|
+
## Documentation
|
|
138
|
+
|
|
139
|
+
- [Shortcut file format](https://github.com/frontboat/shortcutkit/blob/main/docs/shortcut-file-format.md)
|
|
140
|
+
- [Built-in actions](https://github.com/frontboat/shortcutkit/blob/main/docs/builtin-actions-reference.md)
|
|
141
|
+
- [Apple app actions](https://github.com/frontboat/shortcutkit/blob/main/docs/apple-app-intents-reference.md)
|
|
142
|
+
- [Parameter encodings](https://github.com/frontboat/shortcutkit/blob/main/docs/parameter-encodings.md)
|
|
143
|
+
- [How the data was extracted](https://github.com/frontboat/shortcutkit/blob/main/docs/extraction.md),
|
|
144
|
+
and what is still approximate
|
|
145
|
+
|
|
146
|
+
## Contributing
|
|
147
|
+
|
|
148
|
+
Issues and pull requests are welcome. See
|
|
149
|
+
[`CONTRIBUTING.md`](https://github.com/frontboat/shortcutkit/blob/main/CONTRIBUTING.md).
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
[MIT](https://github.com/frontboat/shortcutkit/blob/main/LICENSE).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# shortcutkit
|
|
2
|
+
|
|
3
|
+
Build, validate, and sign Apple Shortcuts (`.shortcut`) files from Python.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/frontboat/shortcutkit/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/frontboat/shortcutkit/actions)
|
|
7
|
+
|
|
8
|
+
A shortcut that asks your name and says hello:
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from shortcutkit import Shortcut, actions, ref, text
|
|
12
|
+
|
|
13
|
+
s = Shortcut("Greeting")
|
|
14
|
+
name = s.action(actions.ASK, WFAskActionPrompt="What's your name?")
|
|
15
|
+
s.action(actions.SHOWRESULT, Text=text("Hello, ", ref(name), "!"))
|
|
16
|
+
|
|
17
|
+
s.write() # Greeting.shortcut
|
|
18
|
+
Shortcut.sign("Greeting.shortcut", "Greeting-signed.shortcut")
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Open `Greeting-signed.shortcut` and Shortcuts adds it to your library. Shortcuts only imports
|
|
22
|
+
signed files, and signing needs a Mac with an iCloud login; everything else runs on any OS.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
The action catalog is extracted from the Shortcuts app's own frameworks, not written by hand,
|
|
27
|
+
and one command refreshes it after a macOS update. Earlier libraries kept hand-written lists and
|
|
28
|
+
are archived ([python-shortcuts](https://github.com/alexander-akhmetov/python-shortcuts),
|
|
29
|
+
[shortcuts-js](https://github.com/joshfarrant/shortcuts-js));
|
|
30
|
+
[Cherri](https://github.com/electrikmilk/cherri) is a separate language with its own compiler.
|
|
31
|
+
|
|
32
|
+
- All 434 built-in actions as constants, so a typo is an `AttributeError`.
|
|
33
|
+
- 1,449 actions from Apple's apps: Reminders, Notes, Messages, Settings, and 73 more.
|
|
34
|
+
- Third-party app actions, given their descriptor.
|
|
35
|
+
- If, Repeat, Repeat with Each, and Choose from Menu blocks.
|
|
36
|
+
- A wrong key or value raises `ValueError` before anything is written.
|
|
37
|
+
- Pure Python, no dependencies.
|
|
38
|
+
|
|
39
|
+
A [TypeScript package](https://www.npmjs.com/package/shortcutkit) with the same name and data
|
|
40
|
+
adds compile-time checks.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install shortcutkit # Python 3.9+
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
These examples build on the one at the top.
|
|
51
|
+
|
|
52
|
+
### Share sheet
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from shortcutkit import shortcut_input
|
|
56
|
+
|
|
57
|
+
s = Shortcut("Page Links", types=["ActionExtension"], input_classes=["WFSafariWebPageContentItem"])
|
|
58
|
+
links = s.action(actions.RUNJAVASCRIPTONWEBPAGE, WFInput=shortcut_input(), WFJavaScript="completion(document.links.length)")
|
|
59
|
+
s.action(actions.SHOWRESULT, Text=text("Links: ", ref(links)))
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Control flow
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
gid = s.if_(ref(name), "has_any_value")
|
|
66
|
+
s.action(actions.SHOWRESULT, Text=text("Hello, ", ref(name)))
|
|
67
|
+
s.otherwise(gid)
|
|
68
|
+
s.action(actions.SHOWRESULT, Text=text("Hello, stranger"))
|
|
69
|
+
s.end_if(gid)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Apple's apps
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
r = s.action(actions.REMINDERS_CREATE_REMINDER, title=text("Buy milk"), priorityLevel="high")
|
|
76
|
+
s.action(actions.NOTES_CREATE_NOTE, name="Shopping", content=text("Added ", ref(r)))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Other apps
|
|
80
|
+
|
|
81
|
+
Pass the identifier and descriptor yourself; the parameters are not checked.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
s.action(
|
|
85
|
+
"com.example.app.CreateNoteIntent",
|
|
86
|
+
AppIntentDescriptor={
|
|
87
|
+
"BundleIdentifier": "com.example.app", "Name": "Example", "TeamIdentifier": "ABCDE12345",
|
|
88
|
+
"AppIntentIdentifier": "CreateNoteIntent", "ActionRequiresAppInstallation": True,
|
|
89
|
+
},
|
|
90
|
+
title=text("Hello"),
|
|
91
|
+
body=ref(name),
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
| Export | What it does |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `Shortcut(name, color=, glyph=, types=, input_classes=)` | A shortcut. |
|
|
100
|
+
| `s.action(id, **params)` | Appends an action and returns it. |
|
|
101
|
+
| `s.if_(subject, condition, value=None)`, `s.otherwise(gid)`, `s.end_if(gid)` | An If block. |
|
|
102
|
+
| `s.repeat_each(items)`, `s.end_repeat_each(gid)` | A Repeat with Each block. |
|
|
103
|
+
| `s.repeat_count(n)`, `s.end_repeat_count(gid)` | A Repeat block. |
|
|
104
|
+
| `s.choose_from_menu(prompt, items)`, `s.menu_item(gid, title)`, `s.end_menu(gid)` | A Choose from Menu block. |
|
|
105
|
+
| `s.write(path=None)` | Writes the unsigned file and returns its path. |
|
|
106
|
+
| `Shortcut.sign(src, dst, mode="anyone")` | Signs a file with `shortcuts sign`. |
|
|
107
|
+
| `s.to_plist()` | The shortcut as a plist dictionary. |
|
|
108
|
+
| `ref(action)` | An action's output. |
|
|
109
|
+
| `text(*parts)` | A string with references in it. |
|
|
110
|
+
| `variable(name)`, `repeat_item()`, `repeat_index()` | Named and repeat variables. |
|
|
111
|
+
| `shortcut_input()`, `clipboard()`, `current_date()`, `ask()`, `device_details()`, `current_app()` | Special variables. |
|
|
112
|
+
| `actions`, `ACTIONS`, `get_action(id)` | Action identifiers and their catalog entries. |
|
|
113
|
+
| `PARAM_KINDS`, `PARAM_CHOICES`, `PARAM_VARIABLE_TYPES` | What each parameter accepts. |
|
|
114
|
+
| `CONDITION`, `ICON_COLORS`, `PROVENANCE` | If conditions, icon colors, and the build the data came from. |
|
|
115
|
+
|
|
116
|
+
## Documentation
|
|
117
|
+
|
|
118
|
+
- [Shortcut file format](https://github.com/frontboat/shortcutkit/blob/main/docs/shortcut-file-format.md)
|
|
119
|
+
- [Built-in actions](https://github.com/frontboat/shortcutkit/blob/main/docs/builtin-actions-reference.md)
|
|
120
|
+
- [Apple app actions](https://github.com/frontboat/shortcutkit/blob/main/docs/apple-app-intents-reference.md)
|
|
121
|
+
- [Parameter encodings](https://github.com/frontboat/shortcutkit/blob/main/docs/parameter-encodings.md)
|
|
122
|
+
- [How the data was extracted](https://github.com/frontboat/shortcutkit/blob/main/docs/extraction.md),
|
|
123
|
+
and what is still approximate
|
|
124
|
+
|
|
125
|
+
## Contributing
|
|
126
|
+
|
|
127
|
+
Issues and pull requests are welcome. See
|
|
128
|
+
[`CONTRIBUTING.md`](https://github.com/frontboat/shortcutkit/blob/main/CONTRIBUTING.md).
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
[MIT](https://github.com/frontboat/shortcutkit/blob/main/LICENSE).
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shortcutkit"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Build, validate and sign Apple Shortcuts
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Build, validate, and sign Apple Shortcuts files from Python. All 434 built-in actions and 1,449 Apple app actions, extracted from the Shortcuts engine."
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = "MIT"
|
|
@@ -25,9 +25,11 @@ Value helpers return the exact structures WorkflowKit's state classes serialize:
|
|
|
25
25
|
current_date() WFTextTokenAttachment -> current date
|
|
26
26
|
ask("Prompt") WFTextTokenAttachment -> ask each time
|
|
27
27
|
text("a ", ref(x)) WFTextTokenString -> text with embedded references
|
|
28
|
-
|
|
28
|
+
subject(att) {"Type": "Variable", "Variable": att} (the If condition's subject; pickers take the bare attachment)
|
|
29
29
|
Plain str / int / float / bool / list are written as-is.
|
|
30
30
|
"""
|
|
31
|
+
import copy
|
|
32
|
+
import functools
|
|
31
33
|
import json
|
|
32
34
|
import pathlib
|
|
33
35
|
import plistlib
|
|
@@ -38,6 +40,13 @@ import uuid
|
|
|
38
40
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
39
41
|
DEFINITIONS = HERE / "data" / "builtin-actions.json"
|
|
40
42
|
|
|
43
|
+
|
|
44
|
+
@functools.lru_cache(maxsize=None)
|
|
45
|
+
def _load_definitions(path):
|
|
46
|
+
"""The definitions file, parsed once per path; {} when it is absent."""
|
|
47
|
+
path = pathlib.Path(path)
|
|
48
|
+
return json.loads(path.read_text()) if path.exists() else {}
|
|
49
|
+
|
|
41
50
|
# WFWorkflowIcon.backgroundColorValue for palette colors 0-14, as the unsigned 32-bit
|
|
42
51
|
# value the plist stores. Names follow Shortcuts' picker order.
|
|
43
52
|
ICON_COLORS = {
|
|
@@ -95,7 +104,9 @@ def _check_kind(kind, value):
|
|
|
95
104
|
if kind in ("string", "text", "plainString") and not isinstance(value, str):
|
|
96
105
|
return f"expected a string{'' if kind == 'plainString' else ' or a reference'}, got {type(value).__name__}"
|
|
97
106
|
if kind == "picker":
|
|
98
|
-
return "expected
|
|
107
|
+
return "expected a reference (ref(), variable(), shortcut_input(), ...)"
|
|
108
|
+
if kind == "subject" and not (isinstance(value, dict) and value.get("Type") == "Variable" and "Variable" in value):
|
|
109
|
+
return "expected a reference or subject(<reference>)"
|
|
99
110
|
return None
|
|
100
111
|
|
|
101
112
|
|
|
@@ -104,31 +115,59 @@ def _attachment(value):
|
|
|
104
115
|
|
|
105
116
|
|
|
106
117
|
def ref(action, output_name=None):
|
|
107
|
-
"""
|
|
118
|
+
"""Another action's output. `action` is what Shortcut.action() returned."""
|
|
108
119
|
return _attachment({"Type": "ActionOutput", "OutputUUID": action["WFWorkflowActionParameters"]["UUID"],
|
|
109
120
|
"OutputName": output_name or action.get("_outputName", "Output")})
|
|
110
121
|
|
|
111
122
|
|
|
112
123
|
def variable(name):
|
|
124
|
+
"""A named variable, as set by Set Variable or Add to Variable."""
|
|
113
125
|
return _attachment({"Type": "Variable", "VariableName": name})
|
|
114
126
|
|
|
115
127
|
|
|
116
128
|
def shortcut_input():
|
|
129
|
+
"""The shortcut's input, from the share sheet, Quick Actions, or Run Shortcut."""
|
|
117
130
|
return _attachment({"Type": "ExtensionInput"})
|
|
118
131
|
|
|
119
132
|
|
|
120
133
|
def clipboard():
|
|
134
|
+
"""The clipboard's contents."""
|
|
121
135
|
return _attachment({"Type": "Clipboard"})
|
|
122
136
|
|
|
123
137
|
|
|
124
138
|
def current_date():
|
|
139
|
+
"""The date and time the shortcut runs."""
|
|
125
140
|
return _attachment({"Type": "CurrentDate"})
|
|
126
141
|
|
|
127
142
|
|
|
128
143
|
def ask(prompt=None):
|
|
144
|
+
"""A value the user is asked for each time the shortcut runs."""
|
|
129
145
|
return _attachment({"Type": "Ask", **({"Prompt": prompt} if prompt else {})})
|
|
130
146
|
|
|
131
147
|
|
|
148
|
+
def device_details():
|
|
149
|
+
"""Device Details (name, model, OS version, ...); the app usually adds a property aggrandizement."""
|
|
150
|
+
return _attachment({"Type": "DeviceDetails"})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def current_app():
|
|
154
|
+
"""The app the shortcut was run from."""
|
|
155
|
+
return _attachment({"Type": "CurrentApp"})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _reference_types(value):
|
|
159
|
+
"""The variable types (Ask, ActionOutput, Variable, ...) referenced anywhere in a value."""
|
|
160
|
+
if not isinstance(value, dict):
|
|
161
|
+
return []
|
|
162
|
+
if value.get("WFSerializationType") == "WFTextTokenAttachment":
|
|
163
|
+
return [str(value.get("Value", {}).get("Type"))]
|
|
164
|
+
if value.get("WFSerializationType") == "WFTextTokenString":
|
|
165
|
+
return [str(a.get("Type")) for a in value.get("Value", {}).get("attachmentsByRange", {}).values()]
|
|
166
|
+
if value.get("Type") == "Variable" and isinstance(value.get("Variable"), dict):
|
|
167
|
+
return _reference_types(value["Variable"])
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
|
|
132
171
|
def repeat_item():
|
|
133
172
|
"""The current item inside a Repeat with Each block."""
|
|
134
173
|
return _attachment({"Type": "Variable", "VariableName": "Repeat Item"})
|
|
@@ -139,13 +178,19 @@ def repeat_index():
|
|
|
139
178
|
return _attachment({"Type": "Variable", "VariableName": "Repeat Index"})
|
|
140
179
|
|
|
141
180
|
|
|
181
|
+
def subject(attachment):
|
|
182
|
+
"""The If-subject form of a reference. if_() and action() apply it for you."""
|
|
183
|
+
return {"Type": "Variable", "Variable": attachment}
|
|
184
|
+
|
|
185
|
+
|
|
142
186
|
def picker(attachment):
|
|
187
|
+
"""Deprecated, misnamed: this is the If-subject form. Pass a bare reference to picker parameters."""
|
|
143
188
|
"""Wrap an attachment for WFVariablePickerParameter keys (e.g. WFInput on Repeat with Each)."""
|
|
144
189
|
return {"Type": "Variable", "Variable": attachment}
|
|
145
190
|
|
|
146
191
|
|
|
147
192
|
def text(*parts):
|
|
148
|
-
"""
|
|
193
|
+
"""Text with references in it: text("Hello, ", ref(name), "!")."""
|
|
149
194
|
string = ""
|
|
150
195
|
attachments = {}
|
|
151
196
|
for part in parts:
|
|
@@ -159,20 +204,27 @@ def text(*parts):
|
|
|
159
204
|
|
|
160
205
|
|
|
161
206
|
class Shortcut:
|
|
207
|
+
"""A shortcut. Add actions, then write() and sign(); Shortcuts names the import after the file,
|
|
208
|
+
which defaults to <name>.shortcut."""
|
|
209
|
+
|
|
162
210
|
def __init__(self, name, color="Blue", glyph=DEFAULT_GLYPH, input_classes=None, types=None, definitions=DEFINITIONS):
|
|
163
|
-
"""
|
|
164
|
-
|
|
165
|
-
"
|
|
211
|
+
"""color: a palette name ("Teal") or a raw value. glyph: a codepoint from Shortcuts' icon font.
|
|
212
|
+
types: where the shortcut appears: "ActionExtension" (share sheet), "QuickActions" (Finder and
|
|
213
|
+
Services menus), "MenuBar", "NCWidget", "WatchKit", "Sleep", "ReceivesOnScreenContent".
|
|
214
|
+
input_classes: content item classes it accepts as input."""
|
|
166
215
|
self.name = name
|
|
167
216
|
self.color = ICON_COLORS[color] if isinstance(color, str) else color
|
|
168
217
|
self.glyph = glyph
|
|
169
218
|
self.input_classes = input_classes or []
|
|
170
219
|
self.types = list(types or [])
|
|
171
220
|
self.actions = []
|
|
172
|
-
self.defs =
|
|
221
|
+
self.defs = _load_definitions(definitions)
|
|
173
222
|
|
|
174
|
-
def action(self, identifier, **params):
|
|
175
|
-
"""Append an action
|
|
223
|
+
def action(self, identifier, /, **params):
|
|
224
|
+
"""Append an action and return it; ref() it to use its output. Catalog identifiers are
|
|
225
|
+
checked: unknown keys, wrong value kinds, and unreadable references raise ValueError. Other
|
|
226
|
+
identifiers pass through unchecked. `identifier` is positional-only, so a parameter can
|
|
227
|
+
have that name."""
|
|
176
228
|
definition = self.defs.get(identifier)
|
|
177
229
|
catalog = ACTIONS.get(identifier) or {}
|
|
178
230
|
descriptor = catalog.get("descriptor")
|
|
@@ -192,13 +244,20 @@ class Shortcut:
|
|
|
192
244
|
if unknown and known - {"UUID", "GroupingIdentifier", "WFControlFlowMode", "CustomOutputName"}:
|
|
193
245
|
raise ValueError(f"{identifier}: unknown parameter(s) {sorted(unknown)}; known: {sorted(known)}")
|
|
194
246
|
kinds = PARAM_KINDS.get(identifier, {})
|
|
247
|
+
restricted = PARAM_VARIABLE_TYPES.get(identifier, {})
|
|
195
248
|
for key, value in params.items():
|
|
196
249
|
problem = _check_kind(kinds.get(key, "any"), value)
|
|
197
250
|
if problem:
|
|
198
251
|
raise ValueError(f"{identifier}.{key}: {problem}")
|
|
252
|
+
# Keys the engine reads no reference for (or only Ask): the same rule as the TypeScript types, at run time.
|
|
253
|
+
if key in restricted:
|
|
254
|
+
bad = [t for t in _reference_types(value) if t not in restricted[key]]
|
|
255
|
+
if bad:
|
|
256
|
+
raise ValueError(f"{identifier}.{key}: the engine does not read a {'/'.join(bad)} reference here (it reads {', '.join(restricted[key]) if restricted[key] else 'only a plain value'})")
|
|
199
257
|
# Text parameters never hold a bare attachment: the engine writes a lone variable as a
|
|
200
258
|
# token string with one placeholder. Wrap it so ref() works for text keys too.
|
|
201
|
-
params = {k: (text(v) if kinds.get(k) == "text" and
|
|
259
|
+
params = {k: (text(v) if kinds.get(k) == "text" and _is_reference(v) and v.get("WFSerializationType") == "WFTextTokenAttachment"
|
|
260
|
+
else subject(v) if kinds.get(k) == "subject" and _is_reference(v) and v.get("WFSerializationType") == "WFTextTokenAttachment" else v)
|
|
202
261
|
for k, v in params.items()}
|
|
203
262
|
# App Intents actions carry a descriptor naming the app that provides them.
|
|
204
263
|
head = {"UUID": str(uuid.uuid4()).upper()}
|
|
@@ -216,9 +275,15 @@ class Shortcut:
|
|
|
216
275
|
|
|
217
276
|
# Control flow: each block is a group sharing a GroupingIdentifier, with
|
|
218
277
|
# WFControlFlowMode 0 = start, 1 = middle (Otherwise / menu item), 2 = end.
|
|
278
|
+
@staticmethod
|
|
279
|
+
def _subject(value):
|
|
280
|
+
return value if isinstance(value, dict) and value.get("Type") == "Variable" and "Variable" in value else subject(value)
|
|
281
|
+
|
|
219
282
|
def if_(self, subject, condition, value=None):
|
|
283
|
+
"""Open an If block on `subject`. `condition` is a CONDITION name or code; `value` is what it
|
|
284
|
+
compares with, if anything. Returns the id for otherwise() and end_if()."""
|
|
220
285
|
gid = str(uuid.uuid4()).upper()
|
|
221
|
-
params = {"GroupingIdentifier": gid, "WFControlFlowMode": 0, "WFInput":
|
|
286
|
+
params = {"GroupingIdentifier": gid, "WFControlFlowMode": 0, "WFInput": self._subject(subject),
|
|
222
287
|
"WFCondition": CONDITION[condition] if isinstance(condition, str) else condition}
|
|
223
288
|
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
224
289
|
params["WFNumberValue"] = value
|
|
@@ -228,49 +293,55 @@ class Shortcut:
|
|
|
228
293
|
return gid
|
|
229
294
|
|
|
230
295
|
def otherwise(self, gid):
|
|
296
|
+
"""Start the Otherwise branch of If block `gid`."""
|
|
231
297
|
self.action("is.workflow.actions.conditional", GroupingIdentifier=gid, WFControlFlowMode=1)
|
|
232
298
|
|
|
233
299
|
def end_if(self, gid):
|
|
300
|
+
"""Close If block `gid`."""
|
|
234
301
|
self.action("is.workflow.actions.conditional", GroupingIdentifier=gid, WFControlFlowMode=2)
|
|
235
302
|
|
|
236
303
|
def repeat_each(self, items):
|
|
237
|
-
"""Open a Repeat with Each block
|
|
304
|
+
"""Open a Repeat with Each block over `items`; returns the id for end_repeat_each(). Inside,
|
|
305
|
+
use repeat_item() and repeat_index()."""
|
|
238
306
|
gid = str(uuid.uuid4()).upper()
|
|
239
|
-
self.action("is.workflow.actions.repeat.each", GroupingIdentifier=gid, WFControlFlowMode=0, WFInput=
|
|
307
|
+
self.action("is.workflow.actions.repeat.each", GroupingIdentifier=gid, WFControlFlowMode=0, WFInput=items)
|
|
240
308
|
return gid
|
|
241
309
|
|
|
242
310
|
def end_repeat_each(self, gid):
|
|
243
|
-
"""Close a Repeat with Each block
|
|
311
|
+
"""Close a Repeat with Each block and return it; ref() it for the Repeat Results."""
|
|
244
312
|
a = self.action("is.workflow.actions.repeat.each", GroupingIdentifier=gid, WFControlFlowMode=2)
|
|
245
313
|
a["_outputName"] = "Repeat Results"
|
|
246
314
|
return a
|
|
247
315
|
|
|
248
316
|
def repeat_count(self, count):
|
|
249
|
-
"""Open a Repeat block that runs `count` times; returns the
|
|
317
|
+
"""Open a Repeat block that runs `count` times; returns the id for end_repeat_count()."""
|
|
250
318
|
gid = str(uuid.uuid4()).upper()
|
|
251
319
|
self.action("is.workflow.actions.repeat.count", GroupingIdentifier=gid, WFControlFlowMode=0, WFRepeatCount=count)
|
|
252
320
|
return gid
|
|
253
321
|
|
|
254
322
|
def end_repeat_count(self, gid):
|
|
255
|
-
"""Close a Repeat block
|
|
323
|
+
"""Close a Repeat block and return it; ref() it for the Repeat Results."""
|
|
256
324
|
a = self.action("is.workflow.actions.repeat.count", GroupingIdentifier=gid, WFControlFlowMode=2)
|
|
257
325
|
a["_outputName"] = "Repeat Results"
|
|
258
326
|
return a
|
|
259
327
|
|
|
260
328
|
def choose_from_menu(self, prompt, items):
|
|
261
|
-
"""Open a Choose from Menu block
|
|
262
|
-
|
|
329
|
+
"""Open a Choose from Menu block; returns the id for menu_item() and end_menu(). Follow with
|
|
330
|
+
one menu_item() per title, in order, each followed by that branch's actions."""
|
|
263
331
|
gid = str(uuid.uuid4()).upper()
|
|
264
332
|
self.action("is.workflow.actions.choosefrommenu", GroupingIdentifier=gid, WFControlFlowMode=0, WFMenuPrompt=prompt, WFMenuItems=list(items))
|
|
265
333
|
return gid
|
|
266
334
|
|
|
267
335
|
def menu_item(self, gid, title):
|
|
336
|
+
"""Start the branch for `title` in menu `gid`."""
|
|
268
337
|
self.action("is.workflow.actions.choosefrommenu", GroupingIdentifier=gid, WFControlFlowMode=1, WFMenuItemTitle=title)
|
|
269
338
|
|
|
270
339
|
def end_menu(self, gid):
|
|
340
|
+
"""Close menu `gid`."""
|
|
271
341
|
self.action("is.workflow.actions.choosefrommenu", GroupingIdentifier=gid, WFControlFlowMode=2)
|
|
272
342
|
|
|
273
343
|
def to_plist(self):
|
|
344
|
+
"""The shortcut as a plist dictionary."""
|
|
274
345
|
actions = []
|
|
275
346
|
for a in self.actions:
|
|
276
347
|
actions.append({k: v for k, v in a.items() if not k.startswith("_")})
|
|
@@ -291,8 +362,7 @@ class Shortcut:
|
|
|
291
362
|
}
|
|
292
363
|
|
|
293
364
|
def write(self, path=None):
|
|
294
|
-
"""Write the unsigned
|
|
295
|
-
default file name is the shortcut's name."""
|
|
365
|
+
"""Write the unsigned file, <name>.shortcut by default, and return its path."""
|
|
296
366
|
path = pathlib.Path(path) if path else pathlib.Path(f"{self.name}.shortcut")
|
|
297
367
|
with open(path, "wb") as f:
|
|
298
368
|
plistlib.dump(self.to_plist(), f, fmt=plistlib.FMT_BINARY)
|
|
@@ -300,13 +370,15 @@ class Shortcut:
|
|
|
300
370
|
|
|
301
371
|
@staticmethod
|
|
302
372
|
def sign(src, dst, mode="anyone"):
|
|
373
|
+
"""Sign `src` into `dst`, which Shortcuts will import, and return `dst`. Needs a Mac with an
|
|
374
|
+
iCloud login."""
|
|
303
375
|
subprocess.run(["shortcuts", "sign", "--mode", mode, "--input", str(src), "--output", str(dst)], check=True)
|
|
304
376
|
return dst
|
|
305
377
|
|
|
306
378
|
|
|
307
379
|
def get_action(identifier, definitions=DEFINITIONS):
|
|
308
|
-
"""What the
|
|
309
|
-
|
|
380
|
+
"""What the catalog knows about any identifier, built-in or App Intent: name, params, kinds,
|
|
381
|
+
output, descriptor (App Intents), and definition (built-ins)."""
|
|
310
382
|
entry = ACTIONS.get(identifier)
|
|
311
383
|
if entry is None:
|
|
312
384
|
return None
|
|
@@ -314,9 +386,9 @@ def get_action(identifier, definitions=DEFINITIONS):
|
|
|
314
386
|
"kinds": dict(PARAM_KINDS.get(identifier, {})), "output": entry.get("output"), "outputTypes": list(entry.get("outputTypes", []))}
|
|
315
387
|
if entry.get("descriptor"):
|
|
316
388
|
out["descriptor"] = entry["descriptor"]
|
|
317
|
-
defs =
|
|
389
|
+
defs = _load_definitions(definitions) if definitions else {}
|
|
318
390
|
if identifier in defs:
|
|
319
|
-
out["definition"] = defs[identifier]
|
|
391
|
+
out["definition"] = copy.deepcopy(defs[identifier])
|
|
320
392
|
return out
|
|
321
393
|
|
|
322
394
|
|
|
@@ -338,11 +410,11 @@ def demo(out):
|
|
|
338
410
|
|
|
339
411
|
|
|
340
412
|
from . import actions # noqa: E402 (generated identifier constants and ACTIONS metadata)
|
|
341
|
-
from .actions import ACTIONS, PARAM_KINDS, PARAM_CHOICES # noqa: E402
|
|
413
|
+
from .actions import ACTIONS, PARAM_KINDS, PARAM_CHOICES, PARAM_VARIABLE_TYPES # noqa: E402
|
|
342
414
|
|
|
343
415
|
PROVENANCE = json.load(open(HERE / "data" / "provenance.json")) if (HERE / "data" / "provenance.json").exists() else {}
|
|
344
416
|
"""Which macOS and Shortcuts build the bundled data was extracted from."""
|
|
345
417
|
|
|
346
|
-
__all__ = ["actions", "ACTIONS", "PARAM_KINDS", "PARAM_CHOICES", "PROVENANCE", "get_action", "repeat_item", "repeat_index", "Shortcut", "ref", "variable", "shortcut_input", "clipboard", "current_date", "ask", "picker", "text",
|
|
418
|
+
__all__ = ["actions", "ACTIONS", "PARAM_KINDS", "PARAM_CHOICES", "PARAM_VARIABLE_TYPES", "PROVENANCE", "get_action", "repeat_item", "repeat_index", "subject", "Shortcut", "ref", "variable", "shortcut_input", "clipboard", "current_date", "ask", "device_details", "current_app", "picker", "text",
|
|
347
419
|
"ICON_COLORS", "CONDITION", "DEFAULT_GLYPH", "LEGACY_KEYS", "demo"]
|
|
348
|
-
__version__ = "0.
|
|
420
|
+
__version__ = "1.0.0"
|