]> git.madduck.net Git - etc/taskwarrior.git/blob - 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:

TaskWarrior: Actually return converted datetime object
[etc/taskwarrior.git] / docs / index.rst
1 Welcome to tasklib's documentation!
2 ===================================
3
4 tasklib is a Python library for interacting with taskwarrior_ databases, using
5 a queryset API similar to that of Django's ORM.
6
7 Supports Python 2.6, 2.7, 3.2, 3.3 and 3.4 with taskwarrior 2.1.x and above.
8 Older versions of taskwarrior are untested and may not work.
9
10 Requirements
11 ------------
12
13 * taskwarrior_ v2.1.x or above, although newest minor release is recommended.
14
15 Installation
16 ------------
17
18 Install via pip (recommended)::
19
20     pip install tasklib
21
22 Or clone from github::
23
24     git clone https://github.com/robgolding63/tasklib.git
25     cd tasklib
26     python setup.py install
27
28 Initialization
29 --------------
30
31 Optionally initialize the ``TaskWarrior`` instance with ``data_location`` (the
32 database directory). If it doesn't already exist, this will be created
33 automatically unless ``create=False``.
34
35 The default location is the same as taskwarrior's::
36
37     >>> tw = TaskWarrior(data_location='~/.task', create=True)
38
39 The ``TaskWarrior`` instance will also use your .taskrc configuration (so that
40 it recognizes the same UDAs as your task binary, uses the same configuration,
41 etc.). To override the location of the .taskrc, use
42 ``taskrc_location=~/some/different/path``.
43
44 Creating Tasks
45 --------------
46
47 To create a task, simply create a new ``Task`` object::
48
49     >>> new_task = Task(tw, description="throw out the trash")
50
51 This task is not yet saved to TaskWarrior (same as in Django), not until
52 you call ``.save()`` method::
53
54     >>> new_task.save()
55
56 You can set any attribute as a keyword argument to the Task object::
57
58     >>> complex_task = Task(tw, description="finally fix the shower", due=datetime(2015,2,14,8,0,0), priority='H')
59
60 or by setting the attributes one by one::
61
62     >>> complex_task = Task(tw)
63     >>> complex_task['description'] = "finally fix the shower"
64     >>> complex_task['due'] = datetime(2015,2,14,8,0,0)
65     >>> complex_task['priority'] = 'H'
66
67 Modifying Task
68 --------------
69
70 To modify a created or retrieved ``Task`` object, use dictionary-like access::
71
72     >>> homework = tw.tasks.get(tags=['chores'])
73     >>> homework['project'] = 'Home'
74
75 The change is not propagated to the TaskWarrior until you run the ``save()`` method::
76
77     >>> homework.save()
78
79 Attributes, which map to native Python objects are converted. See Task Attributes section.
80
81 Task Attributes
82 ---------------
83
84 Attributes of task objects are accessible through indices, like so::
85
86     >>> task = tw.tasks.pending().get(tags__contain='work')  # There is only one pending task with 'work' tag
87     >>> task['description']
88     'Upgrade Ubuntu Server'
89     >>> task['id']
90     15
91     >>> task['due']
92     datetime.datetime(2015, 2, 5, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
93     >>> task['tags']
94     ['work', 'servers']
95
96 The following fields are deserialized into Python objects:
97
98 * ``due``, ``wait``, ``scheduled``, ``until``, ``entry``: deserialized to a ``datetime`` object
99 * ``annotations``: deserialized to a list of ``TaskAnnotation`` objects
100 * ``tags``: deserialized to a list of strings
101 * ``depends``: deserialized to a set of ``Task`` objects
102
103 Attributes should be set using the correct Python representation, which will be
104 serialized into the correct format when the task is saved.
105
106 Task properties
107 ---------------
108
109 Tasklib defines several properties upon ``Task`` object, for convenience::
110
111     >>> t.save()
112     >>> t.saved
113     True
114     >>> t.pending
115     True
116     >>> t.active
117     False
118     >>> t.start()
119     >>> t.active
120     True
121     >>> t.done()
122     >>> t.completed
123     True
124     >>> t.pending
125     False
126     >>> t.delete()
127     >>> t.deleted
128     True
129
130 Operations on Tasks
131 -------------------
132
133 After modifying one or more attributes, simple call ``save()`` to write those
134 changes to the database::
135
136     >>> task = tw.tasks.pending().get(tags__contain='work')
137     >>> task['due'] = datetime(year=2014, month=1, day=5)
138     >>> task.save()
139
140 To mark a task as complete, use ``done()``::
141
142     >>> task = tw.tasks.pending().get(tags__contain='work')
143     >>> task.done()
144     >>> len(tw.tasks.pending().filter(tags__contain='work'))
145     0
146
147 To delete a task, use ``delete()``::
148
149     >>> task = tw.tasks.get(description="task added by mistake")
150     >>> task.delete()
151
152 To update a task object with values from TaskWarrior database, use ``refresh()``. Example::
153
154     >>> task = Task(tw, description="learn to cook")
155     >>> task.save()
156     >>> task['id']
157     5
158     >>> task['tags']
159     []
160
161 Now, suppose the we modify the task using the TaskWarrior interface in another terminal::
162
163     $ task 5 modify +someday
164     Task 5 modified.
165
166 Switching back to the open python process::
167
168    >>> task['tags']
169    []
170    >>> task.refresh()
171    >>> task['tags']
172    ['someday']
173
174 Tasks can also be started and stopped. Use ``start()`` and ``stop()``
175 respectively::
176
177     >>> task.start()
178     >>> task['start']
179     datetime.datetime(2015, 7, 16, 18, 48, 28, tzinfo=<DstTzInfo 'Europe/Prague' CEST+2:00:00 DST>)
180     >>> task.stop()
181     >>> task['start']
182     >>> task.done()
183     >>> task['end']
184     datetime.datetime(2015, 7, 16, 18, 49, 2, tzinfo=<DstTzInfo 'Europe/Prague' CEST+2:00:00 DST>)
185
186
187 Retrieving Tasks
188 ----------------
189
190 ``tw.tasks`` is a ``TaskQuerySet`` object which emulates the Django QuerySet
191 API. To get all tasks (including completed ones)::
192
193     >>> tw.tasks.all()
194     ['First task', 'Completed task', 'Deleted task', ...]
195
196 Filtering
197 ---------
198
199 Filter tasks using the same familiar syntax::
200
201     >>> tw.tasks.filter(status='pending', tags__contains=['work'])
202     ['Upgrade Ubuntu Server']
203
204 Filter arguments are passed to the ``task`` command (``__`` is replaced by
205 a period) so the above example is equivalent to the following command::
206
207     $ task status:pending tags.contain=work
208
209 Tasks can also be filtered using raw commands, like so::
210
211     >>> tw.tasks.filter('status:pending +work')
212     ['Upgrade Ubuntu Server']
213
214 Although this practice is discouraged, as by using raw commands you may lose
215 some of the portablility of your commands over different TaskWarrior versions.
216
217 However, you can mix raw commands with keyword filters, as in the given example::
218
219     >>> tw.tasks.filter('+BLOCKING', project='Home')  # Gets all blocking tasks in project Home
220     ['Fix the toilette']
221
222 This can be a neat way how to use syntax not yet supported by tasklib. The above
223 is excellent example, since virtual tags do not work the same way as the ordinary ones, that is::
224
225     >>> tw.tasks.filter(tags=['BLOCKING'])
226     >>> []
227
228 will not work.
229
230 There are built-in functions for retrieving pending & completed tasks::
231
232     >>> tw.tasks.pending().filter(tags__contain='work')
233     ['Upgrade Ubuntu Server']
234     >>> len(tw.tasks.completed())
235     227
236
237 Use ``get()`` to return the only task in a ``TaskQuerySet``, or raise an
238 exception::
239
240     >>> tw.tasks.get(tags__contain='work')['status']
241     'pending'
242     >>> tw.tasks.get(status='completed', tags__contains='work')  # Status of only task with the work tag is pending, so this should fail
243     Traceback (most recent call last):
244       File "<stdin>", line 1, in <module>
245       File "tasklib/task.py", line 224, in get
246         'Lookup parameters were {0}'.format(kwargs))
247     tasklib.task.DoesNotExist: Task matching query does not exist. Lookup parameters were {'status': 'completed', 'tags__contains': ['work']}
248     >>> tw.tasks.get(status='pending')
249     Traceback (most recent call last):
250       File "<stdin>", line 1, in <module>
251       File "tasklib/task.py", line 227, in get
252         'Lookup parameters were {1}'.format(num, kwargs))
253     ValueError: get() returned more than one Task -- it returned 23! Lookup parameters were {'status': 'pending'}
254
255 Additionally, since filters return ``TaskQuerySets`` you can stack filters on top of each other::
256
257     >>> home_tasks = tw.tasks.filter(project='Wife')
258     >>> home_tasks.filter(due__before=datetime(2015,2,14,14,14,14))  # What I have to do until Valentine's day
259     ['Prepare surprise birthday party']
260
261 Equality of Task objects
262 ------------------------
263
264 Two Tasks are considered equal if they have the same UUIDs::
265
266     >>> task1 = Task(tw, description="Pet the dog")
267     >>> task1.save()
268     >>> task2 = tw.tasks.get(description="Pet the dog")
269     >>> task1 == task2
270     True
271
272 If you compare the two unsaved tasks, they are considered equal only if it's the
273 same Python object::
274
275     >>> task1 = Task(tw, description="Pet the cat")
276     >>> task2 = Task(tw, description="Pet the cat")
277     >>> task1 == task2
278     False
279     >>> task3 = task1
280     >>> task3 == task1
281     True
282
283 Accessing original values
284 -------------------------
285
286 To access the saved state of the Task, use dict-like access using the
287 ``original`` attribute:
288
289     >>> t = Task(tw, description="tidy up")
290     >>> t.save()
291     >>> t['description'] = "tidy up the kitchen and bathroom"
292     >>> t['description']
293     "tidy up the kitchen and bathroom"
294     >>> t.original['description']
295     "tidy up"
296
297 When you save the task, original values are refreshed to reflect the
298 saved state of the task:
299
300     >>> t.save()
301     >>> t.original['description']
302     "tidy up the kitchen and bathroom"
303
304 Dealing with dates and time
305 ---------------------------
306
307 Any timestamp-like attributes of the tasks are converted to timezone-aware
308 datetime objects. To achieve this, Tasklib leverages ``pytz`` Python module,
309 which brings the Olsen timezone databaze to Python.
310
311 This shields you from annoying details of Daylight Saving Time shifts
312 or conversion between different timezones. For example, to list all the
313 tasks which are due midnight if you're currently in Berlin:
314
315     >>> myzone = pytz.timezone('Europe/Berlin')
316     >>> midnight = myzone.localize(datetime(2015,2,2,0,0,0))
317     >>> tw.tasks.filter(due__before=midnight)
318
319 However, this is still a little bit tedious. That's why TaskWarrior object
320 is capable of automatic timezone detection, using the ``tzlocal`` Python
321 module. If your system timezone is set to 'Europe/Berlin', following example
322 will work the same way as the previous one:
323
324     >>> tw.tasks.filter(due__before=datetime(2015,2,2,0,0,0))
325
326 You can also use simple dates when filtering:
327
328     >>> tw.tasks.filter(due__before=date(2015,2,2))
329
330 In such case, a 00:00:00 is used as the time component.
331
332 Of course, you can use datetime naive objects when initializing Task object
333 or assigning values to datetime atrributes:
334
335     >>> t = Task(tw, description="Buy new shoes", due=date(2015,2,5))
336     >>> t['due']
337     datetime.datetime(2015, 2, 5, 0, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
338     >>> t['due'] = date(2015,2,6,15,15,15)
339     >>> t['due']
340     datetime.datetime(2015, 2, 6, 15, 15, 15, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
341
342 However, since timezone-aware and timezone-naive datetimes are not comparable
343 in Python, this can cause some unexpected behaviour:
344
345     >>> from datetime import datetime
346     >>> now = datetime.now()
347     >>> t = Task(tw, description="take out the trash now") 
348     >>> t['due'] = now
349     >>> now
350     datetime.datetime(2015, 2, 1, 19, 44, 4, 770001)
351     >>> t['due']
352     datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
353     >>> t['due'] == now
354     Traceback (most recent call last):
355       File "<stdin>", line 1, in <module>
356       TypeError: can't compare offset-naive and offset-aware datetimes
357
358 If you want to compare datetime aware value with datetime naive value, you need
359 to localize the naive value first:
360
361     >>> from datetime import datetime
362     >>> from tasklib.task import local_zone
363     >>> now = local_zone.localize(datetime.now())
364     >>> t['due'] = now
365     >>> now
366     datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
367     >>> t['due'] == now
368     True
369
370 Also, note that it does not matter whether the timezone aware datetime objects
371 are set in the same timezone:
372
373     >>> import pytz
374     >>> t['due']
375     datetime.datetime(2015, 2, 1, 19, 44, 4, 770001, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
376     >>> now.astimezone(pytz.utc)
377     datetime.datetime(2015, 2, 1, 18, 44, 4, 770001, tzinfo=<UTC>)
378     >>> t['due'] == now.astimezone(pytz.utc)
379     True
380
381 *Note*: Following behaviour is available only for TaskWarrior >= 2.4.0.
382
383 There is a third approach to setting up date time values, which leverages
384 the 'task calc' command. You can simply set any datetime attribute to
385 any string that contains an acceptable TaskWarrior-formatted time expression::
386
387     $ task calc now + 1d
388     2015-07-17T21:17:54
389
390 This syntax can be leveraged in the python interpreter as follows::
391
392     >>> t['due'] = "now + 1d"
393     >>> t['due']
394     datetime.datetime(2015, 7, 17, 21, 19, 31, tzinfo=<DstTzInfo 'Europe/Berlin' CEST+2:00:00 DST>)
395
396 It can be easily seen that the string with TaskWarrior-formatted time expression
397 is automatically converted to native datetime in the local time zone.
398
399 For the list of acceptable formats and keywords, please consult:
400
401 * http://taskwarrior.org/docs/dates.html
402 * http://taskwarrior.org/docs/named_dates.html
403
404 However, as each such assigment involves call to 'task calc' for conversion,
405 it might cause some performance issues when assigning strings to datetime
406 attributes repeatedly, in a automated manner.
407
408 Working with annotations
409 ------------------------
410
411 Annotations of the tasks are represented in tasklib by ``TaskAnnotation`` objects. These
412 are much like ``Task`` objects, albeit very simplified.
413
414     >>> annotated_task = tw.tasks.get(description='Annotated task')
415     >>> annotated_task['annotations']
416     [Yeah, I am annotated!]
417
418 Annotations have only defined ``entry`` and ``description`` values::
419
420     >>> annotation = annotated_task['annotations'][0]
421     >>> annotation['entry']
422     datetime.datetime(2015, 1, 3, 21, 13, 55, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
423     >>> annotation['description']
424     u'Yeah, I am annotated!'
425
426 To add a annotation to a Task, use ``add_annotation()``::
427
428     >>> task = Task(tw, description="new task")
429     >>> task.add_annotation("we can annotate any task")
430     Traceback (most recent call last):
431       File "<stdin>", line 1, in <module>
432         File "build/bdist.linux-x86_64/egg/tasklib/task.py", line 355, in add_annotation
433     tasklib.task.NotSaved: Task needs to be saved to add annotation
434
435 However, Task needs to be saved before you can add a annotation to it::
436
437     >>> task.save()
438     >>> task.add_annotation("we can annotate saved tasks")
439     >>> task['annotations']
440     [we can annotate saved tasks]
441
442 To remove the annotation, pass its description to ``remove_annotation()`` method::
443
444     >>> task.remove_annotation("we can annotate saved tasks")
445
446 Alternatively, you can pass the ``TaskAnnotation`` object itself::
447
448     >>> task.remove_annotation(task['annotations'][0])
449
450
451 Running custom commands
452 -----------------------
453
454 To run a custom commands, use ``execute_command()`` method of ``TaskWarrior`` object::
455
456     >>> tw = TaskWarrior()
457     >>> tw.execute_command(['log', 'Finish high school.'])
458     [u'Logged task.']
459
460 You can use ``config_override`` keyword argument to specify a dictionary of configuration overrides::
461
462     >>> tw.execute_command(['3', 'done'], config_override={'gc': 'off'}) # Will mark 3 as completed and it will retain its ID
463
464
465 Additionally, you can use ``return_all=True`` flag, which returns
466 ``(stdout, sterr, return_code)`` triplet, and ``allow_failure=False``, which will
467 prevent tasklib from raising an exception if the task binary returned non-zero
468 return code::
469
470     >>> tw.execute_command(['invalidcommand'], allow_failure=False, return_all=True)
471     ([u''],
472      [u'Using alternate .taskrc file /home/tbabej/.taskrc',
473       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']",
474       u'Configuration override rc.recurrence.confirmation:no',
475       u'Configuration override rc.json.array:off',
476       u'Configuration override rc.confirmation:no',
477       u'Configuration override rc.bulk:0',
478       u'Configuration override rc.dependency.confirmation:no',
479       u'No matches.',
480       u'There are local changes.  Sync required.'],
481      1)
482
483
484 Setting custom configuration values
485 -----------------------------------
486
487 By default, TaskWarrior uses configuration values stored in your .taskrc.
488 To see what configuration value overrides are passed to each executed
489 task command, have a peek into ``config`` attribute of ``TaskWarrior`` object::
490
491     >>> tw.config
492     {'confirmation': 'no', 'data.location': '/home/tbabej/.task'}
493
494 To pass your own configuration overrides, you just need to update this dictionary::
495
496     >>> tw.config.update({'hooks': 'off'})  # tasklib will not trigger hooks
497
498 Creating hook scripts
499 ---------------------
500
501 From version 2.4.0, TaskWarrior has support for hook scripts. Tasklib provides
502 some very useful helpers to write those. With tasklib, writing these becomes
503 a breeze::
504
505     #!/usr/bin/python
506
507     from tasklib.task import Task
508     task = Task.from_input()
509     # ... <custom logic>
510     print task.export_data()
511
512 For example, plugin which would assign the priority "H" to any task containing
513 three exclamation marks in the description, would go like this::
514
515     #!/usr/bin/python
516
517     from tasklib.task import Task
518     task = Task.from_input()
519
520     if "!!!" in task['description']:
521         task['priority'] = "H"
522
523     print task.export_data()
524
525 Tasklib can automatically detect whether it's running in the ``on-modify`` event,
526 which provides more input than ``on-add`` event and reads the data accordingly.
527
528 This means the example above works both for ``on-add`` and ``on-modify`` events!
529
530 Consenquently, you can create just one hook file for both ``on-add`` and
531 ``on-modify`` events, and you just need to create a symlink for the other one.
532 This removes the need for maintaining two copies of the same code base and/or
533 boilerplate code.
534
535 In ``on-modify`` events, tasklib loads both the original version and the modified
536 version of the task to the returned ``Task`` object. To access the original data
537 (in read-only manner), use ``original`` dict-like attribute:
538
539     >>> t = Task.from_input()
540     >>> t['description']
541     "Modified description"
542     >>> t.original['description']
543     "Original description"
544
545 Working with UDAs
546 -----------------
547
548 Since TaskWarrior does read your .taskrc, you need not to define any UDAs
549 in the TaskWarrior's config dictionary, as described above. Suppose we have
550 a estimate UDA in the .taskrc::
551
552     uda.estimate.type = numeric
553
554 We can simply filter and create tasks using the estimate UDA out of the box::
555
556     >>> tw = TaskWarrior()
557     >>> task = Task(tw, description="Long task", estimate=1000)
558     >>> task.save()
559     >>> task['id']
560     1
561
562 This is saved as UDA in the TaskWarrior::
563
564     $ task 1 export
565     {"id":1,"description":"Long task","estimate":1000, ...}
566
567 We can also speficy UDAs as arguments in the TaskFilter::
568
569     >>> tw.tasks.filter(estimate=1000)
570     Long task
571
572 Syncing
573 -------
574
575 If you have configurated the needed config variables in your .taskrc, syncing
576 is as easy as::
577
578     >>> tw = TaskWarrior()
579     >>> tw.execute_command(['sync'])
580
581 If you want to use non-standard server/credentials, you'll need to provide configuration
582 overrides to the ``TaskWarrior`` instance. Update the ``config`` dictionary with the
583 values you desire to override, and then we can run the sync command using
584 the ``execute_command()`` method::
585
586     >>> tw = TaskWarrior()
587     >>> sync_config = {
588     ...     'taskd.certificate': '/home/tbabej/.task/tbabej.cert.pem',
589     ...     'taskd.credentials': 'Public/tbabej/34af54de-3cb2-4d3d-82be-33ddb8fd3e66',
590     ...     'taskd.server': 'task.server.com:53589',
591     ...     'taskd.ca': '/home/tbabej/.task/ca.cert.pem',
592     ...     'taskd.trust': 'ignore hostname'}
593     >>> tw.config.update(sync_config)
594     >>> tw.execute_command(['sync'])
595
596
597 .. _taskwarrior: http://taskwarrior.org