1. Atsushi Odagiri
  2. pyrtm

Commits

Tetsuya Morimoto  committed a8b0964

set up to make buildout environment

  • Participants
  • Parent commits e8e4340
  • Branches default

Comments (0)

Files changed (18)

File .hgignore

View file
-syntax: glob
-
-*.egg-info
-*.pyc
-
+.pyc
+.swp
 tmp
 
 # buildout paths
+.installed.cfg
 bin
+_build
 develop-eggs
+dist
+eggs
 parts
-.installed.cfg
+src/.*.egg-info

File COPYING

-Copyright (c) 2009 Sridhar Ratnakumar
-
-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.

File ChangeLog

-2009-12-08  Sridhar Ratnakumar <srid at nearfar.org>
-
-	* app.py: Fix for unreliable type of 'taskseries' (#5)
-	* rtm.py: Fixed incorrect number of arguments to "lists" (#3)
-	* rtm.py: Try to import django.utils.simplejson before falling back
-	* app.py: Made it a command-line app accepting key/secret/token as args
-
-2009-08-06  Sridhar Ratnakumar <srid at nearfar.org>
-
-	* setup.py: Write a setup.py and release 0.1
-
-2008-05-05  Mariano Draghi <mariano at chaghi dot com dot ar> 
-
-	* rtm.py: Issue a warnging if simplejson is not available.
-
-	* rtm.py: Fixed a corner case where parsing the response with 
-	  dottedJSON (i.e., when not using simplejson) would fail to
-	  fetch multiple tags. Contributed by Greg Allen.
-
-2008-04-26  Mariano Draghi <mariano at chaghi dot com dot ar> 
-
-	* rtm.py: Fixed invocation of tasks.notes methods, based on code
-	  contributed by David Steele.
-
-2008-03-24  Mariano Draghi <mariano at chaghi dot com dot ar> 
-
-	* rtm.py: Use the standard python logging module to handle
-	  debugging messages.
-
-2008-03-23  Mariano Draghi <mariano at chaghi dot com dot ar> 
-
-	* rtm.py: Implemented the full RTM API. Integrated simplejson to
-	  handle JSON responses, which should fix some encoding problems.
-	  Renamed some internal attributes (name, auth) of RTM class,
-	  because they where conflicting with some methods and arguments
-	  of the API calls wich had the same name. Fixed a minor issue
-	  with dottedDict class and non dict instances. Added a simple
-	  mechanism to turn debugging on/off dynamically.
-	* README / ChangeLog: Added.
-

File MANIFEST.in

View file
+include buildout.cfg
+include bootstrap.py
+include MANIFEST.in
+recursive-include src *.py COPYING ChangeLog README

File README

-========================================
-Python library for Remember The Milk API
-========================================
-
-Copyright (c) 2009 by Sridhar Ratnakumar <srid@nearfar.org>
-
-Contributors:
-
- - Mariano Draghi (cHagHi) <mariano at chaghi dot com dot ar>
-
-Home page: http://bitbucket.org/srid/pyrtm/

File app.py

-#!/usr/bin/env python
-# simple app
-
-from rtm import createRTM
-
-from Tkinter import *
-
-def createApp(rtm):
-    rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
-    tasks = []
-    if hasattr(rspTasks.tasks.list, "__getitem__"):
-        for l in rspTasks.tasks.list:
-            # XXX: taskseries *may* be a list
-            if isinstance(l.taskseries, (list, tuple)):
-                for t in l.taskseries:
-                    tasks.append(t.name)
-            else:
-                tasks.append(l.taskseries.name)
-    print tasks
-    if not tasks:
-        tasks.append('No tasks due within a week')
-
-    root = Tk()
-    root.title('My tasks due within a week')
-    root.wm_attributes('-topmost', 1)
-    root.wm_attributes('-alpha', 0.9)
-    l = Label(text='\n'.join(tasks))
-    l.pack()
-    l.mainloop()
-
-def test(apiKey, secret, token=None):
-    rtm = createRTM(apiKey, secret, token)
-    createApp(rtm)
-
-if __name__ == '__main__':
-    import sys
-    try:
-        api_key, secret = sys.argv[1:3]
-    except ValueError:
-        print >>sys.stderr, 'Usage: ./app.py APIKEY SECRET [TOKEN]'
-    else:
-        try:
-            token = sys.argv[3]
-        except IndexError:
-            token = None
-        test(api_key, secret, token)

File bootstrap.py

View file
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os, shutil, sys, tempfile, textwrap, urllib, urllib2, subprocess
+from optparse import OptionParser
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    quote = str
+
+# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
+stdout, stderr = subprocess.Popen(
+    [sys.executable, '-Sc',
+     'try:\n'
+     '    import ConfigParser\n'
+     'except ImportError:\n'
+     '    print 1\n'
+     'else:\n'
+     '    print 0\n'],
+    stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+has_broken_dash_S = bool(int(stdout.strip()))
+
+# In order to be more robust in the face of system Pythons, we want to
+# run without site-packages loaded.  This is somewhat tricky, in
+# particular because Python 2.6's distutils imports site, so starting
+# with the -S flag is not sufficient.  However, we'll start with that:
+if not has_broken_dash_S and 'site' in sys.modules:
+    # We will restart with python -S.
+    args = sys.argv[:]
+    args[0:0] = [sys.executable, '-S']
+    args = map(quote, args)
+    os.execv(sys.executable, args)
+# Now we are running with -S.  We'll get the clean sys.path, import site
+# because distutils will do it later, and then reset the path and clean
+# out any namespace packages from site-packages that might have been
+# loaded by .pth files.
+clean_path = sys.path[:]
+import site
+sys.path[:] = clean_path
+for k, v in sys.modules.items():
+    if k in ('setuptools', 'pkg_resources') or (
+        hasattr(v, '__path__') and
+        len(v.__path__)==1 and
+        not os.path.exists(os.path.join(v.__path__[0],'__init__.py'))):
+        # This is a namespace package.  Remove it.
+        sys.modules.pop(k)
+
+is_jython = sys.platform.startswith('java')
+
+setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
+distribute_source = 'http://python-distribute.org/distribute_setup.py'
+
+# parsing arguments
+def normalize_to_url(option, opt_str, value, parser):
+    if value:
+        if '://' not in value: # It doesn't smell like a URL.
+            value = 'file://%s' % (
+                urllib.pathname2url(
+                    os.path.abspath(os.path.expanduser(value))),)
+        if opt_str == '--download-base' and not value.endswith('/'):
+            # Download base needs a trailing slash to make the world happy.
+            value += '/'
+    else:
+        value = None
+    name = opt_str[2:].replace('-', '_')
+    setattr(parser.values, name, value)
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --setup-source and --download-base to point to
+local resources, you can keep this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", dest="version",
+                          help="use a specific zc.buildout version")
+parser.add_option("-d", "--distribute",
+                   action="store_true", dest="use_distribute", default=False,
+                   help="Use Distribute rather than Setuptools.")
+parser.add_option("--setup-source", action="callback", dest="setup_source",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or file location for the setup file. "
+                        "If you use Setuptools, this will default to " +
+                        setuptools_source + "; if you use Distribute, this "
+                        "will default to " + distribute_source +"."))
+parser.add_option("--download-base", action="callback", dest="download_base",
+                  callback=normalize_to_url, nargs=1, type="string",
+                  help=("Specify a URL or directory for downloading "
+                        "zc.buildout and either Setuptools or Distribute. "
+                        "Defaults to PyPI."))
+parser.add_option("--eggs",
+                  help=("Specify a directory for storing eggs.  Defaults to "
+                        "a temporary directory that is deleted when the "
+                        "bootstrap script completes."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --version, the "
+                        "bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", None, action="store", dest="config_file",
+                   help=("Specify the path to the buildout configuration "
+                         "file to be used."))
+
+options, args = parser.parse_args()
+
+# if -c was provided, we push it back into args for buildout's main function
+if options.config_file is not None:
+    args += ['-c', options.config_file]
+
+if options.eggs:
+    eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
+else:
+    eggs_dir = tempfile.mkdtemp()
+
+if options.setup_source is None:
+    if options.use_distribute:
+        options.setup_source = distribute_source
+    else:
+        options.setup_source = setuptools_source
+
+if options.accept_buildout_test_releases:
+    args.append('buildout:accept-buildout-test-releases=true')
+args.append('bootstrap')
+
+try:
+    import pkg_resources
+    import setuptools # A flag.  Sometimes pkg_resources is installed alone.
+    if not hasattr(pkg_resources, '_distribute'):
+        raise ImportError
+except ImportError:
+    ez_code = urllib2.urlopen(
+        options.setup_source).read().replace('\r\n', '\n')
+    ez = {}
+    exec ez_code in ez
+    setup_args = dict(to_dir=eggs_dir, download_delay=0)
+    if options.download_base:
+        setup_args['download_base'] = options.download_base
+    if options.use_distribute:
+        setup_args['no_fake'] = True
+    ez['use_setuptools'](**setup_args)
+    if 'pkg_resources' in sys.modules:
+        reload(sys.modules['pkg_resources'])
+    import pkg_resources
+    # This does not (always?) update the default working set.  We will
+    # do it.
+    for path in sys.path:
+        if path not in pkg_resources.working_set.entries:
+            pkg_resources.working_set.add_entry(path)
+
+cmd = [quote(sys.executable),
+       '-c',
+       quote('from setuptools.command.easy_install import main; main()'),
+       '-mqNxd',
+       quote(eggs_dir)]
+
+if not has_broken_dash_S:
+    cmd.insert(1, '-S')
+
+find_links = options.download_base
+if not find_links:
+    find_links = os.environ.get('bootstrap-testing-find-links')
+if find_links:
+    cmd.extend(['-f', quote(find_links)])
+
+if options.use_distribute:
+    setup_requirement = 'distribute'
+else:
+    setup_requirement = 'setuptools'
+ws = pkg_resources.working_set
+setup_requirement_path = ws.find(
+    pkg_resources.Requirement.parse(setup_requirement)).location
+env = dict(
+    os.environ,
+    PYTHONPATH=setup_requirement_path)
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+    def _final_version(parsed_version):
+        for part in parsed_version:
+            if (part[:1] == '*') and (part not in _final_parts):
+                return False
+        return True
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setup_requirement_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+if is_jython:
+    import subprocess
+    exitcode = subprocess.Popen(cmd, env=env).wait()
+else: # Windows prefers this, apparently; otherwise we would prefer subprocess
+    exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
+if exitcode != 0:
+    sys.stdout.flush()
+    sys.stderr.flush()
+    print ("An error occurred when trying to install zc.buildout. "
+           "Look above this message for any errors that "
+           "were output by easy_install.")
+    sys.exit(exitcode)
+
+ws.add_entry(eggs_dir)
+ws.require(requirement)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+if not options.eggs: # clean up temporary egg directory
+    shutil.rmtree(eggs_dir)

File buildout.cfg

View file
+[buildout]
+parts = pyrtm test
+
+develop = .
+
+[pyrtm]
+recipe = zc.recipe.egg
+eggs = pyrtm
+interpreter = py
+
+[test]
+recipe = pbp.recipe.noserunner
+eggs = pyrtm[test]

File rtm.py

-# Python library for Remember The Milk API
-
-__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
-__all__ = (
-    'API',
-    'createRTM',
-    'set_log_level',
-        )
-
-
-import warnings
-import urllib
-import logging
-import sys
-from hashlib import md5
-
-warnings.simplefilter('default', ImportWarning)
-
-_use_simplejson = False
-try:
-    _ver = sys.version_info
-    if _ver[0] == 2 and _ver[1] >= 6:
-        import json as simplejson
-    else:
-        import simplejson
-    _use_simplejson = True
-except ImportError:
-    try:
-        from django.utils import simplejson
-        _use_simplejson = True
-    except ImportError:
-        pass
-    
-if not _use_simplejson:
-    warnings.warn("simplejson module is not available, "
-             "falling back to the internal JSON parser. "
-             "Please consider installing the simplejson module from "
-             "http://pypi.python.org/pypi/simplejson.", ImportWarning,
-             stacklevel=2)
-
-logging.basicConfig()
-LOG = logging.getLogger(__name__)
-LOG.setLevel(logging.INFO)
-
-SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
-AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
-
-
-class RTMError(Exception): pass
-
-class RTMAPIError(RTMError): pass
-
-class AuthStateMachine(object):
-
-    class NoData(RTMError): pass
-
-    def __init__(self, states):
-        self.states = states
-        self.data = {}
-
-    def dataReceived(self, state, datum):
-        if state not in self.states:
-            raise RTMError, "Invalid state <%s>" % state
-        self.data[state] = datum
-
-    def get(self, state):
-        if state in self.data:
-            return self.data[state]
-        else:
-            raise AuthStateMachine.NoData, 'No data for <%s>' % state
-
-
-class RTM(object):
-
-    def __init__(self, apiKey, secret, token=None):
-        self.apiKey = apiKey
-        self.secret = secret
-        self.authInfo = AuthStateMachine(['frob', 'token'])
-
-        # this enables one to do 'rtm.tasks.getList()', for example
-        for prefix, methods in API.items():
-            setattr(self, prefix,
-                    RTMAPICategory(self, prefix, methods))
-
-        if token:
-            self.authInfo.dataReceived('token', token)
-
-    def _sign(self, params):
-        "Sign the parameters with MD5 hash"
-        pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
-        return md5(self.secret+pairs).hexdigest()
-
-    def get(self, **params):
-        "Get the XML response for the passed `params`."
-        params['api_key'] = self.apiKey
-        params['format'] = 'json'
-        params['api_sig'] = self._sign(params)
-
-        json = openURL(SERVICE_URL, params).read()
-
-        LOG.debug("JSON response: \n%s" % json)
-
-        if _use_simplejson:
-            data = dottedDict('ROOT', simplejson.loads(json))
-        else:
-            data = dottedJSON(json)
-        rsp = data.rsp
-
-        if rsp.stat == 'fail':
-            raise RTMAPIError, 'API call failed - %s (%s)' % (
-                rsp.err.msg, rsp.err.code)
-        else:
-            return rsp
-
-    def getNewFrob(self):
-        rsp = self.get(method='rtm.auth.getFrob')
-        self.authInfo.dataReceived('frob', rsp.frob)
-        return rsp.frob
-
-    def getAuthURL(self):
-        try:
-            frob = self.authInfo.get('frob')
-        except AuthStateMachine.NoData:
-            frob = self.getNewFrob()
-
-        params = {
-            'api_key': self.apiKey,
-            'perms'  : 'delete',
-            'frob'   : frob
-            }
-        params['api_sig'] = self._sign(params)
-        return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
-
-    def getToken(self):
-        frob = self.authInfo.get('frob')
-        rsp = self.get(method='rtm.auth.getToken', frob=frob)
-        self.authInfo.dataReceived('token', rsp.auth.token)
-        return rsp.auth.token
-
-class RTMAPICategory:
-    "See the `API` structure and `RTM.__init__`"
-
-    def __init__(self, rtm, prefix, methods):
-        self.rtm = rtm
-        self.prefix = prefix
-        self.methods = methods
-
-    def __getattr__(self, attr):
-        if attr in self.methods:
-            rargs, oargs = self.methods[attr]
-            if self.prefix == 'tasksNotes':
-                aname = 'rtm.tasks.notes.%s' % attr
-            else:
-                aname = 'rtm.%s.%s' % (self.prefix, attr)
-            return lambda **params: self.callMethod(
-                aname, rargs, oargs, **params)
-        else:
-            raise AttributeError, 'No such attribute: %s' % attr
-
-    def callMethod(self, aname, rargs, oargs, **params):
-        # Sanity checks
-        for requiredArg in rargs:
-            if requiredArg not in params:
-                raise TypeError, 'Required parameter (%s) missing' % requiredArg
-
-        for param in params:
-            if param not in rargs + oargs:
-                warnings.warn('Invalid parameter (%s)' % param)
-
-        return self.rtm.get(method=aname,
-                            auth_token=self.rtm.authInfo.get('token'),
-                            **params)
-
-
-
-# Utility functions
-
-def sortedItems(dictionary):
-    "Return a list of (key, value) sorted based on keys"
-    keys = dictionary.keys()
-    keys.sort()
-    for key in keys:
-        yield key, dictionary[key]
-
-def openURL(url, queryArgs=None):
-    if queryArgs:
-        url = url + '?' + urllib.urlencode(queryArgs)
-    LOG.debug("URL> %s", url)
-    return urllib.urlopen(url)
-
-class dottedDict(object):
-    """Make dictionary items accessible via the object-dot notation."""
-
-    def __init__(self, name, dictionary):
-        self._name = name
-
-        if type(dictionary) is dict:
-            for key, value in dictionary.items():
-                if type(value) is dict:
-                    value = dottedDict(key, value)
-                elif type(value) in (list, tuple) and key != 'tag':
-                    value = [dottedDict('%s_%d' % (key, i), item)
-                             for i, item in indexed(value)]
-                setattr(self, key, value)
-        else:
-            raise ValueError, 'not a dict: %s' % dictionary
-
-    def __repr__(self):
-        children = [c for c in dir(self) if not c.startswith('_')]
-        return 'dotted <%s> : %s' % (
-            self._name,
-            ', '.join(children))
-
-
-def safeEval(string):
-    return eval(string, {}, {})
-
-def dottedJSON(json):
-    return dottedDict('ROOT', safeEval(json))
-
-def indexed(seq):
-    index = 0
-    for item in seq:
-        yield index, item
-        index += 1
-
-
-# API spec
-
-API = {
-   'auth': {
-       'checkToken':
-           [('auth_token',), ()],
-       'getFrob':
-           [(), ()],
-       'getToken':
-           [('frob',), ()]
-       },
-    'contacts': {
-        'add':
-            [('timeline', 'contact'), ()],
-        'delete':
-            [('timeline', 'contact_id'), ()],
-        'getList':
-            [(), ()]
-        },
-    'groups': {
-        'add':
-            [('timeline', 'group'), ()],
-        'addContact':
-            [('timeline', 'group_id', 'contact_id'), ()],
-        'delete':
-            [('timeline', 'group_id'), ()],
-        'getList':
-            [(), ()],
-        'removeContact':
-            [('timeline', 'group_id', 'contact_id'), ()],
-        },
-    'lists': {
-        'add':
-            [('timeline', 'name',), ('filter',)],
-        'archive':
-            [('timeline', 'list_id'), ()],
-        'delete':
-            [('timeline', 'list_id'), ()],
-        'getList':
-            [(), ()],
-        'setDefaultList':
-            [('timeline'), ('list_id')],
-        'setName':
-            [('timeline', 'list_id', 'name'), ()],
-        'unarchive':
-            [('timeline',), ('list_id',)]
-        },
-    'locations': {
-        'getList':
-            [(), ()]
-        },
-    'reflection': {
-        'getMethodInfo':
-            [('methodName',), ()],
-        'getMethods':
-            [(), ()]
-        },
-    'settings': {
-        'getList':
-            [(), ()]
-        },
-    'tasks': {
-        'add':
-            [('timeline', 'name',), ('list_id', 'parse',)],
-        'addTags':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
-             ()],
-        'complete':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
-        'delete':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
-        'getList':
-            [(),
-             ('list_id', 'filter', 'last_sync')],
-        'movePriority':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
-             ()],
-        'moveTo':
-            [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
-             ()],
-        'postpone':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ()],
-        'removeTags':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
-             ()],
-        'setDueDate':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('due', 'has_due_time', 'parse')],
-        'setEstimate':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('estimate',)],
-        'setLocation':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('location_id',)],
-        'setName':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
-             ()],
-        'setPriority':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('priority',)],
-        'setRecurrence':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('repeat',)],
-        'setTags':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('tags',)],
-        'setURL':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('url',)],
-        'uncomplete':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ()],
-        },
-    'tasksNotes': {
-        'add':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
-        'delete':
-            [('timeline', 'note_id'), ()],
-        'edit':
-            [('timeline', 'note_id', 'note_title', 'note_text'), ()]
-        },
-    'test': {
-        'echo':
-            [(), ()],
-        'login':
-            [(), ()]
-        },
-    'time': {
-        'convert':
-            [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
-        'parse':
-            [('text',), ('timezone', 'dateformat')]
-        },
-    'timelines': {
-        'create':
-            [(), ()]
-        },
-    'timezones': {
-        'getList':
-            [(), ()]
-        },
-    'transactions': {
-        'undo':
-            [('timeline', 'transaction_id'), ()]
-        },
-    }
-
-def createRTM(apiKey, secret, token=None):
-    rtm = RTM(apiKey, secret, token)
-
-    if token is None:
-        print 'No token found'
-        print 'Give me access here:', rtm.getAuthURL()
-        raw_input('Press enter once you gave access')
-        print 'Note down this token for future use:', rtm.getToken()
-
-    return rtm
-
-def test(apiKey, secret, token=None):
-    rtm = createRTM(apiKey, secret, token)
-
-    rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
-    print [t.name for t in rspTasks.tasks.list.taskseries]
-    print rspTasks.tasks.list.id
-
-    rspLists = rtm.lists.getList()
-    # print rspLists.lists.list
-    print [(x.name, x.id) for x in rspLists.lists.list]
-
-def set_log_level(level):
-    '''Sets the log level of the logger used by the module.
-    
-    >>> import rtm
-    >>> import logging
-    >>> rtm.set_log_level(logging.INFO)
-    '''
-    
-    LOG.setLevel(level)

