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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
|
#!/usr/bin/python
# -*- coding: UTF-8 -*-
# Copyright (C) 2012 Linaro Ltd.
# Author: Loïc Minier <loic.minier@linaro.org>
# License: GPL-3
import jira
from bs4 import BeautifulSoup
import logging
import optparse
import os
import re
import simplejson
import sys
import urllib2
logger = logging.getLogger("linaroroadmap")
def dbg(msg):
'''Print out debugging message if debugging is enabled.'''
logger.debug(msg)
class InMemCollector:
def __init__(self):
self.lanes = []
self.cards = []
def store_lane(self, lane):
self.lanes.append(lane)
def store_card(self, card):
self.cards.append(card)
def lane_is_collected(self, lane_id):
for l in self.lanes:
if l.lane_id == lane_id:
return True
return False
def kanban_request(opts, relpathname, method='GET', **kwargs):
request = urllib2.Request(
'%s/%s.json?_m=%s' % (opts.kanban_api_url, relpathname, method))
if opts.kanban_token:
request.add_header('X-KanbanToolToken', opts.kanban_token)
request_data = None
if kwargs.keys():
request.add_header('Content-Type', 'application/json')
request_data = simplejson.dumps(kwargs)
print request_data
response_data = urllib2.urlopen(request, request_data)
return simplejson.load(response_data)
def get_papyrs_page(papyrs_url, token):
url = '%s?json&auth_token=%s' % (papyrs_url, token)
return simplejson.load(urllib2.urlopen(url))
def get_kanban_boards(opts):
return kanban_request(opts, 'boards')
def get_kanban_board(opts, board_id):
return kanban_request(opts, 'boards/%s' % board_id)
def get_kanban_tasks(opts, board_id):
return kanban_request(opts, 'boards/%s/tasks' % board_id)
def get_kanban_task(opts, board_id, task_id):
return kanban_request(opts, 'boards/%s/tasks/%s' % (board_id, task_id))
def put_kanban_task(opts, board_id, task_id, **kwargs):
return kanban_request(opts, 'boards/%s/tasks/%s' % (board_id, task_id), method='PUT', **kwargs)
def main():
# TODO: add support for passing a card id or papyrs URL
parser = optparse.OptionParser(usage="%prog")
parser.add_option("--kanban-api-url", dest="kanban_api_url",
default="https://linaro.kanbantool.com/api/v1")
parser.add_option("--jira-api-url", dest="jira_api_url",
default="http://cards.linaro.org/rest/api/2")
# defaults are read-only ~linaro-infrastructure tokens
parser.add_option("--kanban-token", dest="kanban_token",
default="9F209W7Y84TE")
parser.add_option("--papyrs-token", dest="papyrs_token",
default="868e9088b53c")
parser.add_option("--jira-username", dest="jira_username",
default="robot")
parser.add_option("--jira-password", dest="jira_password",
default="cuf4moh2")
parser.add_option("--jira-project", dest="jira_project_name",
default="CARD")
parser.add_option("--jira-issuetype", dest="jira_issuetype_name",
default="Roadmap Card")
parser.add_option("--board-id", dest="board_id", default="10721")
parser.add_option('--debug', action='store_true', default=True,
help='Enable debugging output in parsing routines')
parser.add_option('--board',
help='Board id at Kanban Tool', dest='board', default='10721')
opts, args = parser.parse_args()
if os.environ.get("DEBUG", None) is not None:
opts.debug = True
if len(args) != 0:
parser.error("You can not pass any argument")
if opts.kanban_token is None:
sys.stderr.write("No Kanbantool API token given")
if opts.papyrs_token is None:
sys.stderr.write("No Papyrs API token given")
# logging setup
logger = logging.getLogger()
ch = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s %(message)s")
ch.setFormatter(formatter)
logger.addHandler(ch)
if opts.debug:
logger.setLevel(logging.DEBUG)
boards = get_kanban_boards(opts)
# dump
for board in boards:
board = board['board']
dbg('Found board "%s" with id %s' % (board['name'], board['id']))
dbg('')
assert 1 == len(filter(lambda b: str(b['board']['id']) == opts.board_id, boards)), \
'Expected exactly one board with id %s' % opts.board_id
board = get_kanban_board(opts, opts.board_id)
board = board['board']
workflow_stages = board['workflow_stages']
# ideally order wouldn't matter but the "position" field of our workflow stages
# is bogus (always 1) so we can't use it
leaf_workflow_stages = []
for workflow_stage in workflow_stages:
childs = filter(
lambda ws: ws['parent_id'] == workflow_stage['id'], workflow_stages)
if not childs:
# build a name list for leaf workflow stages
name = []
id = workflow_stage['id']
while True:
ws = filter(lambda ws: ws['id'] == id, workflow_stages)[0]
if ws['name'] is None:
break
name = [ws['name']] + name
id = ws['parent_id']
leaf_workflow_stages.append((workflow_stage['id'], name))
# dump
for id, name in leaf_workflow_stages:
pretty_name = "/".join(name)
dbg('Found leaf workflow stage %s with id %s' % (pretty_name, id))
dbg('')
card_types = board['card_types']
# dump
for card_type in card_types:
dbg('Found card type %s with id %s' % (card_type['name'], card_type['id']))
dbg('')
def get_leaf_workflow_stage_name(worfklow_stage_id):
return [name
for id, name
in leaf_workflow_stages
if id == worfklow_stage_id][0]
def get_card_type_name(card_type_id):
return [card_type['name']
for card_type
in card_types
if card_type['id'] == card_type_id][0]
def filter_tasks(task):
# ignore tasks in Legend and Deferred workflow stages
lwsn = get_leaf_workflow_stage_name(task['workflow_stage_id'])
if lwsn in (['Legend'], ['Deferred']):
dbg('Ignoring task %s in workflow stage %s'
% (task['external_id'], "/".join(lwsn)))
return False
# ignore tasks with Summit and Unknown card type names
card_type_name = get_card_type_name(task['card_type_id'])
if card_type_name in ('Summits', 'Unknown'):
dbg('Ignoring task %s with card type name %s'
% (task['external_id'], card_type_name))
return False
return True
tasks = get_kanban_tasks(opts, opts.board_id)
tasks = [t['task'] for t in tasks if filter_tasks(t['task'])]
# dump
for task in tasks:
dbg('Found task %s with id %s, workflow_stage_id %s, priority %s, '
'card_type_id %s, custom_field_2 %s, and external_id %s'
% (task['name'], task['id'], task['workflow_stage_id'],
task['priority'], task['card_type_id'],
task['custom_field_2'], task['external_id']))
CARD_TYPE_NAMES_TO_PREFIXES = {
'LAVA': 'LAVA',
'Android': 'ANDROID',
'Linux & Ubuntu': 'LINUX',
'TCWG': 'TCWG',
'GWG': 'GWG',
'MMWG': 'MMWG',
'KWG': 'KWG',
'PMWG': 'PMWG',
'OCTO': 'OCTO',
}
# check consistency of external_id with external_link and custom_field_2
# (papyrs URL), and of external_id with card_type name
for task in tasks:
external_id = task['external_id']
papyrs_url = task['custom_field_2']
external_link = task['external_link']
assert papyrs_url == 'https://linaro.papyrs.com/%s' % external_id, \
'Incorrect papyrs URL %s for task %s' % (papyrs_url, external_id)
assert external_link == 'http://status.linaro.org/card/%s' % external_id, \
'Incorrect external_link %s for task %s' % (external_link, external_id)
card_type_name = get_card_type_name(task['card_type_id'])
prefix = CARD_TYPE_NAMES_TO_PREFIXES[card_type_name]
assert external_id.startswith(prefix), \
'Incorrect card type prefix %s for task %s' % (prefix, external_id)
# verify papyrs pages
#for task in tasks:
for task in []:
external_id = task['external_id']
papyrs_url = task['custom_field_2']
dbg('Fetching card %s' % task['name'])
papyrs_json = get_papyrs_page(papyrs_url, opts.papyrs_token)
try:
# number of columns
ncols = len(papyrs_json)
assert ncols == 2, 'Expected exactly two columns but got %s' % len(ncols)
# first column
col0 = papyrs_json[0]
p0 = col0[0]
classname = p0['classname']
assert classname == 'Heading', \
"First paragraph of first column should be a a heading but is %s" % classname
assert p0['text'] == p0['html'], \
"Expected text (%s) and HTML (%s) to be identical for first heading" % (p0['text'], p0['html'])
assert p0['text'] == task['name'], \
'Mismatch between first heading (%s) and task (%s)' % (p0['text'], task['name'])
for p in col0[1:-2]:
assert p['classname'] in ('Heading', 'Paragraph'), \
'Got unexpected classname %s' % p['classname']
if p['classname'] == 'Heading':
assert p['text'] == p['html'], \
'Expected heading HTML (%s) to match text (%s)' % (p['html'], p['text'])
if p['classname'] == 'Paragraph':
soup = BeautifulSoup('<root>%s</root>' % p['html'], 'xml')
for tag in soup.root.find_all(True):
assert tag.name in ('font', 'b', 'a', 'ul', 'ol', 'li', 'br', 'p', 'span', 'div', 'u'), 'Unexpected tag %s' % tag.name
# second column
pm1 = col0[-1]
assert pm1['classname'] == 'Discuss', \
'Expect last classname to be Discuss but got %s' % pm1['classname']
col1 = papyrs_json[1]
skip_next_paragraph = False
nattachs = 0
for p in col1:
if p['classname'] in ('Checklist', 'Twitters', 'Navigation'):
pass
elif p['classname'] == 'Attachment':
nattachs += 1
elif p['classname'] == 'Heading' and p['text'] == 'Attachments':
pass
elif p['classname'] == 'Heading' and p['text'] == 'Metadata':
skip_next_paragraph = True
elif p['classname'] == 'Paragraph' and skip_next_paragraph:
skip_next_paragraph = False
else:
assert False, 'Unexpected paragraph %s' % p
if nattachs > 0:
dbg('Found %s attachment(s) on card %s' % (nattachs, task['name']))
except Exception, e:
dbg(e)
# query jira data
jira_project_result = jira.do_request(opts, 'project/%s' % opts.jira_project_name)
jira_statuses_result = jira.do_request(opts, 'status')
jira_fields_result = jira.do_request(opts, 'field')
# not allowed
#jira_securitylevels_result = jira.do_request(opts, 'securitylevel')
jira_priorities_result = jira.do_request(opts, 'priority')
def search_jira_id(jira_result, name):
return [r['id'] for r in jira_result if r['name'] == name][0]
# http://cards.linaro.org/rest/api/2/project/CARD has id 10000
#jira_project_id = 10000
jira_project_id = jira_project_result['id']
# issuetype for "Roadmap Card" http://cards.linaro.org/rest/api/2/issuetype/9
#jira_issuetype_id = 9
jira_issuetype_id = search_jira_id(jira_project_result['issueTypes'], opts.jira_issuetype_name)
dbg('Found id %s for %s issueType' % (jira_issuetype_id, opts.jira_issuetype_name))
for component in jira_project_result['components']:
dbg('Found component %s with id %s' % (component['name'], component['id']))
for version in jira_project_result['versions']:
dbg('Found version %s with id %s' % (version['name'], version['id']))
for status in jira_statuses_result:
dbg('Found status %s with id %s' % (status['name'], status['id']))
TYPE_TO_COMPONENT = {
'LAVA': 'LAVA',
'Android': 'Android',
'Linux & Ubuntu': 'Linux & Ubuntu',
'TCWG': 'Toolchain WG',
'GWG': 'Graphics WG',
'MMWG': 'Multimedia WG',
'KWG': 'Kernel WG',
'PMWG': 'Power Management WG',
'OCTO': 'OCTO',
}
STAGE_TO_STATUS = {
'New/Draft': 'New/Drafting',
'New/Needs Work': 'New/Drafting',
'New/TSC Reviewed': 'New/Reviewed',
'2012Q1/Done': 'Approved',
'2012Q1/Ready': 'Approved',
'2012Q2/Forecast': 'Approved',
'2012Q3/Forecast': 'Approved',
'2012H2/Forecast': 'Approved',
'2013/Forecast': 'Approved',
}
STAGE_TO_VERSION = {
'New/Draft': None,
'New/Needs Work': None,
'New/TSC Reviewed': None,
'2012Q1/Done': '2012Q1',
'2012Q1/Ready': '2012Q1',
'2012Q2/Forecast': '2012Q2',
'2012Q3/Forecast': '2012Q3',
'2012H2/Forecast': '2012H2',
'2013/Forecast': '2013',
}
PRIORITY_MAP = {
-1: 'Minor',
0: 'Major',
1: 'Critical',
}
# actual copy
for task in tasks:
print task['name']
print task['external_id']
external_id = task['external_id']
papyrs_url = task['custom_field_2']
papyrs_json = get_papyrs_page(papyrs_url, opts.papyrs_token)
# first column
col0 = papyrs_json[0]
p0 = col0[0]
# assemble HTML of description
html = ""
for p in col0[1:-1]:
if p['classname'] == 'Heading':
html += '<h1>%s</h1>\n' % p['text']
if p['classname'] == 'Paragraph':
html += '%s\n' % p['html']
html = '{html}\n%s{html}\n' % html
stage = "/".join(get_leaf_workflow_stage_name(task['workflow_stage_id']))
status = STAGE_TO_STATUS[stage]
version = STAGE_TO_VERSION[stage]
type_name = get_card_type_name(task['card_type_id'])
component = TYPE_TO_COMPONENT[type_name]
priority = PRIORITY_MAP[task['priority']]
fields = {'project': {'id': jira_project_id},
'summary': task['name'],
'issuetype': {'id': jira_issuetype_id},
'description': html,
'components': [{'id': search_jira_id(jira_project_result['components'], component)}],
search_jira_id(jira_fields_result, 'Alias Card ID'): task['external_id'],
# XXX hardcoded default security level; also, can't set security level to Public via API
#'security': {'id': search_jira_id(jira_securitylevels_result, 'Public')},
'priority': {'id': search_jira_id(jira_priorities_result, priority)},
}
#'status': search_jira_id(jira_statuses_result, status),
if version:
fields['fixVersions'] = [{'id': search_jira_id(jira_project_result['versions'], version)}]
dbg('Uploading card %s' % fields)
print jira.do_request(opts, 'issue', fields=fields)
if __name__ == "__main__":
main()
|