shortcutkit 2.0.0__tar.gz → 3.1.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-2.0.0/src/shortcutkit.egg-info → shortcutkit-3.1.0}/PKG-INFO +38 -16
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/README.md +36 -14
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/pyproject.toml +2 -2
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit/__init__.py +150 -23
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit/actions.py +2324 -1886
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit/data/provenance.json +2 -2
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit/entities.py +105 -0
- {shortcutkit-2.0.0 → shortcutkit-3.1.0/src/shortcutkit.egg-info}/PKG-INFO +38 -16
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/LICENSE +0 -0
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/setup.cfg +0 -0
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit/__main__.py +0 -0
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit/data/builtin-actions.json +0 -0
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit.egg-info/SOURCES.txt +0 -0
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit.egg-info/dependency_links.txt +0 -0
- {shortcutkit-2.0.0 → shortcutkit-3.1.0}/src/shortcutkit.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shortcutkit
|
|
3
|
-
Version:
|
|
4
|
-
Summary: Build, validate, and sign Apple Shortcuts files from Python. All 449 built-in actions and 1,
|
|
3
|
+
Version: 3.1.0
|
|
4
|
+
Summary: Build, validate, and sign Apple Shortcuts files from Python. All 449 built-in actions and 1,265 Apple app actions, extracted from the Shortcuts engine.
|
|
5
5
|
Author: frontboat
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/frontboat/shortcutkit
|
|
@@ -51,9 +51,9 @@ are archived ([python-shortcuts](https://github.com/alexander-akhmetov/python-sh
|
|
|
51
51
|
[Cherri](https://github.com/electrikmilk/cherri) is a separate language with its own compiler.
|
|
52
52
|
|
|
53
53
|
- All 449 built-in actions as constants, so a typo is an `AttributeError`.
|
|
54
|
-
- 1,
|
|
54
|
+
- 1,265 actions from Apple's apps: Reminders, Notes, Messages, Settings, and 70 more. Only
|
|
55
55
|
actions the Shortcuts editor offers: the Siri-only intents the app refuses to import are left out.
|
|
56
|
-
- Third-party app actions,
|
|
56
|
+
- Third-party app actions, named by app and intent.
|
|
57
57
|
- Checked conditions for 150 Find actions, the 14 built-in ones (Filter Files, Find Photos, Find
|
|
58
58
|
Contacts, ...) and 136 in Apple's apps (Find Messages, Find Notes, ...), from the engine's own
|
|
59
59
|
rules, and the properties of what they find.
|
|
@@ -114,20 +114,38 @@ found = s.action(actions.MESSAGES_FIND_MESSAGE,
|
|
|
114
114
|
s.action(actions.SHOWRESULT, Text=text(property_(found, "body")))
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
### Dictionaries and types
|
|
118
|
+
|
|
119
|
+
A dictionary parameter takes a plain dict: strings, `text()` and references are text items, and
|
|
120
|
+
numbers, booleans, lists and nested dicts keep their type, as the editor's item type menu sets it.
|
|
121
|
+
`coerce()` reads a value as one content type, as the variable "Type" menu does, and `property_()` on the
|
|
122
|
+
result takes that class's properties, the menu's "Get" list (`CONTENT_ITEM_PROPERTIES`):
|
|
123
|
+
`property_(coerce(file, "WFGenericFileContentItem"), "File Size")`, checked at run time. An output the catalogue types, by
|
|
124
|
+
class or by the object class it wraps (`CONTENT_ITEM_TYPES`), is checked the same way without a coercion:
|
|
125
|
+
`property_(asked_number, "Name")`. Where a choice decides the output, the literal you pass narrows it:
|
|
126
|
+
`s.action(actions.ASK, WFInputType="Number")` outputs a Number, so only Name is accepted.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from shortcutkit import coerce
|
|
130
|
+
|
|
131
|
+
d = s.action(actions.DICTIONARY, WFItems={"name": ref(name), "greeted": True, "tags": ["a", "b"]})
|
|
132
|
+
gid = s.if_(coerce(d, "WFStringContentItem"), "contains", "a")
|
|
133
|
+
s.end_if(gid)
|
|
134
|
+
```
|
|
135
|
+
|
|
117
136
|
### Other apps
|
|
118
137
|
|
|
119
|
-
|
|
138
|
+
Name the app and the intent: the identifier is the bundle identifier followed by the intent name, as
|
|
139
|
+
the catalogue identifies an app's own intents, and the descriptor is written as the catalogue's are.
|
|
140
|
+
The parameters are not checked: their keys are the intent's parameter names, from
|
|
141
|
+
`Metadata.appintents/extract.actionsdata` in the app bundle.
|
|
120
142
|
|
|
121
143
|
```python
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
},
|
|
128
|
-
title=text("Hello"),
|
|
129
|
-
body=ref(name),
|
|
130
|
-
)
|
|
144
|
+
from shortcutkit import app_intent
|
|
145
|
+
|
|
146
|
+
app = {"BundleIdentifier": "com.example.app", "Name": "Example", "TeamIdentifier": "ABCDE12345"}
|
|
147
|
+
note = s.action(app_intent(app, "CreateNoteIntent", "Note"), title=text("Hello"), body=ref(name))
|
|
148
|
+
s.action(actions.SHOWRESULT, Text=ref(note))
|
|
131
149
|
```
|
|
132
150
|
|
|
133
151
|
## API
|
|
@@ -149,8 +167,12 @@ s.action(
|
|
|
149
167
|
| `shortcut_input()`, `clipboard()`, `current_date()`, `ask()`, `device_details()`, `current_app()` | Special variables. |
|
|
150
168
|
| `where(*rows)`, `where_any(*rows)` | Conditions for a Find action's `WFContentItemFilter`. |
|
|
151
169
|
| `property_(source, name)` | One property of an action's output or a variable. |
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
170
|
+
| `coerce(source, class)` | A value read as one content type, the editor's "Type" menu (`CONTENT_ITEM_CLASSES`). |
|
|
171
|
+
| `dictionary(items)` | A dictionary parameter's items; `action()` encodes a plain dict itself. |
|
|
172
|
+
| `app_intent(app, intent, output=None)` | Another app's action, for `s.action()`. |
|
|
173
|
+
| `FILTERS`, `ENTITY_PROPERTIES`, `FILTER_OPERATORS`, `CALENDAR_UNITS`, `CONTENT_ITEM_CLASSES` | What each Find action can filter on, each entity type's properties, and the content types a coercion can name. |
|
|
174
|
+
| `actions`, `ACTIONS`, `get_action(id)` | Action identifiers and their catalog entries: parameters and kinds, output and input types, keywords, description, flags, and the rules under which the editor shows each parameter. |
|
|
175
|
+
| `accepted_by(type)`, `produced_by(type)` | Which actions take a content type as input or as an entity parameter, and which output it. |
|
|
154
176
|
| `PARAM_KINDS`, `PARAM_CHOICES`, `PARAM_VARIABLE_TYPES` | What each parameter accepts. |
|
|
155
177
|
| `CONDITION`, `ICON_COLORS`, `PROVENANCE` | If conditions, icon colors, and the build the data came from. |
|
|
156
178
|
|
|
@@ -30,9 +30,9 @@ are archived ([python-shortcuts](https://github.com/alexander-akhmetov/python-sh
|
|
|
30
30
|
[Cherri](https://github.com/electrikmilk/cherri) is a separate language with its own compiler.
|
|
31
31
|
|
|
32
32
|
- All 449 built-in actions as constants, so a typo is an `AttributeError`.
|
|
33
|
-
- 1,
|
|
33
|
+
- 1,265 actions from Apple's apps: Reminders, Notes, Messages, Settings, and 70 more. Only
|
|
34
34
|
actions the Shortcuts editor offers: the Siri-only intents the app refuses to import are left out.
|
|
35
|
-
- Third-party app actions,
|
|
35
|
+
- Third-party app actions, named by app and intent.
|
|
36
36
|
- Checked conditions for 150 Find actions, the 14 built-in ones (Filter Files, Find Photos, Find
|
|
37
37
|
Contacts, ...) and 136 in Apple's apps (Find Messages, Find Notes, ...), from the engine's own
|
|
38
38
|
rules, and the properties of what they find.
|
|
@@ -93,20 +93,38 @@ found = s.action(actions.MESSAGES_FIND_MESSAGE,
|
|
|
93
93
|
s.action(actions.SHOWRESULT, Text=text(property_(found, "body")))
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
### Dictionaries and types
|
|
97
|
+
|
|
98
|
+
A dictionary parameter takes a plain dict: strings, `text()` and references are text items, and
|
|
99
|
+
numbers, booleans, lists and nested dicts keep their type, as the editor's item type menu sets it.
|
|
100
|
+
`coerce()` reads a value as one content type, as the variable "Type" menu does, and `property_()` on the
|
|
101
|
+
result takes that class's properties, the menu's "Get" list (`CONTENT_ITEM_PROPERTIES`):
|
|
102
|
+
`property_(coerce(file, "WFGenericFileContentItem"), "File Size")`, checked at run time. An output the catalogue types, by
|
|
103
|
+
class or by the object class it wraps (`CONTENT_ITEM_TYPES`), is checked the same way without a coercion:
|
|
104
|
+
`property_(asked_number, "Name")`. Where a choice decides the output, the literal you pass narrows it:
|
|
105
|
+
`s.action(actions.ASK, WFInputType="Number")` outputs a Number, so only Name is accepted.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from shortcutkit import coerce
|
|
109
|
+
|
|
110
|
+
d = s.action(actions.DICTIONARY, WFItems={"name": ref(name), "greeted": True, "tags": ["a", "b"]})
|
|
111
|
+
gid = s.if_(coerce(d, "WFStringContentItem"), "contains", "a")
|
|
112
|
+
s.end_if(gid)
|
|
113
|
+
```
|
|
114
|
+
|
|
96
115
|
### Other apps
|
|
97
116
|
|
|
98
|
-
|
|
117
|
+
Name the app and the intent: the identifier is the bundle identifier followed by the intent name, as
|
|
118
|
+
the catalogue identifies an app's own intents, and the descriptor is written as the catalogue's are.
|
|
119
|
+
The parameters are not checked: their keys are the intent's parameter names, from
|
|
120
|
+
`Metadata.appintents/extract.actionsdata` in the app bundle.
|
|
99
121
|
|
|
100
122
|
```python
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
title=text("Hello"),
|
|
108
|
-
body=ref(name),
|
|
109
|
-
)
|
|
123
|
+
from shortcutkit import app_intent
|
|
124
|
+
|
|
125
|
+
app = {"BundleIdentifier": "com.example.app", "Name": "Example", "TeamIdentifier": "ABCDE12345"}
|
|
126
|
+
note = s.action(app_intent(app, "CreateNoteIntent", "Note"), title=text("Hello"), body=ref(name))
|
|
127
|
+
s.action(actions.SHOWRESULT, Text=ref(note))
|
|
110
128
|
```
|
|
111
129
|
|
|
112
130
|
## API
|
|
@@ -128,8 +146,12 @@ s.action(
|
|
|
128
146
|
| `shortcut_input()`, `clipboard()`, `current_date()`, `ask()`, `device_details()`, `current_app()` | Special variables. |
|
|
129
147
|
| `where(*rows)`, `where_any(*rows)` | Conditions for a Find action's `WFContentItemFilter`. |
|
|
130
148
|
| `property_(source, name)` | One property of an action's output or a variable. |
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
149
|
+
| `coerce(source, class)` | A value read as one content type, the editor's "Type" menu (`CONTENT_ITEM_CLASSES`). |
|
|
150
|
+
| `dictionary(items)` | A dictionary parameter's items; `action()` encodes a plain dict itself. |
|
|
151
|
+
| `app_intent(app, intent, output=None)` | Another app's action, for `s.action()`. |
|
|
152
|
+
| `FILTERS`, `ENTITY_PROPERTIES`, `FILTER_OPERATORS`, `CALENDAR_UNITS`, `CONTENT_ITEM_CLASSES` | What each Find action can filter on, each entity type's properties, and the content types a coercion can name. |
|
|
153
|
+
| `actions`, `ACTIONS`, `get_action(id)` | Action identifiers and their catalog entries: parameters and kinds, output and input types, keywords, description, flags, and the rules under which the editor shows each parameter. |
|
|
154
|
+
| `accepted_by(type)`, `produced_by(type)` | Which actions take a content type as input or as an entity parameter, and which output it. |
|
|
133
155
|
| `PARAM_KINDS`, `PARAM_CHOICES`, `PARAM_VARIABLE_TYPES` | What each parameter accepts. |
|
|
134
156
|
| `CONDITION`, `ICON_COLORS`, `PROVENANCE` | If conditions, icon colors, and the build the data came from. |
|
|
135
157
|
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shortcutkit"
|
|
7
|
-
version = "
|
|
8
|
-
description = "Build, validate, and sign Apple Shortcuts files from Python. All 449 built-in actions and 1,
|
|
7
|
+
version = "3.1.0"
|
|
8
|
+
description = "Build, validate, and sign Apple Shortcuts files from Python. All 449 built-in actions and 1,265 Apple app actions, extracted from the Shortcuts engine."
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = "MIT"
|
|
@@ -26,7 +26,10 @@ Value helpers return the exact structures WorkflowKit's state classes serialize:
|
|
|
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
|
+
coerce(x, "WFStringContentItem") x read as one content type (a WFCoercionVariableAggrandizement; CONTENT_ITEM_CLASSES)
|
|
30
|
+
property_(x, "body") one property of x (a WFPropertyVariableAggrandizement; ENTITY_PROPERTIES, CONTENT_ITEM_PROPERTIES)
|
|
31
|
+
dictionary({...}) WFDictionaryFieldValue -> a dictionary parameter's items, typed as the editor types them
|
|
32
|
+
app_intent(app, "CreateNoteIntent") another app's action, for action(): its identifier and descriptor
|
|
30
33
|
where(row, ...) Find conditions, encoded by action() for the Find action's entity
|
|
31
34
|
Plain str / int / float / bool / list / datetime are written as-is.
|
|
32
35
|
"""
|
|
@@ -186,18 +189,110 @@ def subject(attachment):
|
|
|
186
189
|
|
|
187
190
|
|
|
188
191
|
def property_(source, name):
|
|
189
|
-
"""One property of an action's output or of any reference, as the editor's property picker
|
|
190
|
-
it (a WFPropertyVariableAggrandizement): property_(found, "body"),
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
"""One property of an action's output or of any reference, as the editor's property picker (the "Get" list
|
|
193
|
+
under a variable's "Type") writes it (a WFPropertyVariableAggrandizement): property_(found, "body"),
|
|
194
|
+
property_(repeat_item(), "date"), property_(coerce(file, "WFGenericFileContentItem"), "File Size").
|
|
195
|
+
When the output's type is known, one of Apple's entity types (ENTITY_PROPERTIES) or a content item class
|
|
196
|
+
(CONTENT_ITEM_PROPERTIES: named directly, or as the object class the definition names, NSDecimalNumber being a
|
|
197
|
+
Number, through CONTENT_ITEM_TYPES; also what a coerce()d reference has), the name must be one of its properties."""
|
|
198
|
+
if _is_reference(source):
|
|
199
|
+
cls = _coerced_class(source)
|
|
200
|
+
types = [cls] if cls else []
|
|
201
|
+
else:
|
|
202
|
+
declared = (source["_outputClasses"] + [t for t in source.get("_outputTypes", []) if t in ENTITY_PROPERTIES]) if source.get("_outputClasses") else source.get("_outputTypes", [])
|
|
203
|
+
types = list(dict.fromkeys(t if t in ENTITY_PROPERTIES else _item_class_of(t) for t in declared))
|
|
204
|
+
types = [t for t in types if t]
|
|
205
|
+
known = {p for t in types for p in (ENTITY_PROPERTIES.get(t) or CONTENT_ITEM_PROPERTIES.get(t) or [])}
|
|
206
|
+
if types and name not in known:
|
|
207
|
+
raise ValueError(f"{' / '.join(types)} has no property {name}; it has {', '.join(sorted(known))}")
|
|
208
|
+
return _aggrandize(source, {"Type": "WFPropertyVariableAggrandizement", "PropertyName": name})
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _item_class_of(type_name):
|
|
212
|
+
"""An output type name as a content item class: itself when it is one, else the class that owns it (CONTENT_ITEM_TYPES)."""
|
|
213
|
+
return type_name if type_name in CONTENT_ITEM_PROPERTIES else CONTENT_ITEM_TYPES.get(type_name)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _coerced_class(attachment):
|
|
217
|
+
"""The content item class a reference was last coerced to, when it is one the engine lists; else None."""
|
|
218
|
+
ags = attachment["Value"].get("Aggrandizements")
|
|
219
|
+
last = ags[-1] if isinstance(ags, list) and ags else None
|
|
220
|
+
cls = last.get("CoercionItemClass") if isinstance(last, dict) and last.get("Type") == "WFCoercionVariableAggrandizement" else None
|
|
221
|
+
return cls if cls in CONTENT_ITEM_PROPERTIES else None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _aggrandize(source, aggrandizement):
|
|
225
|
+
"""`source` with one more aggrandizement; an action stands for its output."""
|
|
193
226
|
if not _is_reference(source):
|
|
194
|
-
entities = [t for t in source.get("_outputTypes", []) if t in ENTITY_PROPERTIES]
|
|
195
|
-
known = {p for e in entities for p in ENTITY_PROPERTIES[e]}
|
|
196
|
-
if entities and name not in known:
|
|
197
|
-
raise ValueError(f"{' / '.join(entities)} has no property {name}; it has {', '.join(sorted(known))}")
|
|
198
227
|
source = ref(source)
|
|
199
228
|
prior = source["Value"].get("Aggrandizements")
|
|
200
|
-
return _attachment({**source["Value"], "Aggrandizements": [*(prior if isinstance(prior, list) else []),
|
|
229
|
+
return _attachment({**source["Value"], "Aggrandizements": [*(prior if isinstance(prior, list) else []), aggrandizement]})
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def coerce(source, to):
|
|
233
|
+
"""`source` read as one content type, as the editor's variable "Type" menu writes it (a
|
|
234
|
+
WFCoercionVariableAggrandizement): coerce(ref(json), "WFDictionaryContentItem"), coerce(repeat_item(), "WFStringContentItem").
|
|
235
|
+
The classes are the engine's own list (CONTENT_ITEM_CLASSES, each with the name the editor shows). An If whose
|
|
236
|
+
subject's type the engine cannot tell (a dictionary value, a repeat item) needs one: without it "is" has nothing
|
|
237
|
+
to compare against and the run fails (a downstream user's observation; docs/extraction.md, run-time observations)."""
|
|
238
|
+
if to not in CONTENT_ITEM_CLASSES:
|
|
239
|
+
raise ValueError(f"no content item class {to}; the engine coerces to {', '.join(CONTENT_ITEM_CLASSES)}")
|
|
240
|
+
return _aggrandize(source, {"Type": "WFCoercionVariableAggrandizement", "CoercionItemClass": to})
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def dictionary(items):
|
|
244
|
+
"""A dictionary parameter: Dictionary's items, Get Contents of URL's headers, form and JSON bodies. Strings,
|
|
245
|
+
text() and references are text items; numbers, booleans, lists and nested dicts keep their type, as the editor's
|
|
246
|
+
item type menu sets it: dictionary({"apikey": variable("Key"), "count": 5, "nested": {"ok": True}, "tags": ["a", "b"]}).
|
|
247
|
+
action() encodes a plain dict given for a dictionary key itself, so the call is needed only where the key is untyped."""
|
|
248
|
+
return {"WFSerializationType": "WFDictionaryFieldValue",
|
|
249
|
+
"Value": {"WFDictionaryFieldValueItems": [{"WFKey": text(k), **_dictionary_item(v)} for k, v in items.items()]}}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _is_dictionary_items(v):
|
|
253
|
+
"""A plain dict of items for a dictionary parameter (not an encoded value)."""
|
|
254
|
+
return isinstance(v, dict) and "WFSerializationType" not in v and not (v.get("Type") == "Variable" and "Variable" in v)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _dictionary_item(v):
|
|
258
|
+
"""One item's type code and value, as the dictionary state class writes them back (docs/shortcut-file-format.md §3)."""
|
|
259
|
+
if isinstance(v, str):
|
|
260
|
+
return {"WFItemType": 0, "WFValue": text(v)}
|
|
261
|
+
if isinstance(v, dict) and v.get("WFSerializationType") == "WFTextTokenString":
|
|
262
|
+
return {"WFItemType": 0, "WFValue": v}
|
|
263
|
+
if isinstance(v, dict) and v.get("WFSerializationType") == "WFTextTokenAttachment":
|
|
264
|
+
return {"WFItemType": 0, "WFValue": text(v)}
|
|
265
|
+
if isinstance(v, bool):
|
|
266
|
+
return {"WFItemType": 4, "WFValue": {"WFSerializationType": "WFNumberSubstitutableState", "Value": v}}
|
|
267
|
+
if isinstance(v, (int, float)):
|
|
268
|
+
return {"WFItemType": 3, "WFValue": text(_number_text(v))}
|
|
269
|
+
# Array items carry no key; plain text is the bare string, as the engine writes it back.
|
|
270
|
+
if isinstance(v, (list, tuple)):
|
|
271
|
+
return {"WFItemType": 2, "WFValue": {"WFSerializationType": "WFArrayParameterState", "Value": [x if isinstance(x, str) else _dictionary_item(x) for x in v]}}
|
|
272
|
+
# A nested dictionary is wrapped once more than the outer one (the engine serializes it so, and reads the unwrapped form into that).
|
|
273
|
+
if isinstance(v, dict) and v.get("WFSerializationType") == "WFDictionaryFieldValue":
|
|
274
|
+
return {"WFItemType": 1, "WFValue": {"WFSerializationType": "WFDictionaryFieldValue", "Value": v}}
|
|
275
|
+
if _is_dictionary_items(v):
|
|
276
|
+
return {"WFItemType": 1, "WFValue": {"WFSerializationType": "WFDictionaryFieldValue", "Value": dictionary(v)}}
|
|
277
|
+
raise ValueError(f"dictionary(): a value must be a str, text(), a reference, a number, a bool, a list or a dict, got {v!r}")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _number_text(n):
|
|
281
|
+
"""A number as JavaScript's String() writes it, so both packages write the same item (5, not 5.0)."""
|
|
282
|
+
if isinstance(n, float) and n.is_integer():
|
|
283
|
+
return str(int(n))
|
|
284
|
+
return repr(n) if isinstance(n, float) else str(n)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def app_intent(app, intent, output=None):
|
|
288
|
+
"""Name an App Intents action of an app the catalogue does not cover, for action():
|
|
289
|
+
s.action(app_intent(app, "CreateNoteIntent", "Note"), title=text("Hi")). `app` holds BundleIdentifier, Name and
|
|
290
|
+
TeamIdentifier (the bundle's Info.plist and `codesign -dv`); the identifier is the bundle identifier followed by the
|
|
291
|
+
intent's name, as the catalogue identifies an app's own intents, and the descriptor is written as the catalogue's are
|
|
292
|
+
(an intent hosted in a framework or extension has another identifier, which action() still takes by hand with an
|
|
293
|
+
AppIntentDescriptor). `output` names the result for ref() ("Output" when left out). The parameters are not checked: their keys are the intent's parameter names, as
|
|
294
|
+
Metadata.appintents/extract.actionsdata in the app bundle lists them."""
|
|
295
|
+
return {"identifier": f"{app['BundleIdentifier']}.{intent}", "descriptor": {**app, "AppIntentIdentifier": intent}, "output": output}
|
|
201
296
|
|
|
202
297
|
|
|
203
298
|
def where(*rows):
|
|
@@ -322,7 +417,6 @@ def _utc(d):
|
|
|
322
417
|
|
|
323
418
|
def picker(attachment):
|
|
324
419
|
"""Deprecated, misnamed: this is the If-subject form. Pass a bare reference to picker parameters."""
|
|
325
|
-
"""Wrap an attachment for WFVariablePickerParameter keys (e.g. WFInput on Repeat with Each)."""
|
|
326
420
|
return {"Type": "Variable", "Variable": attachment}
|
|
327
421
|
|
|
328
422
|
|
|
@@ -360,14 +454,17 @@ class Shortcut:
|
|
|
360
454
|
def action(self, identifier, /, **params):
|
|
361
455
|
"""Append an action and return it; ref() it to use its output. Catalog identifiers are
|
|
362
456
|
checked: unknown keys, wrong value kinds, and unreadable references raise ValueError. Other
|
|
363
|
-
identifiers pass through unchecked
|
|
364
|
-
have that name."""
|
|
457
|
+
identifiers pass through unchecked; app_intent() names another app's action with its descriptor.
|
|
458
|
+
`identifier` is positional-only, so a parameter can have that name."""
|
|
459
|
+
intent = identifier if isinstance(identifier, dict) else None
|
|
460
|
+
if intent:
|
|
461
|
+
identifier = intent["identifier"]
|
|
365
462
|
definition = self.defs.get(identifier)
|
|
366
463
|
catalog = ACTIONS.get(identifier) or {}
|
|
367
|
-
descriptor = catalog.get("descriptor")
|
|
464
|
+
descriptor = catalog.get("descriptor") or (intent or {}).get("descriptor")
|
|
368
465
|
if self.defs and definition is None and not catalog and identifier.startswith("is.workflow."):
|
|
369
466
|
raise ValueError(f"unknown built-in action {identifier}")
|
|
370
|
-
if definition or descriptor:
|
|
467
|
+
if definition or catalog.get("descriptor"):
|
|
371
468
|
known = {p.get("Key") for p in (definition or {}).get("Parameters", []) if isinstance(p, dict)}
|
|
372
469
|
known |= {"UUID", "GroupingIdentifier", "WFControlFlowMode", "CustomOutputName"}
|
|
373
470
|
known |= LEGACY_KEYS.get(identifier, set())
|
|
@@ -383,7 +480,9 @@ class Shortcut:
|
|
|
383
480
|
kinds = PARAM_KINDS.get(identifier, {})
|
|
384
481
|
restricted = PARAM_VARIABLE_TYPES.get(identifier, {})
|
|
385
482
|
for key, value in params.items():
|
|
386
|
-
|
|
483
|
+
# where() rows belong to a Find action's filter key; an untyped action (another app's Find) is refused by
|
|
484
|
+
# _encode_filter(), which names the missing FILTERS entry.
|
|
485
|
+
if _is_where(value) and key in kinds and kinds[key] != "filter":
|
|
387
486
|
raise ValueError(f"{identifier}.{key}: where() rows go only in a Find action's WFContentItemFilter")
|
|
388
487
|
problem = _check_kind(kinds.get(key, "any"), value)
|
|
389
488
|
if problem:
|
|
@@ -397,6 +496,7 @@ class Shortcut:
|
|
|
397
496
|
# token string with one placeholder. Wrap it so ref() works for text keys too.
|
|
398
497
|
params = {k: (text(v) if kinds.get(k) == "text" and _is_reference(v) and v.get("WFSerializationType") == "WFTextTokenAttachment"
|
|
399
498
|
else subject(v) if kinds.get(k) == "subject" and _is_reference(v) and v.get("WFSerializationType") == "WFTextTokenAttachment"
|
|
499
|
+
else dictionary(v) if kinds.get(k) == "dictionary" and _is_dictionary_items(v)
|
|
400
500
|
else _encode_filter(identifier, v) if _is_where(v) else v)
|
|
401
501
|
for k, v in params.items()}
|
|
402
502
|
# App Intents actions carry a descriptor naming the app that provides them.
|
|
@@ -406,8 +506,17 @@ class Shortcut:
|
|
|
406
506
|
entry = {"WFWorkflowActionIdentifier": identifier,
|
|
407
507
|
"WFWorkflowActionParameters": {**head, **params}}
|
|
408
508
|
output = (definition or {}).get("Output", {})
|
|
409
|
-
entry["_outputName"] = (output.get("OutputName") if isinstance(output, dict) else None) or catalog.get("output")
|
|
509
|
+
entry["_outputName"] = (output.get("OutputName") if isinstance(output, dict) else None) or catalog.get("output") or (intent or {}).get("output")
|
|
410
510
|
entry["_outputTypes"] = list(catalog.get("outputTypes") or (output.get("Types") if isinstance(output, dict) else []) or [])
|
|
511
|
+
# The engine's answer to what comes out: for a choice that decides it (OUTPUT_CLASSES_BY), the literal given selects
|
|
512
|
+
# its classes and a reference means any of them; otherwise the classes for the default parameters.
|
|
513
|
+
by = OUTPUT_CLASSES_BY.get(identifier) if isinstance(identifier, str) else None
|
|
514
|
+
deciding = next((k for k in by if k in params), None) if by else None
|
|
515
|
+
if deciding:
|
|
516
|
+
v = params[deciding]
|
|
517
|
+
entry["_outputClasses"] = list(by[deciding][v]) if isinstance(v, str) and v in by[deciding] else sorted({c for cs in by[deciding].values() for c in cs})
|
|
518
|
+
elif catalog.get("outputClasses"):
|
|
519
|
+
entry["_outputClasses"] = list(catalog["outputClasses"])
|
|
411
520
|
if isinstance(entry["_outputName"], dict):
|
|
412
521
|
entry["_outputName"] = entry["_outputName"].get("format")
|
|
413
522
|
self.actions.append(entry)
|
|
@@ -523,7 +632,11 @@ def get_action(identifier, definitions=DEFINITIONS):
|
|
|
523
632
|
if entry is None:
|
|
524
633
|
return None
|
|
525
634
|
out = {"identifier": identifier, "name": entry["name"], "params": list(entry["params"]),
|
|
526
|
-
"kinds": dict(PARAM_KINDS.get(identifier, {})), "output": entry.get("output"), "outputTypes": list(entry.get("outputTypes", []))
|
|
635
|
+
"kinds": dict(PARAM_KINDS.get(identifier, {})), "output": entry.get("output"), "outputTypes": list(entry.get("outputTypes", [])),
|
|
636
|
+
"inputTypes": list(entry.get("inputTypes", [])), "keywords": list(entry.get("keywords", []))}
|
|
637
|
+
for k in ("outputClasses", "inputKey", "inputRequired", "description", "descriptionNote", "descriptionResult", "discontinued", "unavailable", "requiresUnlock", "disabledOn", "requires"):
|
|
638
|
+
if entry.get(k):
|
|
639
|
+
out[k] = copy.deepcopy(entry[k])
|
|
527
640
|
if entry.get("descriptor"):
|
|
528
641
|
out["descriptor"] = entry["descriptor"]
|
|
529
642
|
defs = _load_definitions(definitions) if definitions else {}
|
|
@@ -532,6 +645,20 @@ def get_action(identifier, definitions=DEFINITIONS):
|
|
|
532
645
|
return out
|
|
533
646
|
|
|
534
647
|
|
|
648
|
+
def accepted_by(type_name):
|
|
649
|
+
"""Where a value of one content type can go: every built-in whose input takes it (with the key the input
|
|
650
|
+
lands in) and every App Intents parameter typed as that entity, as (identifier, key) pairs. Declared types
|
|
651
|
+
only; Shortcuts converts between many types at run time, so this is what the editor would list."""
|
|
652
|
+
out = [(i, e.get("inputKey") or "WFInput") for i, e in ACTIONS.items() if type_name in e.get("inputTypes", [])]
|
|
653
|
+
out += [(i, k) for i, keys in PARAM_ENTITY_TYPES.items() for k, t in keys.items() if t == type_name]
|
|
654
|
+
return out
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def produced_by(type_name):
|
|
658
|
+
"""Every action whose output is declared as this content type (ACTIONS[id]["outputTypes"])."""
|
|
659
|
+
return [i for i, e in ACTIONS.items() if type_name in e.get("outputTypes", []) or type_name in e.get("outputClasses", [])]
|
|
660
|
+
|
|
661
|
+
|
|
535
662
|
def demo(out):
|
|
536
663
|
out = pathlib.Path(out)
|
|
537
664
|
s = Shortcut(out.stem, color="Teal")
|
|
@@ -550,13 +677,13 @@ def demo(out):
|
|
|
550
677
|
|
|
551
678
|
|
|
552
679
|
from . import actions # noqa: E402 (generated identifier constants and ACTIONS metadata)
|
|
553
|
-
from .actions import ACTIONS, PARAM_KINDS, PARAM_CHOICES, PARAM_VARIABLE_TYPES # noqa: E402
|
|
554
|
-
from .entities import FILTERS, FILTER_OPERATORS, CALENDAR_UNITS, BYTE_COUNT_UNITS, BOUNDED_FILTERS, ENTITY_PROPERTIES # noqa: E402 (generated Find filters and
|
|
680
|
+
from .actions import ACTIONS, PARAM_KINDS, PARAM_CHOICES, PARAM_VARIABLE_TYPES, PARAM_ENTITY_TYPES, OUTPUT_CLASSES_BY # noqa: E402
|
|
681
|
+
from .entities import FILTERS, FILTER_OPERATORS, CALENDAR_UNITS, BYTE_COUNT_UNITS, BOUNDED_FILTERS, ENTITY_PROPERTIES, CONTENT_ITEM_CLASSES, CONTENT_ITEM_PROPERTIES, CONTENT_ITEM_TYPES # noqa: E402 (generated Find filters, entity properties, content item classes, their properties and owned types)
|
|
555
682
|
|
|
556
683
|
PROVENANCE = json.load(open(HERE / "data" / "provenance.json")) if (HERE / "data" / "provenance.json").exists() else {}
|
|
557
684
|
"""Which macOS and Shortcuts build the bundled data was extracted from."""
|
|
558
685
|
|
|
559
|
-
__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",
|
|
560
|
-
"property_", "where", "where_any", "FILTERS", "FILTER_OPERATORS", "CALENDAR_UNITS", "BYTE_COUNT_UNITS", "BOUNDED_FILTERS", "ENTITY_PROPERTIES",
|
|
686
|
+
__all__ = ["actions", "ACTIONS", "PARAM_KINDS", "PARAM_CHOICES", "PARAM_VARIABLE_TYPES", "PROVENANCE", "get_action", "accepted_by", "produced_by", "PARAM_ENTITY_TYPES", "OUTPUT_CLASSES_BY", "repeat_item", "repeat_index", "subject", "Shortcut", "ref", "variable", "shortcut_input", "clipboard", "current_date", "ask", "device_details", "current_app", "picker", "text",
|
|
687
|
+
"property_", "coerce", "dictionary", "app_intent", "where", "where_any", "FILTERS", "FILTER_OPERATORS", "CALENDAR_UNITS", "BYTE_COUNT_UNITS", "BOUNDED_FILTERS", "ENTITY_PROPERTIES", "CONTENT_ITEM_CLASSES", "CONTENT_ITEM_PROPERTIES", "CONTENT_ITEM_TYPES",
|
|
561
688
|
"ICON_COLORS", "CONDITION", "DEFAULT_GLYPH", "LEGACY_KEYS", "demo"]
|
|
562
|
-
__version__ = "
|
|
689
|
+
__version__ = "3.1.0"
|