File setup.cfg

View file
+[build]
+build-base = _build
+
+[sdist]
+formats = bztar

File setup.py

View file
-from distutils.core import setup
+# -*- coding: utf-8 -*-
+from setuptools import setup, find_packages
+from os.path import join as pathjoin
 
-setup(name='pyrtm',
-      version='0.3dev',
-      description='Remember The Milk API',
-      author='Sridhar Ratnakumar',
-      author_email='srid@nearfar.org',
-      url='http://bitbucket.org/srid/pyrtm/',
-      license='MIT License',
-      py_modules=['rtm'],
-      classifiers=[
-        'Development Status :: 3 - Alpha',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Topic :: Internet',
-        ]      
-     )
+VERSION = '0.3dev'
+
+LONG_DESCRIPTION = "".join([
+    open(pathjoin("src","README")).read()])
+
+CLASSIFIERS = [
+    "Environment :: Web Environment",
+    'Development Status :: 3 - Alpha',
+    'Intended Audience :: Developers',
+    'License :: OSI Approved :: MIT License',
+    'Operating System :: OS Independent',
+    'Programming Language :: Python',
+    'Topic :: Internet',
+]
+
+setup(
+    name='pyrtm',
+    version=VERSION,
+    description='Remember The Milk API',
+    long_description=LONG_DESCRIPTION,
+    classifiers=CLASSIFIERS,
+    author='Sridhar Ratnakumar',
+    author_email='srid@nearfar.org',
+    url='http://bitbucket.org/srid/pyrtm/',
+    license='MIT License',
+    packages=find_packages("src"),
+    package_dir={'': 'src'},
+    package_data = {'': ['buildout.cfg']},
+    extras_require=dict(
+        test=[
+            "Nose",
+            "minimock",
+            "pep8",
+        ],
+    ),
+    test_suite='nose.collector',
+    tests_require=['Nose','minimock','pep8'],
+    entry_points="""
+       [console_scripts]
+       rtm_appsample = rtm.samples.app:main
+    """,
+)
+

