diff --git a/TODO b/TODO index bb90d768..925754d8 100644 --- a/TODO +++ b/TODO @@ -32,7 +32,11 @@ General: trying to achieve. They would just be a duplication of the comments in the source code in any case. - Add `setup.py` so `Alfred-Workflow` can be added to PyPi and installed with `pip` + - Add explicit `save()` method to `Settings` +background.py: + - Add `stop_process()` function. + Allow `killing` of running processes. Can be used for stopping servers. web.py: diff --git a/alfred-workflow.zip b/alfred-workflow.zip index fee13124..fe4afaba 100644 Binary files a/alfred-workflow.zip and b/alfred-workflow.zip differ diff --git a/doc/howto.rst b/doc/howto.rst index d2edff65..abc40956 100644 --- a/doc/howto.rst +++ b/doc/howto.rst @@ -135,6 +135,10 @@ The data directory is intended for more permanent, user-generated data, or data that cannot be otherwise easily recreated. See :ref:`Storing data ` for details. +It is easy to specify a custom file format for your stored data +via the ``serializer`` argument if you want your data to be readable by the user +or by other software. See :ref:`Serialization ` for more details. + There are also simliar methods related to the root directory of your Workflow (where ``info.plist`` and your code are): @@ -156,8 +160,10 @@ Caching data ------------ :class:`Workflow ` provides a few methods to simplify -caching data that is slow to retrieve or expensive to generate. The main method -is :meth:`Workflow.cached_data() `, which +caching data that is slow to retrieve or expensive to generate (e.g. downloaded +from a web API). These data are cached in your workflow's cache directory (see +:attr:`~workflow.workflow.Workflow.cachedir`). The main method is +:meth:`Workflow.cached_data() `, which takes a name under which the data should be cached, a callable to retrieve the data if they aren't in the cache (or are too old), and a maximum age in seconds for the cached data: @@ -202,6 +208,9 @@ and retrieve permanent data: :meth:`store_data() ` and :meth:`stored_data() `. +These data are stored in your workflow's data directory +(see :attr:`~workflow.workflow.Workflow.datadir`). + .. code-block:: python :linenos: @@ -235,6 +244,20 @@ file in your Workflow's data directory when it is changed. :class:`~workflow.workflow.Settings` can be used just like a normal :class:`dict` with the caveat that all keys and values must be serializable to JSON. +**Note:** A :class:`~workflow.workflow.Settings` instance can only automatically +recognise when you directly alter the values of its own keys: + +.. code-block:: python + :linenos: + + wf = Workflow() + wf.settings['key'] = {'key2': 'value'} # will be automatically saved + wf.settings['key']['key2'] = 'value2' # will *not* be automatically saved + +If you've altered a data structure stored within your workflow's +:attr:`Workflow.settings `, you need to +explicitly call :meth:`Workflow.settings.save() `. + If you need to store arbitrary data, you can use the :ref:`cached data API `. If you need to store data securely (such as passwords and API keys), @@ -469,6 +492,72 @@ or to :class:`Workflow ` on instantiation and then normalised. +.. _background-processes: + +Background processes +==================== + +Many workflows provide a convenient interface to applications and/or web services. + +For performance reasons, it's common for workflows to cache data locally, but +updating this cache typically takes a few seconds, making your workflow +unresponsive while an update is occurring, which is very un-Alfred-like. + +To avoid such delays, **Alfred-Workflow** provides the :mod:`~workflow.background` +module to allow you to easily run scripts in the background. + +There are two functions, :func:`~workflow.background.run_in_background` and +:func:`~workflow.background.is_running`, that provide the interface. The +processes started are proper background daemon processes, so you can start +proper servers as easily as simple scripts. + +Here's an example of a common usage pattern (updating cached data in the +background). What we're doing is: + +1. Check the age of the cached data and run the update script via + :func:`~workflow.background.run_in_background` if the cached data are too old + or don't exist. +2. (Optionally) inform the user that data are being updated. +3. Load the cached data regardless of age. +4. Display the cached data (if any). + +.. code-block:: python + :linenos: + + from workflow import Workflow, ICON_INFO + from workflow.background import run_in_background, is_running + + def main(wf): + # Is cache over 6 hours old or non-existent? + if not wf.cached_data_fresh('exchange-rates', 3600): + run_in_background('update', + ['/usr/bin/python', + wf.workflowfile('update_exchange_rates.py')]) + + # Add a notification if the script is running + if is_running('update'): + wf.add_item('Updating exchange rates...', icon=ICON_INFO) + + exchange_rates = wf.cached_data('exchage-rates') + + # Display (possibly stale) cache data + if exchange_rates: + for rate in exchange_rates: + wf.add_item(rate) + + # Send results to Alfred + wf.send_feedback() + + if __name__ == '__main__': + wf = Workflow() + wf.run(main) + +For a working example, see :ref:`Part 2 of the Tutorial ` or +the `source code `_ +of my `Git Repos `_ workflow, +which is a bit smarter about showing the user update information. + + .. _serialization: Serialization @@ -539,7 +628,7 @@ of the stored file. The :meth:`stored_data() ` method can automatically determine the serialization of the stored data, provided the -relevant serializer is registered. If it isn't, an exception will be raised. +corresponding serializer is registered. If it isn't, an exception will be raised. Built-in icons diff --git a/doc/index.rst b/doc/index.rst index 2d5bd9aa..2bee0599 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -47,8 +47,8 @@ tricks that might also be of interest to intermediate Pythonistas. tutorial2 -Alfred-Workflow howto -===================== +Howto +===== If you know your way around Python and Alfred, here's an overview of what **Alfred-Workflow** does and how to do it. diff --git a/workflow/__init__.py b/workflow/__init__.py index afa23340..50b127e7 100644 --- a/workflow/__init__.py +++ b/workflow/__init__.py @@ -20,12 +20,14 @@ - Parsing script arguments. - Text decoding/normalisation. -- Caching data and settings. -- Secure storage (and sync) of passwords (using OS X Keychain). +- Storing data and settings. +- Caching data from, e.g., web services with a simple API for updating expired data. +- Securely storing (and syncing) passwords using OS X Keychain. - Generating XML output for Alfred. - Including external libraries (adding directories to ``sys.path``). -- Filtering results using an Alfred-like algorithm. +- Filtering results using an Alfred-like, fuzzy search algorithm. - Generating log output for debugging. +- Running background processes to keep your workflow responsive. - Capturing errors, so the workflow doesn't fail silently. Quick Example @@ -108,7 +110,7 @@ def main(wf): """ -__version__ = '1.8' +__version__ = '1.8.1' from .workflow import Workflow, PasswordNotFound, KeychainError diff --git a/workflow/workflow.py b/workflow/workflow.py index 1e6a79f5..9462d928 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -540,7 +540,7 @@ def register(self, name, serializer): :param name: Name to register ``serializer`` under :type name: ``unicode`` or ``str`` - :param serilializer: object with ``load()`` and ``dump()`` + :param serializer: object with ``load()`` and ``dump()`` methods """ @@ -744,7 +744,7 @@ def __init__(self, filepath, defaults=None): elif defaults: for key, val in defaults.items(): self[key] = val - self._save() # save default settings + self.save() # save default settings def _load(self): """Load cached settings from JSON file `self._filepath`""" @@ -755,8 +755,13 @@ def _load(self): self[key] = value self._nosave = False - def _save(self): - """Save settings to JSON file `self._filepath`""" + def save(self): + """Save settings to JSON file specified in ``self._filepath`` + + If you're using this class via :attr:`Workflow.settings`, which + you probably are, ``self._filepath`` will be ``settings.json`` + in your workflow's data directory (see :attr:`~Workflow.datadir`). + """ if self._nosave: return data = {} @@ -768,17 +773,17 @@ def _save(self): # dict methods def __setitem__(self, key, value): super(Settings, self).__setitem__(key, value) - self._save() + self.save() def update(self, *args, **kwargs): """Override :class:`dict` method to save on update.""" super(Settings, self).update(*args, **kwargs) - self._save() + self.save() def setdefault(self, key, value=None): """Override :class:`dict` method to save on update.""" ret = super(Settings, self).setdefault(key, value) - self._save() + self.save() return ret @@ -1030,6 +1035,11 @@ def args(self): def cachedir(self): """Path to workflow's cache directory. + The cache directory is a subdirectory of Alfred's own cache directory in + ``~/Library/Caches``. The full path is: + + ``~/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/`` + :returns: full path to workflow's cache directory :rtype: ``unicode`` @@ -1050,6 +1060,11 @@ def cachedir(self): def datadir(self): """Path to workflow's data directory. + The data directory is a subdirectory of Alfred's own data directory in + ``~/Library/Application Support``. The full path is: + + ``~/Library/Application Support/Alfred 2/Workflow Data/`` + :returns: full path to workflow data directory :rtype: ``unicode`` @@ -1088,7 +1103,8 @@ def workflowdir(self): return self._workflowdir def cachefile(self, filename): - """Return full path to ``filename`` within workflow's cache dir. + """Return full path to ``filename`` within your workflow's + :attr:`cache directory `. :param filename: basename of file :type filename: ``unicode`` @@ -1100,7 +1116,8 @@ def cachefile(self, filename): return os.path.join(self.cachedir, filename) def datafile(self, filename): - """Return full path to ``filename`` within workflow's data dir. + """Return full path to ``filename`` within your workflow's + :attr:`data directory `. :param filename: basename of file :type filename: ``unicode``