shortcutkit 3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shortcutkit
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
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
@@ -118,7 +118,12 @@ s.action(actions.SHOWRESULT, Text=text(property_(found, "body")))
118
118
 
119
119
  A dictionary parameter takes a plain dict: strings, `text()` and references are text items, and
120
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.
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.
122
127
 
123
128
  ```python
124
129
  from shortcutkit import coerce
@@ -162,11 +167,12 @@ s.action(actions.SHOWRESULT, Text=ref(note))
162
167
  | `shortcut_input()`, `clipboard()`, `current_date()`, `ask()`, `device_details()`, `current_app()` | Special variables. |
163
168
  | `where(*rows)`, `where_any(*rows)` | Conditions for a Find action's `WFContentItemFilter`. |
164
169
  | `property_(source, name)` | One property of an action's output or a variable. |
165
- | `coerce(source, class)` | A value read as one content type (`CONTENT_ITEM_CLASSES`). |
170
+ | `coerce(source, class)` | A value read as one content type, the editor's "Type" menu (`CONTENT_ITEM_CLASSES`). |
166
171
  | `dictionary(items)` | A dictionary parameter's items; `action()` encodes a plain dict itself. |
167
172
  | `app_intent(app, intent, output=None)` | Another app's action, for `s.action()`. |
168
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. |
169
- | `actions`, `ACTIONS`, `get_action(id)` | Action identifiers and their catalog entries. |
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. |
170
176
  | `PARAM_KINDS`, `PARAM_CHOICES`, `PARAM_VARIABLE_TYPES` | What each parameter accepts. |
171
177
  | `CONDITION`, `ICON_COLORS`, `PROVENANCE` | If conditions, icon colors, and the build the data came from. |
172
178
 
@@ -97,7 +97,12 @@ s.action(actions.SHOWRESULT, Text=text(property_(found, "body")))
97
97
 
98
98
  A dictionary parameter takes a plain dict: strings, `text()` and references are text items, and
99
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.
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.
101
106
 
102
107
  ```python
103
108
  from shortcutkit import coerce
@@ -141,11 +146,12 @@ s.action(actions.SHOWRESULT, Text=ref(note))
141
146
  | `shortcut_input()`, `clipboard()`, `current_date()`, `ask()`, `device_details()`, `current_app()` | Special variables. |
142
147
  | `where(*rows)`, `where_any(*rows)` | Conditions for a Find action's `WFContentItemFilter`. |
143
148
  | `property_(source, name)` | One property of an action's output or a variable. |
144
- | `coerce(source, class)` | A value read as one content type (`CONTENT_ITEM_CLASSES`). |
149
+ | `coerce(source, class)` | A value read as one content type, the editor's "Type" menu (`CONTENT_ITEM_CLASSES`). |
145
150
  | `dictionary(items)` | A dictionary parameter's items; `action()` encodes a plain dict itself. |
146
151
  | `app_intent(app, intent, output=None)` | Another app's action, for `s.action()`. |
147
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. |
148
- | `actions`, `ACTIONS`, `get_action(id)` | Action identifiers and their catalog entries. |
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. |
149
155
  | `PARAM_KINDS`, `PARAM_CHOICES`, `PARAM_VARIABLE_TYPES` | What each parameter accepts. |
150
156
  | `CONDITION`, `ICON_COLORS`, `PROVENANCE` | If conditions, icon colors, and the build the data came from. |
151
157
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "shortcutkit"
7
- version = "3.0.0"
7
+ version = "3.1.0"
8
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"
@@ -26,8 +26,8 @@ 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
- property_(x, "body") a reference to one property of x (a WFPropertyVariableAggrandizement)
30
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
31
  dictionary({...}) WFDictionaryFieldValue -> a dictionary parameter's items, typed as the editor types them
32
32
  app_intent(app, "CreateNoteIntent") another app's action, for action(): its identifier and descriptor
