~nmu-sscheel/gtg/rework-task-editor

« back to all changes in this revision

Viewing changes to GTG/plugins/rtm_sync/pyrtm/rtm.py

  • Committer: Luca Invernizzi
  • Date: 2010-09-08 14:15:11 UTC
  • mfrom: (825.1.227 liblarch_rebased)
  • Revision ID: invernizzi.l@gmail.com-20100908141511-vsctgw74dj1xp0wi
Liblarch is now in trunk.
Note that performances are still bad and it misses DnD and multi-select
support, but its state is better that the current trunk.
Backends are added in this merge too.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Python library for Remember The Milk API
2
 
 
3
 
__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
4
 
__all__ = (
5
 
    'API',
6
 
    'createRTM',
7
 
    'set_log_level',
8
 
        )
9
 
 
10
 
 
11
 
import warnings
12
 
import urllib
13
 
import time
14
 
from hashlib import md5
15
 
from GTG import _
16
 
 
17
 
warnings.simplefilter('default', ImportWarning)
18
 
 
19
 
_use_simplejson = False
20
 
try:
21
 
    import simplejson
22
 
    _use_simplejson = True
23
 
except ImportError:
24
 
    try:
25
 
        from django.utils import simplejson
26
 
        _use_simplejson = True
27
 
    except ImportError:
28
 
        pass
29
 
    
30
 
if not _use_simplejson:
31
 
    warnings.warn("simplejson module is not available, "
32
 
             "falling back to the internal JSON parser. "
33
 
             "Please consider installing the simplejson module from "
34
 
             "http://pypi.python.org/pypi/simplejson.", ImportWarning,
35
 
             stacklevel=2)
36
 
 
37
 
#logging.basicConfig()
38
 
#LOG = logging.getLogger(__name__)
39
 
#LOG.setLevel(logging.INFO)
40
 
 
41
 
SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
42
 
AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
43
 
 
44
 
 
45
 
class RTMError(Exception): pass
46
 
 
47
 
class RTMAPIError(RTMError): pass
48
 
 
49
 
class AuthStateMachine(object):
50
 
 
51
 
    class NoData(RTMError): pass
52
 
 
53
 
    def __init__(self, states):
54
 
        self.states = states
55
 
        self.data = {}
56
 
 
57
 
    def dataReceived(self, state, datum):
58
 
        if state not in self.states:
59
 
            error_string = _("Invalid state")+" <%s>" 
60
 
 
61
 
            raise RTMError, error_string % state
62
 
        self.data[state] = datum
63
 
 
64
 
    def get(self, state):
65
 
        if state in self.data:
66
 
            return self.data[state]
67
 
        else:
68
 
            raise AuthStateMachine.NoData, 'No data for <%s>' % state
69
 
 
70
 
 
71
 
class RTM(object):
72
 
 
73
 
    def __init__(self, apiKey, secret, token=None):
74
 
        self.apiKey = apiKey
75
 
        self.secret = secret
76
 
        self.authInfo = AuthStateMachine(['frob', 'token'])
77
 
 
78
 
        # this enables one to do 'rtm.tasks.getList()', for example
79
 
        for prefix, methods in API.items():
80
 
            setattr(self, prefix,
81
 
                    RTMAPICategory(self, prefix, methods))
82
 
 
83
 
        if token:
84
 
            self.authInfo.dataReceived('token', token)
85
 
 
86
 
    def _sign(self, params):
87
 
        "Sign the parameters with MD5 hash"
88
 
        pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
89
 
        return md5(self.secret+pairs).hexdigest()
90
 
 
91
 
    def get(self, **params):
92
 
        "Get the XML response for the passed `params`."
93
 
        params['api_key'] = self.apiKey
94
 
        params['format'] = 'json'
95
 
        params['api_sig'] = self._sign(params)
96
 
 
97
 
        json = openURL(SERVICE_URL, params).read()
98
 
 
99
 
        #LOG.debug("JSON response: \n%s" % json)
100
 
        if _use_simplejson:
101
 
            data = dottedDict('ROOT', simplejson.loads(json))
102
 
        else:
103
 
            data = dottedJSON(json)
104
 
        rsp = data.rsp
105
 
 
106
 
        if rsp.stat == 'fail':
107
 
            raise RTMAPIError, 'API call failed - %s (%s)' % (
108
 
                rsp.err.msg, rsp.err.code)
109
 
        else:
110
 
            return rsp
111
 
 
112
 
    def getNewFrob(self):
113
 
        rsp = self.get(method='rtm.auth.getFrob')
114
 
        self.authInfo.dataReceived('frob', rsp.frob)
115
 
        return rsp.frob
116
 
 
117
 
    def getAuthURL(self):
118
 
        try:
119
 
            frob = self.authInfo.get('frob')
120
 
        except AuthStateMachine.NoData:
