]> git.madduck.net Git - etc/taskwarrior.git/blobdiff - docs/index.rst

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

tests: Add tests for the LazyUUIDTaskSet
[etc/taskwarrior.git] / docs / index.rst
index 44ef46949b7dc7698800aada024e18745c715371..17ea42a3c8e00e4c03926b9ef1347e90b73c2f47 100644 (file)
@@ -10,7 +10,7 @@ Older versions of taskwarrior are untested and may not work.
 Requirements
 ------------
 
 Requirements
 ------------
 
-* taskwarrior_ v2.1.x or above.
+* taskwarrior_ v2.1.x or above, although newest minor release is recommended.
 
 Installation
 ------------
 
 Installation
 ------------
@@ -36,6 +36,11 @@ The default location is the same as taskwarrior's::
 
     >>> tw = TaskWarrior(data_location='~/.task', create=True)
 
 
     >>> tw = TaskWarrior(data_location='~/.task', create=True)
 
+The ``TaskWarrior`` instance will also use your .taskrc configuration (so that
+it recognizes the same UDAs as your task binary, uses the same configuration,
+etc.). To override the location of the .taskrc, use
+``taskrc_location=~/some/different/path``.
+
 Creating Tasks
 --------------
 
 Creating Tasks
 --------------
 
@@ -84,7 +89,7 @@ Attributes of task objects are accessible through indices, like so::
     >>> task['id']
     15
     >>> task['due']
     >>> task['id']
     15
     >>> task['due']
