diff --git a/LICENSE b/LICENSE
index 113b6eb..cc2bcb9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020 Fede Calendino
+Copyright (c) 2022 Fede Calendino
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 47bd8c0..b6d22f8 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
### [Alfred Workflow](https://www.alfredapp.com/workflows/) to generate secure passwords 🔑️
-![PWD Gen default](/img/default.png)
+![default](/img/screenshots/default.png)
-![PWD Gen example](/img/low.png)
+![usage](/img/screenshots/usage.png)
diff --git a/alfred-pwd-gen b/alfred-pwd-gen
new file mode 100644
index 0000000..e69de29
diff --git a/generator.py b/generator.py
index d48e0b2..d375b35 100644
--- a/generator.py
+++ b/generator.py
@@ -17,7 +17,7 @@ def generate(letters, digits, symbols):
random.shuffle(characters)
- return ''.join(characters)
+ return "".join(characters)
def _calc_streght(password):
@@ -34,4 +34,4 @@ def streght(password):
if current > baseline:
return 16
- return int(15 * current/baseline)
+ return int(15 * current / baseline)
diff --git a/img/default.png b/img/default.png
deleted file mode 100644
index 41ea52e..0000000
Binary files a/img/default.png and /dev/null differ
diff --git a/img/low.png b/img/low.png
deleted file mode 100644
index 90b344e..0000000
Binary files a/img/low.png and /dev/null differ
diff --git a/img/screenshots/default.png b/img/screenshots/default.png
new file mode 100644
index 0000000..bed7a41
Binary files /dev/null and b/img/screenshots/default.png differ
diff --git a/img/screenshots/usage.png b/img/screenshots/usage.png
new file mode 100644
index 0000000..5954791
Binary files /dev/null and b/img/screenshots/usage.png differ
diff --git a/info.plist b/info.plist
index 7db57b6..a51f3b3 100644
--- a/info.plist
+++ b/info.plist
@@ -120,10 +120,8 @@
25
- variablesdontexport
-
version
- 1.2
+ 1.3
webaddress
https://github.com/fedecalendino/alfred-pwd-gen
diff --git a/main.py b/main.py
index bc6d36a..284803b 100644
--- a/main.py
+++ b/main.py
@@ -44,16 +44,17 @@ def format_subtitle(letters, digits, symbols, streght):
def main(workflow):
letters, digits, symbols = parse_args(workflow.args)
- password = generator.generate(letters, digits, symbols)
- streght = generator.streght(password)
-
- workflow.add_item(
- title=" {}".format(password),
- subtitle=format_subtitle(letters, digits, symbols, streght),
- arg=password,
- copytext=password,
- valid=True
- )
+ for _ in range(5):
+ password = generator.generate(letters, digits, symbols)
+ streght = generator.streght(password)
+
+ workflow.add_item(
+ title=" {}".format(password),
+ subtitle=format_subtitle(letters, digits, symbols, streght),
+ arg=password,
+ copytext=password,
+ valid=True,
+ )
if __name__ == u"__main__":
diff --git a/workflow/__init__.py b/workflow/__init__.py
index 17636a4..5736ad9 100644
--- a/workflow/__init__.py
+++ b/workflow/__init__.py
@@ -60,49 +60,49 @@
)
-__title__ = 'Alfred-Workflow'
-__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read()
-__author__ = 'Dean Jackson'
-__licence__ = 'MIT'
-__copyright__ = 'Copyright 2014-2019 Dean Jackson'
+__title__ = "Alfred-Workflow"
+__version__ = open(os.path.join(os.path.dirname(__file__), "version")).read()
+__author__ = "Dean Jackson"
+__licence__ = "MIT"
+__copyright__ = "Copyright 2014-2019 Dean Jackson"
__all__ = [
- 'Variables',
- 'Workflow',
- 'Workflow3',
- 'manager',
- 'PasswordNotFound',
- 'KeychainError',
- 'ICON_ACCOUNT',
- 'ICON_BURN',
- 'ICON_CLOCK',
- 'ICON_COLOR',
- 'ICON_COLOUR',
- 'ICON_EJECT',
- 'ICON_ERROR',
- 'ICON_FAVORITE',
- 'ICON_FAVOURITE',
- 'ICON_GROUP',
- 'ICON_HELP',
- 'ICON_HOME',
- 'ICON_INFO',
- 'ICON_NETWORK',
- 'ICON_NOTE',
- 'ICON_SETTINGS',
- 'ICON_SWIRL',
- 'ICON_SWITCH',
- 'ICON_SYNC',
- 'ICON_TRASH',
- 'ICON_USER',
- 'ICON_WARNING',
- 'ICON_WEB',
- 'MATCH_ALL',
- 'MATCH_ALLCHARS',
- 'MATCH_ATOM',
- 'MATCH_CAPITALS',
- 'MATCH_INITIALS',
- 'MATCH_INITIALS_CONTAIN',
- 'MATCH_INITIALS_STARTSWITH',
- 'MATCH_STARTSWITH',
- 'MATCH_SUBSTRING',
+ "Variables",
+ "Workflow",
+ "Workflow3",
+ "manager",
+ "PasswordNotFound",
+ "KeychainError",
+ "ICON_ACCOUNT",
+ "ICON_BURN",
+ "ICON_CLOCK",
+ "ICON_COLOR",
+ "ICON_COLOUR",
+ "ICON_EJECT",
+ "ICON_ERROR",
+ "ICON_FAVORITE",
+ "ICON_FAVOURITE",
+ "ICON_GROUP",
+ "ICON_HELP",
+ "ICON_HOME",
+ "ICON_INFO",
+ "ICON_NETWORK",
+ "ICON_NOTE",
+ "ICON_SETTINGS",
+ "ICON_SWIRL",
+ "ICON_SWITCH",
+ "ICON_SYNC",
+ "ICON_TRASH",
+ "ICON_USER",
+ "ICON_WARNING",
+ "ICON_WEB",
+ "MATCH_ALL",
+ "MATCH_ALLCHARS",
+ "MATCH_ATOM",
+ "MATCH_CAPITALS",
+ "MATCH_INITIALS",
+ "MATCH_INITIALS_CONTAIN",
+ "MATCH_INITIALS_STARTSWITH",
+ "MATCH_STARTSWITH",
+ "MATCH_SUBSTRING",
]
diff --git a/workflow/background.py b/workflow/background.py
index c2bd735..1b6a744 100644
--- a/workflow/background.py
+++ b/workflow/background.py
@@ -27,7 +27,7 @@
from workflow import Workflow
-__all__ = ['is_running', 'run_in_background']
+__all__ = ["is_running", "run_in_background"]
_wf = None
@@ -52,7 +52,7 @@ def _arg_cache(name):
:rtype: ``unicode`` filepath
"""
- return wf().cachefile(name + '.argcache')
+ return wf().cachefile(name + ".argcache")
def _pid_file(name):
@@ -64,7 +64,7 @@ def _pid_file(name):
:rtype: ``unicode`` filepath
"""
- return wf().cachefile(name + '.pid')
+ return wf().cachefile(name + ".pid")
def _process_exists(pid):
@@ -96,7 +96,7 @@ def _job_pid(name):
if not os.path.exists(pidfile):
return
- with open(pidfile, 'rb') as fp:
+ with open(pidfile, "rb") as fp:
pid = int(fp.read())
if _process_exists(pid):
@@ -120,8 +120,9 @@ def is_running(name):
return False
-def _background(pidfile, stdin='/dev/null', stdout='/dev/null',
- stderr='/dev/null'): # pragma: no cover
+def _background(
+ pidfile, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null"
+): # pragma: no cover
"""Fork the current process into a background daemon.
:param pidfile: file to write PID of daemon process to.
@@ -134,42 +135,43 @@ def _background(pidfile, stdin='/dev/null', stdout='/dev/null',
:type stderr: filepath
"""
+
def _fork_and_exit_parent(errmsg, wait=False, write=False):
try:
pid = os.fork()
if pid > 0:
if write: # write PID of child process to `pidfile`
- tmp = pidfile + '.tmp'
- with open(tmp, 'wb') as fp:
+ tmp = pidfile + ".tmp"
+ with open(tmp, "wb") as fp:
fp.write(str(pid))
os.rename(tmp, pidfile)
if wait: # wait for child process to exit
os.waitpid(pid, 0)
os._exit(0)
except OSError as err:
- _log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror)
+ _log().critical("%s: (%d) %s", errmsg, err.errno, err.strerror)
raise err
# Do first fork and wait for second fork to finish.
- _fork_and_exit_parent('fork #1 failed', wait=True)
+ _fork_and_exit_parent("fork #1 failed", wait=True)
# Decouple from parent environment.
os.chdir(wf().workflowdir)
os.setsid()
# Do second fork and write PID to pidfile.
- _fork_and_exit_parent('fork #2 failed', write=True)
+ _fork_and_exit_parent("fork #2 failed", write=True)
# Now I am a daemon!
# Redirect standard file descriptors.
- si = open(stdin, 'r', 0)
- so = open(stdout, 'a+', 0)
- se = open(stderr, 'a+', 0)
- if hasattr(sys.stdin, 'fileno'):
+ si = open(stdin, "r", 0)
+ so = open(stdout, "a+", 0)
+ se = open(stderr, "a+", 0)
+ if hasattr(sys.stdin, "fileno"):
os.dup2(si.fileno(), sys.stdin.fileno())
- if hasattr(sys.stdout, 'fileno'):
+ if hasattr(sys.stdout, "fileno"):
os.dup2(so.fileno(), sys.stdout.fileno())
- if hasattr(sys.stderr, 'fileno'):
+ if hasattr(sys.stderr, "fileno"):
os.dup2(se.fileno(), sys.stderr.fileno())
@@ -219,25 +221,25 @@ def run_in_background(name, args, **kwargs):
"""
if is_running(name):
- _log().info('[%s] job already running', name)
+ _log().info("[%s] job already running", name)
return
argcache = _arg_cache(name)
# Cache arguments
- with open(argcache, 'wb') as fp:
- pickle.dump({'args': args, 'kwargs': kwargs}, fp)
- _log().debug('[%s] command cached: %s', name, argcache)
+ with open(argcache, "wb") as fp:
+ pickle.dump({"args": args, "kwargs": kwargs}, fp)
+ _log().debug("[%s] command cached: %s", name, argcache)
# Call this script
- cmd = ['/usr/bin/python', __file__, name]
- _log().debug('[%s] passing job to background runner: %r', name, cmd)
+ cmd = ["/usr/bin/python", __file__, name]
+ _log().debug("[%s] passing job to background runner: %r", name, cmd)
retcode = subprocess.call(cmd)
if retcode: # pragma: no cover
- _log().error('[%s] background runner failed with %d', name, retcode)
+ _log().error("[%s] background runner failed with %d", name, retcode)
else:
- _log().debug('[%s] background job started', name)
+ _log().debug("[%s] background job started", name)
return retcode
@@ -253,7 +255,7 @@ def main(wf): # pragma: no cover
name = wf.args[0]
argcache = _arg_cache(name)
if not os.path.exists(argcache):
- msg = '[{0}] command cache not found: {1}'.format(name, argcache)
+ msg = "[{0}] command cache not found: {1}".format(name, argcache)
log.critical(msg)
raise IOError(msg)
@@ -262,29 +264,29 @@ def main(wf): # pragma: no cover
_background(pidfile)
# Load cached arguments
- with open(argcache, 'rb') as fp:
+ with open(argcache, "rb") as fp:
data = pickle.load(fp)
# Cached arguments
- args = data['args']
- kwargs = data['kwargs']
+ args = data["args"]
+ kwargs = data["kwargs"]
# Delete argument cache file
os.unlink(argcache)
try:
# Run the command
- log.debug('[%s] running command: %r', name, args)
+ log.debug("[%s] running command: %r", name, args)
retcode = subprocess.call(args, **kwargs)
if retcode:
- log.error('[%s] command failed with status %d', name, retcode)
+ log.error("[%s] command failed with status %d", name, retcode)
finally:
os.unlink(pidfile)
- log.debug('[%s] job complete', name)
+ log.debug("[%s] job complete", name)
-if __name__ == '__main__': # pragma: no cover
+if __name__ == "__main__": # pragma: no cover
wf().run(main)
diff --git a/workflow/notify.py b/workflow/notify.py
index 28ec0b9..fe7dfa9 100644
--- a/workflow/notify.py
+++ b/workflow/notify.py
@@ -43,20 +43,20 @@
#: Available system sounds from System Preferences > Sound > Sound Effects
SOUNDS = (
- 'Basso',
- 'Blow',
- 'Bottle',
- 'Frog',
- 'Funk',
- 'Glass',
- 'Hero',
- 'Morse',
- 'Ping',
- 'Pop',
- 'Purr',
- 'Sosumi',
- 'Submarine',
- 'Tink',
+ "Basso",
+ "Blow",
+ "Bottle",
+ "Frog",
+ "Funk",
+ "Glass",
+ "Hero",
+ "Morse",
+ "Ping",
+ "Pop",
+ "Purr",
+ "Sosumi",
+ "Submarine",
+ "Tink",
)
@@ -90,7 +90,7 @@ def notifier_program():
Returns:
unicode: Path to Notify.app ``applet`` executable.
"""
- return wf().datafile('Notify.app/Contents/MacOS/applet')
+ return wf().datafile("Notify.app/Contents/MacOS/applet")
def notifier_icon_path():
@@ -99,7 +99,7 @@ def notifier_icon_path():
Returns:
unicode: Path to ``applet.icns`` within the app bundle.
"""
- return wf().datafile('Notify.app/Contents/Resources/applet.icns')
+ return wf().datafile("Notify.app/Contents/Resources/applet.icns")
def install_notifier():
@@ -108,21 +108,21 @@ def install_notifier():
Changes the bundle ID of the installed app and gives it the
workflow's icon.
"""
- archive = os.path.join(os.path.dirname(__file__), 'Notify.tgz')
+ archive = os.path.join(os.path.dirname(__file__), "Notify.tgz")
destdir = wf().datadir
- app_path = os.path.join(destdir, 'Notify.app')
+ app_path = os.path.join(destdir, "Notify.app")
n = notifier_program()
- log().debug('installing Notify.app to %r ...', destdir)
+ log().debug("installing Notify.app to %r ...", destdir)
# z = zipfile.ZipFile(archive, 'r')
# z.extractall(destdir)
- tgz = tarfile.open(archive, 'r:gz')
+ tgz = tarfile.open(archive, "r:gz")
tgz.extractall(destdir)
if not os.path.exists(n): # pragma: nocover
- raise RuntimeError('Notify.app could not be installed in ' + destdir)
+ raise RuntimeError("Notify.app could not be installed in " + destdir)
# Replace applet icon
icon = notifier_icon_path()
- workflow_icon = wf().workflowfile('icon.png')
+ workflow_icon = wf().workflowfile("icon.png")
if os.path.exists(icon):
os.unlink(icon)
@@ -142,11 +142,11 @@ def install_notifier():
ws.setIcon_forFile_options_(img, app_path, 0)
# Change bundle ID of installed app
- ip_path = os.path.join(app_path, 'Contents/Info.plist')
- bundle_id = '{0}.{1}'.format(wf().bundleid, uuid.uuid4().hex)
+ ip_path = os.path.join(app_path, "Contents/Info.plist")
+ bundle_id = "{0}.{1}".format(wf().bundleid, uuid.uuid4().hex)
data = plistlib.readPlist(ip_path)
- log().debug('changing bundle ID to %r', bundle_id)
- data['CFBundleIdentifier'] = bundle_id
+ log().debug("changing bundle ID to %r", bundle_id)
+ data["CFBundleIdentifier"] = bundle_id
plistlib.writePlist(data, ip_path)
@@ -172,7 +172,7 @@ def validate_sound(sound):
return None
-def notify(title='', text='', sound=None):
+def notify(title="", text="", sound=None):
"""Post notification via Notify.app helper.
Args:
@@ -186,10 +186,10 @@ def notify(title='', text='', sound=None):
Returns:
bool: ``True`` if notification was posted, else ``False``.
"""
- if title == text == '':
- raise ValueError('Empty notification')
+ if title == text == "":
+ raise ValueError("Empty notification")
- sound = validate_sound(sound) or ''
+ sound = validate_sound(sound) or ""
n = notifier_program()
@@ -197,16 +197,16 @@ def notify(title='', text='', sound=None):
install_notifier()
env = os.environ.copy()
- enc = 'utf-8'
- env['NOTIFY_TITLE'] = title.encode(enc)
- env['NOTIFY_MESSAGE'] = text.encode(enc)
- env['NOTIFY_SOUND'] = sound.encode(enc)
+ enc = "utf-8"
+ env["NOTIFY_TITLE"] = title.encode(enc)
+ env["NOTIFY_MESSAGE"] = text.encode(enc)
+ env["NOTIFY_SOUND"] = sound.encode(enc)
cmd = [n]
retcode = subprocess.call(cmd, env=env)
if retcode == 0:
return True
- log().error('Notify.app exited with status {0}.'.format(retcode))
+ log().error("Notify.app exited with status {0}.".format(retcode))
return False
@@ -221,17 +221,13 @@ def convert_image(inpath, outpath, size):
Raises:
RuntimeError: Raised if ``sips`` exits with non-zero status.
"""
- cmd = [
- b'sips',
- b'-z', str(size), str(size),
- inpath,
- b'--out', outpath]
+ cmd = [b"sips", b"-z", str(size), str(size), inpath, b"--out", outpath]
# log().debug(cmd)
- with open(os.devnull, 'w') as pipe:
+ with open(os.devnull, "w") as pipe:
retcode = subprocess.call(cmd, stdout=pipe, stderr=subprocess.STDOUT)
if retcode != 0:
- raise RuntimeError('sips exited with %d' % retcode)
+ raise RuntimeError("sips exited with %d" % retcode)
def png_to_icns(png_path, icns_path):
@@ -248,13 +244,13 @@ def png_to_icns(png_path, icns_path):
Raises:
RuntimeError: Raised if ``iconutil`` or ``sips`` fail.
"""
- tempdir = tempfile.mkdtemp(prefix='aw-', dir=wf().datadir)
+ tempdir = tempfile.mkdtemp(prefix="aw-", dir=wf().datadir)
try:
- iconset = os.path.join(tempdir, 'Icon.iconset')
+ iconset = os.path.join(tempdir, "Icon.iconset")
if os.path.exists(iconset): # pragma: nocover
- raise RuntimeError('iconset already exists: ' + iconset)
+ raise RuntimeError("iconset already exists: " + iconset)
os.makedirs(iconset)
@@ -262,11 +258,11 @@ def png_to_icns(png_path, icns_path):
# sizes needed
configs = []
for i in (16, 32, 128, 256, 512):
- configs.append(('icon_{0}x{0}.png'.format(i), i))
- configs.append((('icon_{0}x{0}@2x.png'.format(i), i * 2)))
+ configs.append(("icon_{0}x{0}.png".format(i), i))
+ configs.append((("icon_{0}x{0}@2x.png".format(i), i * 2)))
- shutil.copy(png_path, os.path.join(iconset, 'icon_256x256.png'))
- shutil.copy(png_path, os.path.join(iconset, 'icon_128x128@2x.png'))
+ shutil.copy(png_path, os.path.join(iconset, "icon_256x256.png"))
+ shutil.copy(png_path, os.path.join(iconset, "icon_128x128@2x.png"))
for name, size in configs:
outpath = os.path.join(iconset, name)
@@ -274,19 +270,14 @@ def png_to_icns(png_path, icns_path):
continue
convert_image(png_path, outpath, size)
- cmd = [
- b'iconutil',
- b'-c', b'icns',
- b'-o', icns_path,
- iconset]
+ cmd = [b"iconutil", b"-c", b"icns", b"-o", icns_path, iconset]
retcode = subprocess.call(cmd)
if retcode != 0:
- raise RuntimeError('iconset exited with %d' % retcode)
+ raise RuntimeError("iconset exited with %d" % retcode)
if not os.path.exists(icns_path): # pragma: nocover
- raise ValueError(
- 'generated ICNS file not found: ' + repr(icns_path))
+ raise ValueError("generated ICNS file not found: " + repr(icns_path))
finally:
try:
shutil.rmtree(tempdir)
@@ -294,7 +285,7 @@ def png_to_icns(png_path, icns_path):
pass
-if __name__ == '__main__': # pragma: nocover
+if __name__ == "__main__": # pragma: nocover
# Simple command-line script to test module with
# This won't work on 2.6, as `argparse` isn't available
# by default.
@@ -304,19 +295,20 @@ def png_to_icns(png_path, icns_path):
def ustr(s):
"""Coerce `s` to normalised Unicode."""
- return normalize('NFD', s.decode('utf-8'))
+ return normalize("NFD", s.decode("utf-8"))
p = argparse.ArgumentParser()
- p.add_argument('-p', '--png', help="PNG image to convert to ICNS.")
- p.add_argument('-l', '--list-sounds', help="Show available sounds.",
- action='store_true')
- p.add_argument('-t', '--title',
- help="Notification title.", type=ustr,
- default='')
- p.add_argument('-s', '--sound', type=ustr,
- help="Optional notification sound.", default='')
- p.add_argument('text', type=ustr,
- help="Notification body text.", default='', nargs='?')
+ p.add_argument("-p", "--png", help="PNG image to convert to ICNS.")
+ p.add_argument(
+ "-l", "--list-sounds", help="Show available sounds.", action="store_true"
+ )
+ p.add_argument("-t", "--title", help="Notification title.", type=ustr, default="")
+ p.add_argument(
+ "-s", "--sound", type=ustr, help="Optional notification sound.", default=""
+ )
+ p.add_argument(
+ "text", type=ustr, help="Notification body text.", default="", nargs="?"
+ )
o = p.parse_args()
# List available sounds
@@ -329,20 +321,20 @@ def ustr(s):
if o.png:
icns = os.path.join(
os.path.dirname(o.png),
- os.path.splitext(os.path.basename(o.png))[0] + '.icns')
+ os.path.splitext(os.path.basename(o.png))[0] + ".icns",
+ )
- print('converting {0!r} to {1!r} ...'.format(o.png, icns),
- file=sys.stderr)
+ print("converting {0!r} to {1!r} ...".format(o.png, icns), file=sys.stderr)
if os.path.exists(icns):
- raise ValueError('destination file already exists: ' + icns)
+ raise ValueError("destination file already exists: " + icns)
png_to_icns(o.png, icns)
sys.exit(0)
# Post notification
- if o.title == o.text == '':
- print('ERROR: empty notification.', file=sys.stderr)
+ if o.title == o.text == "":
+ print("ERROR: empty notification.", file=sys.stderr)
sys.exit(1)
else:
notify(o.title, o.text, o.sound)
diff --git a/workflow/update.py b/workflow/update.py
index c039f7a..3cb0e5f 100644
--- a/workflow/update.py
+++ b/workflow/update.py
@@ -37,8 +37,8 @@
# __all__ = []
-RELEASES_BASE = 'https://api.github.com/repos/{}/releases'
-match_workflow = re.compile(r'\.alfred(\d+)?workflow$').search
+RELEASES_BASE = "https://api.github.com/repos/{}/releases"
+match_workflow = re.compile(r"\.alfred(\d+)?workflow$").search
_wf = None
@@ -70,9 +70,12 @@ class Download(object):
@classmethod
def from_dict(cls, d):
"""Create a `Download` from a `dict`."""
- return cls(url=d['url'], filename=d['filename'],
- version=Version(d['version']),
- prerelease=d['prerelease'])
+ return cls(
+ url=d["url"],
+ filename=d["filename"],
+ version=Version(d["version"]),
+ prerelease=d["prerelease"],
+ )
@classmethod
def from_releases(cls, js):
@@ -95,34 +98,35 @@ def from_releases(cls, js):
releases = json.loads(js)
downloads = []
for release in releases:
- tag = release['tag_name']
+ tag = release["tag_name"]
dupes = defaultdict(int)
try:
version = Version(tag)
except ValueError as err:
- wf().logger.debug('ignored release: bad version "%s": %s',
- tag, err)
+ wf().logger.debug('ignored release: bad version "%s": %s', tag, err)
continue
dls = []
- for asset in release.get('assets', []):
- url = asset.get('browser_download_url')
+ for asset in release.get("assets", []):
+ url = asset.get("browser_download_url")
filename = os.path.basename(url)
m = match_workflow(filename)
if not m:
- wf().logger.debug('unwanted file: %s', filename)
+ wf().logger.debug("unwanted file: %s", filename)
continue
ext = m.group(0)
dupes[ext] = dupes[ext] + 1
- dls.append(Download(url, filename, version,
- release['prerelease']))
+ dls.append(Download(url, filename, version, release["prerelease"]))
valid = True
for ext, n in dupes.items():
if n > 1:
- wf().logger.debug('ignored release "%s": multiple assets '
- 'with extension "%s"', tag, ext)
+ wf().logger.debug(
+ 'ignored release "%s": multiple assets ' 'with extension "%s"',
+ tag,
+ ext,
+ )
valid = False
break
@@ -156,23 +160,29 @@ def alfred_version(self):
"""Minimum Alfred version based on filename extension."""
m = match_workflow(self.filename)
if not m or not m.group(1):
- return Version('0')
+ return Version("0")
return Version(m.group(1))
@property
def dict(self):
"""Convert `Download` to `dict`."""
- return dict(url=self.url, filename=self.filename,
- version=str(self.version), prerelease=self.prerelease)
+ return dict(
+ url=self.url,
+ filename=self.filename,
+ version=str(self.version),
+ prerelease=self.prerelease,
+ )
def __str__(self):
"""Format `Download` for printing."""
- u = ('Download(url={dl.url!r}, '
- 'filename={dl.filename!r}, '
- 'version={dl.version!r}, '
- 'prerelease={dl.prerelease!r})'.format(dl=self))
+ u = (
+ "Download(url={dl.url!r}, "
+ "filename={dl.filename!r}, "
+ "version={dl.version!r}, "
+ "prerelease={dl.prerelease!r})".format(dl=self)
+ )
- return u.encode('utf-8')
+ return u.encode("utf-8")
def __repr__(self):
"""Code-like representation of `Download`."""
@@ -180,10 +190,12 @@ def __repr__(self):
def __eq__(self, other):
"""Compare Downloads based on version numbers."""
- if self.url != other.url \
- or self.filename != other.filename \
- or self.version != other.version \
- or self.prerelease != other.prerelease:
+ if (
+ self.url != other.url
+ or self.filename != other.filename
+ or self.version != other.version
+ or self.prerelease != other.prerelease
+ ):
return False
return True
@@ -222,7 +234,7 @@ class Version(object):
"""
#: Match version and pre-release/build information in version strings
- match_version = re.compile(r'([0-9][0-9\.]*)(.+)?').match
+ match_version = re.compile(r"([0-9][0-9\.]*)(.+)?").match
def __init__(self, vstr):
"""Create new `Version` object.
@@ -231,23 +243,23 @@ def __init__(self, vstr):
vstr (basestring): Semantic version string.
"""
if not vstr:
- raise ValueError('invalid version number: {!r}'.format(vstr))
+ raise ValueError("invalid version number: {!r}".format(vstr))
self.vstr = vstr
self.major = 0
self.minor = 0
self.patch = 0
- self.suffix = ''
- self.build = ''
+ self.suffix = ""
+ self.build = ""
self._parse(vstr)
def _parse(self, vstr):
- if vstr.startswith('v'):
+ if vstr.startswith("v"):
m = self.match_version(vstr[1:])
else:
m = self.match_version(vstr)
if not m:
- raise ValueError('invalid version number: ' + vstr)
+ raise ValueError("invalid version number: " + vstr)
version, suffix = m.groups()
parts = self._parse_dotted_string(version)
@@ -257,24 +269,23 @@ def _parse(self, vstr):
if len(parts):
self.patch = parts.pop(0)
if not len(parts) == 0:
- raise ValueError('version number too long: ' + vstr)
+ raise ValueError("version number too long: " + vstr)
if suffix:
# Build info
- idx = suffix.find('+')
+ idx = suffix.find("+")
if idx > -1:
- self.build = suffix[idx+1:]
+ self.build = suffix[idx + 1 :]
suffix = suffix[:idx]
if suffix:
- if not suffix.startswith('-'):
- raise ValueError(
- 'suffix must start with - : ' + suffix)
+ if not suffix.startswith("-"):
+ raise ValueError("suffix must start with - : " + suffix)
self.suffix = suffix[1:]
def _parse_dotted_string(self, s):
"""Parse string ``s`` into list of ints and strings."""
parsed = []
- parts = s.split('.')
+ parts = s.split(".")
for p in parts:
if p.isdigit():
p = int(p)
@@ -289,7 +300,7 @@ def tuple(self):
def __lt__(self, other):
"""Implement comparison."""
if not isinstance(other, Version):
- raise ValueError('not a Version instance: {0!r}'.format(other))
+ raise ValueError("not a Version instance: {0!r}".format(other))
t = self.tuple[:3]
o = other.tuple[:3]
if t < o:
@@ -299,15 +310,16 @@ def __lt__(self, other):
return True
if other.suffix and not self.suffix:
return False
- return self._parse_dotted_string(self.suffix) \
- < self._parse_dotted_string(other.suffix)
+ return self._parse_dotted_string(self.suffix) < self._parse_dotted_string(
+ other.suffix
+ )
# t > o
return False
def __eq__(self, other):
"""Implement comparison."""
if not isinstance(other, Version):
- raise ValueError('not a Version instance: {0!r}'.format(other))
+ raise ValueError("not a Version instance: {0!r}".format(other))
return self.tuple == other.tuple
def __ne__(self, other):
@@ -317,13 +329,13 @@ def __ne__(self, other):
def __gt__(self, other):
"""Implement comparison."""
if not isinstance(other, Version):
- raise ValueError('not a Version instance: {0!r}'.format(other))
+ raise ValueError("not a Version instance: {0!r}".format(other))
return other.__lt__(self)
def __le__(self, other):
"""Implement comparison."""
if not isinstance(other, Version):
- raise ValueError('not a Version instance: {0!r}'.format(other))
+ raise ValueError("not a Version instance: {0!r}".format(other))
return not other.__lt__(self)
def __ge__(self, other):
@@ -332,11 +344,11 @@ def __ge__(self, other):
def __str__(self):
"""Return semantic version string."""
- vstr = '{0}.{1}.{2}'.format(self.major, self.minor, self.patch)
+ vstr = "{0}.{1}.{2}".format(self.major, self.minor, self.patch)
if self.suffix:
- vstr = '{0}-{1}'.format(vstr, self.suffix)
+ vstr = "{0}-{1}".format(vstr, self.suffix)
if self.build:
- vstr = '{0}+{1}'.format(vstr, self.build)
+ vstr = "{0}+{1}".format(vstr, self.build)
return vstr
def __repr__(self):
@@ -357,11 +369,10 @@ def retrieve_download(dl):
"""
if not match_workflow(dl.filename):
- raise ValueError('attachment not a workflow: ' + dl.filename)
+ raise ValueError("attachment not a workflow: " + dl.filename)
path = os.path.join(tempfile.gettempdir(), dl.filename)
- wf().logger.debug('downloading update from '
- '%r to %r ...', dl.url, path)
+ wf().logger.debug("downloading update from " "%r to %r ...", dl.url, path)
r = web.get(dl.url)
r.raise_for_status()
@@ -381,8 +392,8 @@ def build_api_url(repo):
unicode: URL to the API endpoint for the repo's releases
"""
- if len(repo.split('/')) != 2:
- raise ValueError('invalid GitHub repo: {!r}'.format(repo))
+ if len(repo.split("/")) != 2:
+ raise ValueError("invalid GitHub repo: {!r}".format(repo))
return RELEASES_BASE.format(repo)
@@ -401,12 +412,12 @@ def get_downloads(repo):
url = build_api_url(repo)
def _fetch():
- wf().logger.info('retrieving releases for %r ...', repo)
+ wf().logger.info("retrieving releases for %r ...", repo)
r = web.get(url)
r.raise_for_status()
return r.content
- key = 'github-releases-' + repo.replace('/', '-')
+ key = "github-releases-" + repo.replace("/", "-")
js = wf().cached_data(key, _fetch, max_age=60)
return Download.from_releases(js)
@@ -414,7 +425,7 @@ def _fetch():
def latest_download(dls, alfred_version=None, prereleases=False):
"""Return newest `Download`."""
- alfred_version = alfred_version or os.getenv('alfred_version')
+ alfred_version = alfred_version or os.getenv("alfred_version")
version = None
if alfred_version:
version = Version(alfred_version)
@@ -422,21 +433,24 @@ def latest_download(dls, alfred_version=None, prereleases=False):
dls.sort(reverse=True)
for dl in dls:
if dl.prerelease and not prereleases:
- wf().logger.debug('ignored prerelease: %s', dl.version)
+ wf().logger.debug("ignored prerelease: %s", dl.version)
continue
if version and dl.alfred_version > version:
- wf().logger.debug('ignored incompatible (%s > %s): %s',
- dl.alfred_version, version, dl.filename)
+ wf().logger.debug(
+ "ignored incompatible (%s > %s): %s",
+ dl.alfred_version,
+ version,
+ dl.filename,
+ )
continue
- wf().logger.debug('latest version: %s (%s)', dl.version, dl.filename)
+ wf().logger.debug("latest version: %s (%s)", dl.version, dl.filename)
return dl
return None
-def check_update(repo, current_version, prereleases=False,
- alfred_version=None):
+def check_update(repo, current_version, prereleases=False, alfred_version=None):
"""Check whether a newer release is available on GitHub.
Args:
@@ -454,38 +468,41 @@ def check_update(repo, current_version, prereleases=False,
be cached.
"""
- key = '__workflow_latest_version'
+ key = "__workflow_latest_version"
# data stored when no update is available
no_update = {
- 'available': False,
- 'download': None,
- 'version': None,
+ "available": False,
+ "download": None,
+ "version": None,
}
current = Version(current_version)
dls = get_downloads(repo)
if not len(dls):
- wf().logger.warning('no valid downloads for %s', repo)
+ wf().logger.warning("no valid downloads for %s", repo)
wf().cache_data(key, no_update)
return False
- wf().logger.info('%d download(s) for %s', len(dls), repo)
+ wf().logger.info("%d download(s) for %s", len(dls), repo)
dl = latest_download(dls, alfred_version, prereleases)
if not dl:
- wf().logger.warning('no compatible downloads for %s', repo)
+ wf().logger.warning("no compatible downloads for %s", repo)
wf().cache_data(key, no_update)
return False
- wf().logger.debug('latest=%r, installed=%r', dl.version, current)
+ wf().logger.debug("latest=%r, installed=%r", dl.version, current)
if dl.version > current:
- wf().cache_data(key, {
- 'version': str(dl.version),
- 'download': dl.dict,
- 'available': True,
- })
+ wf().cache_data(
+ key,
+ {
+ "version": str(dl.version),
+ "download": dl.dict,
+ "available": True,
+ },
+ )
return True
wf().cache_data(key, no_update)
@@ -498,50 +515,49 @@ def install_update():
:returns: ``True`` if an update is installed, else ``False``
"""
- key = '__workflow_latest_version'
+ key = "__workflow_latest_version"
# data stored when no update is available
no_update = {
- 'available': False,
- 'download': None,
- 'version': None,
+ "available": False,
+ "download": None,
+ "version": None,
}
status = wf().cached_data(key, max_age=0)
- if not status or not status.get('available'):
- wf().logger.info('no update available')
+ if not status or not status.get("available"):
+ wf().logger.info("no update available")
return False
- dl = status.get('download')
+ dl = status.get("download")
if not dl:
- wf().logger.info('no download information')
+ wf().logger.info("no download information")
return False
path = retrieve_download(Download.from_dict(dl))
- wf().logger.info('installing updated workflow ...')
- subprocess.call(['open', path]) # nosec
+ wf().logger.info("installing updated workflow ...")
+ subprocess.call(["open", path]) # nosec
wf().cache_data(key, no_update)
return True
-if __name__ == '__main__': # pragma: nocover
+if __name__ == "__main__": # pragma: nocover
import sys
prereleases = False
def show_help(status=0):
"""Print help message."""
- print('usage: update.py (check|install) '
- '[--prereleases] ')
+ print("usage: update.py (check|install) " "[--prereleases] ")
sys.exit(status)
argv = sys.argv[:]
- if '-h' in argv or '--help' in argv:
+ if "-h" in argv or "--help" in argv:
show_help()
- if '--prereleases' in argv:
- argv.remove('--prereleases')
+ if "--prereleases" in argv:
+ argv.remove("--prereleases")
prereleases = True
if len(argv) != 4:
@@ -553,9 +569,9 @@ def show_help(status=0):
try:
- if action == 'check':
+ if action == "check":
check_update(repo, version, prereleases)
- elif action == 'install':
+ elif action == "install":
install_update()
else:
show_help(1)
diff --git a/workflow/util.py b/workflow/util.py
index ab5e954..b606bda 100644
--- a/workflow/util.py
+++ b/workflow/util.py
@@ -31,28 +31,28 @@
# "com.runningwithcrayons.Alfred" depending on version.
#
# Open Alfred in search (regular) mode
-JXA_SEARCH = 'Application({app}).search({arg});'
+JXA_SEARCH = "Application({app}).search({arg});"
# Open Alfred's File Actions on an argument
-JXA_ACTION = 'Application({app}).action({arg});'
+JXA_ACTION = "Application({app}).action({arg});"
# Open Alfred's navigation mode at path
-JXA_BROWSE = 'Application({app}).browse({arg});'
+JXA_BROWSE = "Application({app}).browse({arg});"
# Set the specified theme
-JXA_SET_THEME = 'Application({app}).setTheme({arg});'
+JXA_SET_THEME = "Application({app}).setTheme({arg});"
# Call an External Trigger
-JXA_TRIGGER = 'Application({app}).runTrigger({arg}, {opts});'
+JXA_TRIGGER = "Application({app}).runTrigger({arg}, {opts});"
# Save a variable to the workflow configuration sheet/info.plist
-JXA_SET_CONFIG = 'Application({app}).setConfiguration({arg}, {opts});'
+JXA_SET_CONFIG = "Application({app}).setConfiguration({arg}, {opts});"
# Delete a variable from the workflow configuration sheet/info.plist
-JXA_UNSET_CONFIG = 'Application({app}).removeConfiguration({arg}, {opts});'
+JXA_UNSET_CONFIG = "Application({app}).removeConfiguration({arg}, {opts});"
# Tell Alfred to reload a workflow from disk
-JXA_RELOAD_WORKFLOW = 'Application({app}).reloadWorkflow({arg});'
+JXA_RELOAD_WORKFLOW = "Application({app}).reloadWorkflow({arg});"
class AcquisitionError(Exception):
"""Raised if a lock cannot be acquired."""
-AppInfo = namedtuple('AppInfo', ['name', 'path', 'bundleid'])
+AppInfo = namedtuple("AppInfo", ["name", "path", "bundleid"])
"""Information about an installed application.
Returned by :func:`appinfo`. All attributes are Unicode.
@@ -86,14 +86,14 @@ def jxa_app_name():
unicode: Application name or ID.
"""
- if os.getenv('alfred_version', '').startswith('3'):
+ if os.getenv("alfred_version", "").startswith("3"):
# Alfred 3
- return u'Alfred 3'
+ return u"Alfred 3"
# Alfred 4+
- return u'com.runningwithcrayons.Alfred'
+ return u"com.runningwithcrayons.Alfred"
-def unicodify(s, encoding='utf-8', norm=None):
+def unicodify(s, encoding="utf-8", norm=None):
"""Ensure string is Unicode.
.. versionadded:: 1.31
@@ -115,6 +115,7 @@ def unicodify(s, encoding='utf-8', norm=None):
if norm:
from unicodedata import normalize
+
s = normalize(norm, s)
return s
@@ -139,7 +140,7 @@ def utf8ify(s):
return s
if isinstance(s, unicode):
- return s.encode('utf-8')
+ return s.encode("utf-8")
return str(s)
@@ -204,17 +205,17 @@ def run_applescript(script, *args, **kwargs):
str: Output of run command.
"""
- lang = 'AppleScript'
- if 'lang' in kwargs:
- lang = kwargs['lang']
- del kwargs['lang']
+ lang = "AppleScript"
+ if "lang" in kwargs:
+ lang = kwargs["lang"]
+ del kwargs["lang"]
- cmd = ['/usr/bin/osascript', '-l', lang]
+ cmd = ["/usr/bin/osascript", "-l", lang]
if os.path.exists(script):
cmd += [script]
else:
- cmd += ['-e', script]
+ cmd += ["-e", script]
cmd.extend(args)
@@ -236,7 +237,7 @@ def run_jxa(script, *args):
str: Output of script.
"""
- return run_applescript(script, *args, lang='JavaScript')
+ return run_applescript(script, *args, lang="JavaScript")
def run_trigger(name, bundleid=None, arg=None):
@@ -253,17 +254,19 @@ def run_trigger(name, bundleid=None, arg=None):
arg (str, optional): Argument to pass to trigger.
"""
- bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
+ bundleid = bundleid or os.getenv("alfred_workflow_bundleid")
appname = jxa_app_name()
- opts = {'inWorkflow': bundleid}
+ opts = {"inWorkflow": bundleid}
if arg:
- opts['withArgument'] = arg
+ opts["withArgument"] = arg
- script = JXA_TRIGGER.format(app=json.dumps(appname),
- arg=json.dumps(name),
- opts=json.dumps(opts, sort_keys=True))
+ script = JXA_TRIGGER.format(
+ app=json.dumps(appname),
+ arg=json.dumps(name),
+ opts=json.dumps(opts, sort_keys=True),
+ )
- run_applescript(script, lang='JavaScript')
+ run_applescript(script, lang="JavaScript")
def set_theme(theme_name):
@@ -276,9 +279,8 @@ def set_theme(theme_name):
"""
appname = jxa_app_name()
- script = JXA_SET_THEME.format(app=json.dumps(appname),
- arg=json.dumps(theme_name))
- run_applescript(script, lang='JavaScript')
+ script = JXA_SET_THEME.format(app=json.dumps(appname), arg=json.dumps(theme_name))
+ run_applescript(script, lang="JavaScript")
def set_config(name, value, bundleid=None, exportable=False):
@@ -297,19 +299,21 @@ def set_config(name, value, bundleid=None, exportable=False):
as exportable (Don't Export checkbox).
"""
- bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
+ bundleid = bundleid or os.getenv("alfred_workflow_bundleid")
appname = jxa_app_name()
opts = {
- 'toValue': value,
- 'inWorkflow': bundleid,
- 'exportable': exportable,
+ "toValue": value,
+ "inWorkflow": bundleid,
+ "exportable": exportable,
}
- script = JXA_SET_CONFIG.format(app=json.dumps(appname),
- arg=json.dumps(name),
- opts=json.dumps(opts, sort_keys=True))
+ script = JXA_SET_CONFIG.format(
+ app=json.dumps(appname),
+ arg=json.dumps(name),
+ opts=json.dumps(opts, sort_keys=True),
+ )
- run_applescript(script, lang='JavaScript')
+ run_applescript(script, lang="JavaScript")
def unset_config(name, bundleid=None):
@@ -325,15 +329,17 @@ def unset_config(name, bundleid=None):
bundleid (str, optional): Bundle ID of workflow variable belongs to.
"""
- bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
+ bundleid = bundleid or os.getenv("alfred_workflow_bundleid")
appname = jxa_app_name()
- opts = {'inWorkflow': bundleid}
+ opts = {"inWorkflow": bundleid}
- script = JXA_UNSET_CONFIG.format(app=json.dumps(appname),
- arg=json.dumps(name),
- opts=json.dumps(opts, sort_keys=True))
+ script = JXA_UNSET_CONFIG.format(
+ app=json.dumps(appname),
+ arg=json.dumps(name),
+ opts=json.dumps(opts, sort_keys=True),
+ )
- run_applescript(script, lang='JavaScript')
+ run_applescript(script, lang="JavaScript")
def search_in_alfred(query=None):
@@ -347,10 +353,10 @@ def search_in_alfred(query=None):
query (unicode, optional): Search query.
"""
- query = query or u''
+ query = query or u""
appname = jxa_app_name()
script = JXA_SEARCH.format(app=json.dumps(appname), arg=json.dumps(query))
- run_applescript(script, lang='JavaScript')
+ run_applescript(script, lang="JavaScript")
def browse_in_alfred(path):
@@ -364,7 +370,7 @@ def browse_in_alfred(path):
"""
appname = jxa_app_name()
script = JXA_BROWSE.format(app=json.dumps(appname), arg=json.dumps(path))
- run_applescript(script, lang='JavaScript')
+ run_applescript(script, lang="JavaScript")
def action_in_alfred(paths):
@@ -378,7 +384,7 @@ def action_in_alfred(paths):
"""
appname = jxa_app_name()
script = JXA_ACTION.format(app=json.dumps(appname), arg=json.dumps(paths))
- run_applescript(script, lang='JavaScript')
+ run_applescript(script, lang="JavaScript")
def reload_workflow(bundleid=None):
@@ -393,12 +399,13 @@ def reload_workflow(bundleid=None):
bundleid (unicode, optional): Bundle ID of workflow to reload.
"""
- bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
+ bundleid = bundleid or os.getenv("alfred_workflow_bundleid")
appname = jxa_app_name()
- script = JXA_RELOAD_WORKFLOW.format(app=json.dumps(appname),
- arg=json.dumps(bundleid))
+ script = JXA_RELOAD_WORKFLOW.format(
+ app=json.dumps(appname), arg=json.dumps(bundleid)
+ )
- run_applescript(script, lang='JavaScript')
+ run_applescript(script, lang="JavaScript")
def appinfo(name):
@@ -414,22 +421,24 @@ def appinfo(name):
"""
cmd = [
- 'mdfind',
- '-onlyin', '/Applications',
- '-onlyin', '/System/Applications',
- '-onlyin', os.path.expanduser('~/Applications'),
- '(kMDItemContentTypeTree == com.apple.application &&'
- '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))'
- .format(name)
+ "mdfind",
+ "-onlyin",
+ "/Applications",
+ "-onlyin",
+ "/System/Applications",
+ "-onlyin",
+ os.path.expanduser("~/Applications"),
+ "(kMDItemContentTypeTree == com.apple.application &&"
+ '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))'.format(name),
]
output = run_command(cmd).strip()
if not output:
return None
- path = output.split('\n')[0]
+ path = output.split("\n")[0]
- cmd = ['mdls', '-raw', '-name', 'kMDItemCFBundleIdentifier', path]
+ cmd = ["mdls", "-raw", "-name", "kMDItemCFBundleIdentifier", path]
bid = run_command(cmd).strip()
if not bid: # pragma: no cover
return None
@@ -452,7 +461,7 @@ def atomic_writer(fpath, mode):
:type mode: string
"""
- suffix = '.{}.tmp'.format(os.getpid())
+ suffix = ".{}.tmp".format(os.getpid())
temppath = fpath + suffix
with open(temppath, mode) as fp:
try:
@@ -496,7 +505,7 @@ class LockFile(object):
def __init__(self, protected_path, timeout=0.0, delay=0.05):
"""Create new :class:`LockFile` object."""
- self.lockfile = protected_path + '.lock'
+ self.lockfile = protected_path + ".lock"
self._lockfile = None
self.timeout = timeout
self.delay = delay
@@ -525,7 +534,7 @@ def acquire(self, blocking=True):
while True:
# Raise error if we've been waiting too long to acquire the lock
if self.timeout and (time.time() - start) >= self.timeout:
- raise AcquisitionError('lock acquisition timed out')
+ raise AcquisitionError("lock acquisition timed out")
# If already locked, wait then try again
if self.locked:
@@ -534,7 +543,7 @@ def acquire(self, blocking=True):
# Create in append mode so we don't lose any contents
if self._lockfile is None:
- self._lockfile = open(self.lockfile, 'a')
+ self._lockfile = open(self.lockfile, "a")
# Try to acquire the lock
try:
@@ -608,7 +617,7 @@ class uninterruptible(object):
"""
- def __init__(self, func, class_name=''):
+ def __init__(self, func, class_name=""):
"""Decorate `func`."""
self.func = func
functools.update_wrapper(self, func)
@@ -640,5 +649,4 @@ def __call__(self, *args, **kwargs):
def __get__(self, obj=None, klass=None):
"""Decorator API."""
- return self.__class__(self.func.__get__(obj, klass),
- klass.__name__)
+ return self.__class__(self.func.__get__(obj, klass), klass.__name__)
diff --git a/workflow/web.py b/workflow/web.py
index 83212a8..0fa022a 100644
--- a/workflow/web.py
+++ b/workflow/web.py
@@ -25,56 +25,57 @@
import urlparse
import zlib
-__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read()
+__version__ = open(os.path.join(os.path.dirname(__file__), "version")).read()
-USER_AGENT = (u'Alfred-Workflow/' + __version__ +
- ' (+http://www.deanishe.net/alfred-workflow)')
+USER_AGENT = (
+ u"Alfred-Workflow/" + __version__ + " (+http://www.deanishe.net/alfred-workflow)"
+)
# Valid characters for multipart form data boundaries
BOUNDARY_CHARS = string.digits + string.ascii_letters
# HTTP response codes
RESPONSES = {
- 100: 'Continue',
- 101: 'Switching Protocols',
- 200: 'OK',
- 201: 'Created',
- 202: 'Accepted',
- 203: 'Non-Authoritative Information',
- 204: 'No Content',
- 205: 'Reset Content',
- 206: 'Partial Content',
- 300: 'Multiple Choices',
- 301: 'Moved Permanently',
- 302: 'Found',
- 303: 'See Other',
- 304: 'Not Modified',
- 305: 'Use Proxy',
- 307: 'Temporary Redirect',
- 400: 'Bad Request',
- 401: 'Unauthorized',
- 402: 'Payment Required',
- 403: 'Forbidden',
- 404: 'Not Found',
- 405: 'Method Not Allowed',
- 406: 'Not Acceptable',
- 407: 'Proxy Authentication Required',
- 408: 'Request Timeout',
- 409: 'Conflict',
- 410: 'Gone',
- 411: 'Length Required',
- 412: 'Precondition Failed',
- 413: 'Request Entity Too Large',
- 414: 'Request-URI Too Long',
- 415: 'Unsupported Media Type',
- 416: 'Requested Range Not Satisfiable',
- 417: 'Expectation Failed',
- 500: 'Internal Server Error',
- 501: 'Not Implemented',
- 502: 'Bad Gateway',
- 503: 'Service Unavailable',
- 504: 'Gateway Timeout',
- 505: 'HTTP Version Not Supported'
+ 100: "Continue",
+ 101: "Switching Protocols",
+ 200: "OK",
+ 201: "Created",
+ 202: "Accepted",
+ 203: "Non-Authoritative Information",
+ 204: "No Content",
+ 205: "Reset Content",
+ 206: "Partial Content",
+ 300: "Multiple Choices",
+ 301: "Moved Permanently",
+ 302: "Found",
+ 303: "See Other",
+ 304: "Not Modified",
+ 305: "Use Proxy",
+ 307: "Temporary Redirect",
+ 400: "Bad Request",
+ 401: "Unauthorized",
+ 402: "Payment Required",
+ 403: "Forbidden",
+ 404: "Not Found",
+ 405: "Method Not Allowed",
+ 406: "Not Acceptable",
+ 407: "Proxy Authentication Required",
+ 408: "Request Timeout",
+ 409: "Conflict",
+ 410: "Gone",
+ 411: "Length Required",
+ 412: "Precondition Failed",
+ 413: "Request Entity Too Large",
+ 414: "Request-URI Too Long",
+ 415: "Unsupported Media Type",
+ 416: "Requested Range Not Satisfiable",
+ 417: "Expectation Failed",
+ 500: "Internal Server Error",
+ 501: "Not Implemented",
+ 502: "Bad Gateway",
+ 503: "Service Unavailable",
+ 504: "Gateway Timeout",
+ 505: "HTTP Version Not Supported",
}
@@ -93,9 +94,9 @@ def str_dict(dic):
dic2 = {}
for k, v in dic.items():
if isinstance(k, unicode):
- k = k.encode('utf-8')
+ k = k.encode("utf-8")
if isinstance(v, unicode):
- v = v.encode('utf-8')
+ v = v.encode("utf-8")
dic2[k] = v
return dic2
@@ -135,10 +136,10 @@ def __contains__(self, key):
return dict.__contains__(self, key.lower())
def __getitem__(self, key):
- return dict.__getitem__(self, key.lower())['val']
+ return dict.__getitem__(self, key.lower())["val"]
def __setitem__(self, key, value):
- return dict.__setitem__(self, key.lower(), {'key': key, 'val': value})
+ return dict.__setitem__(self, key.lower(), {"key": key, "val": value})
def get(self, key, default=None):
"""Return value for case-insensitive key or default."""
@@ -147,7 +148,7 @@ def get(self, key, default=None):
except KeyError:
return default
else:
- return v['val']
+ return v["val"]
def update(self, other):
"""Update values from other ``dict``."""
@@ -156,30 +157,30 @@ def update(self, other):
def items(self):
"""Return ``(key, value)`` pairs."""
- return [(v['key'], v['val']) for v in dict.itervalues(self)]
+ return [(v["key"], v["val"]) for v in dict.itervalues(self)]
def keys(self):
"""Return original keys."""
- return [v['key'] for v in dict.itervalues(self)]
+ return [v["key"] for v in dict.itervalues(self)]
def values(self):
"""Return all values."""
- return [v['val'] for v in dict.itervalues(self)]
+ return [v["val"] for v in dict.itervalues(self)]
def iteritems(self):
"""Iterate over ``(key, value)`` pairs."""
for v in dict.itervalues(self):
- yield v['key'], v['val']
+ yield v["key"], v["val"]
def iterkeys(self):
"""Iterate over original keys."""
for v in dict.itervalues(self):
- yield v['key']
+ yield v["key"]
def itervalues(self):
"""Interate over values."""
for v in dict.itervalues(self):
- yield v['val']
+ yield v["val"]
class Request(urllib2.Request):
@@ -187,7 +188,7 @@ class Request(urllib2.Request):
def __init__(self, *args, **kwargs):
"""Create a new :class:`Request`."""
- self._method = kwargs.pop('method', None)
+ self._method = kwargs.pop("method", None)
urllib2.Request.__init__(self, *args, **kwargs)
def get_method(self):
@@ -265,8 +266,9 @@ def __init__(self, request, stream=False):
# Transfer-Encoding appears to not be used in the wild
# (contrary to the HTTP standard), but no harm in testing
# for it
- if 'gzip' in headers.get('content-encoding', '') or \
- 'gzip' in headers.get('transfer-encoding', ''):
+ if "gzip" in headers.get("content-encoding", "") or "gzip" in headers.get(
+ "transfer-encoding", ""
+ ):
self._gzipped = True
@property
@@ -282,8 +284,7 @@ def stream(self):
@stream.setter
def stream(self, value):
if self._content_loaded:
- raise RuntimeError("`content` has already been read from "
- "this Response.")
+ raise RuntimeError("`content` has already been read from " "this Response.")
self._stream = value
@@ -294,7 +295,7 @@ def json(self):
:rtype: list, dict or unicode
"""
- return json.loads(self.content, self.encoding or 'utf-8')
+ return json.loads(self.content, self.encoding or "utf-8")
@property
def encoding(self):
@@ -343,8 +344,7 @@ def text(self):
"""
if self.encoding:
- return unicodedata.normalize('NFC', unicode(self.content,
- self.encoding))
+ return unicodedata.normalize("NFC", unicode(self.content, self.encoding))
return self.content
def iter_content(self, chunk_size=4096, decode_unicode=False):
@@ -360,23 +360,24 @@ def iter_content(self, chunk_size=4096, decode_unicode=False):
"""
if not self.stream:
- raise RuntimeError("You cannot call `iter_content` on a "
- "Response unless you passed `stream=True`"
- " to `get()`/`post()`/`request()`.")
+ raise RuntimeError(
+ "You cannot call `iter_content` on a "
+ "Response unless you passed `stream=True`"
+ " to `get()`/`post()`/`request()`."
+ )
if self._content_loaded:
- raise RuntimeError(
- "`content` has already been read from this Response.")
+ raise RuntimeError("`content` has already been read from this Response.")
def decode_stream(iterator, r):
- dec = codecs.getincrementaldecoder(r.encoding)(errors='replace')
+ dec = codecs.getincrementaldecoder(r.encoding)(errors="replace")
for chunk in iterator:
data = dec.decode(chunk)
if data:
yield data
- data = dec.decode(b'', final=True)
+ data = dec.decode(b"", final=True)
if data: # pragma: no cover
yield data
@@ -416,7 +417,7 @@ def save_to_path(self, filepath):
self.stream = True
- with open(filepath, 'wb') as fileobj:
+ with open(filepath, "wb") as fileobj:
for data in self.iter_content():
fileobj.write(data)
@@ -439,39 +440,40 @@ def _get_encoding(self):
headers = self.raw.info()
encoding = None
- if headers.getparam('charset'):
- encoding = headers.getparam('charset')
+ if headers.getparam("charset"):
+ encoding = headers.getparam("charset")
# HTTP Content-Type header
for param in headers.getplist():
- if param.startswith('charset='):
+ if param.startswith("charset="):
encoding = param[8:]
break
if not self.stream: # Try sniffing response content
# Encoding declared in document should override HTTP headers
- if self.mimetype == 'text/html': # sniff HTML headers
- m = re.search(r"""""",
- self.content)
+ if self.mimetype == "text/html": # sniff HTML headers
+ m = re.search(r"""""", self.content)
if m:
encoding = m.group(1)
- elif ((self.mimetype.startswith('application/')
- or self.mimetype.startswith('text/'))
- and 'xml' in self.mimetype):
- m = re.search(r"""]*\?>""",
- self.content)
+ elif (
+ self.mimetype.startswith("application/")
+ or self.mimetype.startswith("text/")
+ ) and "xml" in self.mimetype:
+ m = re.search(
+ r"""]*\?>""", self.content
+ )
if m:
encoding = m.group(1)
# Format defaults
- if self.mimetype == 'application/json' and not encoding:
+ if self.mimetype == "application/json" and not encoding:
# The default encoding for JSON
- encoding = 'utf-8'
+ encoding = "utf-8"
- elif self.mimetype == 'application/xml' and not encoding:
+ elif self.mimetype == "application/xml" and not encoding:
# The default for 'application/xml'
- encoding = 'utf-8'
+ encoding = "utf-8"
if encoding:
encoding = encoding.lower()
@@ -479,9 +481,19 @@ def _get_encoding(self):
return encoding
-def request(method, url, params=None, data=None, headers=None, cookies=None,
- files=None, auth=None, timeout=60, allow_redirects=False,
- stream=False):
+def request(
+ method,
+ url,
+ params=None,
+ data=None,
+ headers=None,
+ cookies=None,
+ files=None,
+ auth=None,
+ timeout=60,
+ allow_redirects=False,
+ stream=False,
+):
"""Initiate an HTTP(S) request. Returns :class:`Response` object.
:param method: 'GET' or 'POST'
@@ -549,16 +561,15 @@ def request(method, url, params=None, data=None, headers=None, cookies=None,
else:
headers = CaseInsensitiveDictionary(headers)
- if 'user-agent' not in headers:
- headers['user-agent'] = USER_AGENT
+ if "user-agent" not in headers:
+ headers["user-agent"] = USER_AGENT
# Accept gzip-encoded content
- encodings = [s.strip() for s in
- headers.get('accept-encoding', '').split(',')]
- if 'gzip' not in encodings:
- encodings.append('gzip')
+ encodings = [s.strip() for s in headers.get("accept-encoding", "").split(",")]
+ if "gzip" not in encodings:
+ encodings.append("gzip")
- headers['accept-encoding'] = ', '.join(encodings)
+ headers["accept-encoding"] = ", ".join(encodings)
if files:
if not data:
@@ -572,7 +583,7 @@ def request(method, url, params=None, data=None, headers=None, cookies=None,
headers = str_dict(headers)
if isinstance(url, unicode):
- url = url.encode('utf-8')
+ url = url.encode("utf-8")
if params: # GET args (POST args are handled in encode_multipart_formdata)
@@ -591,50 +602,126 @@ def request(method, url, params=None, data=None, headers=None, cookies=None,
return Response(req, stream)
-def get(url, params=None, headers=None, cookies=None, auth=None,
- timeout=60, allow_redirects=True, stream=False):
+def get(
+ url,
+ params=None,
+ headers=None,
+ cookies=None,
+ auth=None,
+ timeout=60,
+ allow_redirects=True,
+ stream=False,
+):
"""Initiate a GET request. Arguments as for :func:`request`.
:returns: :class:`Response` instance
"""
- return request('GET', url, params, headers=headers, cookies=cookies,
- auth=auth, timeout=timeout, allow_redirects=allow_redirects,
- stream=stream)
-
-
-def delete(url, params=None, data=None, headers=None, cookies=None, auth=None,
- timeout=60, allow_redirects=True, stream=False):
+ return request(
+ "GET",
+ url,
+ params,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ timeout=timeout,
+ allow_redirects=allow_redirects,
+ stream=stream,
+ )
+
+
+def delete(
+ url,
+ params=None,
+ data=None,
+ headers=None,
+ cookies=None,
+ auth=None,
+ timeout=60,
+ allow_redirects=True,
+ stream=False,
+):
"""Initiate a DELETE request. Arguments as for :func:`request`.
:returns: :class:`Response` instance
"""
- return request('DELETE', url, params, data, headers=headers,
- cookies=cookies, auth=auth, timeout=timeout,
- allow_redirects=allow_redirects, stream=stream)
-
-
-def post(url, params=None, data=None, headers=None, cookies=None, files=None,
- auth=None, timeout=60, allow_redirects=False, stream=False):
+ return request(
+ "DELETE",
+ url,
+ params,
+ data,
+ headers=headers,
+ cookies=cookies,
+ auth=auth,
+ timeout=timeout,
+ allow_redirects=allow_redirects,
+ stream=stream,
+ )
+
+
+def post(
+ url,
+ params=None,
+ data=None,
+ headers=None,
+ cookies=None,
+ files=None,
+ auth=None,
+ timeout=60,
+ allow_redirects=False,
+ stream=False,
+):
"""Initiate a POST request. Arguments as for :func:`request`.
:returns: :class:`Response` instance
"""
- return request('POST', url, params, data, headers, cookies, files, auth,
- timeout, allow_redirects, stream)
-
-
-def put(url, params=None, data=None, headers=None, cookies=None, files=None,
- auth=None, timeout=60, allow_redirects=False, stream=False):
+ return request(
+ "POST",
+ url,
+ params,
+ data,
+ headers,
+ cookies,
+ files,
+ auth,
+ timeout,
+ allow_redirects,
+ stream,
+ )
+
+
+def put(
+ url,
+ params=None,
+ data=None,
+ headers=None,
+ cookies=None,
+ files=None,
+ auth=None,
+ timeout=60,
+ allow_redirects=False,
+ stream=False,
+):
"""Initiate a PUT request. Arguments as for :func:`request`.
:returns: :class:`Response` instance
"""
- return request('PUT', url, params, data, headers, cookies, files, auth,
- timeout, allow_redirects, stream)
+ return request(
+ "PUT",
+ url,
+ params,
+ data,
+ headers,
+ cookies,
+ files,
+ auth,
+ timeout,
+ allow_redirects,
+ stream,
+ )
def encode_multipart_formdata(fields, files):
@@ -662,6 +749,7 @@ def encode_multipart_formdata(fields, files):
will be used.
"""
+
def get_content_type(filename):
"""Return or guess mimetype of ``filename``.
@@ -671,50 +759,51 @@ def get_content_type(filename):
:rtype: str
"""
- return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+ return mimetypes.guess_type(filename)[0] or "application/octet-stream"
- boundary = '-----' + ''.join(random.choice(BOUNDARY_CHARS)
- for i in range(30))
- CRLF = '\r\n'
+ boundary = "-----" + "".join(random.choice(BOUNDARY_CHARS) for i in range(30))
+ CRLF = "\r\n"
output = []
# Normal form fields
for (name, value) in fields.items():
if isinstance(name, unicode):
- name = name.encode('utf-8')
+ name = name.encode("utf-8")
if isinstance(value, unicode):
- value = value.encode('utf-8')
- output.append('--' + boundary)
+ value = value.encode("utf-8")
+ output.append("--" + boundary)
output.append('Content-Disposition: form-data; name="%s"' % name)
- output.append('')
+ output.append("")
output.append(value)
# Files to upload
for name, d in files.items():
- filename = d[u'filename']
- content = d[u'content']
- if u'mimetype' in d:
- mimetype = d[u'mimetype']
+ filename = d[u"filename"]
+ content = d[u"content"]
+ if u"mimetype" in d:
+ mimetype = d[u"mimetype"]
else:
mimetype = get_content_type(filename)
if isinstance(name, unicode):
- name = name.encode('utf-8')
+ name = name.encode("utf-8")
if isinstance(filename, unicode):
- filename = filename.encode('utf-8')
+ filename = filename.encode("utf-8")
if isinstance(mimetype, unicode):
- mimetype = mimetype.encode('utf-8')
- output.append('--' + boundary)
- output.append('Content-Disposition: form-data; '
- 'name="%s"; filename="%s"' % (name, filename))
- output.append('Content-Type: %s' % mimetype)
- output.append('')
+ mimetype = mimetype.encode("utf-8")
+ output.append("--" + boundary)
+ output.append(
+ "Content-Disposition: form-data; "
+ 'name="%s"; filename="%s"' % (name, filename)
+ )
+ output.append("Content-Type: %s" % mimetype)
+ output.append("")
output.append(content)
- output.append('--' + boundary + '--')
- output.append('')
+ output.append("--" + boundary + "--")
+ output.append("")
body = CRLF.join(output)
headers = {
- 'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
- 'Content-Length': str(len(body)),
+ "Content-Type": "multipart/form-data; boundary=%s" % boundary,
+ "Content-Length": str(len(body)),
}
return (headers, body)
diff --git a/workflow/workflow.py b/workflow/workflow.py
index 3935227..a00baeb 100644
--- a/workflow/workflow.py
+++ b/workflow/workflow.py
@@ -68,32 +68,32 @@
# The system icons are all in this directory. There are many more than
# are listed here
-ICON_ROOT = '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources'
+ICON_ROOT = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources"
-ICON_ACCOUNT = os.path.join(ICON_ROOT, 'Accounts.icns')
-ICON_BURN = os.path.join(ICON_ROOT, 'BurningIcon.icns')
-ICON_CLOCK = os.path.join(ICON_ROOT, 'Clock.icns')
-ICON_COLOR = os.path.join(ICON_ROOT, 'ProfileBackgroundColor.icns')
+ICON_ACCOUNT = os.path.join(ICON_ROOT, "Accounts.icns")
+ICON_BURN = os.path.join(ICON_ROOT, "BurningIcon.icns")
+ICON_CLOCK = os.path.join(ICON_ROOT, "Clock.icns")
+ICON_COLOR = os.path.join(ICON_ROOT, "ProfileBackgroundColor.icns")
ICON_COLOUR = ICON_COLOR # Queen's English, if you please
-ICON_EJECT = os.path.join(ICON_ROOT, 'EjectMediaIcon.icns')
+ICON_EJECT = os.path.join(ICON_ROOT, "EjectMediaIcon.icns")
# Shown when a workflow throws an error
-ICON_ERROR = os.path.join(ICON_ROOT, 'AlertStopIcon.icns')
-ICON_FAVORITE = os.path.join(ICON_ROOT, 'ToolbarFavoritesIcon.icns')
+ICON_ERROR = os.path.join(ICON_ROOT, "AlertStopIcon.icns")
+ICON_FAVORITE = os.path.join(ICON_ROOT, "ToolbarFavoritesIcon.icns")
ICON_FAVOURITE = ICON_FAVORITE
-ICON_GROUP = os.path.join(ICON_ROOT, 'GroupIcon.icns')
-ICON_HELP = os.path.join(ICON_ROOT, 'HelpIcon.icns')
-ICON_HOME = os.path.join(ICON_ROOT, 'HomeFolderIcon.icns')
-ICON_INFO = os.path.join(ICON_ROOT, 'ToolbarInfo.icns')
-ICON_NETWORK = os.path.join(ICON_ROOT, 'GenericNetworkIcon.icns')
-ICON_NOTE = os.path.join(ICON_ROOT, 'AlertNoteIcon.icns')
-ICON_SETTINGS = os.path.join(ICON_ROOT, 'ToolbarAdvanced.icns')
-ICON_SWIRL = os.path.join(ICON_ROOT, 'ErasingIcon.icns')
-ICON_SWITCH = os.path.join(ICON_ROOT, 'General.icns')
-ICON_SYNC = os.path.join(ICON_ROOT, 'Sync.icns')
-ICON_TRASH = os.path.join(ICON_ROOT, 'TrashIcon.icns')
-ICON_USER = os.path.join(ICON_ROOT, 'UserIcon.icns')
-ICON_WARNING = os.path.join(ICON_ROOT, 'AlertCautionIcon.icns')
-ICON_WEB = os.path.join(ICON_ROOT, 'BookmarkIcon.icns')
+ICON_GROUP = os.path.join(ICON_ROOT, "GroupIcon.icns")
+ICON_HELP = os.path.join(ICON_ROOT, "HelpIcon.icns")
+ICON_HOME = os.path.join(ICON_ROOT, "HomeFolderIcon.icns")
+ICON_INFO = os.path.join(ICON_ROOT, "ToolbarInfo.icns")
+ICON_NETWORK = os.path.join(ICON_ROOT, "GenericNetworkIcon.icns")
+ICON_NOTE = os.path.join(ICON_ROOT, "AlertNoteIcon.icns")
+ICON_SETTINGS = os.path.join(ICON_ROOT, "ToolbarAdvanced.icns")
+ICON_SWIRL = os.path.join(ICON_ROOT, "ErasingIcon.icns")
+ICON_SWITCH = os.path.join(ICON_ROOT, "General.icns")
+ICON_SYNC = os.path.join(ICON_ROOT, "Sync.icns")
+ICON_TRASH = os.path.join(ICON_ROOT, "TrashIcon.icns")
+ICON_USER = os.path.join(ICON_ROOT, "UserIcon.icns")
+ICON_WARNING = os.path.join(ICON_ROOT, "AlertCautionIcon.icns")
+ICON_WEB = os.path.join(ICON_ROOT, "BookmarkIcon.icns")
####################################################################
# non-ASCII to ASCII diacritic folding.
@@ -101,241 +101,241 @@
####################################################################
ASCII_REPLACEMENTS = {
- 'À': 'A',
- 'Á': 'A',
- 'Â': 'A',
- 'Ã': 'A',
- 'Ä': 'A',
- 'Å': 'A',
- 'Æ': 'AE',
- 'Ç': 'C',
- 'È': 'E',
- 'É': 'E',
- 'Ê': 'E',
- 'Ë': 'E',
- 'Ì': 'I',
- 'Í': 'I',
- 'Î': 'I',
- 'Ï': 'I',
- 'Ð': 'D',
- 'Ñ': 'N',
- 'Ò': 'O',
- 'Ó': 'O',
- 'Ô': 'O',
- 'Õ': 'O',
- 'Ö': 'O',
- 'Ø': 'O',
- 'Ù': 'U',
- 'Ú': 'U',
- 'Û': 'U',
- 'Ü': 'U',
- 'Ý': 'Y',
- 'Þ': 'Th',
- 'ß': 'ss',
- 'à': 'a',
- 'á': 'a',
- 'â': 'a',
- 'ã': 'a',
- 'ä': 'a',
- 'å': 'a',
- 'æ': 'ae',
- 'ç': 'c',
- 'è': 'e',
- 'é': 'e',
- 'ê': 'e',
- 'ë': 'e',
- 'ì': 'i',
- 'í': 'i',
- 'î': 'i',
- 'ï': 'i',
- 'ð': 'd',
- 'ñ': 'n',
- 'ò': 'o',
- 'ó': 'o',
- 'ô': 'o',
- 'õ': 'o',
- 'ö': 'o',
- 'ø': 'o',
- 'ù': 'u',
- 'ú': 'u',
- 'û': 'u',
- 'ü': 'u',
- 'ý': 'y',
- 'þ': 'th',
- 'ÿ': 'y',
- 'Ł': 'L',
- 'ł': 'l',
- 'Ń': 'N',
- 'ń': 'n',
- 'Ņ': 'N',
- 'ņ': 'n',
- 'Ň': 'N',
- 'ň': 'n',
- 'Ŋ': 'ng',
- 'ŋ': 'NG',
- 'Ō': 'O',
- 'ō': 'o',
- 'Ŏ': 'O',
- 'ŏ': 'o',
- 'Ő': 'O',
- 'ő': 'o',
- 'Œ': 'OE',
- 'œ': 'oe',
- 'Ŕ': 'R',
- 'ŕ': 'r',
- 'Ŗ': 'R',
- 'ŗ': 'r',
- 'Ř': 'R',
- 'ř': 'r',
- 'Ś': 'S',
- 'ś': 's',
- 'Ŝ': 'S',
- 'ŝ': 's',
- 'Ş': 'S',
- 'ş': 's',
- 'Š': 'S',
- 'š': 's',
- 'Ţ': 'T',
- 'ţ': 't',
- 'Ť': 'T',
- 'ť': 't',
- 'Ŧ': 'T',
- 'ŧ': 't',
- 'Ũ': 'U',
- 'ũ': 'u',
- 'Ū': 'U',
- 'ū': 'u',
- 'Ŭ': 'U',
- 'ŭ': 'u',
- 'Ů': 'U',
- 'ů': 'u',
- 'Ű': 'U',
- 'ű': 'u',
- 'Ŵ': 'W',
- 'ŵ': 'w',
- 'Ŷ': 'Y',
- 'ŷ': 'y',
- 'Ÿ': 'Y',
- 'Ź': 'Z',
- 'ź': 'z',
- 'Ż': 'Z',
- 'ż': 'z',
- 'Ž': 'Z',
- 'ž': 'z',
- 'ſ': 's',
- 'Α': 'A',
- 'Β': 'B',
- 'Γ': 'G',
- 'Δ': 'D',
- 'Ε': 'E',
- 'Ζ': 'Z',
- 'Η': 'E',
- 'Θ': 'Th',
- 'Ι': 'I',
- 'Κ': 'K',
- 'Λ': 'L',
- 'Μ': 'M',
- 'Ν': 'N',
- 'Ξ': 'Ks',
- 'Ο': 'O',
- 'Π': 'P',
- 'Ρ': 'R',
- 'Σ': 'S',
- 'Τ': 'T',
- 'Υ': 'U',
- 'Φ': 'Ph',
- 'Χ': 'Kh',
- 'Ψ': 'Ps',
- 'Ω': 'O',
- 'α': 'a',
- 'β': 'b',
- 'γ': 'g',
- 'δ': 'd',
- 'ε': 'e',
- 'ζ': 'z',
- 'η': 'e',
- 'θ': 'th',
- 'ι': 'i',
- 'κ': 'k',
- 'λ': 'l',
- 'μ': 'm',
- 'ν': 'n',
- 'ξ': 'x',
- 'ο': 'o',
- 'π': 'p',
- 'ρ': 'r',
- 'ς': 's',
- 'σ': 's',
- 'τ': 't',
- 'υ': 'u',
- 'φ': 'ph',
- 'χ': 'kh',
- 'ψ': 'ps',
- 'ω': 'o',
- 'А': 'A',
- 'Б': 'B',
- 'В': 'V',
- 'Г': 'G',
- 'Д': 'D',
- 'Е': 'E',
- 'Ж': 'Zh',
- 'З': 'Z',
- 'И': 'I',
- 'Й': 'I',
- 'К': 'K',
- 'Л': 'L',
- 'М': 'M',
- 'Н': 'N',
- 'О': 'O',
- 'П': 'P',
- 'Р': 'R',
- 'С': 'S',
- 'Т': 'T',
- 'У': 'U',
- 'Ф': 'F',
- 'Х': 'Kh',
- 'Ц': 'Ts',
- 'Ч': 'Ch',
- 'Ш': 'Sh',
- 'Щ': 'Shch',
- 'Ъ': "'",
- 'Ы': 'Y',
- 'Ь': "'",
- 'Э': 'E',
- 'Ю': 'Iu',
- 'Я': 'Ia',
- 'а': 'a',
- 'б': 'b',
- 'в': 'v',
- 'г': 'g',
- 'д': 'd',
- 'е': 'e',
- 'ж': 'zh',
- 'з': 'z',
- 'и': 'i',
- 'й': 'i',
- 'к': 'k',
- 'л': 'l',
- 'м': 'm',
- 'н': 'n',
- 'о': 'o',
- 'п': 'p',
- 'р': 'r',
- 'с': 's',
- 'т': 't',
- 'у': 'u',
- 'ф': 'f',
- 'х': 'kh',
- 'ц': 'ts',
- 'ч': 'ch',
- 'ш': 'sh',
- 'щ': 'shch',
- 'ъ': "'",
- 'ы': 'y',
- 'ь': "'",
- 'э': 'e',
- 'ю': 'iu',
- 'я': 'ia',
+ "À": "A",
+ "Á": "A",
+ "Â": "A",
+ "Ã": "A",
+ "Ä": "A",
+ "Å": "A",
+ "Æ": "AE",
+ "Ç": "C",
+ "È": "E",
+ "É": "E",
+ "Ê": "E",
+ "Ë": "E",
+ "Ì": "I",
+ "Í": "I",
+ "Î": "I",
+ "Ï": "I",
+ "Ð": "D",
+ "Ñ": "N",
+ "Ò": "O",
+ "Ó": "O",
+ "Ô": "O",
+ "Õ": "O",
+ "Ö": "O",
+ "Ø": "O",
+ "Ù": "U",
+ "Ú": "U",
+ "Û": "U",
+ "Ü": "U",
+ "Ý": "Y",
+ "Þ": "Th",
+ "ß": "ss",
+ "à": "a",
+ "á": "a",
+ "â": "a",
+ "ã": "a",
+ "ä": "a",
+ "å": "a",
+ "æ": "ae",
+ "ç": "c",
+ "è": "e",
+ "é": "e",
+ "ê": "e",
+ "ë": "e",
+ "ì": "i",
+ "í": "i",
+ "î": "i",
+ "ï": "i",
+ "ð": "d",
+ "ñ": "n",
+ "ò": "o",
+ "ó": "o",
+ "ô": "o",
+ "õ": "o",
+ "ö": "o",
+ "ø": "o",
+ "ù": "u",
+ "ú": "u",
+ "û": "u",
+ "ü": "u",
+ "ý": "y",
+ "þ": "th",
+ "ÿ": "y",
+ "Ł": "L",
+ "ł": "l",
+ "Ń": "N",
+ "ń": "n",
+ "Ņ": "N",
+ "ņ": "n",
+ "Ň": "N",
+ "ň": "n",
+ "Ŋ": "ng",
+ "ŋ": "NG",
+ "Ō": "O",
+ "ō": "o",
+ "Ŏ": "O",
+ "ŏ": "o",
+ "Ő": "O",
+ "ő": "o",
+ "Œ": "OE",
+ "œ": "oe",
+ "Ŕ": "R",
+ "ŕ": "r",
+ "Ŗ": "R",
+ "ŗ": "r",
+ "Ř": "R",
+ "ř": "r",
+ "Ś": "S",
+ "ś": "s",
+ "Ŝ": "S",
+ "ŝ": "s",
+ "Ş": "S",
+ "ş": "s",
+ "Š": "S",
+ "š": "s",
+ "Ţ": "T",
+ "ţ": "t",
+ "Ť": "T",
+ "ť": "t",
+ "Ŧ": "T",
+ "ŧ": "t",
+ "Ũ": "U",
+ "ũ": "u",
+ "Ū": "U",
+ "ū": "u",
+ "Ŭ": "U",
+ "ŭ": "u",
+ "Ů": "U",
+ "ů": "u",
+ "Ű": "U",
+ "ű": "u",
+ "Ŵ": "W",
+ "ŵ": "w",
+ "Ŷ": "Y",
+ "ŷ": "y",
+ "Ÿ": "Y",
+ "Ź": "Z",
+ "ź": "z",
+ "Ż": "Z",
+ "ż": "z",
+ "Ž": "Z",
+ "ž": "z",
+ "ſ": "s",
+ "Α": "A",
+ "Β": "B",
+ "Γ": "G",
+ "Δ": "D",
+ "Ε": "E",
+ "Ζ": "Z",
+ "Η": "E",
+ "Θ": "Th",
+ "Ι": "I",
+ "Κ": "K",
+ "Λ": "L",
+ "Μ": "M",
+ "Ν": "N",
+ "Ξ": "Ks",
+ "Ο": "O",
+ "Π": "P",
+ "Ρ": "R",
+ "Σ": "S",
+ "Τ": "T",
+ "Υ": "U",
+ "Φ": "Ph",
+ "Χ": "Kh",
+ "Ψ": "Ps",
+ "Ω": "O",
+ "α": "a",
+ "β": "b",
+ "γ": "g",
+ "δ": "d",
+ "ε": "e",
+ "ζ": "z",
+ "η": "e",
+ "θ": "th",
+ "ι": "i",
+ "κ": "k",
+ "λ": "l",
+ "μ": "m",
+ "ν": "n",
+ "ξ": "x",
+ "ο": "o",
+ "π": "p",
+ "ρ": "r",
+ "ς": "s",
+ "σ": "s",
+ "τ": "t",
+ "υ": "u",
+ "φ": "ph",
+ "χ": "kh",
+ "ψ": "ps",
+ "ω": "o",
+ "А": "A",
+ "Б": "B",
+ "В": "V",
+ "Г": "G",
+ "Д": "D",
+ "Е": "E",
+ "Ж": "Zh",
+ "З": "Z",
+ "И": "I",
+ "Й": "I",
+ "К": "K",
+ "Л": "L",
+ "М": "M",
+ "Н": "N",
+ "О": "O",
+ "П": "P",
+ "Р": "R",
+ "С": "S",
+ "Т": "T",
+ "У": "U",
+ "Ф": "F",
+ "Х": "Kh",
+ "Ц": "Ts",
+ "Ч": "Ch",
+ "Ш": "Sh",
+ "Щ": "Shch",
+ "Ъ": "'",
+ "Ы": "Y",
+ "Ь": "'",
+ "Э": "E",
+ "Ю": "Iu",
+ "Я": "Ia",
+ "а": "a",
+ "б": "b",
+ "в": "v",
+ "г": "g",
+ "д": "d",
+ "е": "e",
+ "ж": "zh",
+ "з": "z",
+ "и": "i",
+ "й": "i",
+ "к": "k",
+ "л": "l",
+ "м": "m",
+ "н": "n",
+ "о": "o",
+ "п": "p",
+ "р": "r",
+ "с": "s",
+ "т": "t",
+ "у": "u",
+ "ф": "f",
+ "х": "kh",
+ "ц": "ts",
+ "ч": "ch",
+ "ш": "sh",
+ "щ": "shch",
+ "ъ": "'",
+ "ы": "y",
+ "ь": "'",
+ "э": "e",
+ "ю": "iu",
+ "я": "ia",
# 'ᴀ': '',
# 'ᴁ': '',
# 'ᴂ': '',
@@ -374,18 +374,18 @@
# 'ᴣ': '',
# 'ᴤ': '',
# 'ᴥ': '',
- 'ᴦ': 'G',
- 'ᴧ': 'L',
- 'ᴨ': 'P',
- 'ᴩ': 'R',
- 'ᴪ': 'PS',
- 'ẞ': 'Ss',
- 'Ỳ': 'Y',
- 'ỳ': 'y',
- 'Ỵ': 'Y',
- 'ỵ': 'y',
- 'Ỹ': 'Y',
- 'ỹ': 'y',
+ "ᴦ": "G",
+ "ᴧ": "L",
+ "ᴨ": "P",
+ "ᴩ": "R",
+ "ᴪ": "PS",
+ "ẞ": "Ss",
+ "Ỳ": "Y",
+ "ỳ": "y",
+ "Ỵ": "Y",
+ "ỵ": "y",
+ "Ỹ": "Y",
+ "ỹ": "y",
}
####################################################################
@@ -393,14 +393,14 @@
####################################################################
DUMB_PUNCTUATION = {
- '‘': "'",
- '’': "'",
- '‚': "'",
- '“': '"',
- '”': '"',
- '„': '"',
- '–': '-',
- '—': '-'
+ "‘": "'",
+ "’": "'",
+ "‚": "'",
+ "“": '"',
+ "”": '"',
+ "„": '"',
+ "–": "-",
+ "—": "-",
}
@@ -413,7 +413,7 @@
INITIALS = string.ascii_uppercase + string.digits
#: Split on non-letters, numbers
-split_on_delimiters = re.compile('[^a-zA-Z0-9]').split
+split_on_delimiters = re.compile("[^a-zA-Z0-9]").split
# Match filter flags
#: Match items that start with ``query``
@@ -483,6 +483,7 @@ class PasswordExists(KeychainError):
# Helper functions
####################################################################
+
def isascii(text):
"""Test if ``text`` contains only ASCII characters.
@@ -493,7 +494,7 @@ def isascii(text):
"""
try:
- text.encode('ascii')
+ text.encode("ascii")
except UnicodeEncodeError:
return False
return True
@@ -503,6 +504,7 @@ def isascii(text):
# Implementation classes
####################################################################
+
class SerializerManager(object):
"""Contains registered serializers.
@@ -540,8 +542,8 @@ def register(self, name, serializer):
"""
# Basic validation
- getattr(serializer, 'load')
- getattr(serializer, 'dump')
+ getattr(serializer, "load")
+ getattr(serializer, "dump")
self._serializers[name] = serializer
@@ -568,8 +570,7 @@ def unregister(self, name):
"""
if name not in self._serializers:
- raise ValueError('No such serializer registered : {0}'.format(
- name))
+ raise ValueError("No such serializer registered : {0}".format(name))
serializer = self._serializers[name]
del self._serializers[name]
@@ -619,7 +620,7 @@ def dump(cls, obj, file_obj):
:type file_obj: ``file`` object
"""
- return json.dump(obj, file_obj, indent=2, encoding='utf-8')
+ return json.dump(obj, file_obj, indent=2, encoding="utf-8")
class CPickleSerializer(object):
@@ -701,9 +702,9 @@ def dump(cls, obj, file_obj):
# Set up default manager and register built-in serializers
manager = SerializerManager()
-manager.register('cpickle', CPickleSerializer)
-manager.register('pickle', PickleSerializer)
-manager.register('json', JSONSerializer)
+manager.register("cpickle", CPickleSerializer)
+manager.register("pickle", PickleSerializer)
+manager.register("json", JSONSerializer)
class Item(object):
@@ -717,10 +718,22 @@ class Item(object):
"""
- def __init__(self, title, subtitle='', modifier_subtitles=None,
- arg=None, autocomplete=None, valid=False, uid=None,
- icon=None, icontype=None, type=None, largetext=None,
- copytext=None, quicklookurl=None):
+ def __init__(
+ self,
+ title,
+ subtitle="",
+ modifier_subtitles=None,
+ arg=None,
+ autocomplete=None,
+ valid=False,
+ uid=None,
+ icon=None,
+ icontype=None,
+ type=None,
+ largetext=None,
+ copytext=None,
+ quicklookurl=None,
+ ):
"""Same arguments as :meth:`Workflow.add_item`."""
self.title = title
self.subtitle = subtitle
@@ -747,35 +760,36 @@ def elem(self):
# Attributes on - element
attr = {}
if self.valid:
- attr['valid'] = 'yes'
+ attr["valid"] = "yes"
else:
- attr['valid'] = 'no'
+ attr["valid"] = "no"
# Allow empty string for autocomplete. This is a useful value,
# as TABing the result will revert the query back to just the
# keyword
if self.autocomplete is not None:
- attr['autocomplete'] = self.autocomplete
+ attr["autocomplete"] = self.autocomplete
# Optional attributes
- for name in ('uid', 'type'):
+ for name in ("uid", "type"):
value = getattr(self, name, None)
if value:
attr[name] = value
- root = ET.Element('item', attr)
- ET.SubElement(root, 'title').text = self.title
- ET.SubElement(root, 'subtitle').text = self.subtitle
+ root = ET.Element("item", attr)
+ ET.SubElement(root, "title").text = self.title
+ ET.SubElement(root, "subtitle").text = self.subtitle
# Add modifier subtitles
- for mod in ('cmd', 'ctrl', 'alt', 'shift', 'fn'):
+ for mod in ("cmd", "ctrl", "alt", "shift", "fn"):
if mod in self.modifier_subtitles:
- ET.SubElement(root, 'subtitle',
- {'mod': mod}).text = self.modifier_subtitles[mod]
+ ET.SubElement(
+ root, "subtitle", {"mod": mod}
+ ).text = self.modifier_subtitles[mod]
# Add arg as element instead of attribute on
- , as it's more
# flexible (newlines aren't allowed in attributes)
if self.arg:
- ET.SubElement(root, 'arg').text = self.arg
+ ET.SubElement(root, "arg").text = self.arg
# Add icon if there is one
if self.icon:
@@ -783,18 +797,16 @@ def elem(self):
attr = dict(type=self.icontype)
else:
attr = {}
- ET.SubElement(root, 'icon', attr).text = self.icon
+ ET.SubElement(root, "icon", attr).text = self.icon
if self.largetext:
- ET.SubElement(root, 'text',
- {'type': 'largetype'}).text = self.largetext
+ ET.SubElement(root, "text", {"type": "largetype"}).text = self.largetext
if self.copytext:
- ET.SubElement(root, 'text',
- {'type': 'copy'}).text = self.copytext
+ ET.SubElement(root, "text", {"type": "copy"}).text = self.copytext
if self.quicklookurl:
- ET.SubElement(root, 'quicklookurl').text = self.quicklookurl
+ ET.SubElement(root, "quicklookurl").text = self.quicklookurl
return root
@@ -834,7 +846,7 @@ def _load(self):
"""Load cached settings from JSON file `self._filepath`."""
data = {}
with LockFile(self._filepath, 0.5):
- with open(self._filepath, 'rb') as fp:
+ with open(self._filepath, "rb") as fp:
data.update(json.load(fp))
self._original = deepcopy(data)
@@ -858,9 +870,8 @@ def save(self):
data.update(self)
with LockFile(self._filepath, 0.5):
- with atomic_writer(self._filepath, 'wb') as fp:
- json.dump(data, fp, sort_keys=True, indent=2,
- encoding='utf-8')
+ with atomic_writer(self._filepath, "wb") as fp:
+ json.dump(data, fp, sort_keys=True, indent=2, encoding="utf-8")
# dict methods
def __setitem__(self, key, value):
@@ -936,10 +947,16 @@ class Workflow(object):
# won't want to change this
item_class = Item
- def __init__(self, default_settings=None, update_settings=None,
- input_encoding='utf-8', normalization='NFC',
- capture_args=True, libraries=None,
- help_url=None):
+ def __init__(
+ self,
+ default_settings=None,
+ update_settings=None,
+ input_encoding="utf-8",
+ normalization="NFC",
+ capture_args=True,
+ libraries=None,
+ help_url=None,
+ ):
"""Create new :class:`Workflow` object."""
self._default_settings = default_settings or {}
self._update_settings = update_settings or {}
@@ -953,8 +970,8 @@ def __init__(self, default_settings=None, update_settings=None,
self._bundleid = None
self._debugging = None
self._name = None
- self._cache_serializer = 'cpickle'
- self._data_serializer = 'cpickle'
+ self._cache_serializer = "cpickle"
+ self._data_serializer = "cpickle"
self._info = None
self._info_loaded = False
self._logger = None
@@ -969,7 +986,7 @@ def __init__(self, default_settings=None, update_settings=None,
#: Prefix for all magic arguments.
#: The default value is ``workflow:`` so keyword
#: ``config`` would match user query ``workflow:config``.
- self.magic_prefix = 'workflow:'
+ self.magic_prefix = "workflow:"
#: Mapping of available magic arguments. The built-in magic
#: arguments are registered by default. To add your own magic arguments
#: (or override built-ins), add a key:value pair where the key is
@@ -997,7 +1014,8 @@ def __init__(self, default_settings=None, update_settings=None,
def alfred_version(self):
"""Alfred version as :class:`~workflow.update.Version` object."""
from update import Version
- return Version(self.alfred_env.get('version'))
+
+ return Version(self.alfred_env.get("version"))
@property
def alfred_env(self):
@@ -1052,25 +1070,26 @@ def alfred_env(self):
data = {}
for key in (
- 'debug',
- 'preferences',
- 'preferences_localhash',
- 'theme',
- 'theme_background',
- 'theme_subtext',
- 'version',
- 'version_build',
- 'workflow_bundleid',
- 'workflow_cache',
- 'workflow_data',
- 'workflow_name',
- 'workflow_uid',
- 'workflow_version'):
-
- value = os.getenv('alfred_' + key, '')
+ "debug",
+ "preferences",
+ "preferences_localhash",
+ "theme",
+ "theme_background",
+ "theme_subtext",
+ "version",
+ "version_build",
+ "workflow_bundleid",
+ "workflow_cache",
+ "workflow_data",
+ "workflow_name",
+ "workflow_uid",
+ "workflow_version",
+ ):
+
+ value = os.getenv("alfred_" + key, "")
if value:
- if key in ('debug', 'version_build', 'theme_subtext'):
+ if key in ("debug", "version_build", "theme_subtext"):
value = int(value)
else:
value = self.decode(value)
@@ -1097,10 +1116,10 @@ def bundleid(self):
"""
if not self._bundleid:
- if self.alfred_env.get('workflow_bundleid'):
- self._bundleid = self.alfred_env.get('workflow_bundleid')
+ if self.alfred_env.get("workflow_bundleid"):
+ self._bundleid = self.alfred_env.get("workflow_bundleid")
else:
- self._bundleid = unicode(self.info['bundleid'], 'utf-8')
+ self._bundleid = unicode(self.info["bundleid"], "utf-8")
return self._bundleid
@@ -1112,7 +1131,7 @@ def debugging(self):
:rtype: ``bool``
"""
- return self.alfred_env.get('debug') == 1
+ return self.alfred_env.get("debug") == 1
@property
def name(self):
@@ -1123,10 +1142,10 @@ def name(self):
"""
if not self._name:
- if self.alfred_env.get('workflow_name'):
- self._name = self.decode(self.alfred_env.get('workflow_name'))
+ if self.alfred_env.get("workflow_name"):
+ self._name = self.decode(self.alfred_env.get("workflow_name"))
else:
- self._name = self.decode(self.info['name'])
+ self._name = self.decode(self.info["name"])
return self._name
@@ -1151,27 +1170,28 @@ def version(self):
version = None
# environment variable has priority
- if self.alfred_env.get('workflow_version'):
- version = self.alfred_env['workflow_version']
+ if self.alfred_env.get("workflow_version"):
+ version = self.alfred_env["workflow_version"]
# Try `update_settings`
elif self._update_settings:
- version = self._update_settings.get('version')
+ version = self._update_settings.get("version")
# `version` file
if not version:
- filepath = self.workflowfile('version')
+ filepath = self.workflowfile("version")
if os.path.exists(filepath):
- with open(filepath, 'rb') as fileobj:
+ with open(filepath, "rb") as fileobj:
version = fileobj.read()
# info.plist
if not version:
- version = self.info.get('version')
+ version = self.info.get("version")
if version:
from update import Version
+
version = Version(version)
self._version = version
@@ -1204,7 +1224,7 @@ def args(self):
# Handle magic args
if len(args) and self._capture_args:
for name in self.magic_arguments:
- key = '{0}{1}'.format(self.magic_prefix, name)
+ key = "{0}{1}".format(self.magic_prefix, name)
if key in args:
msg = self.magic_arguments[name]()
@@ -1235,8 +1255,8 @@ def cachedir(self):
unicode: full path to workflow's cache directory
"""
- if self.alfred_env.get('workflow_cache'):
- dirpath = self.alfred_env.get('workflow_cache')
+ if self.alfred_env.get("workflow_cache"):
+ dirpath = self.alfred_env.get("workflow_cache")
else:
dirpath = self._default_cachedir
@@ -1248,9 +1268,10 @@ def _default_cachedir(self):
"""Alfred 2's default cache directory."""
return os.path.join(
os.path.expanduser(
- '~/Library/Caches/com.runningwithcrayons.Alfred-2/'
- 'Workflow Data/'),
- self.bundleid)
+ "~/Library/Caches/com.runningwithcrayons.Alfred-2/" "Workflow Data/"
+ ),
+ self.bundleid,
+ )
@property
def datadir(self):
@@ -1271,8 +1292,8 @@ def datadir(self):
unicode: full path to workflow data directory
"""
- if self.alfred_env.get('workflow_data'):
- dirpath = self.alfred_env.get('workflow_data')
+ if self.alfred_env.get("workflow_data"):
+ dirpath = self.alfred_env.get("workflow_data")
else:
dirpath = self._default_datadir
@@ -1282,9 +1303,10 @@ def datadir(self):
@property
def _default_datadir(self):
"""Alfred 2's default data directory."""
- return os.path.join(os.path.expanduser(
- '~/Library/Application Support/Alfred 2/Workflow Data/'),
- self.bundleid)
+ return os.path.join(
+ os.path.expanduser("~/Library/Application Support/Alfred 2/Workflow Data/"),
+ self.bundleid,
+ )
@property
def workflowdir(self):
@@ -1300,7 +1322,8 @@ def workflowdir(self):
# a workflow is being run in Alfred
candidates = [
os.path.abspath(os.getcwdu()),
- os.path.dirname(os.path.abspath(os.path.dirname(__file__)))]
+ os.path.dirname(os.path.abspath(os.path.dirname(__file__))),
+ ]
# climb the directory tree until we find `info.plist`
for dirpath in candidates:
@@ -1309,11 +1332,11 @@ def workflowdir(self):
dirpath = self.decode(dirpath)
while True:
- if os.path.exists(os.path.join(dirpath, 'info.plist')):
+ if os.path.exists(os.path.join(dirpath, "info.plist")):
self._workflowdir = dirpath
break
- elif dirpath == '/':
+ elif dirpath == "/":
# no `info.plist` found
break
@@ -1376,7 +1399,7 @@ def logfile(self):
:rtype: ``unicode``
"""
- return self.cachefile('%s.log' % self.bundleid)
+ return self.cachefile("%s.log" % self.bundleid)
@property
def logger(self):
@@ -1394,7 +1417,7 @@ def logger(self):
return self._logger
# Initialise new logger and optionally handlers
- logger = logging.getLogger('')
+ logger = logging.getLogger("")
# Only add one set of handlers
# Exclude from coverage, as pytest will have configured the
@@ -1402,14 +1425,13 @@ def logger(self):
if not len(logger.handlers): # pragma: no cover
fmt = logging.Formatter(
- '%(asctime)s %(filename)s:%(lineno)s'
- ' %(levelname)-8s %(message)s',
- datefmt='%H:%M:%S')
+ "%(asctime)s %(filename)s:%(lineno)s" " %(levelname)-8s %(message)s",
+ datefmt="%H:%M:%S",
+ )
logfile = logging.handlers.RotatingFileHandler(
- self.logfile,
- maxBytes=1024 * 1024,
- backupCount=1)
+ self.logfile, maxBytes=1024 * 1024, backupCount=1
+ )
logfile.setFormatter(fmt)
logger.addHandler(logfile)
@@ -1445,7 +1467,7 @@ def settings_path(self):
"""
if not self._settings_path:
- self._settings_path = self.datafile('settings.json')
+ self._settings_path = self.datafile("settings.json")
return self._settings_path
@property
@@ -1465,9 +1487,8 @@ def settings(self):
"""
if not self._settings:
- self.logger.debug('reading settings from %s', self.settings_path)
- self._settings = Settings(self.settings_path,
- self._default_settings)
+ self.logger.debug("reading settings from %s", self.settings_path)
+ self._settings = Settings(self.settings_path, self._default_settings)
return self._settings
@property
@@ -1506,10 +1527,11 @@ def cache_serializer(self, serializer_name):
"""
if manager.serializer(serializer_name) is None:
raise ValueError(
- 'Unknown serializer : `{0}`. Register your serializer '
- 'with `manager` first.'.format(serializer_name))
+ "Unknown serializer : `{0}`. Register your serializer "
+ "with `manager` first.".format(serializer_name)
+ )
- self.logger.debug('default cache serializer: %s', serializer_name)
+ self.logger.debug("default cache serializer: %s", serializer_name)
self._cache_serializer = serializer_name
@@ -1548,10 +1570,11 @@ def data_serializer(self, serializer_name):
"""
if manager.serializer(serializer_name) is None:
raise ValueError(
- 'Unknown serializer : `{0}`. Register your serializer '
- 'with `manager` first.'.format(serializer_name))
+ "Unknown serializer : `{0}`. Register your serializer "
+ "with `manager` first.".format(serializer_name)
+ )
- self.logger.debug('default data serializer: %s', serializer_name)
+ self.logger.debug("default data serializer: %s", serializer_name)
self._data_serializer = serializer_name
@@ -1565,39 +1588,40 @@ def stored_data(self, name):
:param name: name of datastore
"""
- metadata_path = self.datafile('.{0}.alfred-workflow'.format(name))
+ metadata_path = self.datafile(".{0}.alfred-workflow".format(name))
if not os.path.exists(metadata_path):
- self.logger.debug('no data stored for `%s`', name)
+ self.logger.debug("no data stored for `%s`", name)
return None
- with open(metadata_path, 'rb') as file_obj:
+ with open(metadata_path, "rb") as file_obj:
serializer_name = file_obj.read().strip()
serializer = manager.serializer(serializer_name)
if serializer is None:
raise ValueError(
- 'Unknown serializer `{0}`. Register a corresponding '
- 'serializer with `manager.register()` '
- 'to load this data.'.format(serializer_name))
+ "Unknown serializer `{0}`. Register a corresponding "
+ "serializer with `manager.register()` "
+ "to load this data.".format(serializer_name)
+ )
- self.logger.debug('data `%s` stored as `%s`', name, serializer_name)
+ self.logger.debug("data `%s` stored as `%s`", name, serializer_name)
- filename = '{0}.{1}'.format(name, serializer_name)
+ filename = "{0}.{1}".format(name, serializer_name)
data_path = self.datafile(filename)
if not os.path.exists(data_path):
- self.logger.debug('no data stored: %s', name)
+ self.logger.debug("no data stored: %s", name)
if os.path.exists(metadata_path):
os.unlink(metadata_path)
return None
- with open(data_path, 'rb') as file_obj:
+ with open(data_path, "rb") as file_obj:
data = serializer.load(file_obj)
- self.logger.debug('stored data loaded: %s', data_path)
+ self.logger.debug("stored data loaded: %s", data_path)
return data
@@ -1626,29 +1650,31 @@ def delete_paths(paths):
for path in paths:
if os.path.exists(path):
os.unlink(path)
- self.logger.debug('deleted data file: %s', path)
+ self.logger.debug("deleted data file: %s", path)
serializer_name = serializer or self.data_serializer
# In order for `stored_data()` to be able to load data stored with
# an arbitrary serializer, yet still have meaningful file extensions,
# the format (i.e. extension) is saved to an accompanying file
- metadata_path = self.datafile('.{0}.alfred-workflow'.format(name))
- filename = '{0}.{1}'.format(name, serializer_name)
+ metadata_path = self.datafile(".{0}.alfred-workflow".format(name))
+ filename = "{0}.{1}".format(name, serializer_name)
data_path = self.datafile(filename)
if data_path == self.settings_path:
raise ValueError(
- 'Cannot save data to' +
- '`{0}` with format `{1}`. '.format(name, serializer_name) +
- "This would overwrite Alfred-Workflow's settings file.")
+ "Cannot save data to"
+ + "`{0}` with format `{1}`. ".format(name, serializer_name)
+ + "This would overwrite Alfred-Workflow's settings file."
+ )
serializer = manager.serializer(serializer_name)
if serializer is None:
raise ValueError(
- 'Invalid serializer `{0}`. Register your serializer with '
- '`manager.register()` first.'.format(serializer_name))
+ "Invalid serializer `{0}`. Register your serializer with "
+ "`manager.register()` first.".format(serializer_name)
+ )
if data is None: # Delete cached data
delete_paths((metadata_path, data_path))
@@ -1658,15 +1684,15 @@ def delete_paths(paths):
@uninterruptible
def _store():
# Save file extension
- with atomic_writer(metadata_path, 'wb') as file_obj:
+ with atomic_writer(metadata_path, "wb") as file_obj:
file_obj.write(serializer_name)
- with atomic_writer(data_path, 'wb') as file_obj:
+ with atomic_writer(data_path, "wb") as file_obj:
serializer.dump(data, file_obj)
_store()
- self.logger.debug('saved data: %s', data_path)
+ self.logger.debug("saved data: %s", data_path)
def cached_data(self, name, data_func=None, max_age=60):
"""Return cached data if younger than ``max_age`` seconds.
@@ -1686,13 +1712,13 @@ def cached_data(self, name, data_func=None, max_age=60):
"""
serializer = manager.serializer(self.cache_serializer)
- cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
+ cache_path = self.cachefile("%s.%s" % (name, self.cache_serializer))
age = self.cached_data_age(name)
if (age < max_age or max_age == 0) and os.path.exists(cache_path):
- with open(cache_path, 'rb') as file_obj:
- self.logger.debug('loading cached data: %s', cache_path)
+ with open(cache_path, "rb") as file_obj:
+ self.logger.debug("loading cached data: %s", cache_path)
return serializer.load(file_obj)
if not data_func:
@@ -1716,18 +1742,18 @@ def cache_data(self, name, data):
"""
serializer = manager.serializer(self.cache_serializer)
- cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
+ cache_path = self.cachefile("%s.%s" % (name, self.cache_serializer))
if data is None:
if os.path.exists(cache_path):
os.unlink(cache_path)
- self.logger.debug('deleted cache file: %s', cache_path)
+ self.logger.debug("deleted cache file: %s", cache_path)
return
- with atomic_writer(cache_path, 'wb') as file_obj:
+ with atomic_writer(cache_path, "wb") as file_obj:
serializer.dump(data, file_obj)
- self.logger.debug('cached data: %s', cache_path)
+ self.logger.debug("cached data: %s", cache_path)
def cached_data_fresh(self, name, max_age):
"""Whether cache `name` is less than `max_age` seconds old.
@@ -1755,16 +1781,25 @@ def cached_data_age(self, name):
:rtype: ``int``
"""
- cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
+ cache_path = self.cachefile("%s.%s" % (name, self.cache_serializer))
if not os.path.exists(cache_path):
return 0
return time.time() - os.stat(cache_path).st_mtime
- def filter(self, query, items, key=lambda x: x, ascending=False,
- include_score=False, min_score=0, max_results=0,
- match_on=MATCH_ALL, fold_diacritics=True):
+ def filter(
+ self,
+ query,
+ items,
+ key=lambda x: x,
+ ascending=False,
+ include_score=False,
+ min_score=0,
+ max_results=0,
+ match_on=MATCH_ALL,
+ fold_diacritics=True,
+ ):
"""Fuzzy search filter. Returns list of ``items`` that match ``query``.
``query`` is case-insensitive. Any item that does not contain the
@@ -1873,23 +1908,23 @@ def filter(self, query, items, key=lambda x: x, ascending=False,
return items
# Use user override if there is one
- fold_diacritics = self.settings.get('__workflow_diacritic_folding',
- fold_diacritics)
+ fold_diacritics = self.settings.get(
+ "__workflow_diacritic_folding", fold_diacritics
+ )
results = []
for item in items:
skip = False
score = 0
- words = [s.strip() for s in query.split(' ')]
+ words = [s.strip() for s in query.split(" ")]
value = key(item).strip()
- if value == '':
+ if value == "":
continue
for word in words:
- if word == '':
+ if word == "":
continue
- s, rule = self._filter_item(value, word, match_on,
- fold_diacritics)
+ s, rule = self._filter_item(value, word, match_on, fold_diacritics)
if not s: # Skip items that don't match part of the query
skip = True
@@ -1902,8 +1937,9 @@ def filter(self, query, items, key=lambda x: x, ascending=False,
# use "reversed" `score` (i.e. highest becomes lowest) and
# `value` as sort key. This means items with the same score
# will be sorted in alphabetical not reverse alphabetical order
- results.append(((100.0 / score, value.lower(), score),
- (item, score, rule)))
+ results.append(
+ ((100.0 / score, value.lower(), score), (item, score, rule))
+ )
# sort on keys, then discard the keys
results.sort(reverse=ascending)
@@ -1950,7 +1986,7 @@ def _filter_item(self, value, query, match_on, fold_diacritics):
# query matches capitalised letters in item,
# e.g. of = OmniFocus
if match_on & MATCH_CAPITALS:
- initials = ''.join([c for c in value if c in INITIALS])
+ initials = "".join([c for c in value if c in INITIALS])
if initials.lower().startswith(query):
score = 100.0 - (len(initials) / len(query))
@@ -1958,13 +1994,15 @@ def _filter_item(self, value, query, match_on, fold_diacritics):
# split the item into "atoms", i.e. words separated by
# spaces or other non-word characters
- if (match_on & MATCH_ATOM or
- match_on & MATCH_INITIALS_CONTAIN or
- match_on & MATCH_INITIALS_STARTSWITH):
+ if (
+ match_on & MATCH_ATOM
+ or match_on & MATCH_INITIALS_CONTAIN
+ or match_on & MATCH_INITIALS_STARTSWITH
+ ):
atoms = [s.lower() for s in split_on_delimiters(value)]
# print('atoms : %s --> %s' % (value, atoms))
# initials of the atoms
- initials = ''.join([s[0] for s in atoms if s])
+ initials = "".join([s[0] for s in atoms if s])
if match_on & MATCH_ATOM:
# is `query` one of the atoms in item?
@@ -1979,16 +2017,14 @@ def _filter_item(self, value, query, match_on, fold_diacritics):
# atoms, e.g. ``himym`` matches "How I Met Your Mother"
# *and* "how i met your mother" (the ``capitals`` rule only
# matches the former)
- if (match_on & MATCH_INITIALS_STARTSWITH and
- initials.startswith(query)):
+ if match_on & MATCH_INITIALS_STARTSWITH and initials.startswith(query):
score = 100.0 - (len(initials) / len(query))
return (score, MATCH_INITIALS_STARTSWITH)
# `query` is a substring of initials, e.g. ``doh`` matches
# "The Dukes of Hazzard"
- elif (match_on & MATCH_INITIALS_CONTAIN and
- query in initials):
+ elif match_on & MATCH_INITIALS_CONTAIN and query in initials:
score = 95.0 - (len(initials) / len(query))
return (score, MATCH_INITIALS_CONTAIN)
@@ -2005,8 +2041,9 @@ def _filter_item(self, value, query, match_on, fold_diacritics):
search = self._search_for_query(query)
match = search(value)
if match:
- score = 100.0 / ((1 + match.start()) *
- (match.end() - match.start() + 1))
+ score = 100.0 / (
+ (1 + match.start()) * (match.end() - match.start() + 1)
+ )
return (score, MATCH_ALLCHARS)
@@ -2021,8 +2058,8 @@ def _search_for_query(self, query):
pattern = []
for c in query:
# pattern.append('[^{0}]*{0}'.format(re.escape(c)))
- pattern.append('.*?{0}'.format(re.escape(c)))
- pattern = ''.join(pattern)
+ pattern.append(".*?{0}".format(re.escape(c)))
+ pattern = "".join(pattern)
search = re.compile(pattern, re.IGNORECASE).search
self._search_pattern_cache[query] = search
@@ -2051,16 +2088,17 @@ def run(self, func, text_errors=False):
start = time.time()
# Write to debugger to ensure "real" output starts on a new line
- print('.', file=sys.stderr)
+ print(".", file=sys.stderr)
# Call workflow's entry function/method within a try-except block
# to catch any errors and display an error message in Alfred
try:
if self.version:
- self.logger.debug('---------- %s (%s) ----------',
- self.name, self.version)
+ self.logger.debug(
+ "---------- %s (%s) ----------", self.name, self.version
+ )
else:
- self.logger.debug('---------- %s ----------', self.name)
+ self.logger.debug("---------- %s ----------", self.name)
# Run update check if configured for self-updates.
# This call has to go in the `run` try-except block, as it will
@@ -2079,11 +2117,11 @@ def run(self, func, text_errors=False):
except Exception as err:
self.logger.exception(err)
if self.help_url:
- self.logger.info('for assistance, see: %s', self.help_url)
+ self.logger.info("for assistance, see: %s", self.help_url)
if not sys.stdout.isatty(): # Show error in Alfred
if text_errors:
- print(unicode(err).encode('utf-8'), end='')
+ print(unicode(err).encode("utf-8"), end="")
else:
self._items = []
if self._name:
@@ -2092,24 +2130,37 @@ def run(self, func, text_errors=False):
name = self._bundleid
else: # pragma: no cover
name = os.path.dirname(__file__)
- self.add_item("Error in workflow '%s'" % name,
- unicode(err),
- icon=ICON_ERROR)
+ self.add_item(
+ "Error in workflow '%s'" % name, unicode(err), icon=ICON_ERROR
+ )
self.send_feedback()
return 1
finally:
- self.logger.debug('---------- finished in %0.3fs ----------',
- time.time() - start)
+ self.logger.debug(
+ "---------- finished in %0.3fs ----------", time.time() - start
+ )
return 0
# Alfred feedback methods ------------------------------------------
- def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None,
- autocomplete=None, valid=False, uid=None, icon=None,
- icontype=None, type=None, largetext=None, copytext=None,
- quicklookurl=None):
+ def add_item(
+ self,
+ title,
+ subtitle="",
+ modifier_subtitles=None,
+ arg=None,
+ autocomplete=None,
+ valid=False,
+ uid=None,
+ icon=None,
+ icontype=None,
+ type=None,
+ largetext=None,
+ copytext=None,
+ quicklookurl=None,
+ ):
"""Add an item to be output to Alfred.
:param title: Title shown in Alfred
@@ -2167,19 +2218,31 @@ def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None,
edit it or do something with it other than send it to Alfred.
"""
- item = self.item_class(title, subtitle, modifier_subtitles, arg,
- autocomplete, valid, uid, icon, icontype, type,
- largetext, copytext, quicklookurl)
+ item = self.item_class(
+ title,
+ subtitle,
+ modifier_subtitles,
+ arg,
+ autocomplete,
+ valid,
+ uid,
+ icon,
+ icontype,
+ type,
+ largetext,
+ copytext,
+ quicklookurl,
+ )
self._items.append(item)
return item
def send_feedback(self):
"""Print stored items to console/Alfred as XML."""
- root = ET.Element('items')
+ root = ET.Element("items")
for item in self._items:
root.append(item.elem)
sys.stdout.write('\n')
- sys.stdout.write(ET.tostring(root).encode('utf-8'))
+ sys.stdout.write(ET.tostring(root).encode("utf-8"))
sys.stdout.flush()
####################################################################
@@ -2196,7 +2259,7 @@ def first_run(self):
"""
if not self.version:
- raise ValueError('No workflow version set')
+ raise ValueError("No workflow version set")
if not self.last_version_run:
return True
@@ -2215,14 +2278,15 @@ def last_version_run(self):
"""
if self._last_version_run is UNSET:
- version = self.settings.get('__workflow_last_version')
+ version = self.settings.get("__workflow_last_version")
if version:
from update import Version
+
version = Version(version)
self._last_version_run = version
- self.logger.debug('last run version: %s', self._last_version_run)
+ self.logger.debug("last run version: %s", self._last_version_run)
return self._last_version_run
@@ -2239,19 +2303,19 @@ def set_last_version(self, version=None):
"""
if not version:
if not self.version:
- self.logger.warning(
- "Can't save last version: workflow has no version")
+ self.logger.warning("Can't save last version: workflow has no version")
return False
version = self.version
if isinstance(version, basestring):
from update import Version
+
version = Version(version)
- self.settings['__workflow_last_version'] = str(version)
+ self.settings["__workflow_last_version"] = str(version)
- self.logger.debug('set last run version: %s', version)
+ self.logger.debug("set last run version: %s", version)
return True
@@ -2267,16 +2331,16 @@ def update_available(self):
:returns: ``True`` if an update is available, else ``False``
"""
- key = '__workflow_latest_version'
+ key = "__workflow_latest_version"
# Create a new workflow object to ensure standard serialiser
# is used (update.py is called without the user's settings)
status = Workflow().cached_data(key, max_age=0)
# self.logger.debug('update status: %r', status)
- if not status or not status.get('available'):
+ if not status or not status.get("available"):
return False
- return status['available']
+ return status["available"]
@property
def prereleases(self):
@@ -2289,10 +2353,10 @@ def prereleases(self):
``False``.
"""
- if self._update_settings.get('prereleases'):
+ if self._update_settings.get("prereleases"):
return True
- return self.settings.get('__workflow_prereleases') or False
+ return self.settings.get("__workflow_prereleases") or False
def check_update(self, force=False):
"""Call update script if it's time to check for a new release.
@@ -2309,38 +2373,36 @@ def check_update(self, force=False):
:type force: ``Boolean``
"""
- key = '__workflow_latest_version'
- frequency = self._update_settings.get('frequency',
- DEFAULT_UPDATE_FREQUENCY)
+ key = "__workflow_latest_version"
+ frequency = self._update_settings.get("frequency", DEFAULT_UPDATE_FREQUENCY)
- if not force and not self.settings.get('__workflow_autoupdate', True):
- self.logger.debug('Auto update turned off by user')
+ if not force and not self.settings.get("__workflow_autoupdate", True):
+ self.logger.debug("Auto update turned off by user")
return
# Check for new version if it's time
- if (force or not self.cached_data_fresh(key, frequency * 86400)):
+ if force or not self.cached_data_fresh(key, frequency * 86400):
- repo = self._update_settings['github_slug']
+ repo = self._update_settings["github_slug"]
# version = self._update_settings['version']
version = str(self.version)
from background import run_in_background
# update.py is adjacent to this file
- update_script = os.path.join(os.path.dirname(__file__),
- b'update.py')
+ update_script = os.path.join(os.path.dirname(__file__), b"update.py")
- cmd = ['/usr/bin/python', update_script, 'check', repo, version]
+ cmd = ["/usr/bin/python", update_script, "check", repo, version]
if self.prereleases:
- cmd.append('--prereleases')
+ cmd.append("--prereleases")
- self.logger.info('checking for update ...')
+ self.logger.info("checking for update ...")
- run_in_background('__workflow_update_check', cmd)
+ run_in_background("__workflow_update_check", cmd)
else:
- self.logger.debug('update check not due')
+ self.logger.debug("update check not due")
def start_update(self):
"""Check for update and download and install new workflow file.
@@ -2356,7 +2418,7 @@ def start_update(self):
"""
import update
- repo = self._update_settings['github_slug']
+ repo = self._update_settings["github_slug"]
# version = self._update_settings['version']
version = str(self.version)
@@ -2366,16 +2428,15 @@ def start_update(self):
from background import run_in_background
# update.py is adjacent to this file
- update_script = os.path.join(os.path.dirname(__file__),
- b'update.py')
+ update_script = os.path.join(os.path.dirname(__file__), b"update.py")
- cmd = ['/usr/bin/python', update_script, 'install', repo, version]
+ cmd = ["/usr/bin/python", update_script, "install", repo, version]
if self.prereleases:
- cmd.append('--prereleases')
+ cmd.append("--prereleases")
- self.logger.debug('downloading update ...')
- run_in_background('__workflow_update_install', cmd)
+ self.logger.debug("downloading update ...")
+ run_in_background("__workflow_update_install", cmd)
return True
@@ -2406,22 +2467,24 @@ def save_password(self, account, password, service=None):
service = self.bundleid
try:
- self._call_security('add-generic-password', service, account,
- '-w', password)
- self.logger.debug('saved password : %s:%s', service, account)
+ self._call_security(
+ "add-generic-password", service, account, "-w", password
+ )
+ self.logger.debug("saved password : %s:%s", service, account)
except PasswordExists:
- self.logger.debug('password exists : %s:%s', service, account)
+ self.logger.debug("password exists : %s:%s", service, account)
current_password = self.get_password(account, service)
if current_password == password:
- self.logger.debug('password unchanged')
+ self.logger.debug("password unchanged")
else:
self.delete_password(account, service)
- self._call_security('add-generic-password', service,
- account, '-w', password)
- self.logger.debug('save_password : %s:%s', service, account)
+ self._call_security(
+ "add-generic-password", service, account, "-w", password
+ )
+ self.logger.debug("save_password : %s:%s", service, account)
def get_password(self, account, service=None):
"""Retrieve the password saved at ``service/account``.
@@ -2441,24 +2504,23 @@ def get_password(self, account, service=None):
if not service:
service = self.bundleid
- output = self._call_security('find-generic-password', service,
- account, '-g')
+ output = self._call_security("find-generic-password", service, account, "-g")
# Parsing of `security` output is adapted from python-keyring
# by Jason R. Coombs
# https://pypi.python.org/pypi/keyring
m = re.search(
- r'password:\s*(?:0x(?P[0-9A-F]+)\s*)?(?:"(?P.*)")?',
- output)
+ r'password:\s*(?:0x(?P[0-9A-F]+)\s*)?(?:"(?P.*)")?', output
+ )
if m:
groups = m.groupdict()
- h = groups.get('hex')
- password = groups.get('pw')
+ h = groups.get("hex")
+ password = groups.get("pw")
if h:
- password = unicode(binascii.unhexlify(h), 'utf-8')
+ password = unicode(binascii.unhexlify(h), "utf-8")
- self.logger.debug('got password : %s:%s', service, account)
+ self.logger.debug("got password : %s:%s", service, account)
return password
@@ -2478,9 +2540,9 @@ def delete_password(self, account, service=None):
if not service:
service = self.bundleid
- self._call_security('delete-generic-password', service, account)
+ self._call_security("delete-generic-password", service, account)
- self.logger.debug('deleted password : %s:%s', service, account)
+ self.logger.debug("deleted password : %s:%s", service, account)
####################################################################
# Methods for workflow:* magic args
@@ -2497,91 +2559,98 @@ def wrapper():
return wrapper
- self.magic_arguments['delcache'] = callback(self.clear_cache,
- 'Deleted workflow cache')
- self.magic_arguments['deldata'] = callback(self.clear_data,
- 'Deleted workflow data')
- self.magic_arguments['delsettings'] = callback(
- self.clear_settings, 'Deleted workflow settings')
- self.magic_arguments['reset'] = callback(self.reset,
- 'Reset workflow')
- self.magic_arguments['openlog'] = callback(self.open_log,
- 'Opening workflow log file')
- self.magic_arguments['opencache'] = callback(
- self.open_cachedir, 'Opening workflow cache directory')
- self.magic_arguments['opendata'] = callback(
- self.open_datadir, 'Opening workflow data directory')
- self.magic_arguments['openworkflow'] = callback(
- self.open_workflowdir, 'Opening workflow directory')
- self.magic_arguments['openterm'] = callback(
- self.open_terminal, 'Opening workflow root directory in Terminal')
+ self.magic_arguments["delcache"] = callback(
+ self.clear_cache, "Deleted workflow cache"
+ )
+ self.magic_arguments["deldata"] = callback(
+ self.clear_data, "Deleted workflow data"
+ )
+ self.magic_arguments["delsettings"] = callback(
+ self.clear_settings, "Deleted workflow settings"
+ )
+ self.magic_arguments["reset"] = callback(self.reset, "Reset workflow")
+ self.magic_arguments["openlog"] = callback(
+ self.open_log, "Opening workflow log file"
+ )
+ self.magic_arguments["opencache"] = callback(
+ self.open_cachedir, "Opening workflow cache directory"
+ )
+ self.magic_arguments["opendata"] = callback(
+ self.open_datadir, "Opening workflow data directory"
+ )
+ self.magic_arguments["openworkflow"] = callback(
+ self.open_workflowdir, "Opening workflow directory"
+ )
+ self.magic_arguments["openterm"] = callback(
+ self.open_terminal, "Opening workflow root directory in Terminal"
+ )
# Diacritic folding
def fold_on():
- self.settings['__workflow_diacritic_folding'] = True
- return 'Diacritics will always be folded'
+ self.settings["__workflow_diacritic_folding"] = True
+ return "Diacritics will always be folded"
def fold_off():
- self.settings['__workflow_diacritic_folding'] = False
- return 'Diacritics will never be folded'
+ self.settings["__workflow_diacritic_folding"] = False
+ return "Diacritics will never be folded"
def fold_default():
- if '__workflow_diacritic_folding' in self.settings:
- del self.settings['__workflow_diacritic_folding']
- return 'Diacritics folding reset'
+ if "__workflow_diacritic_folding" in self.settings:
+ del self.settings["__workflow_diacritic_folding"]
+ return "Diacritics folding reset"
- self.magic_arguments['foldingon'] = fold_on
- self.magic_arguments['foldingoff'] = fold_off
- self.magic_arguments['foldingdefault'] = fold_default
+ self.magic_arguments["foldingon"] = fold_on
+ self.magic_arguments["foldingoff"] = fold_off
+ self.magic_arguments["foldingdefault"] = fold_default
# Updates
def update_on():
- self.settings['__workflow_autoupdate'] = True
- return 'Auto update turned on'
+ self.settings["__workflow_autoupdate"] = True
+ return "Auto update turned on"
def update_off():
- self.settings['__workflow_autoupdate'] = False
- return 'Auto update turned off'
+ self.settings["__workflow_autoupdate"] = False
+ return "Auto update turned off"
def prereleases_on():
- self.settings['__workflow_prereleases'] = True
- return 'Prerelease updates turned on'
+ self.settings["__workflow_prereleases"] = True
+ return "Prerelease updates turned on"
def prereleases_off():
- self.settings['__workflow_prereleases'] = False
- return 'Prerelease updates turned off'
+ self.settings["__workflow_prereleases"] = False
+ return "Prerelease updates turned off"
def do_update():
if self.start_update():
- return 'Downloading and installing update ...'
+ return "Downloading and installing update ..."
else:
- return 'No update available'
+ return "No update available"
- self.magic_arguments['autoupdate'] = update_on
- self.magic_arguments['noautoupdate'] = update_off
- self.magic_arguments['prereleases'] = prereleases_on
- self.magic_arguments['noprereleases'] = prereleases_off
- self.magic_arguments['update'] = do_update
+ self.magic_arguments["autoupdate"] = update_on
+ self.magic_arguments["noautoupdate"] = update_off
+ self.magic_arguments["prereleases"] = prereleases_on
+ self.magic_arguments["noprereleases"] = prereleases_off
+ self.magic_arguments["update"] = do_update
# Help
def do_help():
if self.help_url:
self.open_help()
- return 'Opening workflow help URL in browser'
+ return "Opening workflow help URL in browser"
else:
- return 'Workflow has no help URL'
+ return "Workflow has no help URL"
def show_version():
if self.version:
- return 'Version: {0}'.format(self.version)
+ return "Version: {0}".format(self.version)
else:
- return 'This workflow has no version number'
+ return "This workflow has no version number"
def list_magic():
"""Display all available magic args in Alfred."""
isatty = sys.stderr.isatty()
for name in sorted(self.magic_arguments.keys()):
- if name == 'magic':
+ if name == "magic":
continue
arg = self.magic_prefix + name
self.logger.debug(arg)
@@ -2592,9 +2661,9 @@ def list_magic():
if not isatty:
self.send_feedback()
- self.magic_arguments['help'] = do_help
- self.magic_arguments['magic'] = list_magic
- self.magic_arguments['version'] = show_version
+ self.magic_arguments["help"] = do_help
+ self.magic_arguments["magic"] = list_magic
+ self.magic_arguments["version"] = show_version
def clear_cache(self, filter_func=lambda f: True):
"""Delete all files in workflow's :attr:`cachedir`.
@@ -2624,7 +2693,7 @@ def clear_settings(self):
"""Delete workflow's :attr:`settings_path`."""
if os.path.exists(self.settings_path):
os.unlink(self.settings_path)
- self.logger.debug('deleted : %r', self.settings_path)
+ self.logger.debug("deleted : %r", self.settings_path)
def reset(self):
"""Delete workflow settings, cache and data.
@@ -2639,29 +2708,29 @@ def reset(self):
def open_log(self):
"""Open :attr:`logfile` in default app (usually Console.app)."""
- subprocess.call(['open', self.logfile]) # nosec
+ subprocess.call(["open", self.logfile]) # nosec
def open_cachedir(self):
"""Open the workflow's :attr:`cachedir` in Finder."""
- subprocess.call(['open', self.cachedir]) # nosec
+ subprocess.call(["open", self.cachedir]) # nosec
def open_datadir(self):
"""Open the workflow's :attr:`datadir` in Finder."""
- subprocess.call(['open', self.datadir]) # nosec
+ subprocess.call(["open", self.datadir]) # nosec
def open_workflowdir(self):
"""Open the workflow's :attr:`workflowdir` in Finder."""
- subprocess.call(['open', self.workflowdir]) # nosec
+ subprocess.call(["open", self.workflowdir]) # nosec
def open_terminal(self):
"""Open a Terminal window at workflow's :attr:`workflowdir`."""
- subprocess.call(['open', '-a', 'Terminal', self.workflowdir]) # nosec
+ subprocess.call(["open", "-a", "Terminal", self.workflowdir]) # nosec
def open_help(self):
"""Open :attr:`help_url` in default browser."""
- subprocess.call(['open', self.help_url]) # nosec
+ subprocess.call(["open", self.help_url]) # nosec
- return 'Opening workflow help URL in browser'
+ return "Opening workflow help URL in browser"
####################################################################
# Helper methods
@@ -2716,9 +2785,8 @@ def fold_to_ascii(self, text):
"""
if isascii(text):
return text
- text = ''.join([ASCII_REPLACEMENTS.get(c, c) for c in text])
- return unicode(unicodedata.normalize('NFKD',
- text).encode('ascii', 'ignore'))
+ text = "".join([ASCII_REPLACEMENTS.get(c, c) for c in text])
+ return unicode(unicodedata.normalize("NFKD", text).encode("ascii", "ignore"))
def dumbify_punctuation(self, text):
"""Convert non-ASCII punctuation to closest ASCII equivalent.
@@ -2738,7 +2806,7 @@ def dumbify_punctuation(self, text):
if isascii(text):
return text
- text = ''.join([DUMB_PUNCTUATION.get(c, c) for c in text])
+ text = "".join([DUMB_PUNCTUATION.get(c, c) for c in text])
return text
def _delete_directory_contents(self, dirpath, filter_func):
@@ -2760,12 +2828,12 @@ def _delete_directory_contents(self, dirpath, filter_func):
shutil.rmtree(path)
else:
os.unlink(path)
- self.logger.debug('deleted : %r', path)
+ self.logger.debug("deleted : %r", path)
def _load_info_plist(self):
"""Load workflow info from ``info.plist``."""
# info.plist should be in the directory above this one
- self._info = plistlib.readPlist(self.workflowfile('info.plist'))
+ self._info = plistlib.readPlist(self.workflowfile("info.plist"))
self._info_loaded = True
def _create(self, dirpath):
@@ -2805,16 +2873,15 @@ def _call_security(self, action, service, account, *args):
:rtype: `tuple` (`int`, ``unicode``)
"""
- cmd = ['security', action, '-s', service, '-a', account] + list(args)
- p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ cmd = ["security", action, "-s", service, "-a", account] + list(args)
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
stdout, _ = p.communicate()
if p.returncode == 44: # password does not exist
raise PasswordNotFound()
elif p.returncode == 45: # password already exists
raise PasswordExists()
elif p.returncode > 0:
- err = KeychainError('Unknown Keychain error : %s' % stdout)
+ err = KeychainError("Unknown Keychain error : %s" % stdout)
err.retcode = p.returncode
raise err
- return stdout.strip().decode('utf-8')
+ return stdout.strip().decode("utf-8")
diff --git a/workflow/workflow3.py b/workflow/workflow3.py
index 23a7aae..c554a63 100644
--- a/workflow/workflow3.py
+++ b/workflow/workflow3.py
@@ -78,15 +78,15 @@ def obj(self):
d2 = {}
for k, v in self.items():
d2[k] = v
- o['variables'] = d2
+ o["variables"] = d2
if self.config:
- o['config'] = self.config
+ o["config"] = self.config
if self.arg is not None:
- o['arg'] = self.arg
+ o["arg"] = self.arg
- return {'alfredworkflow': o}
+ return {"alfredworkflow": o}
def __unicode__(self):
"""Convert to ``alfredworkflow`` JSON object.
@@ -97,7 +97,7 @@ def __unicode__(self):
"""
if not self and not self.config:
if not self.arg:
- return u''
+ return ""
if isinstance(self.arg, unicode):
return self.arg
@@ -110,7 +110,7 @@ def __str__(self):
str: UTF-8 encoded ``alfredworkflow`` JSON object
"""
- return unicode(self).encode('utf-8')
+ return unicode(self).encode("utf-8")
class Modifier(object):
@@ -153,8 +153,9 @@ class Modifier(object):
"""
- def __init__(self, key, subtitle=None, arg=None, valid=None, icon=None,
- icontype=None):
+ def __init__(
+ self, key, subtitle=None, arg=None, valid=None, icon=None, icontype=None
+ ):
"""Create a new :class:`Modifier`.
Don't use this class directly (as it won't be associated with any
@@ -216,23 +217,23 @@ def obj(self):
o = {}
if self.subtitle is not None:
- o['subtitle'] = self.subtitle
+ o["subtitle"] = self.subtitle
if self.arg is not None:
- o['arg'] = self.arg
+ o["arg"] = self.arg
if self.valid is not None:
- o['valid'] = self.valid
+ o["valid"] = self.valid
if self.variables:
- o['variables'] = self.variables
+ o["variables"] = self.variables
if self.config:
- o['config'] = self.config
+ o["config"] = self.config
icon = self._icon()
if icon:
- o['icon'] = icon
+ o["icon"] = icon
return o
@@ -245,10 +246,10 @@ def _icon(self):
"""
icon = {}
if self.icon is not None:
- icon['path'] = self.icon
+ icon["path"] = self.icon
if self.icontype is not None:
- icon['type'] = self.icontype
+ icon["type"] = self.icontype
return icon
@@ -265,9 +266,22 @@ class Item3(object):
"""
- def __init__(self, title, subtitle='', arg=None, autocomplete=None,
- match=None, valid=False, uid=None, icon=None, icontype=None,
- type=None, largetext=None, copytext=None, quicklookurl=None):
+ def __init__(
+ self,
+ title,
+ subtitle="",
+ arg=None,
+ autocomplete=None,
+ match=None,
+ valid=False,
+ uid=None,
+ icon=None,
+ icontype=None,
+ type=None,
+ largetext=None,
+ copytext=None,
+ quicklookurl=None,
+ ):
"""Create a new :class:`Item3` object.
Use same arguments as for
@@ -318,8 +332,9 @@ def getvar(self, name, default=None):
"""
return self.variables.get(name, default)
- def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None,
- icontype=None):
+ def add_modifier(
+ self, key, subtitle=None, arg=None, valid=None, icon=None, icontype=None
+ ):
"""Add alternative values for a modifier key.
Args:
@@ -358,49 +373,49 @@ def obj(self):
"""
# Required values
o = {
- 'title': self.title,
- 'subtitle': self.subtitle,
- 'valid': self.valid,
+ "title": self.title,
+ "subtitle": self.subtitle,
+ "valid": self.valid,
}
# Optional values
if self.arg is not None:
- o['arg'] = self.arg
+ o["arg"] = self.arg
if self.autocomplete is not None:
- o['autocomplete'] = self.autocomplete
+ o["autocomplete"] = self.autocomplete
if self.match is not None:
- o['match'] = self.match
+ o["match"] = self.match
if self.uid is not None:
- o['uid'] = self.uid
+ o["uid"] = self.uid
if self.type is not None:
- o['type'] = self.type
+ o["type"] = self.type
if self.quicklookurl is not None:
- o['quicklookurl'] = self.quicklookurl
+ o["quicklookurl"] = self.quicklookurl
if self.variables:
- o['variables'] = self.variables
+ o["variables"] = self.variables
if self.config:
- o['config'] = self.config
+ o["config"] = self.config
# Largetype and copytext
text = self._text()
if text:
- o['text'] = text
+ o["text"] = text
icon = self._icon()
if icon:
- o['icon'] = icon
+ o["icon"] = icon
# Modifiers
mods = self._modifiers()
if mods:
- o['mods'] = mods
+ o["mods"] = mods
return o
@@ -413,10 +428,10 @@ def _icon(self):
"""
icon = {}
if self.icon is not None:
- icon['path'] = self.icon
+ icon["path"] = self.icon
if self.icontype is not None:
- icon['type'] = self.icontype
+ icon["type"] = self.icontype
return icon
@@ -429,10 +444,10 @@ def _text(self):
"""
text = {}
if self.largetext is not None:
- text['largetype'] = self.largetext
+ text["largetype"] = self.largetext
if self.copytext is not None:
- text['copy'] = self.copytext
+ text["copy"] = self.copytext
return text
@@ -477,25 +492,27 @@ def __init__(self, **kwargs):
self.variables = {}
self._rerun = 0
# Get session ID from environment if present
- self._session_id = os.getenv('_WF_SESSION_ID') or None
+ self._session_id = os.getenv("_WF_SESSION_ID") or None
if self._session_id:
- self.setvar('_WF_SESSION_ID', self._session_id)
+ self.setvar("_WF_SESSION_ID", self._session_id)
@property
def _default_cachedir(self):
"""Alfred 4's default cache directory."""
return os.path.join(
os.path.expanduser(
- '~/Library/Caches/com.runningwithcrayons.Alfred/'
- 'Workflow Data/'),
- self.bundleid)
+ "~/Library/Caches/com.runningwithcrayons.Alfred/" "Workflow Data/"
+ ),
+ self.bundleid,
+ )
@property
def _default_datadir(self):
"""Alfred 4's default data directory."""
- return os.path.join(os.path.expanduser(
- '~/Library/Application Support/Alfred/Workflow Data/'),
- self.bundleid)
+ return os.path.join(
+ os.path.expanduser("~/Library/Application Support/Alfred/Workflow Data/"),
+ self.bundleid,
+ )
@property
def rerun(self):
@@ -524,8 +541,9 @@ def session_id(self):
"""
if not self._session_id:
from uuid import uuid4
+
self._session_id = uuid4().hex
- self.setvar('_WF_SESSION_ID', self._session_id)
+ self.setvar("_WF_SESSION_ID", self._session_id)
return self._session_id
@@ -548,9 +566,11 @@ def setvar(self, name, value, persist=False):
self.variables[name] = value
if persist:
from .util import set_config
+
set_config(name, value, self.bundleid)
- self.logger.debug('saved variable %r with value %r to info.plist',
- name, value)
+ self.logger.debug(
+ "saved variable %r with value %r to info.plist", name, value
+ )
def getvar(self, name, default=None):
"""Return value of workflow variable for ``name`` or ``default``.
@@ -565,9 +585,22 @@ def getvar(self, name, default=None):
"""
return self.variables.get(name, default)
- def add_item(self, title, subtitle='', arg=None, autocomplete=None,
- valid=False, uid=None, icon=None, icontype=None, type=None,
- largetext=None, copytext=None, quicklookurl=None, match=None):
+ def add_item(
+ self,
+ title,
+ subtitle="",
+ arg=None,
+ autocomplete=None,
+ valid=False,
+ uid=None,
+ icon=None,
+ icontype=None,
+ type=None,
+ largetext=None,
+ copytext=None,
+ quicklookurl=None,
+ match=None,
+ ):
"""Add an item to be output to Alfred.
Args:
@@ -589,9 +622,21 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None,
Item3: Alfred feedback item.
"""
- item = self.item_class(title, subtitle, arg, autocomplete,
- match, valid, uid, icon, icontype, type,
- largetext, copytext, quicklookurl)
+ item = self.item_class(
+ title,
+ subtitle,
+ arg,
+ autocomplete,
+ match,
+ valid,
+ uid,
+ icon,
+ icontype,
+ type,
+ largetext,
+ copytext,
+ quicklookurl,
+ )
# Add variables to child item
item.variables.update(self.variables)
@@ -602,7 +647,7 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None,
@property
def _session_prefix(self):
"""Filename prefix for current session."""
- return '_wfsess-{0}-'.format(self.session_id)
+ return "_wfsess-{0}-".format(self.session_id)
def _mk_session_name(self, name):
"""New cache name/key based on session ID."""
@@ -672,11 +717,13 @@ def clear_session_cache(self, current=False):
current session.
"""
+
def _is_session_file(filename):
if current:
- return filename.startswith('_wfsess-')
- return filename.startswith('_wfsess-') \
- and not filename.startswith(self._session_prefix)
+ return filename.startswith("_wfsess-")
+ return filename.startswith("_wfsess-") and not filename.startswith(
+ self._session_prefix
+ )
self.clear_cache(_is_session_file)
@@ -692,14 +739,14 @@ def obj(self):
for item in self._items:
items.append(item.obj)
- o = {'items': items}
+ o = {"items": items}
if self.variables:
- o['variables'] = self.variables
+ o["variables"] = self.variables
if self.rerun:
- o['rerun'] = self.rerun
+ o["rerun"] = self.rerun
return o
- def warn_empty(self, title, subtitle=u'', icon=None):
+ def warn_empty(self, title, subtitle="", icon=None):
"""Add a warning to feedback if there are no items.
.. versionadded:: 1.31
@@ -728,7 +775,7 @@ def warn_empty(self, title, subtitle=u'', icon=None):
def send_feedback(self):
"""Print stored items to console/Alfred as JSON."""
if self.debugging:
- json.dump(self.obj, sys.stdout, indent=2, separators=(',', ': '))
+ json.dump(self.obj, sys.stdout, indent=2, separators=(",", ": "))
else:
json.dump(self.obj, sys.stdout)
sys.stdout.flush()