121
 
            frob = self.getNewFrob()
122
 
 
123
 
        params = {
124
 
            'api_key': self.apiKey,
125
 
            'perms'  : 'delete',
126
 
            'frob'   : frob
127
 
            }
128
 
        params['api_sig'] = self._sign(params)
129
 
        return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
130
 
 
131
 
    def getToken(self):
132
 
        frob = self.authInfo.get('frob')
133
 
        rsp = self.get(method='rtm.auth.getToken', frob=frob)
134
 
        self.authInfo.dataReceived('token', rsp.auth.token)
135
 
        return rsp.auth.token
136
 
 
137
 
class RTMAPICategory:
138
 
    "See the `API` structure and `RTM.__init__`"
139
 
 
140
 
    def __init__(self, rtm, prefix, methods):
141
 
        self.rtm = rtm
142
 
        self.prefix = prefix
143
 
        self.methods = methods
144
 
 
145
 
    def __getattr__(self, attr):
146
 
        if attr in self.methods:
147
 
            rargs, oargs = self.methods[attr]
148
 
            if self.prefix == 'tasksNotes':
149
 
                aname = 'rtm.tasks.notes.%s' % attr
150
 
            else:
151
 
                aname = 'rtm.%s.%s' % (self.prefix, attr)
152
 
            return lambda **params: self.callMethod(
153
 
                aname, rargs, oargs, **params)
154
 
        else:
155
 
            raise AttributeError, 'No such attribute: %s' % attr
156
 
 
157
 
    def callMethod(self, aname, rargs, oargs, **params):
158
 
        # Sanity checks
159
 
        for requiredArg in rargs:
160
 
            if requiredArg not in params:
161
 
                raise TypeError, 'Required parameter (%s) missing' % requiredArg
162
 
 
163
 
        for param in params:
164
 
            if param not in rargs + oargs:
165
 
                warnings.warn('Invalid parameter (%s)' % param)
166
 
 
167
 
        return self.rtm.get(method=aname,
168
 
                            auth_token=self.rtm.authInfo.get('token'),
169
 
                            **params)
170
 
 
171
 
 
172
 
 
173
 
# Utility functions
174
 
 
175
 
def sortedItems(dictionary):
176
 
    "Return a list of (key, value) sorted based on keys"
177
 
    keys = dictionary.keys()
178
 
    keys.sort()
179
 
    for key in keys:
180
 
        yield key, dictionary[key]
181
 
 
182
 
def openURL(url, queryArgs=None):
183
 
    if queryArgs:
184
 
        url = url + '?' + urllib.urlencode(queryArgs)
185
 
    #LOG.debug("URL> %s", url)
186
 
    return urllib.urlopen(url)
187
 
 
188
 
class dottedDict(object):
189
 
    """Make dictionary items accessible via the object-dot notation."""
190
 
 
191
 
    def __init__(self, name, dictionary):
192
 
        self._name = name
193
 
 
194
 
        if type(dictionary) is dict:
195
 
            for key, value in dictionary.items():
196
 
                if type(value) is dict:
197
 
                    value = dottedDict(key, value)
198
 
                elif type(value) in (list, tuple) and key != 'tag':
199
 
                    value = [dottedDict('%s_%d' % (key, i), item)
200
 
                             for i, item in indexed(value)]
201
 
                setattr(self, key, value)
202
 
        else:
203
 
            raise ValueError, 'not a dict: %s' % dictionary
204
 
 
205
 
    def __repr__(self):
206
 
        children = [c for c in dir(self) if not c.startswith('_')]
207
 
        return 'dotted <%s> : %s' % (
208
 
            self._name,
209
 
            ', '.join(children))
210
 
 
211
 
 
212
 
def safeEval(string):
213
 
    return eval(string, {}, {})
214
 
 
215
 
def dottedJSON(json):
216
 
    return dottedDict('ROOT', safeEval(json))
217
 
 
218
 
def indexed(seq):
219
 
    index = 0
220
 
    for item in seq:
221
 
        yield index, item
222
 
        index += 1
223
 
 
224
 
 
225
 
# API spec
226
 
 
227
 
