alpyki 0.2.0__py3-none-any.whl
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.
- alpyki/__init__.py +3 -0
- alpyki/cli.py +671 -0
- alpyki/model.py +351 -0
- alpyki/template/.gitea/workflows/build_and_release.yaml +75 -0
- alpyki/template/.github/workflows/build_and_release.yaml +45 -0
- alpyki/template/.gitignore +205 -0
- alpyki/template/.gitlab-ci.yml +81 -0
- alpyki/template/LICENSE +21 -0
- alpyki/template/Workflow/workflow.py +23 -0
- alpyki/template/requirements.txt +1 -0
- alpyki/workflow.py +86 -0
- alpyki-0.2.0.dist-info/METADATA +145 -0
- alpyki-0.2.0.dist-info/RECORD +17 -0
- alpyki-0.2.0.dist-info/WHEEL +5 -0
- alpyki-0.2.0.dist-info/entry_points.txt +2 -0
- alpyki-0.2.0.dist-info/licenses/LICENSE +17 -0
- alpyki-0.2.0.dist-info/top_level.txt +1 -0
alpyki/__init__.py
ADDED
alpyki/cli.py
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import datetime
|
|
3
|
+
import logging
|
|
4
|
+
import plistlib
|
|
5
|
+
import re
|
|
6
|
+
import stat
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable, Optional
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
from zipfile import ZIP_DEFLATED, ZipFile
|
|
13
|
+
|
|
14
|
+
from jinja2 import Environment, PackageLoader
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _info_plist_path() -> Path:
|
|
18
|
+
wf_dir = Path.cwd() / "Workflow"
|
|
19
|
+
info_plist_path = wf_dir / "info.plist"
|
|
20
|
+
return info_plist_path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure_no_local_changes(
|
|
24
|
+
fn: Callable[[argparse.Namespace], None],
|
|
25
|
+
) -> Callable[[argparse.Namespace], None]:
|
|
26
|
+
"""Ensure that there are no changes to the local directory"""
|
|
27
|
+
|
|
28
|
+
def decorator(args: argparse.Namespace) -> None:
|
|
29
|
+
git_status_command = ["git", "status", "-s"]
|
|
30
|
+
if subprocess.check_output(git_status_command).decode("utf-8").strip():
|
|
31
|
+
logging.critical("Local changes detected. Ensure git history has all changes committed.")
|
|
32
|
+
exit(1)
|
|
33
|
+
|
|
34
|
+
fn(args)
|
|
35
|
+
|
|
36
|
+
return decorator
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _must_be_run_from_workflow_project_root(
|
|
40
|
+
fn: Callable[[argparse.Namespace], None],
|
|
41
|
+
) -> Callable[[argparse.Namespace], None]:
|
|
42
|
+
"""Validates that the command is run from a directory that contains a workflow"""
|
|
43
|
+
|
|
44
|
+
def decorator(args: argparse.Namespace):
|
|
45
|
+
if not _info_plist_path().exists():
|
|
46
|
+
logging.critical("Cannot find workflow. You need to run this command from the root of the project")
|
|
47
|
+
exit(1)
|
|
48
|
+
|
|
49
|
+
fn(args)
|
|
50
|
+
|
|
51
|
+
return decorator
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_sync_directory() -> Optional[Path]:
|
|
55
|
+
"""
|
|
56
|
+
:return: The path to Alfred's sync directory
|
|
57
|
+
"""
|
|
58
|
+
prefs_path = Path.home() / "Library" / "Preferences" / "com.runningwithcrayons.Alfred-Preferences.plist"
|
|
59
|
+
|
|
60
|
+
if not prefs_path.exists():
|
|
61
|
+
raise ValueError("Alfred doesn't appear to be installed")
|
|
62
|
+
|
|
63
|
+
with prefs_path.open("rb") as f:
|
|
64
|
+
pl = plistlib.load(f)
|
|
65
|
+
|
|
66
|
+
if "syncfolder" not in pl:
|
|
67
|
+
logging.debug("Alfred's synchronisation directory not set")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
sync_dir = Path(pl["syncfolder"]).expanduser()
|
|
71
|
+
|
|
72
|
+
if not sync_dir.exists():
|
|
73
|
+
raise OSError("Cannot find workflow directory")
|
|
74
|
+
|
|
75
|
+
return sync_dir.expanduser()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_workflows_directory() -> Path:
|
|
79
|
+
"""
|
|
80
|
+
Get the directory where Alfred stores workflows
|
|
81
|
+
|
|
82
|
+
Finds the Alfred.alfredpreferences dir either in the sync location set in the Alfred settings or in the default
|
|
83
|
+
location.
|
|
84
|
+
|
|
85
|
+
:return: The path to the directory with Alfred's workflows
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
sync_dir = _get_sync_directory()
|
|
89
|
+
prefs_dir = sync_dir or Path.home() / "Library" / "Application Support" / "Alfred/"
|
|
90
|
+
|
|
91
|
+
return prefs_dir / "Alfred.alfredpreferences" / "workflows"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _make_plist(
|
|
95
|
+
name: str,
|
|
96
|
+
keyword: str,
|
|
97
|
+
bundle_id: str,
|
|
98
|
+
author: Optional[str],
|
|
99
|
+
website: Optional[str],
|
|
100
|
+
description: Optional[str],
|
|
101
|
+
) -> dict:
|
|
102
|
+
"""
|
|
103
|
+
Create a dictionary representation of the info.plist file describing the workflow
|
|
104
|
+
|
|
105
|
+
:param name:
|
|
106
|
+
The name of the workflow
|
|
107
|
+
:param keyword:
|
|
108
|
+
The keyword to trigger the workflow
|
|
109
|
+
:param bundle_id:
|
|
110
|
+
The bundle ID to identify the workflow. Should use reverse-DNS notation
|
|
111
|
+
:param author:
|
|
112
|
+
The name of the author
|
|
113
|
+
:param website:
|
|
114
|
+
The website of the workflow
|
|
115
|
+
:param description:
|
|
116
|
+
The description of the workflow. Will be shown to the user when importing
|
|
117
|
+
:return: a dictionary representation of the info.plist file
|
|
118
|
+
"""
|
|
119
|
+
script_uuid = str(uuid4())
|
|
120
|
+
clipboard_uuid = str(uuid4())
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"name": name,
|
|
124
|
+
"description": description or "",
|
|
125
|
+
"bundleid": bundle_id,
|
|
126
|
+
"createdby": author or "",
|
|
127
|
+
"connections": {script_uuid: [{"destinationuid": clipboard_uuid}]},
|
|
128
|
+
"uidata": [],
|
|
129
|
+
# Environment variables
|
|
130
|
+
# Add the vendored directory to the PYTHONPATH so that we're also searching there for dependencies
|
|
131
|
+
"variables": {"PYTHONPATH": ".:vendored"},
|
|
132
|
+
# The workflow version
|
|
133
|
+
"version": "0.0.0",
|
|
134
|
+
# The contact website
|
|
135
|
+
"webaddress": website or "",
|
|
136
|
+
"objects": [
|
|
137
|
+
{
|
|
138
|
+
"uid": clipboard_uuid,
|
|
139
|
+
"type": "alfred.workflow.output.clipboard",
|
|
140
|
+
"config": {"clipboardtext": "{query}"},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"uid": script_uuid,
|
|
144
|
+
"type": "alfred.workflow.input.scriptfilter",
|
|
145
|
+
"config": {
|
|
146
|
+
"keyword": keyword,
|
|
147
|
+
"scriptfile": "workflow.py",
|
|
148
|
+
# Keyword should be followed by whitespace
|
|
149
|
+
"withspace": True,
|
|
150
|
+
# Argument optional
|
|
151
|
+
"argumenttype": 1,
|
|
152
|
+
# Placeholder title
|
|
153
|
+
"title": "Search",
|
|
154
|
+
# "Please wait" subtext
|
|
155
|
+
"runningsubtext": "Loading...",
|
|
156
|
+
# External script
|
|
157
|
+
"type": 8,
|
|
158
|
+
# Terminate previous script
|
|
159
|
+
"queuemode": 2,
|
|
160
|
+
# Always run immediately for first typed character
|
|
161
|
+
"queuedelayimmediatelyinitially": True,
|
|
162
|
+
# Don't set argv when empty
|
|
163
|
+
"argumenttreatemptyqueryasnil": True,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _zip_dir(directory: Path, output_file: Path):
|
|
171
|
+
"""
|
|
172
|
+
Zip the contents of the provided directory recursively
|
|
173
|
+
|
|
174
|
+
:param directory: The directory to compress
|
|
175
|
+
:param output_file: The target file
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
with ZipFile(output_file, "w", ZIP_DEFLATED) as zip_file:
|
|
179
|
+
for entry in directory.rglob("**/*"):
|
|
180
|
+
if entry.is_file():
|
|
181
|
+
logging.debug("Adding to package: %s", entry)
|
|
182
|
+
zip_file.write(entry, entry.relative_to(directory))
|
|
183
|
+
logging.info("Produced package at %s", output_file)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def find_workflow_link(target: Path) -> Optional[Path]:
|
|
187
|
+
"""
|
|
188
|
+
Finds a link to the workflow in Alfred's workflows directory
|
|
189
|
+
|
|
190
|
+
:param target: The path to the workflow we're looking for
|
|
191
|
+
:return: The path if found; `None` otherwise
|
|
192
|
+
"""
|
|
193
|
+
target = target.expanduser()
|
|
194
|
+
workflows = _get_workflows_directory()
|
|
195
|
+
|
|
196
|
+
for wf in workflows.iterdir():
|
|
197
|
+
if wf.is_symlink() and wf.readlink().expanduser() == target:
|
|
198
|
+
return wf
|
|
199
|
+
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def new(args: argparse.Namespace):
|
|
204
|
+
"""
|
|
205
|
+
Entry point for the `new` command. Creates a new Alfred workflow.
|
|
206
|
+
|
|
207
|
+
This creates a directory of the name in the `name` argument and links it into Alfred's workflows directory. The
|
|
208
|
+
workflow shows in the Alfred Preferences app and can still be easily edited with an external editor.
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
usage: alpyki new [-h] -k KEYWORD -b BUNDLE_ID --author AUTHOR
|
|
212
|
+
[--website WEBSITE] [--description DESCRIPTION]
|
|
213
|
+
[--git | --no-git] [--link | --no-link]
|
|
214
|
+
[--vendor | --no-vendor] [--actions {github,gitea,gitlab,none}]
|
|
215
|
+
name
|
|
216
|
+
|
|
217
|
+
positional arguments:
|
|
218
|
+
name Name of the new workflow
|
|
219
|
+
|
|
220
|
+
options:
|
|
221
|
+
-h, --help show this help message and exit
|
|
222
|
+
-k KEYWORD, --keyword KEYWORD
|
|
223
|
+
The keyword to trigger the workflow
|
|
224
|
+
-b BUNDLE_ID, --bundle-id BUNDLE_ID
|
|
225
|
+
The bundle identifier, usually in reverse DNS notation
|
|
226
|
+
--author AUTHOR Name of the author
|
|
227
|
+
--website WEBSITE The workflow website
|
|
228
|
+
--description DESCRIPTION
|
|
229
|
+
A description for the workflow
|
|
230
|
+
--git, --no-git Whether to create a git repository
|
|
231
|
+
--link, --no-link Create a symbolic link to this workflow
|
|
232
|
+
--vendor, --no-vendor
|
|
233
|
+
Install workflow dependencies
|
|
234
|
+
--actions, --ci CI platform for release workflow: github (default), gitea, gitlab, or none
|
|
235
|
+
```
|
|
236
|
+
""" # noqa: E501
|
|
237
|
+
name = args.name
|
|
238
|
+
logging.info("Creating new workflow: %s", name)
|
|
239
|
+
|
|
240
|
+
root_dir = Path.cwd().joinpath(name)
|
|
241
|
+
wf_dir = root_dir.joinpath("Workflow")
|
|
242
|
+
|
|
243
|
+
context = {
|
|
244
|
+
"year": datetime.datetime.now().year,
|
|
245
|
+
"system_python_version": subprocess.check_output(
|
|
246
|
+
["/usr/bin/python3", "-c", "print(__import__('platform').python_version())"]
|
|
247
|
+
)
|
|
248
|
+
.decode("utf-8")
|
|
249
|
+
.strip(),
|
|
250
|
+
**vars(args),
|
|
251
|
+
}
|
|
252
|
+
try:
|
|
253
|
+
logging.debug("Copying template")
|
|
254
|
+
env = Environment(loader=PackageLoader("alpyki", "template"))
|
|
255
|
+
logging.debug("Generating templates from %s to %s", env.list_templates(), root_dir)
|
|
256
|
+
|
|
257
|
+
def filter_actions(path: str) -> bool:
|
|
258
|
+
if ".github" not in path and ".gitea" not in path and ".gitlab" not in path:
|
|
259
|
+
return True
|
|
260
|
+
if args.actions == "none":
|
|
261
|
+
return False
|
|
262
|
+
if args.actions == "gitlab":
|
|
263
|
+
return ".gitlab" in path
|
|
264
|
+
return f".{args.actions}" in path
|
|
265
|
+
|
|
266
|
+
for t in [t for t in env.list_templates(filter_func=filter_actions) if "__pycache__" not in t]:
|
|
267
|
+
tmp = env.get_template(t)
|
|
268
|
+
outfile = root_dir.joinpath(t)
|
|
269
|
+
outfile.parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
with open(outfile, "w") as fd:
|
|
271
|
+
fd.write(tmp.render(context))
|
|
272
|
+
except OSError as e:
|
|
273
|
+
logging.error("Cannot create workflow: %s", e)
|
|
274
|
+
exit(1)
|
|
275
|
+
|
|
276
|
+
wf_file_path = wf_dir.joinpath("workflow.py")
|
|
277
|
+
|
|
278
|
+
logging.debug("Adding +x permission to workflow")
|
|
279
|
+
wf_file_path.chmod(wf_file_path.stat().st_mode | stat.S_IEXEC)
|
|
280
|
+
|
|
281
|
+
if args.git:
|
|
282
|
+
logging.debug("Initialising git repository")
|
|
283
|
+
if subprocess.call(["git", "init", name]) != 0:
|
|
284
|
+
logging.warning("Failed to create git repository. Ignoring.")
|
|
285
|
+
|
|
286
|
+
logging.debug("Creating info.plist")
|
|
287
|
+
with wf_dir.joinpath("info.plist").open(mode="xb") as f:
|
|
288
|
+
plistlib.dump(
|
|
289
|
+
_make_plist(
|
|
290
|
+
name=name,
|
|
291
|
+
keyword=args.keyword,
|
|
292
|
+
bundle_id=args.bundle_id,
|
|
293
|
+
author=args.author,
|
|
294
|
+
website=args.website,
|
|
295
|
+
description=args.description,
|
|
296
|
+
),
|
|
297
|
+
f,
|
|
298
|
+
sort_keys=True,
|
|
299
|
+
)
|
|
300
|
+
if args.vendor:
|
|
301
|
+
_vendor(root_dir, upgrade=False)
|
|
302
|
+
if args.link:
|
|
303
|
+
_link(relink=True, same_path=False, wf_dir=wf_dir)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@_must_be_run_from_workflow_project_root
|
|
307
|
+
def link(args: argparse.Namespace):
|
|
308
|
+
"""
|
|
309
|
+
Entry point for the `link` command. Links or relinks the workflow into Alfred's workflows directory.
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
usage: alpyki link [-h] [--relink | --no-relink] [--same-path | --no-same-path]
|
|
313
|
+
|
|
314
|
+
options:
|
|
315
|
+
-h, --help show this help message and exit
|
|
316
|
+
--relink, --no-relink
|
|
317
|
+
Whether to delete (if exists) and recreate the link (default: False)
|
|
318
|
+
--same-path, --no-same-path
|
|
319
|
+
Whether to reuse (if exists) the previous path for the link (default: False)
|
|
320
|
+
```
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
_link(
|
|
324
|
+
relink=args.relink,
|
|
325
|
+
same_path=args.same_path,
|
|
326
|
+
wf_dir=Path.cwd().joinpath("Workflow"),
|
|
327
|
+
)
|
|
328
|
+
except ValueError as e:
|
|
329
|
+
logging.error("Error creating link: %s", e)
|
|
330
|
+
exit(1)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _link(relink: bool, same_path: bool, wf_dir: Path):
|
|
334
|
+
"""
|
|
335
|
+
Create a link to the workflow in Alfred's workflows directory
|
|
336
|
+
|
|
337
|
+
:param relink:
|
|
338
|
+
Whether to recreate the link if it exists
|
|
339
|
+
:param same_path:
|
|
340
|
+
Whether to reuse the same link if one exists
|
|
341
|
+
:param wf_dir:
|
|
342
|
+
The directory to link to
|
|
343
|
+
:return:
|
|
344
|
+
"""
|
|
345
|
+
if not wf_dir.exists():
|
|
346
|
+
raise ValueError(f"{wf_dir} doesn't exist")
|
|
347
|
+
|
|
348
|
+
if not wf_dir.is_dir():
|
|
349
|
+
raise ValueError(f"{wf_dir} is not a directory")
|
|
350
|
+
|
|
351
|
+
existing_link = find_workflow_link(wf_dir)
|
|
352
|
+
|
|
353
|
+
if existing_link:
|
|
354
|
+
if not relink:
|
|
355
|
+
logging.debug("Found link: %s", existing_link)
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
logging.debug("Removing existing link: %s", existing_link)
|
|
359
|
+
existing_link.unlink()
|
|
360
|
+
|
|
361
|
+
logging.info("Creating link to workflow directory %s", wf_dir)
|
|
362
|
+
|
|
363
|
+
if same_path and existing_link:
|
|
364
|
+
source = existing_link
|
|
365
|
+
else:
|
|
366
|
+
workflow_id = str(uuid4()).upper()
|
|
367
|
+
source = _get_workflows_directory().joinpath(f"user.workflow.{workflow_id}")
|
|
368
|
+
|
|
369
|
+
logging.debug("Creating link: %s", source)
|
|
370
|
+
source.symlink_to(wf_dir)
|
|
371
|
+
|
|
372
|
+
if not source.exists():
|
|
373
|
+
logging.error("Error linking from %s to %s", source, wf_dir)
|
|
374
|
+
source.unlink(missing_ok=True)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@_must_be_run_from_workflow_project_root
|
|
378
|
+
def vendor(args: argparse.Namespace):
|
|
379
|
+
"""
|
|
380
|
+
Entry point for the `vendor` command
|
|
381
|
+
|
|
382
|
+
Downloads dependencies specified in the `requirements.txt` file into the workflow's `vendored` directory.
|
|
383
|
+
This way, the dependencies don't need to be installed into the system Python interpreter.
|
|
384
|
+
|
|
385
|
+
The workflow sets the `PYTHONPATH` environment variable to `.:vendored`, making the interpreter search for
|
|
386
|
+
dependencies in that directory, in addition to the workflow directory.
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
usage: alpyki vendor [-h] [--upgrade | --no-upgrade]
|
|
390
|
+
|
|
391
|
+
options:
|
|
392
|
+
-h, --help show this help message and exit
|
|
393
|
+
--upgrade, --no-upgrade
|
|
394
|
+
Whether to pass `--upgrade` to `pip install` when vendoring (default: False)
|
|
395
|
+
```
|
|
396
|
+
"""
|
|
397
|
+
_vendor(root_path=Path.cwd(), upgrade=args.upgrade)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _vendor(root_path: Path, upgrade: bool) -> bool:
|
|
401
|
+
"""
|
|
402
|
+
Download dependencies from `requirements.txt`
|
|
403
|
+
|
|
404
|
+
:param root_path: The root path of the workflow project
|
|
405
|
+
:param upgrade: Whether to pass `--upgrade` to `pip install`
|
|
406
|
+
:return: whether the download was successful
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
vendored_path = root_path / "Workflow" / "vendored"
|
|
410
|
+
vendored_path.mkdir(parents=True, exist_ok=True)
|
|
411
|
+
|
|
412
|
+
import subprocess
|
|
413
|
+
|
|
414
|
+
pip_command = [
|
|
415
|
+
sys.executable,
|
|
416
|
+
"-m",
|
|
417
|
+
"pip",
|
|
418
|
+
"install",
|
|
419
|
+
"-r",
|
|
420
|
+
f"{root_path}/requirements.txt",
|
|
421
|
+
f"--target={vendored_path}",
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
if upgrade:
|
|
425
|
+
pip_command.append("--upgrade")
|
|
426
|
+
|
|
427
|
+
logging.debug("Running pip: python %s", " ".join(pip_command[1:]))
|
|
428
|
+
|
|
429
|
+
return subprocess.call(pip_command) == 0
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@_ensure_no_local_changes
|
|
433
|
+
@_must_be_run_from_workflow_project_root
|
|
434
|
+
def release(args: argparse.Namespace):
|
|
435
|
+
# Update version number in info.plist
|
|
436
|
+
plist_path = _info_plist_path()
|
|
437
|
+
with plist_path.open("rb") as f:
|
|
438
|
+
pl = plistlib.load(f)
|
|
439
|
+
|
|
440
|
+
pl["version"] = args.version
|
|
441
|
+
with plist_path.open(mode="wb") as f:
|
|
442
|
+
plistlib.dump(
|
|
443
|
+
pl,
|
|
444
|
+
f,
|
|
445
|
+
sort_keys=True,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# commit changes to git
|
|
449
|
+
commit_command = ["git", "commit", "-a", "-m", f"release {args.version}"]
|
|
450
|
+
if subprocess.call(commit_command) != 0:
|
|
451
|
+
logging.error("Error committing changes")
|
|
452
|
+
exit(1)
|
|
453
|
+
|
|
454
|
+
# create annotated tag with version
|
|
455
|
+
tag_command = ["git", "tag", "-a", args.version, "-m", args.version]
|
|
456
|
+
if subprocess.call(tag_command) != 0:
|
|
457
|
+
logging.error("Error tagging changes")
|
|
458
|
+
exit(1)
|
|
459
|
+
|
|
460
|
+
# push changes & tag
|
|
461
|
+
push_command = ["git", "push", "origin", "--follow-tags"]
|
|
462
|
+
if subprocess.call(push_command) != 0:
|
|
463
|
+
logging.error("Error pushing changes")
|
|
464
|
+
exit(1)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@_must_be_run_from_workflow_project_root
|
|
468
|
+
def package(args: argparse.Namespace):
|
|
469
|
+
"""
|
|
470
|
+
Entry point for the `package` command. Creates a package for distribution.
|
|
471
|
+
|
|
472
|
+
Packages the workflow into a `workflow.alfredworkflow` file in the `dist` directory.
|
|
473
|
+
Users can import the package by double-clicking the file.
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
usage: alpyki package [-h] --name NAME
|
|
477
|
+
|
|
478
|
+
options:
|
|
479
|
+
-h, --help show this help message and exit
|
|
480
|
+
--name NAME The name of the workflow file
|
|
481
|
+
```
|
|
482
|
+
"""
|
|
483
|
+
root_dir = Path.cwd()
|
|
484
|
+
|
|
485
|
+
has_requirements = root_dir.joinpath("requirements.txt").exists()
|
|
486
|
+
logging.debug("requirements.txt exists %s", has_requirements)
|
|
487
|
+
if has_requirements:
|
|
488
|
+
if not _vendor(root_dir, upgrade=True):
|
|
489
|
+
logging.error("Failed to download dependencies. Exiting")
|
|
490
|
+
exit(1)
|
|
491
|
+
|
|
492
|
+
output = root_dir / "dist"
|
|
493
|
+
output.mkdir(exist_ok=True)
|
|
494
|
+
|
|
495
|
+
_zip_dir(root_dir / "Workflow", output / f"{args.name}.alfredworkflow")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@_must_be_run_from_workflow_project_root
|
|
499
|
+
def version(args: argparse.Namespace) -> None:
|
|
500
|
+
with _info_plist_path().open("rb") as f:
|
|
501
|
+
pl = plistlib.load(f)
|
|
502
|
+
|
|
503
|
+
print(pl["version"])
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@_must_be_run_from_workflow_project_root
|
|
507
|
+
def name(args: argparse.Namespace) -> None:
|
|
508
|
+
with _info_plist_path().open("rb") as f:
|
|
509
|
+
pl = plistlib.load(f)
|
|
510
|
+
|
|
511
|
+
print(pl["name"])
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@_must_be_run_from_workflow_project_root
|
|
515
|
+
def show_link(args: argparse.Namespace) -> None:
|
|
516
|
+
"""
|
|
517
|
+
Entry point for the `show-link` command. Displays the link in the installed workflows directory for this project.
|
|
518
|
+
|
|
519
|
+
```
|
|
520
|
+
usage: alpyki show-link [-h]
|
|
521
|
+
|
|
522
|
+
options:
|
|
523
|
+
-h, --help show this help message and exit
|
|
524
|
+
```
|
|
525
|
+
"""
|
|
526
|
+
wf_dir = Path.cwd().joinpath("Workflow")
|
|
527
|
+
link_path = find_workflow_link(wf_dir)
|
|
528
|
+
|
|
529
|
+
if link_path:
|
|
530
|
+
print(link_path)
|
|
531
|
+
else:
|
|
532
|
+
logging.error("No workflow link found. Use 'alpyki link' to create one.")
|
|
533
|
+
sys.exit(1)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _cli():
|
|
537
|
+
"""
|
|
538
|
+
The entry point for the CLI.
|
|
539
|
+
|
|
540
|
+
```
|
|
541
|
+
usage: alpyki [-h] {new,vendor,link,package,version,name,show-link} ...
|
|
542
|
+
|
|
543
|
+
Build Python workflows for Alfred with ease
|
|
544
|
+
|
|
545
|
+
positional arguments:
|
|
546
|
+
{new,vendor,link,package,version,name,show-link}
|
|
547
|
+
new Create a new workflow
|
|
548
|
+
vendor Install workflow dependencies
|
|
549
|
+
link Create a symbolic link to this workflow in Alfred
|
|
550
|
+
package Package the workflow for distribution
|
|
551
|
+
version Display the version of the workflow
|
|
552
|
+
name Display the name of the workflow
|
|
553
|
+
show-link Display the link in the installed workflows directory for this project
|
|
554
|
+
|
|
555
|
+
options:
|
|
556
|
+
-h, --help show this help message and exit
|
|
557
|
+
--debug, --no-debug Whether to enable debug logging (default: False)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
"""
|
|
561
|
+
parser = argparse.ArgumentParser(prog="alpyki", description="Build Python workflows for Alfred with ease")
|
|
562
|
+
parser.add_argument(
|
|
563
|
+
"--debug",
|
|
564
|
+
action=argparse.BooleanOptionalAction,
|
|
565
|
+
default=False,
|
|
566
|
+
help="Whether to enable debug logging",
|
|
567
|
+
)
|
|
568
|
+
subparsers = parser.add_subparsers(required=True)
|
|
569
|
+
|
|
570
|
+
new_parser = subparsers.add_parser("new", help="Create a new workflow")
|
|
571
|
+
new_parser.add_argument("name", type=str, help="Name of the new workflow")
|
|
572
|
+
new_parser.add_argument(
|
|
573
|
+
"-k",
|
|
574
|
+
"--keyword",
|
|
575
|
+
type=str,
|
|
576
|
+
required=True,
|
|
577
|
+
help="The keyword to trigger the workflow",
|
|
578
|
+
)
|
|
579
|
+
new_parser.add_argument(
|
|
580
|
+
"-b",
|
|
581
|
+
"--bundle-id",
|
|
582
|
+
type=str,
|
|
583
|
+
required=True,
|
|
584
|
+
help="The bundle identifier, usually in reverse DNS notation",
|
|
585
|
+
)
|
|
586
|
+
new_parser.add_argument("--author", type=str, required=True, help="Name of the author")
|
|
587
|
+
new_parser.add_argument("--website", type=str, help="The workflow website")
|
|
588
|
+
new_parser.add_argument("--description", type=str, help="A description for the workflow")
|
|
589
|
+
new_parser.add_argument(
|
|
590
|
+
"--git",
|
|
591
|
+
action=argparse.BooleanOptionalAction,
|
|
592
|
+
default=True,
|
|
593
|
+
help="Whether to create a git repository",
|
|
594
|
+
)
|
|
595
|
+
new_parser.add_argument(
|
|
596
|
+
"--link", action=argparse.BooleanOptionalAction, default=True, help="Create a symbolic link to this workflow"
|
|
597
|
+
)
|
|
598
|
+
new_parser.add_argument(
|
|
599
|
+
"--vendor", action=argparse.BooleanOptionalAction, default=True, help="Install workflow dependencies"
|
|
600
|
+
)
|
|
601
|
+
new_parser.add_argument(
|
|
602
|
+
"--actions",
|
|
603
|
+
"--ci",
|
|
604
|
+
type=str,
|
|
605
|
+
choices=["github", "gitea", "gitlab", "none"],
|
|
606
|
+
default="github",
|
|
607
|
+
help="CI platform for release workflow (default: github)",
|
|
608
|
+
)
|
|
609
|
+
new_parser.set_defaults(func=new)
|
|
610
|
+
|
|
611
|
+
vendor_parser = subparsers.add_parser("vendor", help="Install workflow dependencies")
|
|
612
|
+
vendor_parser.add_argument(
|
|
613
|
+
"--upgrade",
|
|
614
|
+
action=argparse.BooleanOptionalAction,
|
|
615
|
+
default=False,
|
|
616
|
+
help="Whether to pass `--upgrade` to `pip install` when vendoring",
|
|
617
|
+
)
|
|
618
|
+
vendor_parser.set_defaults(func=vendor)
|
|
619
|
+
|
|
620
|
+
link_parser = subparsers.add_parser("link", help="Create a symbolic link to this workflow in Alfred")
|
|
621
|
+
link_parser.add_argument(
|
|
622
|
+
"--relink",
|
|
623
|
+
action=argparse.BooleanOptionalAction,
|
|
624
|
+
default=False,
|
|
625
|
+
help="Whether to delete (if exists) and recreate the link",
|
|
626
|
+
)
|
|
627
|
+
link_parser.add_argument(
|
|
628
|
+
"--same-path",
|
|
629
|
+
action=argparse.BooleanOptionalAction,
|
|
630
|
+
default=False,
|
|
631
|
+
help="Whether to reuse (if exists) the previous path for the link",
|
|
632
|
+
)
|
|
633
|
+
link_parser.set_defaults(func=link)
|
|
634
|
+
|
|
635
|
+
def version_str(arg_value: str, pat=re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$")) -> str:
|
|
636
|
+
if not pat.match(arg_value):
|
|
637
|
+
raise argparse.ArgumentTypeError("invalid version string")
|
|
638
|
+
return arg_value
|
|
639
|
+
|
|
640
|
+
release_parser = subparsers.add_parser("release", help="Update version & tag for release build")
|
|
641
|
+
release_parser.add_argument("--version", type=version_str, required=True, help="Version to update")
|
|
642
|
+
release_parser.set_defaults(func=release)
|
|
643
|
+
|
|
644
|
+
package_parser = subparsers.add_parser("package", help="Package the workflow for distribution")
|
|
645
|
+
package_parser.add_argument("--name", type=str, required=True, help="The name of the workflow file")
|
|
646
|
+
package_parser.set_defaults(func=package)
|
|
647
|
+
|
|
648
|
+
version_parser = subparsers.add_parser("version", help="Display the version of the workflow")
|
|
649
|
+
version_parser.set_defaults(func=version)
|
|
650
|
+
|
|
651
|
+
name_parser = subparsers.add_parser("name", help="Display the name of the workflow")
|
|
652
|
+
name_parser.set_defaults(func=name)
|
|
653
|
+
|
|
654
|
+
show_link_parser = subparsers.add_parser(
|
|
655
|
+
"show-link", help="Display the link in the installed workflows directory for this project"
|
|
656
|
+
)
|
|
657
|
+
show_link_parser.set_defaults(func=show_link)
|
|
658
|
+
|
|
659
|
+
args = parser.parse_args()
|
|
660
|
+
|
|
661
|
+
logging.basicConfig(
|
|
662
|
+
format="%(asctime)s %(levelname)-8s %(message)s",
|
|
663
|
+
level=logging.DEBUG if args.debug else logging.INFO,
|
|
664
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
args.func(args)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
if __name__ == "__main__":
|
|
671
|
+
_cli()
|