-    datetime.datetime(2013, 12, 5, 0, 0)
+    datetime.datetime(2015, 2, 5, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
     >>> task['tags']
     ['work', 'servers']
 
     >>> task['tags']
     ['work', 'servers']
 
@@ -98,6 +103,30 @@ The following fields are deserialized into Python objects:
 Attributes should be set using the correct Python representation, which will be
 serialized into the correct format when the task is saved.
 
 Attributes should be set using the correct Python representation, which will be
 serialized into the correct format when the task is saved.
 
+Task properties
+---------------
+
+Tasklib defines several properties upon ``Task`` object, for convenience::
+
+    >>> t.save()
+    >>> t.saved
+    True
+    >>> t.pending
+    True
+    >>> t.active
+    False
+    >>> t.start()
+    >>> t.active
+    True
+    >>> t.done()
+    >>> t.completed
+    True
+    >>> t.pending
+    False
+    >>> t.delete()
+    >>> t.deleted
+    True
+
 Operations on Tasks
 -------------------
 
 Operations on Tasks
 -------------------
 
@@ -142,6 +171,18 @@ Switching back to the open python process::
    >>> task['tags']
    ['someday']
 
    >>> task['tags']
    ['someday']
 
+Tasks can also be started and stopped. Use ``start()`` and ``stop()``
+respectively::
+
+    >>> task.start()
+    >>> task['start']
+    datetime.datetime(2015, 7, 16, 18, 48, 28, tzinfo=<DstTzInfo 'Europe/Prague' CEST+2:00:00 DST>)
+    >>> task.stop()
+    >>> task['start']
+    >>> task.done()
+    >>> task['end']
+    datetime.datetime(2015, 7, 16, 18, 49, 2, tzinfo=<DstTzInfo 'Europe/Prague' CEST+2:00:00 DST>)
+
 
 Retrieving Tasks
 ----------------
 
 Retrieving Tasks
 ----------------
@@ -239,6 +280,131 @@ same Python object::
     >>> task3 == task1
     True
 
     >>> task3 == task1
     True
 
+Accessing original values
+-------------------------
+
+To access the saved state of the Task, use dict-like access using the
+``original`` attribute:
+
+    >>> t = Task(tw, description="tidy up")
+    >>> t.save()
+    >>> t['description'] = "tidy up the kitchen and bathroom"
+    >>> t['description']
+    "tidy up the kitchen and bathroom"
+    >>> t.original['description']
+    "tidy up"
+
+When you save the task, original values are refreshed to reflect the
+saved state of the task:
+
+    >>> t.save()
+    >>> t.original['description']
+    "tidy up the kitchen and bathroom"
+
+Dealing with dates and time
+---------------------------
+
+Any timestamp-like attributes of the tasks are converted to timezone-aware
+datetime objects. To achieve this, Tasklib leverages ``pytz`` Python module,
+which brings the Olsen timezone databaze to Python.
+
+This shields you from annoying details of Daylight Saving Time shifts
+or conversion between different timezones. For example, to list all the
+tasks which are due midnight if you're currently in Berlin:
+
+    >>> myzone = pytz.timezone('Europe/Berlin')
+    >>> midnight = myzone.localize(datetime(2015,2,2,0,0,0))
+    >>> tw.tasks.filter(due__before=midnight)
+
+However, this is still a little bit tedious. That's why TaskWarrior object
+is capable of automatic timezone detection, using the ``tzlocal`` Python
+module. If your system timezone is set to 'Europe/Berlin', following example
+will work the same way as the previous one:
+
+    >>> tw.tasks.filter(due__before=datetime(2015,2,2,0,0,0))
+
+You can also use simple dates when filtering:
+
+    >>> tw.tasks.filter(due__before=date(2015,2,2))
+
+In such case, a 00:00:00 is used as the time component.
+
+Of course, you can use datetime naive objects when initializing Task object
+or assigning values to datetime atrributes:
+
+    >>> t = Task(tw, description="Buy new shoes", due=date(2015,2,5))
+    >>> t['due']
+    datetime.datetime(2015, 2, 5, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+    >>> t['due'] = date(2015,2,6,15,15,15)
+    >>> t['due']
+    datetime.datetime(2015, 2, 6, 15, 15, 15, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+
+However, since timezone-aware and timezone-naive datetimes are not comparable
+in Python, this can cause some unexpected behaviour:
+
+    >>> from datetime import datetime
+    >>> now = datetime.now()
+    >>> t = Task(tw, description="take out the trash now") 
+    >>> t['due'] = now
+    >>> now
+    datetime.datetime(2015, 2, 1, 19, 44, 4, 770001)
+    >>> t['due']
+    datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+    >>> t['due'] == now
+    Traceback (most recent call last):
+      File "<stdin>", line 1, in <module>
+      TypeError: can't compare offset-naive and offset-aware datetimes
+
+If you want to compare datetime aware value with datetime naive value, you need
+to localize the naive value first:
+
+    >>> from datetime import datetime
+    >>> from tasklib.task import local_zone
+    >>> now = local_zone.localize(datetime.now())
+    >>> t['due'] = now
+    >>> now
+    datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+    >>> t['due'] == now
+    True
+
+Also, note that it does not matter whether the timezone aware datetime objects
+are set in the same timezone:
+
+    >>> import pytz
+    >>> t['due']
+    datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
+    >>> now.astimezone(pytz.utc)
+    datetime.datetime(2015, 2, 1, 18, 44, 4, 770001, tzinfo=<UTC>)
+    >>> t['due'] == now.astimezone(pytz.utc)
+    True
+
+*Note*: Following behaviour is available only for TaskWarrior >= 2.4.0.
+
+There is a third approach to setting up date time values, which leverages
+the 'task calc' command. You can simply set any datetime attribute to
+any string that contains an acceptable TaskWarrior-formatted time expression::
+
+    $ task calc now + 1d
+    2015-07-17T21:17:54
+
+This syntax can be leveraged in the python interpreter as follows::
+
+    >>> t['due'] = "now + 1d"
+    >>> t['due']
+    datetime.datetime(2015, 7, 17, 21, 19, 31, tzinfo=<DstTzInfo 'Europe/Berlin' CEST+2:00:00 DST>)
+
+It can be easily seen that the string with TaskWarrior-formatted time expression
+is automatically converted to native datetime in the local time zone.
+
+For the list of acceptable formats and keywords, please consult:
+
+* http://taskwarrior.org/docs/dates.html
+* http://taskwarrior.org/docs/named_dates.html
+
+However, as each such assigment involves call to 'task calc' for conversion,
+it might cause some performance issues when assigning strings to datetime
+attributes repeatedly, in a automated manner.
+
 Working with annotations
 ------------------------
 
 Working with annotations
 ------------------------
 
@@ -253,7 +419,7 @@ Annotations have only defined ``entry`` and ``description`` values::
 
     >>> annotation = annotated_task['annotations'][0]
     >>> annotation['entry']
 
     >>> annotation = annotated_task['annotations'][0]
     >>> annotation['entry']
-    datetime.datetime(2015, 1, 3, 21, 13, 55)
+    datetime.datetime(2015, 1, 3, 21, 13, 55, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
     >>> annotation['description']
     u'Yeah, I am annotated!'
 
     >>> annotation['description']
     u'Yeah, I am annotated!'
 
@@ -295,33 +461,99 @@ You can use ``config_override`` keyword argument to specify a dictionary of conf
 
     >>> tw.execute_command(['3', 'done'], config_override={'gc': 'off'}) # Will mark 3 as completed and it will retain its ID
 
 
     >>> tw.execute_command(['3', 'done'], config_override={'gc': 'off'}) # Will mark 3 as completed and it will retain its ID
 
+
+Additionally, you can use ``return_all=True`` flag, which returns
+``(stdout, sterr, return_code)`` triplet, and ``allow_failure=False``, which will
+prevent tasklib from raising an exception if the task binary returned non-zero
+return code::
+
+    >>> tw.execute_command(['invalidcommand'], allow_failure=False, return_all=True)
+    ([u''],
+     [u'Using alternate .taskrc file /home/tbabej/.taskrc',
+      u"[task next rc:/home/tbabej/.taskrc rc.recurrence.confirmation=no rc.json.array=off rc.confirmation=no rc.bulk=0 rc.dependency.confirmation=no description ~ 'invalidcommand']",
+      u'Configuration override rc.recurrence.confirmation:no',
+      u'Configuration override rc.json.array:off',
+      u'Configuration override rc.confirmation:no',
+      u'Configuration override rc.bulk:0',
+      u'Configuration override rc.dependency.confirmation:no',
+      u'No matches.',
+      u'There are local changes.  Sync required.'],
+     1)
+
+
 Setting custom configuration values
 -----------------------------------
 
 Setting custom configuration values
 -----------------------------------
 
-By default, TaskWarrior does not use any of configuration values stored in
-your .taskrc. To see what configuration values are passed to each executed
-task command, have a peek into ``config`` attribute of ``TaskWarrior`` object::
+By default, TaskWarrior uses configuration values stored in your .taskrc.
+To see what configuration value overrides are passed to each executed
+task command, have a peek into ``overrides`` attribute of ``TaskWarrior`` object::
 
 
-    >>> tw.config
+    >>> tw.overrides
     {'confirmation': 'no', 'data.location': '/home/tbabej/.task'}
 
     {'confirmation': 'no', 'data.location': '/home/tbabej/.task'}
 
-To pass your own configuration, you just need to update this dictionary::
+To pass your own configuration overrides, you just need to update this dictionary::
+
+    >>> tw.overrides.update({'hooks': 'off'})  # tasklib will not trigger hooks
+
+Creating hook scripts
+---------------------
+
+From version 2.4.0, TaskWarrior has support for hook scripts. Tasklib provides
+some very useful helpers to write those. With tasklib, writing these becomes
+a breeze::
 
 
-    >>> tw.config.update({'hooks': 'off'})  # tasklib will not trigger hooks
+    #!/usr/bin/python
+
+    from tasklib.task import Task
+    task = Task.from_input()
+    # ... <custom logic>
+    print task.export_data()
+
+For example, plugin which would assign the priority "H" to any task containing
+three exclamation marks in the description, would go like this::
+
+    #!/usr/bin/python
+
+    from tasklib.task import Task
+    task = Task.from_input()
+
+    if "!!!" in task['description']:
+        task['priority'] = "H"
+
+    print task.export_data()
+
+Tasklib can automatically detect whether it's running in the ``on-modify`` event,
+which provides more input than ``on-add`` event and reads the data accordingly.
+
+This means the example above works both for ``on-add`` and ``on-modify`` events!
+
+Consenquently, you can create just one hook file for both ``on-add`` and
+``on-modify`` events, and you just need to create a symlink for the other one.
+This removes the need for maintaining two copies of the same code base and/or
+boilerplate code.
+
+In ``on-modify`` events, tasklib loads both the original version and the modified
+version of the task to the returned ``Task`` object. To access the original data
+(in read-only manner), use ``original`` dict-like attribute:
+
+    >>> t = Task.from_input()
+    >>> t['description']
+    "Modified description"
+    >>> t.original['description']
+    "Original description"
 
 Working with UDAs
 -----------------
 
 
 Working with UDAs
 -----------------
 
-Since TaskWarrior does not read your .taskrc, you need to define any UDAs
-in the TaskWarrior's config dictionary, as described above.
+Since TaskWarrior does read your .taskrc, you need not to define any UDAs
+in the TaskWarrior's config dictionary, as described above. Suppose we have
+a estimate UDA in the .taskrc::
 
 
-Let us demonstrate this on the same example as in the TaskWarrior's docs::
+    uda.estimate.type = numeric
 
 
-    >>> tw = TaskWarrior()
-    >>> tw.config.update({'uda.estimate.type': 'numeric'})
-
-Now we can filter and create tasks using the estimate UDA::
+We can simply filter and create tasks using the estimate UDA out of the box::
 
 
+    >>> tw = TaskWarrior()
     >>> task = Task(tw, description="Long task", estimate=1000)
     >>> task.save()
     >>> task['id']
     >>> task = Task(tw, description="Long task", estimate=1000)
     >>> task.save()
     >>> task['id']
@@ -332,7 +564,7 @@ This is saved as UDA in the TaskWarrior::
     $ task 1 export
     {"id":1,"description":"Long task","estimate":1000, ...}
 
     $ task 1 export
     {"id":1,"description":"Long task","estimate":1000, ...}
 
-As long as ``TaskWarrior``'s config is updated, we can approach UDAs as built in attributes::
+We can also speficy UDAs as arguments in the TaskFilter::
 
     >>> tw.tasks.filter(estimate=1000)
     Long task
 
     >>> tw.tasks.filter(estimate=1000)
     Long task
@@ -340,9 +572,16 @@ As long as ``TaskWarrior``'s config is updated, we can approach UDAs as built in
 Syncing
 -------
 
 Syncing
 -------
 
-Syncing is not directly supported by tasklib, but it can be made to work in a similiar way
-as the UDAs. First we need to update the ``config`` dictionary by the values required for
-sync to work, and then we can run the sync command using the ``execute_command()`` method::
+If you have configurated the needed config variables in your .taskrc, syncing
+is as easy as::
+
+    >>> tw = TaskWarrior()
+    >>> tw.execute_command(['sync'])
+
+If you want to use non-standard server/credentials, you'll need to provide configuration
+overrides to the ``TaskWarrior`` instance. Update the ``config`` dictionary with the
+values you desire to override, and then we can run the sync command using
+the ``execute_command()`` method::
 
     >>> tw = TaskWarrior()
     >>> sync_config = {
 
     >>> tw = TaskWarrior()
     >>> sync_config = {