diff --git a/MullvadVPN.alfredworkflow b/MullvadVPN.alfredworkflow index 1147c35..85faad6 100644 Binary files a/MullvadVPN.alfredworkflow and b/MullvadVPN.alfredworkflow differ diff --git a/src/mullvad.py b/src/mullvad.py index 3beb336..87e2157 100644 --- a/src/mullvad.py +++ b/src/mullvad.py @@ -6,7 +6,7 @@ import subprocess from datetime import datetime -from workflow import Workflow, MATCH_SUBSTRING +from workflow import Workflow3, MATCH_SUBSTRING from workflow.background import run_in_background import mullvad_actions @@ -19,6 +19,14 @@ ############################# def execute(cmdList): + """ Execute a terminal command from list of arguments + + Arguments: + cmdList -- command line command (list of strings) + + Returns: + cmd/err -- output of terminal command (tuple of strings) + """ newEnv = os.environ.copy() newEnv['PATH'] = '/usr/local/bin:%s' % newEnv['PATH'] # prepend the path to `mullvad` executable to the system path cmd, err = subprocess.Popen(cmdList, @@ -31,32 +39,84 @@ def execute(cmdList): def get_auto_connect(): + """ Get Mullvad Auto-Connect Status + + Arguments: + None + + Returns: + String of status -- "Autoconnect: on/off" + """ return execute(['mullvad', 'auto-connect', 'get']).splitlines() def get_lan(): + """ Get Mullvad Local Network Sharing Status + + Arguments: + None + + Returns: + String of status -- "Local network sharing setting: allow/block" + """ return execute(['mullvad', 'lan', 'get']).splitlines() def get_kill_switch(): + """ Get Mullvad Kill-Switch Status + + Arguments: + None + + Returns: + String of status -- "Network traffic will be allowed/blocked when the VPN is disconnected" + """ return execute(['mullvad', 'always-require-vpn', 'get']).splitlines() def get_version(): - return [execute(['mullvad', 'version']).splitlines()[1].split()[2], execute(['mullvad', 'version']).splitlines()[2].split()[4]] # version [supported, up-to-date] + """ Get Mullvad Version + + Arguments: + None + + Returns: + List of strings -- [mullvad supported:"true/false", ] + """ + # TODO: simplify this return segment + mullVersion = execute(['mullvad', 'version']) + # print mullVersion + supported = mullVersion.splitlines()[1].split()[2] + if supported == 'true': + supported = True + elif supported == 'false': + supported = False + # print supported + currentVersion = mullVersion.splitlines()[0].split(':')[1].strip() + # print currentVersion + latestVersion = mullVersion.splitlines()[3].split(':')[1].strip() + # print latestVersion + # print currentVersion==latestVersion + return [supported, currentVersion==latestVersion] def connection_status(): + """ Add workflow item of current connection status + Arguments: + None + Returns: + Item -- Connected/Disconnected/Blocked + """ for status in get_connection(): -# print 'status:', status + # print 'status:', status stat = str(status.split()[2]) -# print 'stat:', stat + # print 'stat:', stat if stat == 'Connected': countryString, cityString = get_country_city() -# print '{} to: {} {}'.format(stat, cityString, countryString).decode('utf8') -# print ' '.join(status.split()[4:])+'. Select to Disconnect.' + # print '{} to: {} {}'.format(stat, cityString, countryString).decode('utf8') + # print ' '.join(status.split()[4:])+'. Select to Disconnect.' wf.add_item('{} to: {} {}'.format(stat, cityString, countryString).decode('utf8'), - subtitle=' '.join(status.split()[4:])+'. Select to Disconnect. Type "Relay" to change.', + subtitle=' '.join(status.split()[4:])+'. Select to Disconnect. Type "relay" to change.', arg='/usr/local/bin/mullvad disconnect', valid=True, icon='icons/mullvad_green.png') @@ -75,26 +135,39 @@ def connection_status(): def get_country_city(): - countryCodeSearch = '({})'.format(get_protocol()[9]) -# print 'countryCodeSearch', countryCodeSearch - cityCodeSearch = '({})'.format(get_protocol()[8][0:3]) -# print 'cityCodeSearch', cityCodeSearch + """ Get the current country and city relay information + :returns countryString: + """ + # TODO: make this work for OpenVPN as well as Wireguard + getProt = get_protocol() + # print 'getProt: {}'.format(getProt) + sep = getProt.index(',') + # print 'sep: {}'.format(sep) + countryCodeSearch = '({})'.format(getProt[sep+2:sep+4]) + # print 'DEBUG: countryCodeSearch', countryCodeSearch #debug + cityCodeSearch = '({})'.format(getProt[sep-3:sep]) + # cityCodeSearch = '({})'.format(get_protocol()[8][0:3]) + # print 'DEBUG: cityCodeSearch', cityCodeSearch countries = wf.cached_data('mullvad_country_list', get_country_list, max_age=432000) -# print 'countries', countries + # print 'DEBUG: countries', countries #debug index = [i for i,s in enumerate(countries) if countryCodeSearch in s][0] relayList = wf.cached_data('mullvad_relay_list', get_relay_list, max_age=432000) countryString = countries[index].split()[:-1][0] -# print countryString + # print countryString #debug cityString = ' '.join([city[0] for city in relayList[index][1:] if cityCodeSearch in city[0]][0].split()[:-1]) -# print cityString + # print cityString #debug return countryString, cityString def get_connection(): + """ VPN connection tunnel status + :returns: sentence of tunnel status + :type returns: tuple of a single sentence string + """ return execute(['mullvad', 'status']).splitlines() @@ -105,6 +178,7 @@ def check_connection(): valid=True, icon='icons/mullvad_yellow.png') + def set_kill_switch(): for status in get_kill_switch(): if status == 'Network traffic will be blocked when the VPN is disconnected': @@ -119,11 +193,11 @@ def set_kill_switch(): def get_protocol(): - return execute(['mullvad','relay','get']).split() + return execute(['mullvad','relay','get']) def protocol_status(): - status = get_protocol()[2] + status = get_connection()[0].split()[4] wf.add_item('Tunnel-protocol: {}'.format(status), subtitle='Change tunnel-protocol', autocomplete='protocol', @@ -206,25 +280,39 @@ def add_time_account(): def update_relay_list(): - execute(['mullvad', 'relay', 'update']) #TODO add this to its own subroutine that gets run in the background + # TODO add this to its own subroutine that gets run in the background + execute(['mullvad', 'relay', 'update']) def list_relay_countries(wf, query): + """ List countries with servers + Arguments: + query -- "relay" + """ + # TODO: does `query` need to be here? + # print query for country in filter_relay_countries(wf, query): countryName = country.split(' (')[0] countryCode = country.split('(')[1].split(')')[0] wf.add_item(country, subtitle='List cities in {}'.format(countryName), - valid=False, # TABing and RETURN have the same effect. take you to city selection + valid=False, # TABing and RETURN have the same effect, take you to city selection autocomplete='country:{} '.format(countryCode), - icon='icons/chevron-right-dark.png') #TODO lock icon? or maybe just chevron + icon='icons/chevron-right-dark.png') # TODO lock icon, or maybe just chevron def filter_relay_countries(wf, query): + """ List contries based on fuzzy match of query + Returns: + List of countries as strings + """ + # print query countries = wf.cached_data('mullvad_country_list', get_country_list, max_age=432000) + # print query queryFilter = query.split() + # print query, queryFilter if len(queryFilter) > 1: return wf.filter(queryFilter[1], countries, match_on=MATCH_SUBSTRING) return countries @@ -258,6 +346,12 @@ def get_relay_list(): def list_relay_cities(wf, query): + """ List cities of country + Argument: + query -- country:`countryCode` where `countryCode` is a two letter abbreviation of a country from list_relay_countries() + Returns: + List of Items of cities + """ countryCode = query.split(':')[1].split()[0] for city in filter_relay_cities(wf, countryCode, query): cityCode = city.split('(')[1].split(')')[0] @@ -308,14 +402,14 @@ def main(wf): # extract query query = wf.args[0] if len(wf.args) else None # if there's an argument(s) `query` is the first one. Otherwise it's `None` - if not query: + if not query: # starting screen of information. if wf.cached_data('mullvad_version', get_version, - max_age=86400)[1] == 'false': + max_age=86400)[1] == False: update_mullvad() if wf.cached_data('mullvad_version', get_version, - max_age=86400)[0] == 'false': + max_age=86400)[0] == False: unsupported_mullvad() if wf.cached_data('mullvad_account', get_account, @@ -411,5 +505,5 @@ def main(wf): ############################# if __name__ == '__main__': - wf = Workflow(update_settings={'github_slug': GITHUB_SLUG}) + wf = Workflow3(update_settings={'github_slug': GITHUB_SLUG}) sys.exit(wf.run(main)) diff --git a/src/mullvad.pyc b/src/mullvad.pyc index 2b928a1..99d22d0 100644 Binary files a/src/mullvad.pyc and b/src/mullvad.pyc differ diff --git a/src/mullvad_actions.pyc b/src/mullvad_actions.pyc index 9ca5f6e..337b8c7 100644 Binary files a/src/mullvad_actions.pyc and b/src/mullvad_actions.pyc differ diff --git a/src/version b/src/version index b63ba69..68c123c 100644 --- a/src/version +++ b/src/version @@ -1 +1 @@ -0.9 +0.10 diff --git a/src/workflow/.alfredversionchecked b/src/workflow/.alfredversionchecked deleted file mode 100644 index e69de29..0000000 diff --git a/src/workflow/__init__.pyc b/src/workflow/__init__.pyc index 853e69e..207c5ed 100644 Binary files a/src/workflow/__init__.pyc and b/src/workflow/__init__.pyc differ diff --git a/src/workflow/__pycache__/__init__.cpython-37.pyc b/src/workflow/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 86fa82f..0000000 Binary files a/src/workflow/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/src/workflow/__pycache__/workflow.cpython-37.pyc b/src/workflow/__pycache__/workflow.cpython-37.pyc deleted file mode 100644 index b0b77f2..0000000 Binary files a/src/workflow/__pycache__/workflow.cpython-37.pyc and /dev/null differ diff --git a/src/workflow/background.py b/src/workflow/background.py index ba5c52a..c2bd735 100644 --- a/src/workflow/background.py +++ b/src/workflow/background.py @@ -102,10 +102,7 @@ def _job_pid(name): if _process_exists(pid): return pid - try: - os.unlink(pidfile) - except Exception: # pragma: no cover - pass + os.unlink(pidfile) def is_running(name): diff --git a/src/workflow/background.pyc b/src/workflow/background.pyc index 1f2480c..ed4d6de 100644 Binary files a/src/workflow/background.pyc and b/src/workflow/background.pyc differ diff --git a/src/workflow/notify.py b/src/workflow/notify.py index a4b7f40..28ec0b9 100644 --- a/src/workflow/notify.py +++ b/src/workflow/notify.py @@ -117,8 +117,8 @@ def install_notifier(): # z.extractall(destdir) tgz = tarfile.open(archive, 'r:gz') tgz.extractall(destdir) - assert os.path.exists(n), \ - 'Notify.app could not be installed in %s' % destdir + if not os.path.exists(n): # pragma: nocover + raise RuntimeError('Notify.app could not be installed in ' + destdir) # Replace applet icon icon = notifier_icon_path() @@ -253,8 +253,9 @@ def png_to_icns(png_path, icns_path): try: iconset = os.path.join(tempdir, 'Icon.iconset') - assert not os.path.exists(iconset), \ - 'iconset already exists: ' + iconset + if os.path.exists(iconset): # pragma: nocover + raise RuntimeError('iconset already exists: ' + iconset) + os.makedirs(iconset) # Copy source icon to icon set and generate all the other @@ -283,8 +284,9 @@ def png_to_icns(png_path, icns_path): if retcode != 0: raise RuntimeError('iconset exited with %d' % retcode) - assert os.path.exists(icns_path), \ - 'generated ICNS file not found: ' + repr(icns_path) + if not os.path.exists(icns_path): # pragma: nocover + raise ValueError( + 'generated ICNS file not found: ' + repr(icns_path)) finally: try: shutil.rmtree(tempdir) @@ -332,8 +334,8 @@ def ustr(s): print('converting {0!r} to {1!r} ...'.format(o.png, icns), file=sys.stderr) - assert not os.path.exists(icns), \ - 'destination file already exists: ' + icns + if os.path.exists(icns): + raise ValueError('destination file already exists: ' + icns) png_to_icns(o.png, icns) sys.exit(0) diff --git a/src/workflow/update.py b/src/workflow/update.py index ffc6353..c039f7a 100644 --- a/src/workflow/update.py +++ b/src/workflow/update.py @@ -222,7 +222,7 @@ class Version(object): """ #: Match version and pre-release/build information in version strings - match_version = re.compile(r'([0-9\.]+)(.+)?').match + match_version = re.compile(r'([0-9][0-9\.]*)(.+)?').match def __init__(self, vstr): """Create new `Version` object. @@ -247,7 +247,7 @@ def _parse(self, vstr): else: m = self.match_version(vstr) if not m: - raise ValueError('invalid version number: {!r}'.format(vstr)) + raise ValueError('invalid version number: ' + vstr) version, suffix = m.groups() parts = self._parse_dotted_string(version) @@ -257,7 +257,7 @@ def _parse(self, vstr): if len(parts): self.patch = parts.pop(0) if not len(parts) == 0: - raise ValueError('version number too long: {!r}'.format(vstr)) + raise ValueError('version number too long: ' + vstr) if suffix: # Build info @@ -268,11 +268,9 @@ def _parse(self, vstr): if suffix: if not suffix.startswith('-'): raise ValueError( - 'suffix must start with - : {0}'.format(suffix)) + 'suffix must start with - : ' + suffix) self.suffix = suffix[1:] - # wf().logger.debug('version str `{}` -> {}'.format(vstr, repr(self))) - def _parse_dotted_string(self, s): """Parse string ``s`` into list of ints and strings.""" parsed = [] @@ -521,7 +519,7 @@ def install_update(): path = retrieve_download(Download.from_dict(dl)) wf().logger.info('installing updated workflow ...') - subprocess.call(['open', path]) + subprocess.call(['open', path]) # nosec wf().cache_data(key, no_update) return True diff --git a/src/workflow/update.pyc b/src/workflow/update.pyc index 07c9f34..38eaa0a 100644 Binary files a/src/workflow/update.pyc and b/src/workflow/update.pyc differ diff --git a/src/workflow/util.py b/src/workflow/util.py index 27209d8..ab5e954 100644 --- a/src/workflow/util.py +++ b/src/workflow/util.py @@ -31,19 +31,21 @@ # "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});' class AcquisitionError(Exception): @@ -148,17 +150,16 @@ def applescriptify(s): .. versionadded:: 1.31 Replaces ``"`` with `"& quote &"`. Use this function if you want - to insert a string into an AppleScript script: - >>> query = 'g "python" test' - >>> applescriptify(query) + + >>> applescriptify('g "python" test') 'g " & quote & "python" & quote & "test' Args: s (unicode): Unicode string to escape. Returns: - unicode: Escaped string + unicode: Escaped string. """ return s.replace(u'"', u'" & quote & "') @@ -173,11 +174,11 @@ def run_command(cmd, **kwargs): all arguments are encoded to UTF-8 first. Args: - cmd (list): Command arguments to pass to ``check_output``. - **kwargs: Keyword arguments to pass to ``check_output``. + cmd (list): Command arguments to pass to :func:`~subprocess.check_output`. + **kwargs: Keyword arguments to pass to :func:`~subprocess.check_output`. Returns: - str: Output returned by ``check_output``. + str: Output returned by :func:`~subprocess.check_output`. """ cmd = [utf8ify(s) for s in cmd] @@ -197,6 +198,7 @@ def run_applescript(script, *args, **kwargs): script (str, optional): Filepath of script or code to run. *args: Optional command-line arguments to pass to the script. **kwargs: Pass ``lang`` to run a language other than AppleScript. + Any other keyword arguments are passed to :func:`run_command`. Returns: str: Output of run command. @@ -242,8 +244,8 @@ def run_trigger(name, bundleid=None, arg=None): .. versionadded:: 1.31 - If ``bundleid`` is not specified, reads the bundle ID of the current - workflow from Alfred's environment variables. + If ``bundleid`` is not specified, the bundle ID of the calling + workflow is used. Args: name (str): Name of External Trigger to call. @@ -264,11 +266,29 @@ def run_trigger(name, bundleid=None, arg=None): run_applescript(script, lang='JavaScript') +def set_theme(theme_name): + """Change Alfred's theme. + + .. versionadded:: 1.39.0 + + Args: + theme_name (unicode): Name of theme Alfred should use. + + """ + appname = jxa_app_name() + 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): """Set a workflow variable in ``info.plist``. .. versionadded:: 1.33 + If ``bundleid`` is not specified, the bundle ID of the calling + workflow is used. + Args: name (str): Name of variable to set. value (str): Value to set variable to. @@ -297,6 +317,9 @@ def unset_config(name, bundleid=None): .. versionadded:: 1.33 + If ``bundleid`` is not specified, the bundle ID of the calling + workflow is used. + Args: name (str): Name of variable to delete. bundleid (str, optional): Bundle ID of workflow variable belongs to. @@ -313,6 +336,71 @@ def unset_config(name, bundleid=None): run_applescript(script, lang='JavaScript') +def search_in_alfred(query=None): + """Open Alfred with given search query. + + .. versionadded:: 1.39.0 + + Omit ``query`` to simply open Alfred's main window. + + Args: + query (unicode, optional): Search query. + + """ + 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') + + +def browse_in_alfred(path): + """Open Alfred's filesystem navigation mode at ``path``. + + .. versionadded:: 1.39.0 + + Args: + path (unicode): File or directory path. + + """ + appname = jxa_app_name() + script = JXA_BROWSE.format(app=json.dumps(appname), arg=json.dumps(path)) + run_applescript(script, lang='JavaScript') + + +def action_in_alfred(paths): + """Action the give filepaths in Alfred. + + .. versionadded:: 1.39.0 + + Args: + paths (list): Unicode paths to files/directories to action. + + """ + appname = jxa_app_name() + script = JXA_ACTION.format(app=json.dumps(appname), arg=json.dumps(paths)) + run_applescript(script, lang='JavaScript') + + +def reload_workflow(bundleid=None): + """Tell Alfred to reload a workflow from disk. + + .. versionadded:: 1.39.0 + + If ``bundleid`` is not specified, the bundle ID of the calling + workflow is used. + + Args: + bundleid (unicode, optional): Bundle ID of workflow to reload. + + """ + 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)) + + run_applescript(script, lang='JavaScript') + + def appinfo(name): """Get information about an installed application. @@ -325,11 +413,15 @@ def appinfo(name): AppInfo: :class:`AppInfo` tuple or ``None`` if app isn't found. """ - cmd = ['mdfind', '-onlyin', '/Applications', - '-onlyin', os.path.expanduser('~/Applications'), - '(kMDItemContentTypeTree == com.apple.application &&' - '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))' - .format(name)] + cmd = [ + '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: diff --git a/src/workflow/util.pyc b/src/workflow/util.pyc index 26d0448..8a0ad32 100644 Binary files a/src/workflow/util.pyc and b/src/workflow/util.pyc differ diff --git a/src/workflow/version b/src/workflow/version index a537514..ebc91b4 100644 --- a/src/workflow/version +++ b/src/workflow/version @@ -1 +1 @@ -1.37.1 \ No newline at end of file +1.40.0 \ No newline at end of file diff --git a/src/workflow/web.py b/src/workflow/web.py index 0781911..83212a8 100644 --- a/src/workflow/web.py +++ b/src/workflow/web.py @@ -9,6 +9,8 @@ """Lightweight HTTP library with a requests-like interface.""" +from __future__ import absolute_import, print_function + import codecs import json import mimetypes @@ -23,8 +25,10 @@ import urlparse import zlib +__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read() -USER_AGENT = u'Alfred-Workflow/1.36 (+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 @@ -178,6 +182,18 @@ def itervalues(self): yield v['val'] +class Request(urllib2.Request): + """Subclass of :class:`urllib2.Request` that supports custom methods.""" + + def __init__(self, *args, **kwargs): + """Create a new :class:`Request`.""" + self._method = kwargs.pop('method', None) + urllib2.Request.__init__(self, *args, **kwargs) + + def get_method(self): + return self._method.upper() + + class Response(object): """ Returned by :func:`request` / :func:`get` / :func:`post` functions. @@ -200,7 +216,7 @@ class Response(object): def __init__(self, request, stream=False): """Call `request` with :mod:`urllib2` and process results. - :param request: :class:`urllib2.Request` instance + :param request: :class:`Request` instance :param stream: Whether to stream response or retrieve it all at once :type stream: bool @@ -512,7 +528,7 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, socket.setdefaulttimeout(timeout) # Default handlers - openers = [] + openers = [urllib2.ProxyHandler(urllib2.getproxies())] if not allow_redirects: openers.append(NoRedirectHandler()) @@ -544,10 +560,6 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, headers['accept-encoding'] = ', '.join(encodings) - # Force POST by providing an empty data string - if method == 'POST' and not data: - data = '' - if files: if not data: data = {} @@ -575,7 +587,7 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, query = urllib.urlencode(str_dict(params), doseq=True) url = urlparse.urlunsplit((scheme, netloc, path, query, fragment)) - req = urllib2.Request(url, data, headers) + req = Request(url, data, headers, method=method) return Response(req, stream) @@ -591,6 +603,18 @@ def get(url, params=None, headers=None, cookies=None, auth=None, 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): """Initiate a POST request. Arguments as for :func:`request`. @@ -602,6 +626,17 @@ def post(url, params=None, data=None, headers=None, cookies=None, files=None, 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) + + def encode_multipart_formdata(fields, files): """Encode form data (``fields``) and ``files`` for POST request. diff --git a/src/workflow/web.pyc b/src/workflow/web.pyc index 4ef041c..a85bcb9 100644 Binary files a/src/workflow/web.pyc and b/src/workflow/web.pyc differ diff --git a/src/workflow/workflow.py b/src/workflow/workflow.py index 2a057b0..3935227 100644 --- a/src/workflow/workflow.py +++ b/src/workflow/workflow.py @@ -2639,28 +2639,27 @@ def reset(self): def open_log(self): """Open :attr:`logfile` in default app (usually Console.app).""" - subprocess.call(['open', self.logfile]) + subprocess.call(['open', self.logfile]) # nosec def open_cachedir(self): """Open the workflow's :attr:`cachedir` in Finder.""" - subprocess.call(['open', self.cachedir]) + subprocess.call(['open', self.cachedir]) # nosec def open_datadir(self): """Open the workflow's :attr:`datadir` in Finder.""" - subprocess.call(['open', self.datadir]) + subprocess.call(['open', self.datadir]) # nosec def open_workflowdir(self): """Open the workflow's :attr:`workflowdir` in Finder.""" - subprocess.call(['open', self.workflowdir]) + 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]) + 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]) + subprocess.call(['open', self.help_url]) # nosec return 'Opening workflow help URL in browser' diff --git a/src/workflow/workflow.pyc b/src/workflow/workflow.pyc index 056f063..7fdb140 100644 Binary files a/src/workflow/workflow.pyc and b/src/workflow/workflow.pyc differ diff --git a/src/workflow/workflow3.py b/src/workflow/workflow3.py index b92c4be..23a7aae 100644 --- a/src/workflow/workflow3.py +++ b/src/workflow/workflow3.py @@ -50,12 +50,16 @@ class Variables(dict): information. Args: - arg (unicode, optional): Main output/``{query}``. + arg (unicode or list, optional): Main output/``{query}``. **variables: Workflow variables to set. + In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a + :class:`list` or :class:`tuple`. Attributes: - arg (unicode): Output value (``{query}``). + arg (unicode or list): Output value (``{query}``). + In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a + :class:`list` or :class:`tuple`. config (dict): Configuration for downstream workflow element. """ @@ -68,7 +72,7 @@ def __init__(self, arg=None, **variables): @property def obj(self): - """Return ``alfredworkflow`` `dict`.""" + """``alfredworkflow`` :class:`dict`.""" o = {} if self: d2 = {} @@ -92,10 +96,10 @@ def __unicode__(self): """ if not self and not self.config: - if self.arg: - return self.arg - else: + if not self.arg: return u'' + if isinstance(self.arg, unicode): + return self.arg return json.dumps(self.obj) @@ -328,6 +332,9 @@ def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None, :meth:`Workflow.add_item() ` for valid values. + In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a + :class:`list` or :class:`tuple`. + Returns: Modifier: Configured :class:`Modifier`. @@ -568,6 +575,9 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None, turned on for your Script Filter, Alfred (version 3.5 and above) will filter against this field, not ``title``. + In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a + :class:`list` or :class:`tuple`. + See :meth:`Workflow.add_item() ` for the main documentation and other parameters. @@ -717,5 +727,8 @@ def warn_empty(self, title, subtitle=u'', icon=None): def send_feedback(self): """Print stored items to console/Alfred as JSON.""" - json.dump(self.obj, sys.stdout) + if self.debugging: + json.dump(self.obj, sys.stdout, indent=2, separators=(',', ': ')) + else: + json.dump(self.obj, sys.stdout) sys.stdout.flush() diff --git a/src/workflow/workflow3.pyc b/src/workflow/workflow3.pyc index 091a0b4..e070679 100644 Binary files a/src/workflow/workflow3.pyc and b/src/workflow/workflow3.pyc differ