File src/COPYING

View file
+Copyright (c) 2009 Sridhar Ratnakumar
+
+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.

File src/ChangeLog

View file
+2009-12-08  Sridhar Ratnakumar <srid at nearfar.org>
+
+	* app.py: Fix for unreliable type of 'taskseries' (#5)
+	* rtm.py: Fixed incorrect number of arguments to "lists" (#3)
+	* rtm.py: Try to import django.utils.simplejson before falling back
+	* app.py: Made it a command-line app accepting key/secret/token as args
+
+2009-08-06  Sridhar Ratnakumar <srid at nearfar.org>
+
+	* setup.py: Write a setup.py and release 0.1
+
+2008-05-05  Mariano Draghi <mariano at chaghi dot com dot ar> 
+
+	* rtm.py: Issue a warnging if simplejson is not available.
+
+	* rtm.py: Fixed a corner case where parsing the response with 
+	  dottedJSON (i.e., when not using simplejson) would fail to
+	  fetch multiple tags. Contributed by Greg Allen.
+
+2008-04-26  Mariano Draghi <mariano at chaghi dot com dot ar> 
+
+	* rtm.py: Fixed invocation of tasks.notes methods, based on code
+	  contributed by David Steele.
+
+2008-03-24  Mariano Draghi <mariano at chaghi dot com dot ar> 
+
+	* rtm.py: Use the standard python logging module to handle
+	  debugging messages.
+
+2008-03-23  Mariano Draghi <mariano at chaghi dot com dot ar> 
+
+	* rtm.py: Implemented the full RTM API. Integrated simplejson to
+	  handle JSON responses, which should fix some encoding problems.
+	  Renamed some internal attributes (name, auth) of RTM class,
+	  because they where conflicting with some methods and arguments
+	  of the API calls wich had the same name. Fixed a minor issue
+	  with dottedDict class and non dict instances. Added a simple
+	  mechanism to turn debugging on/off dynamically.
+	* README / ChangeLog: Added.
+

File src/README

View file
+========================================
+Python library for Remember The Milk API
+========================================
+
+Copyright (c) 2009 by Sridhar Ratnakumar <srid@nearfar.org>
+
+Contributors:
+
+ - Mariano Draghi (cHagHi) <mariano at chaghi dot com dot ar>
+
+Home page: http://bitbucket.org/srid/pyrtm/

File src/rtm/__init__.py

View file
+# -*- coding: utf-8 -*-
+
+from rtm import *
+from samples import *

File src/rtm/rtm.py

View file
+# Python library for Remember The Milk API
+
+__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
+__all__ = (
+    'API',
+    'createRTM',
+    'set_log_level',
+        )
+
+
+import warnings
+import urllib
+import logging
+import sys
+from hashlib import md5
+
+warnings.simplefilter('default', ImportWarning)
+
+_use_simplejson = False
+try:
+    _ver = sys.version_info
+    if _ver[0] == 2 and _ver[1] >= 6:
+        import json as simplejson
+    else:
+        import simplejson
+    _use_simplejson = True
+except ImportError:
+    try:
+        from django.utils import simplejson
+        _use_simplejson = True
+    except ImportError:
+        pass
+    
+if not _use_simplejson:
+    warnings.warn("simplejson module is not available, "
+             "falling back to the internal JSON parser. "
+             "Please consider installing the simplejson module from "
+             "http://pypi.python.org/pypi/simplejson.", ImportWarning,
+             stacklevel=2)
+
+logging.basicConfig()
+LOG = logging.getLogger(__name__)
+LOG.setLevel(logging.INFO)
+
+SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
+AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
+
+
+class RTMError(Exception): pass
+
+class RTMAPIError(RTMError): pass
+
+class AuthStateMachine(object):
+
+    class NoData(RTMError): pass
+
+    def __init__(self, states):
+        self.states = states
+        self.data = {}
+
+    def dataReceived(self, state, datum):
+        if state not in self.states:
+            raise RTMError, "Invalid state <%s>" % state
+        self.data[state] = datum
+
+    def get(self, state):
+        if state in self.data:
+            return self.data[state]
+        else:
+            raise AuthStateMachine.NoData, 'No data for <%s>' % state
+
+
+class RTM(object):
+
+    def __init__(self, apiKey, secret, token=None):
+        self.apiKey = apiKey
+        self.secret = secret
+        self.authInfo = AuthStateMachine(['frob', 'token'])
+
+        # this enables one to do 'rtm.tasks.getList()', for example
+        for prefix, methods in API.items():
+            setattr(self, prefix,
+                    RTMAPICategory(self, prefix, methods))
+
+        if token:
+            self.authInfo.dataReceived('token', token)
+
+    def _sign(self, params):
+        "Sign the parameters with MD5 hash"
+        pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
+        return md5(self.secret+pairs).hexdigest()
+
+    def get(self, **params):
+        "Get the XML response for the passed `params`."
+        params['api_key'] = self.apiKey
+        params['format'] = 'json'
+        params['api_sig'] = self._sign(params)
+
+        json = openURL(SERVICE_URL, params).read()
+
+        LOG.debug("JSON response: \n%s" % json)
+
+        if _use_simplejson:
+            data = dottedDict('ROOT', simplejson.loads(json))
+        else:
+            data = dottedJSON(json)
+        rsp = data.rsp
+
+        if rsp.stat == 'fail':
+            raise RTMAPIError, 'API call failed - %s (%s)' % (
+                rsp.err.msg, rsp.err.code)
+        else:
+            return rsp
+
+    def getNewFrob(self):
+        rsp = self.get(method='rtm.auth.getFrob')
+        self.authInfo.dataReceived('frob', rsp.frob)
+        return rsp.frob
+
+    def getAuthURL(self):
+        try:
+            frob = self.authInfo.get('frob')
+        except AuthStateMachine.NoData:
+            frob = self.getNewFrob()
+
+        params = {
+            'api_key': self.apiKey,
+            'perms'  : 'delete',
+            'frob'   : frob
+            }
+        params['api_sig'] = self._sign(params)
+        return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
+
+    def getToken(self):
+        frob = self.authInfo.get('frob')
+        rsp = self.get(method='rtm.auth.getToken', frob=frob)
+        self.authInfo.dataReceived('token', rsp.auth.token)
+        return rsp.auth.token
+
+class RTMAPICategory:
+    "See the `API` structure and `RTM.__init__`"
+
+    def __init__(self, rtm, prefix, methods):
+        self.rtm = rtm
+        self.prefix = prefix
+        self.methods = methods
+
+    def __getattr__(self, attr):
+        if attr in self.methods:
+            rargs, oargs = self.methods[attr]
+            if self.prefix == 'tasksNotes':
+                aname = 'rtm.tasks.notes.%s' % attr
+            else:
+                aname = 'rtm.%s.%s' % (self.prefix, attr)
+            return lambda **params: self.callMethod(
+                aname, rargs, oargs, **params)
+        else:
+            raise AttributeError, 'No such attribute: %s' % attr
+
+    def callMethod(self, aname, rargs, oargs, **params):
+        # Sanity checks
+        for requiredArg in rargs:
+            if requiredArg not in params:
+                raise TypeError, 'Required parameter (%s) missing' % requiredArg
+
+        for param in params:
+            if param not in rargs + oargs:
+                warnings.warn('Invalid parameter (%s)' % param)
+
+        return self.rtm.get(method=aname,
+                            auth_token=self.rtm.authInfo.get('token'),
+                            **params)
+
+
+
+# Utility functions
+
+def sortedItems(dictionary):
+    "Return a list of (key, value) sorted based on keys"
+    keys = dictionary.keys()
+    keys.sort()
+    for key in keys:
+        yield key, dictionary[key]
+
+def openURL(url, queryArgs=None):
+    if queryArgs:
+        url = url + '?' + urllib.urlencode(queryArgs)
+    LOG.debug("URL> %s", url)
+    return urllib.urlopen(url)
+
+class dottedDict(object):
+    """Make dictionary items accessible via the object-dot notation."""
+
+    def __init__(self, name, dictionary):
+        self._name = name
+
+        if type(dictionary) is dict:
+            for key, value in dictionary.items():
+                if type(value) is dict:
+                    value = dottedDict(key, value)
+                elif type(value) in (list, tuple) and key != 'tag':
+                    value = [dottedDict('%s_%d' % (key, i), item)
+                             for i, item in indexed(value)]
+                setattr(self, key, value)
+        else:
+            raise ValueError, 'not a dict: %s' % dictionary
+
+    def __repr__(self):
+        children = [c for c in dir(self) if not c.startswith('_')]
+        return 'dotted <%s> : %s' % (
+            self._name,
+            ', '.join(children))
+
+
+def safeEval(string):
+    return eval(string, {}, {})
+
+def dottedJSON(json):
+    return dottedDict('ROOT', safeEval(json))
+
+def indexed(seq):
+    index = 0
+    for item in seq:
+        yield index, item
+        index += 1
+
+
+# API spec
+
+API = {
+   'auth': {
+       'checkToken':
+           [('auth_token',), ()],
+       'getFrob':
+           [(), ()],
+       'getToken':
+           [('frob',), ()]
+       },
+    'contacts': {
+        'add':
+            [('timeline', 'contact'), ()],
+        'delete':
+            [('timeline', 'contact_id'), ()],
+        'getList':
+            [(), ()]
+        },
+    'groups': {
+        'add':
+            [('timeline', 'group'), ()],
+        'addContact':
+            [('timeline', 'group_id', 'contact_id'), ()],
+        'delete':
+            [('timeline', 'group_id'), ()],
+        'getList':
+            [(), ()],
+        'removeContact':
+            [('timeline', 'group_id', 'contact_id'), ()],
+        },
+    'lists': {
+        'add':
+            [('timeline', 'name',), ('filter',)],
+        'archive':
+            [('timeline', 'list_id'), ()],
+        'delete':
+            [('timeline', 'list_id'), ()],
+        'getList':
+            [(), ()],
+        'setDefaultList':
+            [('timeline'), ('list_id')],
+        'setName':
+            [('timeline', 'list_id', 'name'), ()],
+        'unarchive':
+            [('timeline',), ('list_id',)]
+        },
+    'locations': {
+        'getList':
+            [(), ()]
+        },
+    'reflection': {
+        'getMethodInfo':
+            [('methodName',), ()],
+        'getMethods':
+            [(), ()]
+        },
+    'settings': {
+        'getList':
+            [(), ()]
+        },
+    'tasks': {
+        'add':
+            [('timeline', 'name',), ('list_id', 'parse',)],
+        'addTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
+             ()],
+        'complete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
+        'delete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
+        'getList':
+            [(),
+             ('list_id', 'filter', 'last_sync')],
+        'movePriority':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
+             ()],
+        'moveTo':
+            [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
+             ()],
+        'postpone':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ()],
+        'removeTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
+             ()],
+        'setDueDate':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('due', 'has_due_time', 'parse')],
+        'setEstimate':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('estimate',)],
+        'setLocation':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('location_id',)],
+        'setName':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
+             ()],
+        'setPriority':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('priority',)],
+        'setRecurrence':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('repeat',)],
+        'setTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('tags',)],
+        'setURL':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('url',)],
+        'uncomplete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ()],
+        },
+    'tasksNotes': {
+        'add':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
+        'delete':
+            [('timeline', 'note_id'), ()],
+        'edit':
+            [('timeline', 'note_id', 'note_title', 'note_text'), ()]
+        },
+    'test': {
+        'echo':
+            [(), ()],
+        'login':
+            [(), ()]
+        },
+    'time': {
+        'convert':
+            [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
+        'parse':
+            [('text',), ('timezone', 'dateformat')]
+        },
+    'timelines': {
+        'create':
+            [(), ()]
+        },
+    'timezones': {
+        'getList':
+            [(), ()]
+        },
+    'transactions': {
+        'undo':
+            [('timeline', 'transaction_id'), ()]
+        },
+    }
+
+def createRTM(apiKey, secret, token=None):
+    rtm = RTM(apiKey, secret, token)
+
+    if token is None:
+        print 'No token found'
+        print 'Give me access here:', rtm.getAuthURL()
+        raw_input('Press enter once you gave access')
+        print 'Note down this token for future use:', rtm.getToken()
+
+    return rtm
+
+def test(apiKey, secret, token=None):
+    rtm = createRTM(apiKey, secret, token)
+
+    rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
+    print [t.name for t in rspTasks.tasks.list.taskseries]
+    print rspTasks.tasks.list.id
+
+    rspLists = rtm.lists.getList()
+    # print rspLists.lists.list
+    print [(x.name, x.id) for x in rspLists.lists.list]
+
+def set_log_level(level):
+    '''Sets the log level of the logger used by the module.
+    
+    >>> import rtm
+    >>> import logging
+    >>> rtm.set_log_level(logging.INFO)
+    '''
+    
+    LOG.setLevel(level)