33
33
  where(row, ...) Find conditions, encoded by action() for the Find action's entity
@@ -189,19 +189,38 @@ def subject(attachment):
189
189
 
190
190
 
191
191
  def property_(source, name):
192
- """One property of an action's output or of any reference, as the editor's property picker writes
193
- it (a WFPropertyVariableAggrandizement): property_(found, "body"), property_(repeat_item(), "date").
194
- Given an action whose output is one of Apple's entity types, the name must be one of its
195
- properties (ENTITY_PROPERTIES)."""
196
- if not _is_reference(source):
197
- entities = [t for t in source.get("_outputTypes", []) if t in ENTITY_PROPERTIES]
198
- known = {p for e in entities for p in ENTITY_PROPERTIES[e]}
199
- if entities and name not in known:
200
- raise ValueError(f"{' / '.join(entities)} has no property {name}; it has {', '.join(sorted(known))}")
201
- source = ref(source)
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))}")
202
208
  return _aggrandize(source, {"Type": "WFPropertyVariableAggrandizement", "PropertyName": name})
203
209
 
204
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
+
205
224
  def _aggrandize(source, aggrandizement):
206
225
  """`source` with one more aggrandizement; an action stands for its output."""
207
226
  if not _is_reference(source):
@@ -489,6 +508,15 @@ class Shortcut:
489
508
  output = (definition or {}).get("Output", {})
490
509
  entry["_outputName"] = (output.get("OutputName") if isinstance(output, dict) else None) or catalog.get("output") or (intent or {}).get("output")
491
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"])
492
520
  if isinstance(entry["_outputName"], dict):
493
521
  entry["_outputName"] = entry["_outputName"].get("format")
494
522
  self.actions.append(entry)
@@ -604,7 +632,11 @@ def get_action(identifier, definitions=DEFINITIONS):
604
632
  if entry is None:
605
633
  return None
606
634
  out = {"identifier": identifier, "name": entry["name"], "params": list(entry["params"]),
607
- "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])
608
640
  if entry.get("descriptor"):
609
641
  out["descriptor"] = entry["descriptor"]
610
642
  defs = _load_definitions(definitions) if definitions else {}
@@ -613,6 +645,20 @@ def get_action(identifier, definitions=DEFINITIONS):
613
645
  return out
614
646
 
615
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
+
616
662
  def demo(out):
617
663
  out = pathlib.Path(out)
618
664
  s = Shortcut(out.stem, color="Teal")
@@ -631,13 +677,13 @@ def demo(out):
631
677
 
632
678
 
633
679
  from . import actions # noqa: E402 (generated identifier constants and ACTIONS metadata)
634
- from .actions import ACTIONS, PARAM_KINDS, PARAM_CHOICES, PARAM_VARIABLE_TYPES # noqa: E402
635
- from .entities import FILTERS, FILTER_OPERATORS, CALENDAR_UNITS, BYTE_COUNT_UNITS, BOUNDED_FILTERS, ENTITY_PROPERTIES, CONTENT_ITEM_CLASSES # noqa: E402 (generated Find filters, entity properties and content item classes)
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)
636
682
 
637
683
  PROVENANCE = json.load(open(HERE / "data" / "provenance.json")) if (HERE / "data" / "provenance.json").exists() else {}
638
684
  """Which macOS and Shortcuts build the bundled data was extracted from."""
639
685
 
640
- __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",
641
- "property_", "coerce", "dictionary", "app_intent", "where", "where_any", "FILTERS", "FILTER_OPERATORS", "CALENDAR_UNITS", "BYTE_COUNT_UNITS", "BOUNDED_FILTERS", "ENTITY_PROPERTIES", "CONTENT_ITEM_CLASSES",
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",
642
688
  "ICON_COLORS", "CONDITION", "DEFAULT_GLYPH", "LEGACY_KEYS", "demo"]
643
- __version__ = "3.0.0"
689
+ __version__ = "3.1.0"