diff --git a/Coon_v1.2.0.alfredworkflow b/Conic_v1.3.0.alfredworkflow similarity index 94% rename from Coon_v1.2.0.alfredworkflow rename to Conic_v1.3.0.alfredworkflow index 09528fa..1beb16e 100644 Binary files a/Coon_v1.2.0.alfredworkflow and b/Conic_v1.3.0.alfredworkflow differ diff --git a/README.md b/README.md index 92bee29..d0d51b8 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,15 @@ -# Coon +# Coinc -Coon is an Alfred workflow which do currency conversion by using live currency rates from Open Exchange Rates API. +Coinc is an Alfred workflow which do currency conversion by using live currency rates from Open Exchange Rates API. For list of support currencies, Installation, Setup and Usage, see [Wiki](../../wiki) -# Name Change +## Naming -I name this workflow with an online business name generator, with no background acknowledge or concept of the naming. - -My thoughts are simple, "con-" for "conversion", a mistype of "coin", and a same pronunciation "cone🍦" with a food I'm in favor. - -However, after the terrible, horrifing tragedy happened recently, this name is abandoned, and will be updated recently. - -My sincerly apologize for any unconfort that might arouse. #BlackLivesMattters +This project has been renamed since v1.3.0. Eveything should be patched during the auto update, but if any problem happened, see this [documentation](../../wiki/Naming-&-v1.3.0-Manual-Update-Guide) for more info. ## Credit & License diff --git a/src/currency/__init__.py b/src/coinc/__init__.py similarity index 97% rename from src/currency/__init__.py rename to src/coinc/__init__.py index f63adb6..a545fbb 100644 --- a/src/currency/__init__.py +++ b/src/coinc/__init__.py @@ -2,7 +2,7 @@ """Functions to be called by workflow""" import os from datetime import datetime -from .exceptions import CoonError, ConfigError +from .exceptions import CoincError, ConfigError from .query import Query from .utils import (init_workflow, load_currencies, refresh_rates, refresh_currencies, generate_list_items) @@ -57,8 +57,8 @@ def convert(workflow): init_workflow(workflow) query = Query(workflow.args[1:]) query.run_pattern(workflow) - except CoonError as error: - workflow.logger.info("Coon: {}".format(type(error).__name__)) + except CoincError as error: + workflow.logger.info("Coinc: {}".format(type(error).__name__)) workflow.logger.info(error) workflow.add_item(title=error.args[0], subtitle=error.args[1], @@ -158,10 +158,10 @@ def refresh(workflow): try: refresh_rates(workflow.config) refresh_currencies() - except CoonError as error: + except CoincError as error: workflow.logger.info(error) print("{},{}".format("❌Error occured during refresh", - "Coon: {}".format(type(error).__name__))) + "Coinc: {}".format(type(error).__name__))) except Exception as error: workflow.logger.info(error) print("{},{}".format("❌Error occured during refresh", diff --git a/src/currency/config.py b/src/coinc/config.py similarity index 100% rename from src/currency/config.py rename to src/coinc/config.py diff --git a/src/currency/exceptions.py b/src/coinc/exceptions.py similarity index 62% rename from src/currency/exceptions.py rename to src/coinc/exceptions.py index 60fe372..f724c97 100644 --- a/src/currency/exceptions.py +++ b/src/coinc/exceptions.py @@ -2,8 +2,8 @@ """Exceptions used in this module""" -class CoonError(Exception): - """Base Class used to declare other errors for Coon +class CoincError(Exception): + """Base Class used to declare other errors for Coinc Extends: Exception @@ -11,46 +11,46 @@ class CoonError(Exception): pass -class ConfigError(CoonError): +class ConfigError(CoincError): """Raised when there are invalid value filled in Configuration Sheet Extends: - CoonError + CoincError """ pass -class QueryError(CoonError): +class QueryError(CoincError): """Raised when invalid query were given Extends: - CoonError + CoincError """ pass -class AppIDError(CoonError): +class AppIDError(CoincError): """Raised when App ID can not be used Extends: - CoonError + CoincError """ pass -class ApiError(CoonError): +class ApiError(CoincError): """Raised when API is unreachable or return bad response Extends: - CoonError + CoincError """ pass -class UnknownPythonError(CoonError): +class UnknownPythonError(CoincError): """Raised when Python runtime version can not be correctly detacted Extends: - CoonError + CoincError """ pass diff --git a/src/currency/query.py b/src/coinc/query.py similarity index 100% rename from src/currency/query.py rename to src/coinc/query.py diff --git a/src/currency/utils.py b/src/coinc/utils.py similarity index 80% rename from src/currency/utils.py rename to src/coinc/utils.py index cd31f16..7fe59a0 100644 --- a/src/currency/utils.py +++ b/src/coinc/utils.py @@ -8,10 +8,61 @@ import unicodedata from .exceptions import ApiError, AppIDError, UnknownPythonError -RATE_ENDPOINT = ("https://openexchangerates.org/api/latest.json" - "?show_alternative=1&app_id={}") -CURRENCY_ENDPOINT = ("https://openexchangerates.org/api/currencies.json" - "?show_alternative=1") +INFO_PLIST_PATH = "info.plist" +OLD_BUNDLE_ID = "tech.tomy.coon" +NEW_BUNDLE_ID = "tech.tomy.coinc" +WORKFLOW_DATA_PATH = "~/Library/Application Support/Alfred/Workflow Data" +RATE_ENDPOINT = ( + "https://openexchangerates.org/api/latest.json" "?show_alternative=1&app_id={}" +) +CURRENCY_ENDPOINT = ( + "https://openexchangerates.org/api/currencies.json" "?show_alternative=1" +) + + +def manual_update_patch(workflow): + """manual update metadatas for v1.3.0 name change + + Update include two section, change Bundle ID in info.plist to a new one, + and rename the old data directory into new one + + Arguments: + workflow {workflow.Workflow3} -- The workflow object + + Returns: + bool -- Whether any modification got invoked + + Raises: + UnknownPythonError -- Raised when Python runtime version can not be + correctly detacted + """ + updated = False + # Fix Bundle ID + if workflow.bundleid.encode("utf-8") == OLD_BUNDLE_ID: + import plistlib + + if sys.version_info.major == 2: + info = plistlib.readPlist(INFO_PLIST_PATH) + info["bundleid"] = NEW_BUNDLE_ID + plistlib.writePlist(info, INFO_PLIST_PATH) + elif sys.version_info.major == 3: + with open(INFO_PLIST_PATH, "rw") as file: + info = plistlib.load(file) + info["bundleid"] = NEW_BUNDLE_ID + plistlib.dump(info, INFO_PLIST_PATH) + else: + raise UnknownPythonError("Unexpected Python Version", sys.version_info) + workflow.logger.info("Bundle ID modified") + updated = True + + # Move Data Directory + old_path = os.path.expanduser(os.path.join(WORKFLOW_DATA_PATH, OLD_BUNDLE_ID)) + if os.path.exists(old_path): + new_path = os.path.expanduser(os.path.join(WORKFLOW_DATA_PATH, NEW_BUNDLE_ID)) + os.rename(old_path, new_path) + workflow.logger.info("Data Directory moved") + updated = True + return updated def init_workflow(workflow): @@ -26,6 +77,7 @@ def init_workflow(workflow): workflow -- the passed in workflow object """ from .config import Config + workflow.config = Config() return workflow @@ -43,8 +95,7 @@ def _calculate(value, from_currency, to_currency, rates, precision): Returns: float -- The result of the conversion """ - return round(value * (rates[to_currency] / rates[from_currency]), - precision) + return round(value * (rates[to_currency] / rates[from_currency]), precision) def _byteify(loaded_dict): @@ -58,8 +109,7 @@ def _byteify(loaded_dict): """ if isinstance(loaded_dict, dict): return { - _byteify(key): _byteify(value) - for key, value in loaded_dict.iteritems() + _byteify(key): _byteify(value) for key, value in loaded_dict.iteritems() } if isinstance(loaded_dict, list): return [_byteify(element) for element in loaded_dict] @@ -152,8 +202,9 @@ def is_it_something_mixed(query): return (value, currency) # Type 3: {symbol}{number} - match_result = re.match(r"^(.+?)([0-9,]+(\.\d+)?)$", - query) # Use '+?' for non-progressive match + match_result = re.match( + r"^(.+?)([0-9,]+(\.\d+)?)$", query + ) # Use '+?' for non-progressive match if match_result: value = is_it_float(match_result.groups()[1]) currency_symbol = is_it_symbol(match_result.groups()[0]) @@ -189,8 +240,7 @@ def load_currencies(path="currencies.json"): elif sys.version_info.major == 3: currencies = json.load(file) else: - raise UnknownPythonError("Unexpected Python Version", - sys.version_info) + raise UnknownPythonError("Unexpected Python Version", sys.version_info) return currencies @@ -211,6 +261,7 @@ def refresh_currencies(path="currencies.json"): """ if sys.version_info.major == 2: import urllib2 + try: response = urllib2.urlopen(CURRENCY_ENDPOINT) except urllib2.HTTPError as err: @@ -219,6 +270,7 @@ def refresh_currencies(path="currencies.json"): currencies = _byteify(json.load(response, "utf-8")) elif sys.version_info.major == 3: from urllib import request, error + try: response = request.urlopen(CURRENCY_ENDPOINT) except error.HTTPError as err: @@ -277,13 +329,15 @@ def refresh_rates(config, path="rates.json"): """ if sys.version_info.major == 2: import urllib2 + try: response = urllib2.urlopen(RATE_ENDPOINT.format(config.app_id)) except urllib2.HTTPError as err: response = _byteify(json.load(err, "utf-8")) if err.code == 401: - raise AppIDError("Invalid App ID: {}".format(config.app_id), - response["description"]) + raise AppIDError( + "Invalid App ID: {}".format(config.app_id), response["description"] + ) elif err.code == 429: raise AppIDError("Access Restricted", response["description"]) else: @@ -291,13 +345,15 @@ def refresh_rates(config, path="rates.json"): rates = _byteify(json.load(response, "utf-8")) elif sys.version_info.major == 3: from urllib import request, error + try: response = request.urlopen(RATE_ENDPOINT.format(config.app_id)) except error.HTTPError as err: response = json.load(err) if err.code == 401: - raise AppIDError("Invalid App ID: {}".format(config.app_id), - response["description"]) + raise AppIDError( + "Invalid App ID: {}".format(config.app_id), response["description"] + ) elif err.code == 429: raise AppIDError("Access Restricted", response["description"]) else: @@ -332,8 +388,7 @@ def load_alias(path="alias.json"): elif sys.version_info.major == 3: alias = json.load(file) else: - raise UnknownPythonError("Unexpected Python Version", - sys.version_info) + raise UnknownPythonError("Unexpected Python Version", sys.version_info) return alias @@ -359,13 +414,11 @@ def load_symbols(path="symbols.json"): elif sys.version_info.major == 3: symbols = json.load(file) else: - raise UnknownPythonError("Unexpected Python Version", - sys.version_info) + raise UnknownPythonError("Unexpected Python Version", sys.version_info) return symbols -def generate_result_item(workflow, value, from_currency, to_currency, rates, - icon): +def generate_result_item(workflow, value, from_currency, to_currency, rates, icon): """Calculate conversion result and append item to workflow Arguments: @@ -381,29 +434,28 @@ def generate_result_item(workflow, value, from_currency, to_currency, rates, """ symbols = load_symbols() result = str( - _calculate(value, from_currency, to_currency, rates, - workflow.config.precision)) + _calculate(value, from_currency, to_currency, rates, workflow.config.precision) + ) result_symboled = "{}{}".format(symbols[to_currency], result) - item = workflow.add_item(title="{} {} = {} {}".format( - value, from_currency, result, to_currency), - subtitle="Copy '{}' to clipboard".format(result), - icon="flags/{}.png".format(icon), - valid=True, - arg=result, - copytext=result) + item = workflow.add_item( + title="{} {} = {} {}".format(value, from_currency, result, to_currency), + subtitle="Copy '{}' to clipboard".format(result), + icon="flags/{}.png".format(icon), + valid=True, + arg=result, + copytext=result, + ) item.add_modifier( key="alt", subtitle="Copy '{}' to clipboard".format(result_symboled), icon="flags/{}.png".format(icon), valid=True, - arg=result_symboled) + arg=result_symboled, + ) return item -def generate_list_items(query, - currency_codes, - favorite_filter=None, - sort=False): +def generate_list_items(query, currency_codes, favorite_filter=None, sort=False): """Generate items from currency codes that can be add to workflow Arguments: @@ -424,13 +476,15 @@ def generate_list_items(query, items = [] for code in currency_codes: if currencies_filter(query, code, currencies[code], favorite_filter): - items.append({ - "title": currencies[code], - "subtitle": code, - "icon": "flags/{}.png".format(code), - "valid": True, - "arg": code - }) + items.append( + { + "title": currencies[code], + "subtitle": code, + "icon": "flags/{}.png".format(code), + "valid": True, + "arg": code, + } + ) if sort: items = sorted(items, key=lambda item: item["subtitle"]) return items diff --git a/src/currency/workflow/.alfredversionchecked b/src/currency/workflow/.alfredversionchecked deleted file mode 100644 index e69de29..0000000 diff --git a/src/currency/workflow/version b/src/currency/workflow/version deleted file mode 100644 index 673b6a6..0000000 --- a/src/currency/workflow/version +++ /dev/null @@ -1 +0,0 @@ -1.37.2 \ No newline at end of file diff --git a/src/main.py b/src/main.py index 22334fa..3a050d7 100644 --- a/src/main.py +++ b/src/main.py @@ -1,25 +1,29 @@ # -*- coding: utf-8 -*- """Script for Default keyword""" import sys -import currency -from currency.workflow import Workflow3 +import coinc +from workflow import Workflow3 +from workflow.util import reload_workflow def main(workflow): """The main workflow entry function""" method = str(workflow.args.pop(0)) - if method in currency.__all__: - workflow.run(getattr(currency, method)) + if coinc.utils.manual_update_patch(workflow): + reload_workflow() + workflow.logger.info("Workflow Reloaded") + if method in coinc.__all__: + workflow.run(getattr(coinc, method)) else: - workflow.run(currency.help_me) + workflow.run(coinc.help_me) if __name__ == "__main__": WF = Workflow3( default_settings={"favorites": ["EUR", "CNY", "JPY", "GBP"]}, update_settings={ - "github_slug": "tomy0000000/coon", + "github_slug": "tomy0000000/Coinc", "frequency": 7 }, - help_url="https://git.io/fjD6M") + help_url="https://git.io/JfjXg") sys.exit(WF.run(main)) diff --git a/src/workflow/LICENCE.txt b/src/workflow/LICENCE.txt new file mode 100644 index 0000000..f55c710 --- /dev/null +++ b/src/workflow/LICENCE.txt @@ -0,0 +1,36 @@ +All Python source code is under the MIT Licence. + +The documentation, in particular the tutorials, are under the +Creative Commons Attribution-NonCommercial (CC BY-NC) licence. + +--------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2014 Dean Jackson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +--------------------------------------------------------------------- + +Creative Commons Attribution-NonCommercial (CC BY-NC) licence + +https://creativecommons.org/licenses/by-nc/4.0/legalcode + +(This one's quite long.) diff --git a/src/currency/workflow/Notify.tgz b/src/workflow/Notify.tgz similarity index 100% rename from src/currency/workflow/Notify.tgz rename to src/workflow/Notify.tgz diff --git a/src/currency/workflow/__init__.py b/src/workflow/__init__.py similarity index 100% rename from src/currency/workflow/__init__.py rename to src/workflow/__init__.py diff --git a/src/currency/workflow/background.py b/src/workflow/background.py similarity index 98% rename from src/currency/workflow/background.py rename to src/workflow/background.py index ba5c52a..c2bd735 100644 --- a/src/currency/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/currency/workflow/notify.py b/src/workflow/notify.py similarity index 94% rename from src/currency/workflow/notify.py rename to src/workflow/notify.py index a4b7f40..28ec0b9 100644 --- a/src/currency/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/currency/workflow/update.py b/src/workflow/update.py similarity index 99% rename from src/currency/workflow/update.py rename to src/workflow/update.py index 6affc94..c039f7a 100644 --- a/src/currency/workflow/update.py +++ b/src/workflow/update.py @@ -519,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/currency/workflow/util.py b/src/workflow/util.py similarity index 80% rename from src/currency/workflow/util.py rename to src/workflow/util.py index 27209d8..ab5e954 100644 --- a/src/currency/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/version b/src/workflow/version new file mode 100644 index 0000000..9c235b4 --- /dev/null +++ b/src/workflow/version @@ -0,0 +1 @@ +1.39.0 \ No newline at end of file diff --git a/src/currency/workflow/web.py b/src/workflow/web.py similarity index 92% rename from src/currency/workflow/web.py rename to src/workflow/web.py index 0781911..84a7062 100644 --- a/src/currency/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 @@ -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/currency/workflow/workflow.py b/src/workflow/workflow.py similarity index 99% rename from src/currency/workflow/workflow.py rename to src/workflow/workflow.py index 2a057b0..3935227 100644 --- a/src/currency/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/currency/workflow/workflow3.py b/src/workflow/workflow3.py similarity index 99% rename from src/currency/workflow/workflow3.py rename to src/workflow/workflow3.py index b92c4be..22dc5c6 100644 --- a/src/currency/workflow/workflow3.py +++ b/src/workflow/workflow3.py @@ -717,5 +717,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()