File src/rtm/samples/__init__.py

View file
+# -*- coding: utf-8 -*-

File src/rtm/samples/app.py

View file
+#!/usr/bin/env python
+# simple app
+
+from rtm import createRTM
+
+from Tkinter import *
+
+def createApp(rtm):
+    rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
+    tasks = []
+    if hasattr(rspTasks.tasks.list, "__getitem__"):
+        for l in rspTasks.tasks.list:
+            # XXX: taskseries *may* be a list
+            if isinstance(l.taskseries, (list, tuple)):
+                for t in l.taskseries:
+                    tasks.append(t.name)
+            else:
+                tasks.append(l.taskseries.name)
+    print tasks
+    if not tasks:
+        tasks.append('No tasks due within a week')
+
+    root = Tk()
+    root.title('My tasks due within a week')
+    root.wm_attributes('-topmost', 1)
+    root.wm_attributes('-alpha', 0.9)
+    l = Label(text='\n'.join(tasks))
+    l.pack()
+    l.mainloop()
+
+def test(apiKey, secret, token=None):
+    rtm = createRTM(apiKey, secret, token)
+    createApp(rtm)
+
+def main():
+    import sys
+    try:
+        api_key, secret = sys.argv[1:3]
+    except ValueError:
+        print >>sys.stderr, 'Usage: rtm_appsample APIKEY SECRET [TOKEN]'
+    else:
+        try:
+            token = sys.argv[3]
+        except IndexError:
+            token = None
+        test(api_key, secret, token)
+
+if __name__ == '__main__':
+    main()