Commits

Brent Tubbs committed 270b20d

initial commit

  • Participants

Comments (0)

Files changed (6)

+syntax: glob
+*.pyc
+.*.swp
+.*.swo
+*~
+.DS_Store
+pip-log.txt
+build/*
+dist/*
+tmp/*
+run/*
+*egg-info/*
+Meet Trollop
+============
+
+Trollop is a Python library for working with the `Trello API`_ <https://trello.com/docs/api/index.html>`_.
+
+Quick Start
+===========
+
+A Trello connection is instantiated with your `API key`_ and a user's `oauth token`_::
+
+    In [1]: from trollop import TrelloConnection
+
+    In [2]: conn = TrelloConnection(<your developer key>, <user's oauth token>)
+
+The connection object will automatically have a Member object attached,
+representing the user whose oauth token was used to connect::
+
+    In [3]: conn.me
+    Out[3]: <trollop.lib.Member object at 0x101707650>
+
+    In [4]: conn.me.username
+    Out[4]: u'btubbs'
+
+In the previous example no HTTP request was made until command 4, the access
+to conn.me.username.  Trollop objects are lazy.
+
+The connection object has methods for getting objects by their IDs.  The
+objects will have attributes named exactly as they are in the Trello API docs.::
+
+    In [5]: card = conn.get_card('4f2e454cefab2bbd4ea71b02')
+
+    In [6]: card.name
+    Out[6]: u'Build a Python Trello Library'
+
+    In [7]: card.desc
+    Out[7]: u'And call it Trollop.'
+
+    In [8]: card.idList
+    Out[8]: u'4f17cb04d5c817032301c179' 
+
+In addition, there are convenience properties to automatically look up related
+objects.::
+
+    In [9]: lst = card.list
+
+    In [10]: lst
+    Out[10]: <trollop.lib.List object at 0x101707890>
+
+    In [11]: lst.name
+    Out[11]: u'Icebox'
+
+    In [12]: lst.id
+    Out[12]: u'4f17cb04d5c817032301c179'
+
+    In [13]: len(lst.cards)
+    Out[13]: 20
+
+    In [14]: lst.cards[-1].name
+    Out[14]: u'Build a Python Trello Library'
+
+Help Wanted
+===========
+
+Some objects are not yet implemented (e.g. actions), as well as some relations
+between objects.  They're easy to add though; just take a look at the
+already-implemented classes in trollop/lib.py to see how to declare a new
+class, attribute, or relation.  If you'd like to pitch in to finish covering
+the whole API, please send a pull request with your changes.
+
+.. _Trello API: https://trello.com/docs/api/index.html
+.. _API key: https://trello.com/card/board/generating-your-developer-key/4ed7e27fe6abb2517a21383d/4eea75831576578f2713f460
+.. _oauth token: https://trello.com/card/board/getting-a-user-token-and-oauth-urls/4ed7e27fe6abb2517a21383d/4eea75bc1576578f2713fc5f 
+#!/usr/bin/python
+from setuptools import setup, find_packages
+
+setup(
+    name='trollop',
+    version='0.0.1',
+    author='Brent Tubbs',
+    author_email='brent.tubbs@gmail.com',
+	packages=find_packages(),
+    include_package_data=True,
+	install_requires = [
+        'httplib2',
+	],
+    url='http://bits.btubbs.com/trollop',
+    description='A Python library for working with the Trello api.',
+)

trollop/__init__.py

+from lib import *
+from urllib import urlencode
+
+import httplib2
+import json
+
+
+class TrelloError(Exception):
+    pass
+
+
+class TrelloConnection(object):
+
+    def __init__(self, api_key, oauth_token):
+        self.client = httplib2.Http()
+        self.key = api_key
+        self.token = oauth_token
+
+    def request(self, method, path, params=None, body=None):
+        headers = {'Accept': 'application/json'}
+        if method == 'POST' or method == 'PUT':
+            headers.update({'Content-Type': 'application/json'})
+
+        if not path.startswith('/'):
+            path = '/' + path
+        url = 'https://api.trello.com/1' + path
+
+        params = params or {}
+        params.update({'key': self.key, 'token': self.token})
+        url += '?' + urlencode(params)
+        response, content = self.client.request(url, method,
+                                                headers=headers,body=body)
+        if response.status != 200:
+            # TODO: confirm that Trello never returns a 201, for example, when
+            # creating a new object. If it does, then we shouldn't restrict
+            # ourselves to a 200 here.
+            raise TrelloError(content)
+        return content
+
+    def get(self, path, params=None):
+        return self.request('GET', path, params)
+
+    def post(self, path, params=None, body=None):
+        return self.request('POST', path, params, body)
+
+    def put(self, path, params=None, body=None):
+        return self.request('PUT', path, params, body)
+
+    def get_board(self, board_id):
+        return Board(self, board_id)
+
+    def get_list(self, list_id):
+        return List(self, list_id)
+
+    def get_card(self, card_id):
+        return Card(self, card_id)
+
+    def get_member(self, member_id):
+        return Member(self, member_id)
+
+    @property
+    def me(self):
+        """
+        Return a Membership object for the user whose credentials were used to
+        connect.
+        """
+        return Member(self, 'me')
+
+
+class LazyTrello(object):
+    """
+    Parent class for Trello objects (cards, lists, boards, members, etc).  This
+    should always be subclassed, never used directly.
+    """
+    # The Trello API path where objects of this type may be found.
+    _prefix = '' # eg '/cards/'
+
+    # In subclasses, these values should be filled in with a set of the
+    # attribute names that class has data for, and the path prefix like
+    # '/cards/'.  A set is used instead of a list in order to provide more
+    # efficient 'in' tests.  These are the attributes that will trigger an http
+    # GET before trying to return a value.
+    _attrs = set() # eg set(['name', 'url'])
+
+    # Here you can specify related objects that can be looked up on a sub-path
+    # of the object you've got.  Use the name of the subpath as the key
+    # (without any slashes), and the class to use to instantiate those objects
+    # as the value.  If you run into a class declaration ordering problem, you
+    # can also put the name of the class in a string and use that for the
+    # value.
+    _sublists = {} # eg {'boards': Board} or {'boards': 'Board'}
+
+    def __init__(self, conn, obj_id, data=None):
+        self.id = obj_id
+        self.conn = conn
+        self.path = self._prefix + obj_id
+
+        # If we've been passed the data, then remember it and don't bother
+        # fetching later.
+        if data:
+            self.data = data
+
+    def __getattr__(self, attr):
+        # For attributes specified in self._attrs, query Trello upon
+        # access
+        if (attr == 'data' or
+           attr in self._attrs or
+           attr in self._sublists):
+            if not 'data' in self.__dict__:
+                self.data = json.loads(self.conn.get(self.path))
+
+            # 'data' is special-cased, since it can be looked up on its own,
+            # but is also the source of our other dynamic attributes.
+            if attr == 'data':
+                return self.data
+            elif attr in self.data:
+                return self.data[attr]
+            elif attr in self._sublists:
+                # classes may be values right in the dict, or may be identified
+                # by name as strings (for cases where you want to reference a
+                # class that's not defined yet.)
+                if isinstance(self._sublists[attr], str):
+                    klass = globals()[self._sublists[attr]]
+                else:
+                    klass = self._sublists[attr]
+                path = self._prefix + self.id + '/' + attr
+                data = json.loads(self.conn.get(path))
+                # TODO: cache these on the object so you don't have to do
+                # multiple http requests if, for example, list.cards is called
+                # multiple times on the same object.
+                return [klass(self.conn, d['id'], d) for d in data]
+
+            raise AttributeError("Trello data has %s key" % attr)
+        else:
+            raise AttributeError("%r object has no attribute %r" %
+                                 (type(self).__name__, attr))
+
+    # FIXME: Not all objects are closable.  This should be moved to a mixin.
+    def close(self):
+        path = self._prefix + self.id + '/closed'
+        params = {'value': 'true'}
+        result = self.conn.put(path, params=params)
+
+
+class Board(LazyTrello):
+
+    _prefix = '/boards/'
+
+    _attrs = set([
+        'url',
+        'name',
+        'pinned',
+        'prefs',
+        'desc',
+        'closed',
+        'idOrganization',
+    ])
+
+    _sublists = {
+        'members': 'Member',
+    }
+
+    def all_lists(self):
+        """Returns all lists on this board"""
+        return self.get_lists('all')
+
+    def open_lists(self):
+        """Returns all open lists on this board"""
+        return self.get_lists('open')
+
+    def closed_lists(self):
+        """Returns all closed lists on this board"""
+        return self.get_lists('closed')
+
+    def get_lists(self, filtr):
+        # 'filter' is a Python built in function, so we misspell here to avoid
+        # clobbering it.
+
+        path = self.path + '/lists'
+        params = {'cards': 'none', 'filter': filtr}
+        data = json.loads(self.conn.get(path, params))
+
+        return [List(self.conn, ldata['id'], ldata) for ldata in data]
+
+
+class List(LazyTrello):
+
+    _prefix = '/lists/'
+    _attrs = set([
+        'url',
+        'idBoard',
+        'closed',
+        'name'
+    ])
+    _sublists = {
+        'cards': 'Card',
+    }
+
+
+    def add_card(self, name, desc=None):
+        path = self._prefix + self.id + '/cards'
+        body = json.dumps({'name': name, 'idList': self.id, 'desc': desc,
+                           'key': self.conn.key, 'token': self.conn.token})
+        data = json.loads(self.conn.post(path, body=body))
+        card = Card(self.conn, data['id'], data)
+        return card
+
+
+class Card(LazyTrello):
+
+    _prefix = '/cards/'
+    _attrs = set([
+        'url',
+        'idList',
+        'closed',
+        'name',
+        'badges',
+        'checkItemStates',
+        'desc',
+        'idBoard',
+        'idMembers',
+        'labels',
+    ])
+
+    # XXX: This is a common pattern.  Make it generic? Possibly with
+    # '_properties'
+    @property
+    def board(self):
+        return Board(self.conn, self.idBoard)
+
+    @property
+    def list(self):
+        return List(self.conn, self.idList)
+
+    # XXX: Another common pattern.  Maybe add magic for '_lists'
+    @property
+    def members(self):
+        return [Member(self.conn, mid) for mid in self.idMembers]
+
+
+class Member(LazyTrello):
+
+    _prefix = '/members/'
+    _attrs = set([
+        'url',
+        'fullName',
+        'bio',
+        'gravatar',
+        'username',
+    ])
+    _sublists = {
+        'boards': Board,
+        'cards': Card,
+    }
+
+
+class Organization(LazyTrello):
+
+    _prefix = '/organizations/'
+    _attrs = set([
+        'url',
+        'desc',
+        'displayName',
+        'name',
+    ])
+    _sublists = {
+        'boards': Board,
+        'members': Member,
+    }
+
+# TODO: implement the rest of the objects, and their methods.  Given the
+# patterns above, that should just mean more typing, at this point.
+import unittest
+import json
+from trollop import TrelloConnection
+
+
+class Namespace(dict):
+    def __init__(self, *args, **kwargs):
+        dict.__init__(self, *args, **kwargs)
+        self.__dict__ = self
+
+
+class FakeHttp(object):
+    """Mock for httplib2.Http"""
+
+    def __init__(self, response, content):
+        self.response = response
+        self.content = content
+        # store requests in this list so they can be inspected later.
+        self.requests = []
+
+    def request(self, *args, **kwargs):
+        self.requests.append(locals())
+        return Namespace(self.response), self.content
+
+
+class TrollopTestCase(unittest.TestCase):
+
+    response = {'status': 200}
+    content = {}
+
+    def setUp(self):
+        self.conn = TrelloConnection('blah', 'blerg')
+        # monkeypatch the http client
+        self.conn.client = FakeHttp(self.response, json.dumps(self.content))
+
+
+class TestGetMe(TrollopTestCase):
+
+    content = {
+        "id":"4e73a7ef5571166c5f53a93f",
+        "fullName":"Brent Tubbs",
+        "username":"btubbs",
+        "gravatar":"e60b3c53235cd53f5e2b6401678c4f6a",
+        "bio":"",
+        "url":"https://trello.com/btubbs"
+    }
+
+    def test_get_me(self):
+
+        # Ensure that the connection has a 'me' property, with attributes for
+        # the json keys returned in the response.  Accessing this attribute
+        # will also force an http request
+        assert self.conn.me.username == self.content['username']
+
+        # Make sure that client.request was called with the right path and
+        # method, by inspecting the list of requests made to the mock.
+        req1 = self.conn.client.requests[0]
+        assert req1['args'][0].startswith('https://api.trello.com/1/members/me')
+        assert req1['args'][1] == 'GET'