API = {
228
 
   'auth': {
229
 
       'checkToken':
230
 
           [('auth_token',), ()],
231
 
       'getFrob':
232
 
           [(), ()],
233
 
       'getToken':
234
 
           [('frob',), ()]
235
 
       },
236
 
    'contacts': {
237
 
        'add':
238
 
            [('timeline', 'contact'), ()],
239
 
        'delete':
240
 
            [('timeline', 'contact_id'), ()],
241
 
        'getList':
242
 
            [(), ()]
243
 
        },
244
 
    'groups': {
245
 
        'add':
246
 
            [('timeline', 'group'), ()],
247
 
        'addContact':
248
 
            [('timeline', 'group_id', 'contact_id'), ()],
249
 
        'delete':
250
 
            [('timeline', 'group_id'), ()],
251
 
        'getList':
252
 
            [(), ()],
253
 
        'removeContact':
254
 
            [('timeline', 'group_id', 'contact_id'), ()],
255
 
        },
256
 
    'lists': {
257
 
        'add':
258
 
            [('timeline', 'name',), ('filter',)],
259
 
        'archive':
260
 
            [('timeline', 'list_id'), ()],
261
 
        'delete':
262
 
            [('timeline', 'list_id'), ()],
263
 
        'getList':
264
 
            [(), ()],
265
 
        'setDefaultList':
266
 
            [('timeline'), ('list_id')],
267
 
        'setName':
268
 
            [('timeline', 'list_id', 'name'), ()],
269
 
        'unarchive':
270
 
            [('timeline',), ('list_id',)]
271
 
        },
272
 
    'locations': {
273
 
        'getList':
274
 
            [(), ()]
275
 
        },
276
 
    'reflection': {
277
 
        'getMethodInfo':
278
 
            [('methodName',), ()],
279
 
        'getMethods':
280
 
            [(), ()]
281
 
        },
282
 
    'settings': {
283
 
        'getList':
284
 
            [(), ()]
285
 
        },
286
 
    'tasks': {
287
 
        'add':
288
 
            [('timeline', 'name',), ('list_id', 'parse',)],
289
 
        'addTags':
290
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
291
 
             ()],
292
 
        'complete':
293
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
294
 
        'delete':
295
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
296
 
        'getList':
297
 
            [(),
298
 
             ('list_id', 'filter', 'last_sync')],
299
 
        'movePriority':
300
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
301
 
             ()],
302
 
        'moveTo':
303
 
            [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
304
 
             ()],
305
 
        'postpone':
306
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
307
 
             ()],
308
 
        'removeTags':
309
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
310
 
             ()],
311
 
        'setDueDate':
312
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
313
 
             ('due', 'has_due_time', 'parse')],
314
 
        'setEstimate':
315
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
316
 
             ('estimate',)],
317
 
        'setLocation':
318
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
319
 
             ('location_id',)],
320
 
        'setName':
321
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
322
 
             ()],
323
 
        'setPriority':
324
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
325
 
             ('priority',)],
326
 
        'setRecurrence':
327
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
328
 
             ('repeat',)],
329
 
        'setTags':
330
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
331
 
             ('tags',)],
332
 
        'setURL':
333
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
334
 
             ('url',)],
335
 
        'uncomplete':
336
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
337
 
             ()],
338
 
        },
339
 
    'tasksNotes': {
340
 
        'add':
341
 
            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
342
 
        'delete':
343
 
            [('timeline', 'note_id'), ()],
344
 
        'edit':
345
 
            [('timeline', 'note_id', 'note_title', 'note_text'), ()]
346
 
        },
347
 
    'test': {
348
 
        'echo':
349
 
            [(), ()],
350
 
        'login':
351
 
            [(), ()]
352
 
        },
353
 
    'time': {
354
 
        'convert':
355
 
            [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
356
 
        'parse':
357
 
            [('text',), ('timezone', 'dateformat')]
358
 
        },
359
 
    'timelines': {
360
 
        'create':
361
 
            [(), ()]
362
 
        },
363
 
    'timezones': {
364
 
        'getList':
365
 
            [(), ()]
366
 
        },
367
 
    'transactions': {
368
 
        'undo':
369
 
            [('timeline', 'transaction_id'), ()]
370
 
        },
371
 
    }
372
 
 
373
 
def createRTM(apiKey, secret, token=None):
374
 
    rtm = RTM(apiKey, secret, token)
375
 
#    if token is None:
376
 
#        print 'No token found'
377
 
#        print 'Give me access here:', rtm.getAuthURL()
378
 
#        raw_input('Press enter once you gave access')
379
 
#        print 'Note down this token for future use:', rtm.getToken()
380
 
 
381
 
    return rtm
382
 
 
383
 
def test(apiKey, secret, token=None):
384
 
    rtm = createRTM(apiKey, secret, token)
385
 
 
386
 
    rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
387
 
    print [t.name for t in rspTasks.tasks.list.taskseries]
388
 
    print rspTasks.tasks.list.id
389
 
 
390
 
    rspLists = rtm.lists.getList()
391
 
    # print rspLists.lists.list
392
 
    print [(x.name, x.id) for x in rspLists.lists.list]
393
 
 
394
 
def set_log_level(level):
395
 
    '''Sets the log level of the logger used by the module.
396
 
    
397
 
    >>> import rtm
398
 
    >>> import logging
399
 
    >>> rtm.set_log_level(logging.INFO)
400
 
    '''
401
 
    
402
 
    #LOG.setLevel(level)