File: trello.py

package info (click to toggle)
bugwarrior 1.8.0-12
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,280 kB
  • sloc: python: 9,226; makefile: 147
file content (208 lines) | stat: -rw-r--r-- 8,063 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
"""
Trello service

Pulls trello cards as tasks.

Trello API documentation available at https://developers.trello.com/
"""
from __future__ import unicode_literals
from six.moves.configparser import NoOptionError

from jinja2 import Template
import requests

from bugwarrior.services import IssueService, Issue, ServiceClient
from bugwarrior.config import die, asbool, aslist

DEFAULT_LABEL_TEMPLATE = "{{label|replace(' ', '_')}}"


class TrelloIssue(Issue):
    NAME = 'trellocard'
    CARDID = 'trellocardid'
    SHORTCARDID = 'trellocardidshort'
    DESCRIPTION = 'trellodescription'
    BOARD = 'trelloboard'
    LIST = 'trellolist'
    SHORTLINK = 'trelloshortlink'
    SHORTURL = 'trelloshorturl'
    URL = 'trellourl'

    UDAS = {
        NAME: {'type': 'string', 'label': 'Trello card name'},
        CARDID: {'type': 'string', 'label': 'Trello card ID'},
        SHORTCARDID: {'type': 'numeric', 'label': 'Trello short card ID'},
        DESCRIPTION: {'type': 'string', 'label': 'Trello description'},
        BOARD: {'type': 'string', 'label': 'Trello board name'},
        LIST: {'type': 'string', 'label': 'Trello list name'},
        SHORTLINK: {'type': 'string', 'label': 'Trello shortlink'},
        SHORTURL: {'type': 'string', 'label': 'Trello short URL'},
        URL: {'type': 'string', 'label': 'Trello URL'},
    }
    UNIQUE_KEY = (CARDID,)

    def get_default_description(self):
        """ Return the old-style verbose description from bugwarrior.
        """
        return self.build_default_description(
            title=self.record['name'],
            url=self.record['shortUrl'],
            number=self.record['idShort'],
            cls='task',
        )

    def get_tags(self, twdict):
        tmpl = Template(
            self.origin.get('label_template', DEFAULT_LABEL_TEMPLATE))
        return [tmpl.render(twdict, label=label['name'])
                for label in self.record['labels']]

    def to_taskwarrior(self):
        twdict = {
            'project': self.extra['boardname'],
            'due': self.parse_date(self.record['due']),
            'priority': self.origin['default_priority'],
            self.NAME: self.record['name'],
            self.CARDID: self.record['id'],
            self.SHORTCARDID: self.record['idShort'],
            self.DESCRIPTION: self.record['desc'],
            self.BOARD: self.extra['boardname'],
            self.LIST: self.extra['listname'],
            self.SHORTLINK: self.record['shortLink'],
            self.SHORTURL: self.record['shortUrl'],
            self.URL: self.record['url'],
            'annotations': self.extra.get('annotations', []),
        }
        if self.origin['import_labels_as_tags']:
            twdict['tags'] = self.get_tags(twdict)
        return twdict


class TrelloService(IssueService, ServiceClient):
    ISSUE_CLASS = TrelloIssue
    # What prefix should we use for this service's configuration values
    CONFIG_PREFIX = 'trello'

    @classmethod
    def validate_config(cls, service_config, target):
        def check_key(opt):
            """ Check that the given key exist in the configuration  """
            if opt not in service_config:
                die("[{}] has no 'trello.{}'".format(target, opt))
        super(TrelloService, cls).validate_config(service_config, target)
        check_key('token')
        check_key('api_key')

    @staticmethod
    def get_keyring_service(service_config):
        api_key = service_config.get('api_key')
        return "trello://{api_key}@trello.com".format(api_key=api_key)

    def get_service_metadata(self):
        """
        Return extra config options to be passed to the TrelloIssue class
        """
        return {
            'import_labels_as_tags':
            self.config.get('import_labels_as_tags', False, asbool),
            'label_template':
            self.config.get('label_template', DEFAULT_LABEL_TEMPLATE),
            }

    def issues(self):
        """
        Returns a list of dicts representing issues from a remote service.
        """
        for board in self.get_boards():
            for lst in self.get_lists(board['id']):
                listextra = dict(boardname=board['name'], listname=lst['name'])
                for card in self.get_cards(lst['id']):
                    issue = self.get_issue_for_record(card, extra=listextra)
                    issue.update_extra({"annotations": self.annotations(card)})
                    yield issue

    def annotations(self, card_json):
        """ A wrapper around get_comments that build the taskwarrior
        annotations. """
        comments = self.get_comments(card_json['id'])
        annotations = self.build_annotations(
            ((c['memberCreator']['username'], c['data']['text']) for c in comments),
            card_json["shortUrl"])
        return annotations


    def get_boards(self):
        """
        Get the list of boards to pull cards from.  If the user gave a value to
        trello.include_boards use that, otherwise ask the Trello API for the
        user's boards.
        """
        if 'include_boards' in self.config:
            for boardid in self.config.get('include_boards', to_type=aslist):
                # Get the board name
                yield self.api_request(
                    "/1/boards/{id}".format(id=boardid), fields='name')
        else:
            boards = self.api_request("/1/members/me/boards", fields='name')
            for board in boards:
                yield board

    def get_lists(self, board):
        """
        Returns a list of the filtered lists for the given board
        This filters the trello lists according to the configuration values of
        trello.include_lists and trello.exclude_lists.
        """
        lists = self.api_request(
            "/1/boards/{board_id}/lists/open".format(board_id=board),
            fields='name')

        include_lists = self.config.get('include_lists', to_type=aslist)
        if include_lists:
            lists = [l for l in lists if l['name'] in include_lists]

        exclude_lists = self.config.get('exclude_lists', to_type=aslist)
        if exclude_lists:
            lists = [l for l in lists if l['name'] not in exclude_lists]

        return lists

    def get_cards(self, list_id):
        """ Returns an iterator for the cards in a given list, filtered
        according to configuration values of trello.only_if_assigned and
        trello.also_unassigned """
        params = {'fields': 'name,idShort,shortLink,shortUrl,url,labels,due,desc'}
        member = self.config.get('only_if_assigned', None)
        unassigned = self.config.get('also_unassigned', False, asbool)
        if member is not None:
            params['members'] = 'true'
            params['member_fields'] = 'username'
        cards = self.api_request(
            "/1/lists/{list_id}/cards/open".format(list_id=list_id),
            **params)
        for card in cards:
            if (member is None
                    or member in [m['username'] for m in card['members']]
                    or (unassigned and not card['members'])):
                yield card

    def get_comments(self, card_id):
        """ Returns an iterator for the comments on a certain card. """
        params = {'filter': 'commentCard', 'memberCreator_fields': 'username'}
        comments = self.api_request(
            "/1/cards/{card_id}/actions".format(card_id=card_id),
            **params)
        for comment in comments:
            assert comment['type'] == 'commentCard'
            yield comment

    def api_request(self, url, **params):
        """
        Make a trello API request. This takes an absolute url (without protocol
        and host) and a list of argumnets and return a GET request with the
        key and token from the configuration
        """
        params['key'] = self.config.get('api_key'),
        params['token'] = self.get_password('token', self.config),
        url = "https://api.trello.com" + url
        return self.json_response(requests.get(url, params=params))