-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathptrello.py
More file actions
executable file
·338 lines (285 loc) · 10.9 KB
/
Copy pathptrello.py
File metadata and controls
executable file
·338 lines (285 loc) · 10.9 KB
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
#!/usr/bin/env python
"""
Test getting trello cards by API and printing
Dirty piece of hackery.
"""
import csv
import sys
import json
import StringIO
from optparse import OptionParser
from collections import defaultdict
from xml.sax.saxutils import escape
from jinja2 import Environment, PackageLoader
import trello
import tablib
import settings
jinja_env = Environment(loader=PackageLoader('trello', 'templates'))
# map the value of --type to a template
# keys used to determine valid choices for --type
TEMPLATES = {'text': 'board_summary.txt',
'html': 'board_summary.html',
'shtml': 'board_summary_standalone.html',
'tagmapped': 'tagmap_standalone.html',
}
OUTPUT_TYPES = TEMPLATES.keys()
def get_lists(tconn, board_id):
return tconn.boards[board_id].lists()
def cards_for_list(tconn, list_id):
return tconn.lists[list_id].cards()
def get_checklist(tconn, c_id):
return tconn.checklists[c_id]()
def set_checked(checklist):
"""
Add a `checked` property to the items in checklist dict.
Set `True` if checklist item `state` is 'Complete'
"""
for item in checklist['checkItems']:
item['checked'] = True if item['state'] == u'complete' else False
return checklist
def augment_card(tconn, card):
"""
De-normalise some properties, e.g. check lists, into this card record
Action history also extracted
"""
card['checklists'] = [set_checked(get_checklist(tconn, cid))
for cid in card['idChecklists']]
actions = tconn.cards[card['id']].actions()
actions = [x for x in actions if 'text' in x['data']]
card['actions'] = actions
return card
def strip(s):
"""
Custom filter, calls strip on the string and returns
Unlike `trim` builtin filter this removes \\n and \\r
"""
return s.strip()
def html_escape(s):
"""
Custom filter than HTML escapes the string, e.g. replacing ampersand with
entity
"""
return escape(s)
def subst(s, target, replace):
"""
Custom filter to replace all occurences of `target` with `replace`.
"""
return s.replace(target, replace)
def parformat(s, line_len=80, split_str='\n'):
"""
Jinja filter that inserts `split_str` every `line_len` characters
or fewer to fit on a word boundary.
"""
tokens = s.split()
new_tokens = []
count = 0
while tokens:
token = tokens.pop(0)
l = len(token)
if count+l > line_len:
new_tokens.append(split_str)
count = 0
new_tokens.append(token)
count += l+1 # +1 for the space needed
return " ".join(new_tokens)
def pluralise(root, testval, singular, plural):
"""
Rather than get i18n to work, here we test testval
to be > 1 and if so append plural to `root`, otherwise
append singular.
"""
if testval > 1:
return root+plural
return root+singular
def augment_list_stats(data):
"""
Add summary data to each list: cards per tag.
"""
aggregate = defaultdict(dict)
for _list in data:
cards_per_tag = defaultdict(dict)
for card in _list['cards']:
for label in card['labels']:
record = cards_per_tag[label['name']]
count = record.get('value', 0) + 1
record['value'] = count
record['color'] = label['color']
record['label'] = label['name']
arec = aggregate[label['name']]
count = arec.get('value', 0) +1
arec['value'] = count
arec['color'] = label['color']
arec['label'] = label['name']
_list['cards_per_tag'] = cards_per_tag.values()
data[0]['aggregate_tags'] = aggregate.values()
return data
def render(data, suffix='text', title='Stories', highlights=[], labels=False):
"""
Render the dataset with template suggested by suffix
"""
data = augment_list_stats(data)
if suffix == 'csv':
render_csv(data, title, labels)
elif suffix == 'excel':
render_excel(data, title, labels)
else:
render_textual(data, suffix, title, highlights, labels)
def render_textual(data, suffix='text', title='Stories',
highlights=[], labels=False):
filters = dict(strip=strip, html_escape=html_escape,
parformat=parformat, subst=subst,
pluralise=pluralise)
jinja_env.filters.update(filters)
filename = TEMPLATES[suffix]
template = jinja_env.get_template(filename)
output = template.render(lists=data,
title=title,
highlights=highlights,
show_labels=labels)
sys.stdout.write(output.encode('utf-8'))
def render_csv(data, title='Stories', labels=False):
strout = StringIO.StringIO()
writer = csv.writer(strout, quoting=csv.QUOTE_ALL)
for _list in data:
writer.writerow([])
writer.writerow([_list['name'].encode('utf-8')])
writer.writerow(['ID', 'Name', 'Description', 'URL'])
cards = _list['cards']
if cards:
for card in cards:
# @todo deal with labels
writer.writerow(
[card['idShort'],
card['name'].encode('utf-8'),
card['desc'].encode('utf-8'),
card['url'].encode('utf-8'),
])
else:
writer.writerow(['No cards in this list'])
sys.stdout.write(strout.getvalue())
def render_excel(data, title='Stories', labels=False):
"""
Renders to Excel, one sheet per list. This does not output to stdout
but to a file named <title>.xls.
Known issues:
* Sheets are not named - first column header of each sheet is the
sheet name; value is the story ID.
"""
headers = ['ID', 'Name', 'Description', 'URL']
datasets = []
for _list in data:
ds = tablib.Dataset(headers=headers)
ds.append_separator(_list['name'])
for card in _list['cards']:
# @todo deal with labels
ds.append((
card['idShort'],
card['name'].encode('utf-8'),
card['desc'].encode('utf-8'),
card['url'].encode('utf-8')))
datasets.append(ds)
book = tablib.Databook(sets=datasets)
with open('{}.xls'.format(title), 'wb') as f:
f.write(book.xls)
def print_board(tconn, board, suffix='text', card_filter="", dump=False,
title='Stories', prune=False, highlights=[], labels=False):
inlists = get_lists(tconn, board)
outlists = []
while inlists:
lst = inlists.pop(0)
lst['cards'] = [augment_card(tconn, card)
for card in cards_for_list(tconn, lst['id'])
if card['name'].find(card_filter) >= 0]
if prune and not lst['cards']:
pass
else:
outlists.append(lst)
if dump:
print json.dumps(outlists)
else:
render(outlists, suffix, title, highlights=highlights, labels=labels)
def load(tconn, input_file, suffix='text', title='Stories', highlights=[],
labels=False):
"""
Load JSON dataset from `input_file` and render with the
appropriate template.
"""
with open(input_file, 'rt') as inf:
data = json.loads(inf.read())
render(data, suffix, title, highlights=highlights, labels=labels)
def list_boards(tconn, options):
"""
List all boards available to a user
"""
boards = tconn.members["me"].boards()
print("ID Name")
print("------------------------------------------------")
for board in boards:
print("{}: {}".format(board['id'], board['name']))
if __name__ == '__main__':
parser = OptionParser()
parser.add_option('-b', '--board',
default=settings.DEFAULT_BOARD,
help="The board ID to use [default: %default]")
parser.add_option('-f', '--filter',
default="",
help="Display only cards with description containing "
"this value")
parser.add_option('-t', '--type',
default='shtml',
choices=OUTPUT_TYPES,
help=("Output format text, one of {} "
"(default: %default)".format(OUTPUT_TYPES)))
parser.add_option('', '--title',
default='User Stories: Trello story board',
help="Set the document title (default: %default)")
parser.add_option('', '--load',
default=None,
help="Load a previously dumped data set for rendering")
parser.add_option('', '--dump',
action="store_true",
default=False,
help="Dump acquired data as JSON rather than rendering")
parser.add_option('', '--prune',
action="store_true",
default=False,
help="Prune out lists with no stories - has no impact "
"with --load (default: %default)")
parser.add_option('', '--key',
default=settings.KEY,
help="API key to use (overrides that in settings)")
parser.add_option('', '--secret',
default=settings.SECRET,
help="Secret key to use (overrides that in settings)")
parser.add_option('', '--token',
default=settings.TOKEN,
help="API token to use (overrides that in settings)")
parser.add_option('', '--highlight',
default='',
help="Comma delimited list of story IDs to highlight")
parser.add_option('', '--labels',
default=False,
action="store_true",
help="Display Trello labels in output "
"(default: %default)")
parser.add_option('', '--list-boards',
dest="list_boards",
default=False,
action="store_true",
help="List all boards and board IDs accessible with "
"these credentials")
(options, args) = parser.parse_args()
tconn = trello.Trello(options.key, options.token)
highlights = [int(x) for x in options.highlight.split(',') if x]
if options.list_boards:
list_boards(tconn, options)
elif options.load:
load(tconn, options.load, options.type, title=options.title,
highlights=highlights, labels=options.labels)
else:
print_board(tconn, options.board, options.type, options.filter,
title=options.title,
dump=options.dump,
prune=options.prune,
highlights=highlights,
labels=options.labels)