Spage has uploaded a new change for review.
https://gerrit.wikimedia.org/r/188287
Change subject: Use export_trello logic and trello_PHIds mapping
......................................................................
Use export_trello logic and trello_PHIds mapping
* Refactor to use a TrelloImporter object.
* Identify card members, start trying to map them.
* Change argument to phab_project to match export_trello.py.
* Don't die on no checklist (from normal board export), nor on missing
column in get_column_name.
* Handle idLabels, add get_labelnames command
* Cards in enterprise export have an idLabels array, not a labels array.
* Use card's idList to set the card's column.
* Adapt to use trello_makePHIDs.py output file.
* Handle card shortUrl and shortLink; they're not present in Trello's
Enterprise Export JSON :( , derive from card.url if necessary.
* Use export_trello.py's card logic in trello_import.py.
Import import TrelloCard, TrelloDAO, TrelloScrubber from
export_trello.py. Load the json file into TrelloDAO object,
and use TrelloCard and TrelloScrubber to fill in each card.
* Convert export_trello.py's checklist handling from adding a comment into
adding strings to description. Also fake out export_trello.py's
setup_logging().
Sample run:
* export all Trello boards
* modify /etc/phabtools.conf for either phab-01.wmflabs.org or
phabricator.wmflabs.org
* set up conf/trello-scrub.yaml for names on phab-01 or phabricator
* run trello_makePHIDs.py
* run
python trello_create.py -v -vv -t -d \
-j
trabulous_dir/wikimediafoundation_20150112_213549/boards/flow_backlog.json \
-l 'Send to Phabricator - Collaboration-Team board'
Change-Id: I70897def2a781191fe06213cf157284f4e29b1a6
---
A export_trello.py
M trello_create.py
2 files changed, 861 insertions(+), 70 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/phabricator/tools
refs/changes/87/188287/1
diff --git a/export_trello.py b/export_trello.py
new file mode 100644
index 0000000..526b5a4
--- /dev/null
+++ b/export_trello.py
@@ -0,0 +1,589 @@
+#!/usr/bin/env python
+
+# This is an example script as part of a trac/trello --> phabricator
+# migration. It may or may not have worked at a particular point in
+# time but could be totally broken by the time you read this. Even if
+# it did work it is still completely tied to the internal
+# idiosyncrasies of a decade of trac use and migration goals of a
+# single company. There will never be any documentation.
+#
+# YOU SHOULD NOT RUN THIS SCRIPT.
+#
+# If you are willing to take COMPLETE RESPONSIBILITY FOR WRECKING YOUR
+# PHABRICATOR INSTALL you should MAYBE consider this script as an
+# EXAMPLE to help your own design. NO ONE CAN HELP YOU. If you were
+# not already willing to write such scripts from scratch run away from
+# the dragon now. It is provided for that purpose and as a
+# demonstration for upstream of a migration to guide feature
+# development that may make that very slightly less painful one day.
+#
+# YOU SHOULD NOT RUN THIS SCRIPT. A DRAGON WILL EAT ALL OF YOUR DATA.
+#
+# /===-_---~~~~~~~~~------____
+# |===-~___ _,-'
+# -==\\ `//~\\ ~~~~`---.___.-~~
+# ______-==| | | \\ _-~`
+# __--~~~ ,-/-==\\ | | `\ ,'
+# _-~ /' | \\ / / \ /
+# .' / | \\ /' / \ /'
+# / ____ / | \`\.__/-~~ ~ \ _ _/' / \/'
+# /-'~ ~~~~~---__ | ~-/~ ( ) /' _--~`
+# \_| / _) ; ), __--~~
+# '~~--_/ _-~/- / \ '-~ \
+# {\__--_/} / \\_>- )<__\ \
+# /' (_/ _-~ | |__>--<__| |
+# |0 0 _/) )-~ | |__>--<__| |
+# / /~ ,_/ / /__>---<__/ |
+# o o _// /-~_>---<__-~ /
+# (^(~ /~_>---<__- _-~
+# ,/| /__>--<__/ _-~
+# ,//('( |__>--<__| / .----_
+# ( ( ')) |__>--<__| | /' _---_~\
+# `-)) )) ( |__>--<__| | /' / ~\`\
+# ,/,'//( ( \__>--<__\ \ /' // ||
+# ,( ( ((, )) ~-__>--<_~-_ ~--____---~' _/'/ /'
+# `~/ )` ) ,/| ~-_~>--<_/-__ __-~ _/
+# ._-~//( )/ )) ` ~~-'_/_/ /~~~~~~~__--~
+# ;'( ')/ ,)( ~~~~~~~~~~
+# ' ') '( (/
+# ' ' `
+
+# This script uses a trello enterprise export which even if you have
+# an enterprise account is different from what you get by hitting the
+# export button on a board.
+
+import argparse
+import calendar
+import collections
+import errno
+import json
+import logging
+import logging.handlers
+import os
+import pprint
+import sys
+import time
+import traceback
+
+import dateutil.parser
+import yaml
+
+##### Utility #####
+
+def mkdir_p(path):
+ try:
+ os.makedirs(path)
+ except OSError as exc: # Python >2.5
+ if exc.errno == errno.EEXIST and os.path.isdir(path):
+ pass
+ else:
+ raise
+
+##### Trello's world view #####
+
+# Why doesn't trello just just unix time and why is python's date/time
+# handling such a mess?
+def parse_trello_ts_str(s):
+ d = dateutil.parser.parse(s)
+ return calendar.timegm(d.utctimetuple())
+
+
+def s_to_us(ts):
+ return ts * 1000000
+
+class TrelloDAO(object):
+
+ # Trello exports a board as a blob of json. It's a werid hybrid
+ # between 'just make a giant json blob' and 'this looks
+ # suspicously like internal represenation'
+ def __init__(self, fname):
+ with open(fname) as f:
+ self.blob = json.load(f)
+ self.uid2label = None
+ self.uid2username = None
+ self.column_id2name = None
+
+ def get_board_id(self):
+ return self.blob['id']
+
+ def get_board_name(self):
+ return self.blob['name']
+
+ def get_board_url(self):
+ return self.blob['url']
+
+ # Label definitions. Like members, an array of dicts
+ def get_labelnames(self):
+ labelnames = []
+ for label in self.blob['labels']:
+ # TODO? the label['idBoard'] might not be the current board, but
if it's unique maybe write it anyway.
+ labelnames.append('%s (%s)' % (label['name'],label['color']))
+ return sorted(labelnames)
+
+ # Returns a string for the Trello label.
+ def get_label(self, uid):
+ if self.uid2label is None:
+ self.uid2label = {}
+ for label in self.blob['labels']:
+ self.uid2label[label['id']] = ('%s (%s)' %
(label['name'],label['color']))
+ if uid in self.uid2label:
+ return self.uid2label[uid]
+ else:
+ return 'UNKNOWN_LABEL_' + uid
+
+
+ def get_usernames(self):
+ usernames = []
+ for user in self.blob['members']:
+ usernames.append(user['username'])
+ return sorted(usernames)
+
+ def get_username(self, uid):
+ if self.uid2username is None:
+ self.uid2username = {}
+ for user in self.blob['members']:
+ self.uid2username[user['id']] = user['username']
+ if uid in self.uid2username:
+ return self.uid2username[uid]
+ else:
+ return 'UNKNOWN_' + uid
+
+ def get_column_name(self, column_id):
+ if self.column_id2name is None:
+ self.column_id2name = {}
+ for t_list in self.blob['lists']:
+ self.column_id2name[t_list['id']] = t_list['name']
+ # This failed in card 108 of a simple Trello export, referencing a
+ # "Done" column that is no longer part of this board, or maybe the card
+ # was on a different board.
+ try:
+ column_name = self.column_id2name[column_id]
+ except KeyError as e:
+ log.warn('get_column_name found no name for column id' + column_id)
+ column_name = 'NOTFOUND-'+column_id
+ self.column_id2name[column_id] = column_name
+ return column_name
+
+
+ # due to cards moving and whatnot constraints like 'there should
+ # be a created record for each card' can not be satisfied
+ def figure_out_when_card_created(self, card):
+ eldest_record = parse_trello_ts_str(card.dateLastActivity)
+ has_create_record = False
+ for action in self.blob['actions']:
+ if action['type'] == 'createCard':
+ if action['data']['card']['id'] == card.card_id:
+ change_time = parse_trello_ts_str(action['date'])
+ if change_time < eldest_record:
+ eldest_record = change_time
+ has_create_record = True
+ if not has_create_record:
+ for action in self.blob['actions']:
+ if action['type'] == 'updateCard':
+ if action['data']['card']['id'] == card.card_id:
+ change_time = parse_trello_ts_str(action['date'])
+ if change_time < eldest_record:
+ eldest_record = change_time
+ return eldest_record
+
+ def figure_out_first_card_column(self, card):
+ for action in self.blob['actions']:
+ if action['type'] == 'createCard' and action['data']['card']['id']
== card.card_id:
+ if 'list' in action['data']:
+ try:
+ return
self.get_column_name(action['data']['list']['id'])
+ except KeyError as e:
+ pass
+ # column IDs can be referenced that are not defined
+ return 'UNKNOWN'
+
+
+ def guess_card_reporter(self, card, scrubber):
+ reporter = None
+ for action in self.blob['actions']:
+ if action['type'] == 'createCard':
+ if action['data']['card']['id'] == card.card_id:
+ reporter =
scrubber.get_phab_uid(self.get_username(action['idMemberCreator']))
+ # TODO: spagewmf: found a reporter, break out of the loop
+ if reporter is None and card.idMembers:
+ reporter =
scrubber.get_phab_uid(self.get_username(card.idMembers[0]))
+ return reporter if reporter else 'import-john-doe'
+
+
+ def guess_card_owner(self, card, scrubber):
+ owner = None
+ if card.idMembers:
+ owner = scrubber.get_phab_uid(self.get_username(card.idMembers[0]))
+ return owner if owner else 'import-john-doe'
+
+ def figure_out_all_subscribers(self, card, scrubber):
+ subscriber_ids = set(card.idMembers)
+ for action in self.blob['actions']:
+ if 'card' in action['data'] and action['data']['card']['id'] ==
card.card_id:
+ subscriber_ids.add(action['idMemberCreator'])
+ subscribers = map(lambda s:
scrubber.get_phab_uid(self.get_username(s)), subscriber_ids)
+ return sorted(subscribers)
+
+
+ # Note: this method is unused.
+ def get_checklist_items(self, card_id):
+ for c_list in self.blob['checklists']: # TODO Only in Enterprise
export, not available in board export
+ if c_list['idCard'] == card_id:
+ check_items = c_list['checkItems']
+ return sorted(check_items, key=lambda e: e['pos'])
+
+
+ def get_relevant_actions(self, card_id):
+ actions = []
+ for action in self.blob['actions']:
+ if 'card' in action['data'] and action['data']['card']['id'] ==
card_id:
+ if action['type'] in ['commentCard', 'updateCard']:
+ actions.append(action)
+ return sorted(actions, key=lambda a: parse_trello_ts_str(a['date']))
+
+class TrelloScrubber(object):
+
+ def __init__(self, conf_file):
+ with open(conf_file) as f:
+ self.conf = yaml.load(f)
+
+
+ def get_phab_uid(self, trello_username):
+ # trello exports can include user ids that are are not defined
+ # as members or anywhere within in the export
+ if trello_username.startswith('UNKNOWN_'):
+ junk = trello_username.split('UNKNOWN_')[1]
+ if junk in self.conf['uid-cheatsheet']:
+ return self.conf['uid-cheatsheet'][junk]
+ else:
+ return 'FAILED-'+trello_username # TODO log error
+ else:
+ if trello_username in self.conf['uid-map']:
+ return self.conf['uid-map'][trello_username]
+ else:
+ return 'FAILED-'+trello_username # TODO log error
+
+
+class TrelloCard(object):
+
+ # [u'attachments', Never used on board
+ # OLD u'labels', Rarely used color stuff
+ # u'idLabels', array of ids of labels for card
+ # u'pos', Physical position, ridiculous LOE to port so ignoring
+ # u'manualCoverAttachment', Duno but it's always false
+ # u'id', unique id
+ # u'badges', something about fogbugz integration?
+ # u'idBoard', parent board id
+ # u'idShort', "short" and thus not unique uid
+ # u'due', rarely used duedate
+ # u'shortUrl', pre-shorted url
+ # u'closed', boolean for if it's archived
+ # u'subscribed', boolean, no idea what it means
+ # u'email', no idea, always none
+ # u'dateLastActivity', 2014-04-22T14:09:49.917Z
+ # u'idList', it's the ID of the current column of the card
+ # u'idMembersVoted', never used
+ # u'idMembers', # Whose face shows up next to it
+ # u'checkItemStates', Something to do with checklists?
+ # u'desc', # description field
+ # u'descData', Almost always None, probably not important
+ # u'name', # title
+ # u'shortLink', # some short linky thing
+ # u'idAttachmentCover', Always None
+ # u'url', # link back to trello
+ # u'idChecklists'] # a bunch of ids for checklists?
+
+ def __init__(self, blob, scrubber):
+ self.scrubber = scrubber
+ self.card_id = blob['id']
+ if "labels" in blob:
+ self.labels = blob['labels']
+ else:
+ self.labels = []
+ self.idLabels = blob['idLabels']
+ self.idBoard = blob['idBoard']
+ self.due = blob['due']
+ self.closed = blob['closed']
+ self.dateLastActivity = blob['dateLastActivity']
+ self.idList = blob['idList']
+ self.idMembers = blob['idMembers']
+ self.desc = blob['desc']
+ self.name = blob['name']
+ self.url = blob['url']
+ # Board export has shortUrl and shortLink, but Enterprise export
doesn't - crazy.
+ if 'shortUrl' in blob:
+ self.shortUrl = blob['shortUrl']
+ else:
+ # Trim the end off e.g.
https://trello.com/c/mpFNXCXp/464-long-title-here
+ self.shortUrl = self.url[0:self.url.rfind('/')]
+ if 'shortLink' in blob:
+ self.shortLink = blob['shortLink']
+ else:
+ # Again, the last piece.
+ self.shortLink = self.shortUrl[self.shortUrl.rfind('/')+1:]
+
+ self.idChecklists = blob['idChecklists']
+ if 'checklists' in blob:
+ self.checklists = blob['checklists']
+ else:
+ # TODO Only in Enterprise export, not available in board export
+ self.checklists = None
+
+ self.checklist_strs = []
+ self.change_history = []
+ self.column = None
+ self.final_comment_fields = {}
+
+
+ def figure_stuff_out(self, dao):
+ self.board_name = dao.get_board_name()
+ self.create_time_s = dao.figure_out_when_card_created(self)
+ self.column = dao.figure_out_first_card_column(self)
+ self.reporter = dao.guess_card_reporter(self, self.scrubber)
+ self.owner = dao.guess_card_owner(self, self.scrubber)
+ self.subscribers = dao.figure_out_all_subscribers(self, self.scrubber)
+ self.build_checklist_comment(dao)
+
+ for action in dao.get_relevant_actions(self.card_id):
+ self.handle_change(action, dao)
+
+ self.column = dao.get_column_name(self.idList)
+ # labels is text, idLabels is UIDs, append them to the end.
+ label_comment = ''
+ if self.labels:
+ label_comment = sorted(map(lambda k: k['color'], self.labels))
+ if self.idLabels:
+ for label_id in self.idLabels:
+ label_comment = ' ' + dao.get_label(label_id)
+ self.final_comment_fields['labels'] = label_comment
+
+ if self.due:
+ self.final_comment_fields['due'] = self.due
+
+ def build_checklist_comment(self, dao):
+ if not self.idChecklists:
+ return None
+ s = ''
+ if self.checklists is None:
+ log.warning('Failed to find checklists for card %s' % self.card_id)
+ return
+ for checklist in self.checklists:
+ s += 'Checklist: \n'
+ for item in checklist['checkItems']:
+ s+= ' * [%s] %s \n' % ('x' if item['state'] == 'complete' else
'', item['name'])
+ s += '\n'
+ change = {'type': 'comment', 'author': self.owner,
+ 'comment': s,
+ 'change_time_us':
s_to_us(parse_trello_ts_str(self.dateLastActivity))}
+ # SPage: cburroughs turns the checklist into a comment:
+ # self.change_history.append(change)
+ # SPage: instead, add to checklists string
+ self.checklist_strs.append(s)
+
+ def make_final_comment(self):
+ s = 'Trello Board: %s `%s` \n' % (self.board_name, self.idBoard)
+ s += "Trello Card: `%s` %s \n" % (self.card_id, self.url)
+ if len(self.final_comment_fields) > 0:
+ s += '\nExtra Info:\n'
+ for key in sorted(self.final_comment_fields):
+ s += ' * `%s`: `%s`\n' % (str(key),
unicode(self.final_comment_fields[key]))
+ return {'comment': s, 'ts_us': None}
+
+ def handle_change(self, j_change, dao):
+ if j_change['type'] == 'updateCard' and 'listBefore' in
j_change['data']:
+ change = {'type': 'custom-field',
+ 'author':
self.scrubber.get_phab_uid(dao.get_username(j_change['idMemberCreator'])),
+ 'key': 'std:maniphest:' + 'addthis:import-trello-column',
+ 'val':
dao.get_column_name(j_change['data']['listAfter']['id']),
+ 'change_time_us':
s_to_us(parse_trello_ts_str(j_change['date']))}
+ self.change_history.append(change)
+ # XXX This doesn't result in the card having the right column,
+ # elsewhere it's set to the column from the card's idList.
+ self.column =
dao.get_column_name(j_change['data']['listBefore']['id'])
+ elif j_change['type'] == 'commentCard':
+ change = {'type': 'comment',
+ 'author':
self.scrubber.get_phab_uid(dao.get_username(j_change['idMemberCreator'])),
+ 'comment': j_change['data']['text'],
+ 'change_time_us':
s_to_us(parse_trello_ts_str(j_change['date']))}
+ self.change_history.append(change)
+ elif j_change['type'] == 'updateCard' and 'closed' in
j_change['data']['card']:
+ phab_status = 'resolved' if j_change['data']['card']['closed']
else 'open'
+ change = {'type': 'status', 'author': self.owner,
+ 'status': phab_status,
+ 'change_time_us':
s_to_us(parse_trello_ts_str(j_change['date']))}
+ self.change_history.append(change)
+ self.closed = not j_change['data']['card']['closed']
+ elif j_change['type'] == 'updateCard' and 'old' in j_change['data']
and 'name' in j_change['data']['old']:
+ comment = 'Title change:\n * old: %s \n * new: %s' %
(j_change['data']['old']['name'], j_change['data']['card']['name'])
+ change = {'type': 'comment',
+ 'author':
self.scrubber.get_phab_uid(dao.get_username(j_change['idMemberCreator'])),
+ 'comment': comment,
+ 'change_time_us':
s_to_us(parse_trello_ts_str(j_change['date']))}
+ self.change_history.append(change)
+ elif j_change['type'] == 'updateCard' and 'old' in j_change['data']
and 'desc' in j_change['data']['old']:
+ comment = 'Desc change\n\n=== Old === \n\n %s \n\n=== New === \n\n
%s' % (j_change['data']['old']['desc'], j_change['data']['card']['desc'])
+ change = {'type': 'comment',
+ 'author':
self.scrubber.get_phab_uid(dao.get_username(j_change['idMemberCreator'])),
+ 'comment': comment,
+ 'change_time_us':
s_to_us(parse_trello_ts_str(j_change['date']))}
+ self.change_history.append(change)
+ elif j_change['type'] == 'updateCard' and 'old' in j_change['data']
and 'due' in j_change['data']['old']:
+ pass # Will just use the final due date
+ elif j_change['type'] == 'updateCard' and 'old' in j_change['data']
and 'pos' in j_change['data']['old']:
+ pass # just moving cards around in a list
+ else:
+ print j_change
+ log.warn('Unknown change condition type:%s id:%s for card %s' %
(j_change['type'], j_change['id'], self.card_id))
+
+ def to_transform_dict(self, import_project, task_id):
+ transform = {}
+ transform['must-preserve-id'] = False if task_id is None else True
+ transform['import-project'] = import_project
+ transform['base'] = {
+ 'ticket-id': self.card_id if task_id is None else task_id,
+ 'create-time-us': s_to_us(self.create_time_s),
+ 'owner': self.owner,
+ 'reporter': self.reporter,
+ 'summary': self.name,
+ 'description': self.desc,
+ 'priority': 50,
+ }
+ transform['init-custom'] = {}
+ transform['init-custom']['std:maniphest:' +
'addthis:import-trello-column'] = self.column
+ transform['changes'] = sorted(self.change_history, key=lambda d:
d['change_time_us'])
+ transform['final-comment'] = self.make_final_comment()
+ transform['final-subscribers'] = self.subscribers
+
+ return transform
+
+
+
+##### cmds #####
+
+def cmd_foo(args):
+ pass
+
+
+def cmd_print_labelnames(args):
+ board = TrelloDAO(args.trello_file)
+ pprint.pprint(board.get_labelnames())
+
+ pass
+
+def cmd_print_users(args):
+ board = TrelloDAO(args.trello_file)
+ pprint.pprint(board.get_usernames())
+
+ pass
+
+def cmd_print_user_map_test(args):
+ board = TrelloDAO(args.trello_file)
+ scrubber = TrelloScrubber('conf/trello-scrub.yaml')
+ for user in board.blob['members']:
+ print '%s <--> %s <--> %s' % (user['id'],
board.get_username(user['id']),
scrubber.get_phab_uid(board.get_username(user['id'])))
+
+def cmd_dump_cards(args):
+ mkdir_p('out/tickets')
+ board = TrelloDAO(args.trello_file)
+ scrubber = TrelloScrubber('conf/trello-scrub.yaml')
+ task_id = args.start_id
+ for j_card in board.blob['cards']:
+ card = TrelloCard(j_card, scrubber)
+ fname = os.path.join(args.dump_dir, card.card_id + '.json')
+ card.figure_stuff_out(board)
+ with open(fname, 'w') as f:
+ d = card.to_transform_dict(args.phab_project, task_id)
+ f.write(json.dumps(d, sort_keys=True,
+ indent=4, separators=(',', ': ')))
+ if task_id is not None:
+ task_id += 1
+
+##### main and friends #####
+
+def parse_args(argv):
+ def db_cmd(sub_p, cmd_name, cmd_help):
+ cmd_p = sub_p.add_parser(cmd_name, help=cmd_help)
+ cmd_p.add_argument('--log',
+ action='store', dest='log', default='stdout',
choices=['stdout', 'syslog', 'both'],
+ help='log to stdout and/or syslog')
+ cmd_p.add_argument('--log-level',
+ action='store', dest='log_level', default='WARNING',
+ choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO',
'DEBUG', 'NOTSET'],
+ help='log to stdout and/or syslog')
+ cmd_p.add_argument('--log-facility',
+ action='store', dest='log_facility', default='user',
+ help='facility to use when using syslog')
+ cmd_p.add_argument('--trello-file',
+ action='store', dest='trello_file', required=True,
+ help='trello exported json file')
+
+ return cmd_p
+
+ parser = argparse.ArgumentParser(description="")
+ sub_p = parser.add_subparsers(dest='cmd')
+
+ foo_p = db_cmd(sub_p, 'foo', '')
+ foo_p.set_defaults(func=cmd_foo)
+
+ print_labelnames_p = db_cmd(sub_p, 'print-labelnames', '')
+ print_labelnames_p.set_defaults(func=cmd_print_labelnames)
+
+ print_users_p = db_cmd(sub_p, 'print-users', '')
+ print_users_p.set_defaults(func=cmd_print_users)
+
+ print_user_map_test_p = db_cmd(sub_p, 'print-user-map-test', '')
+ print_user_map_test_p.set_defaults(func=cmd_print_user_map_test)
+
+ dump_cards_p = db_cmd(sub_p, 'dump-cards', '')
+ dump_cards_p.set_defaults(func=cmd_dump_cards)
+ dump_cards_p.add_argument('--dump-dir',
+ action='store', dest='dump_dir',
default='out/tickets')
+ dump_cards_p.add_argument('--phab-project',
+ action='store', dest='phab_project',
required=True)
+ dump_cards_p.add_argument('--start-id', type=int,
+ action='store', dest='start_id')
+ dump_cards_p.set_defaults(func=cmd_dump_cards)
+
+ args = parser.parse_args(argv)
+ return args
+
+
+def setup_logging(handlers, facility, level):
+ global log
+
+ log = logging.getLogger('export-trac')
+ formatter = logging.Formatter(' | '.join(['%(asctime)s', '%(name)s',
'%(levelname)s', '%(message)s']))
+ if handlers in ['syslog', 'both']:
+ sh = logging.handlers.SysLogHandler(address='/dev/log',
facility=facility)
+ sh.setFormatter(formatter)
+ log.addHandler(sh)
+ if handlers in ['stdout', 'both']:
+ ch = logging.StreamHandler()
+ ch.setFormatter(formatter)
+ log.addHandler(ch)
+ lmap = {
+ 'CRITICAL': logging.CRITICAL,
+ 'ERROR': logging.ERROR,
+ 'WARNING': logging.WARNING,
+ 'INFO': logging.INFO,
+ 'DEBUG': logging.DEBUG,
+ 'NOTSET': logging.NOTSET
+ }
+ log.setLevel(lmap[level])
+
+
+def main(argv):
+ args = parse_args(argv)
+ try:
+ setup_logging(args.log, args.log_facility, args.log_level)
+ except Exception as e:
+ print >> sys.stderr, 'Failed to setup logging'
+ traceback.print_exc()
+ raise e
+
+ args.func(args)
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
+
diff --git a/trello_create.py b/trello_create.py
index 5a4716e..2b7fe04 100644
--- a/trello_create.py
+++ b/trello_create.py
@@ -1,96 +1,298 @@
#
# Basic processor of Trello board export as JSON file.
# Documentation at
https://www.mediawiki.org/wiki/Phabricator/Trello_to_Phabricator
+# TODO 2015-01-25: log and write out the mapping card_id -> trello Tnnnn
+# TODO 2015-01-25: explicitly load data/phab-01 or phabricator.yaml file
instead of symlink
+# TODO 2015-01-25: figure out PROJ_phid, is it bust?
+# TODO 2015-01-25: imported https://phabricator.wikimedia.org/T87530 has " *
`labels`: ``", is this confusion or a bug with empty values?
+# TODO 2015-01-25: test with an actual label
+# TODO 2015-01-25: test with Ccs and Owners.
+
+
import argparse
import json
import sys
+import yaml
+
+# cburroughs' work
+from export_trello import TrelloCard, TrelloDAO, TrelloScrubber, setup_logging
from phabricator import Phabricator
+from wmfphablib import Phab as phabmacros # Cleaner (?), see
wmfphablib/phabapi.py
from wmfphablib import config
+from wmfphablib import log
+from wmfphablib import vlog
from wmfphablib import errorlog as elog
+class TrelloImporter:
+ # Map from Trello username to phabricator user.
+ userMapPhab01 = {
+ 'gtisza': 'Tgr',
+ "legoktm": "legoktm",
+ "matthewflaschen": "mattflaschen",
+ "pauginer": None,
+ "spage1": "spage",
+ }
+ userMapPhabWMF = {
+ # from mingleterminator.py
+ 'fflorin': 'Fabrice_Florin',
+ 'gdubuc': 'Gilles',
+ 'mholmquist': 'MarkTraceur',
+ 'gtisza': 'Tgr',
+ 'pginer': 'Pginer-WMF',
+ # From ack username
trabulous_dir/flow-current-iteration_lOh4XCy7_fm.json \
+ # | perl -pe 's/^\s+//' | sort | uniq
+ # Collaboration-Team members: Eloquence, DannyH,
Pginer-WMF, Spage, Quiddity, Mattflaschen, matthiasmullie, EBernhardson
+ "alexandermonk": None,
+ "antoinemusso": None,
+ "benmq": None,
+ "bsitu": None,
+ "dannyhorn1": "DannyH",
+ "erikbernhardson": "EBernhardson",
+ "jaredmzimmerman": None,
+ "jonrobson1": None,
+ "kaityhammerstein": None,
+ "legoktm": None,
+ "matthewflaschen": "mattflaschen",
+ "matthiasmullie": "matthiasmullie",
+ "maygalloway": None,
+ "moizsyed_": None,
+ "oliverkeyes": None,
+ "pauginer": "Pginer-WMF",
+ "quiddity1": "Quiddity",
+ "shahyarghobadpour": None,
+ "spage1": "Spage",
+ "wctaiwan": None,
+ "werdnum": None,
+ }
-def sanity_check(trello_json, data_file):
- if not 'cards' in trello_json:
- elog('No cards in input file %s' % (data_file))
- sys.exit(1)
+ def __init__(self, jsonName, args):
+ self.jsonName = jsonName
+ self.args = args # FIXME unpack all the args into member
variables?
+ self.verbose = args.verbose
+ if config.phab_host.find('phab-01') != -1:
+ self.host = 'phab-01'
+ elif config.phab_host.find('phabricator.wikimedia.org') != -1:
+ self.host = 'phabricator'
+ else:
+ self.json_error('Unrecognized host %s in config' %
(config.phab_host))
+ sys.exit(3)
+ self.board = TrelloDAO(self.jsonName)
+ trelloBoardName = self.board.get_board_name();
+ vlog('Trello board = %s' % (trelloBoardName))
-def get_board(trello_json, data_file):
- if 'name' in trello_json:
- return trello_json["name"]
- else:
- elog('No "name" for Trello board in input file %s' %
(data_file))
- sys.exit(2)
-
-# Determine projectPHID for the project name.
-def get_projectPHID(trelloProjectName):
- elog('FAKING IT WITH Flow (test) on phab-01.wmflabs.org!')
- # not implemented yet, need to run project.query see
- #
http://stackoverflow.com/questions/25753749/how-do-you-find-the-phid-of-a-phabricator-object
- return 'PHID-PROJ-ofxg2d73cpefxqnworfs'
-
-def create(card, dump_conduit):
- title = card["name"]
- # TODO: if Trello board is using scrum for Trello browser extension,
- # could extract story points /\s+\((\d+)\)' from card title to feed
into Sprint extension.
- desc = card["desc"]
- desc_tail = "\n--------------------------"
- desc_tail += "\n**Trello card**: [[ %s | %s ]]" % (card["url"],
card["shortLink"])
- full_description = desc + '\n' + desc_tail
- if dump_conduit:
- # This prints fields for maniphest.createtask
- print '"%s"\n"%s"\n\n' % (title.encode('unicode-escape'),
full_description.encode('unicode-escape'))
- else:
- elog('Have not actually implemented teh API calls yet :(')
+ self.projectPHID = self.get_projectPHID(self.args.phab_project);
+ vlog('Phabricator project PHID = %s' % (self.projectPHID))
-def process_cards(trello_json, dump_conduit):
- for card in trello_json["cards"]:
- # Skip archived cards ("closed" seems to correspond)
- if card["closed"]:
- continue
+ def connect_to_phab(self):
+ self.phab = Phabricator(config.phab_user,
+ config.phab_cert,
+ config.phab_host)
- # TODO: skip cards that are bugs
- # TODO: skip cards that already exist in Phab.
- create(card, dump_conduit)
+ self.phab.update_interfaces()
+ self.phabm = phabmacros('', '', '')
+ self.phabm.con = self.phab
+ # DEBUG to verify the API connection worked: print
phab.user.whoami()
+ vlog('Looks like we connected to the phab API \o/')
+
+
+ def sanity_check(self):
+ if not 'cards' in self.trelloJson:
+ self.json_error('No cards in input file')
+ sys.exit(1)
+
+ def testify(self, str):
+ if self.args.dry_run:
+ str = "TEST TEST run " + str
+
+ return str
+
+ def json_error(self, str):
+ elog('ERROR: %s in input file %s' % (str, self.jsonName))
+
+ # Determine projectPHID for the project name.
+ def get_projectPHID(self, trelloProjectName):
+ elog('FAKING IT WITH Flow (test) on phab-01.wmflabs.org!')
+ # not implemented yet, need to run project.query see
+ #
http://stackoverflow.com/questions/25753749/how-do-you-find-the-phid-of-a-phabricator-object
+ return 'PHID-PROJ-ofxg2d73cpefxqnworfs'
+
+ def get_trelloUserPHIDMap(self):
+ fname = 'data/trello_PHIDs_' + self.host + '.yaml'
+ with open(fname) as f:
+ yam = yaml.load(f)
+ if yam == None or len(yam) == 0:
+ elog('Warning: did not load trello->PHID mappings from
%s, did you run trello_makePHIDs.py?' % (fname))
+ else:
+ vlog('Looks like we loaded %d trello->PHID mappings
from %s' % (len(yam), fname))
+ return yam
+
+
+ # not implemented yet, need to run project.query see
+ #
http://stackoverflow.com/questions/25753749/how-do-you-find-the-phid-of-a-phabricator-object
+ return 'PHID-PROJ-ofxg2d73cpefxqnworfs'
+
+ # This is the workhorse
+ def createTask(self, card):
+ # Default some keys we always pass to createtask.
+ taskinfo = {
+ 'ownerPHID' : None,
+ 'ccPHIDs' : [],
+ 'projectPHIDs' : [self.projectPHID],
+ }
+
+ taskinfo["title"] = self.testify(card.name)
+
+ # TODO: if Trello board is using scrum for Trello browser
extension,
+ # could extract story points /\s+\((\d+)\)' from card title to
feed into Sprint extension.
+
+ # TODO: process attachments
+ # TODO: taskinfo["assignee"]
+ desc = self.testify(card.desc)
+
+ if card.checklist_strs:
+ desc += '\n' + '\n\n'.join(card.checklist_strs)
+ desc_tail = "\n--------------------------"
+ desc_tail += "\n**Trello card**: [[ %s | %s ]]" % (card.url,
card.shortLink)
+ desc_tail += "\nTrello column: " + card.column
+ if len(card.final_comment_fields) > 0:
+ s = ''
+ s += '\n'
+ for key in sorted(card.final_comment_fields):
+ s += ' * `%s`: `%s`\n' % (str(key),
unicode(card.final_comment_fields[key]))
+ desc_tail += s
+ #
+ # TODO: could add labels, label color, main attachment, etc.,
etc. to desc_tail.
+ taskinfo["description"] = desc + '\n' + desc_tail
+ # TODO: chasemp: what priority?
+ taskinfo['priority'] = 50
+ # TODO: chasemp: can I put "Trello lOh4XCy7" in "Reference"
field?
+
+ # Take the set of members
+ idMembers = card.idMembers
+ # Get the Trello username for the idMember
+ # memberNames = [ TrelloDAO.get_username(id) for id in
idMembers if TrelloDAO.get_username(id)]
+
+ if not 'FAILED' in card.owner:
+ taskinfo['ownerPHID'] = card.owner
+ taskinfo['ccPHIDs'] = [u for u in card.subscribers if not
'FAILED' in u]
+
+ # TODO: Add any other members with a PHID to the ccPHIDs
+ # TODO: Note remaining Trello members in desc_tail
+
+ vlog("card %s has members %s" % (card.shortLink, idMembers) )
+
+ # TODO: bugzilla_create.py and wmfphablib/phabapi.py use
axuiliary for
+ # BZ ref, but it doesn't work for Trello ref?
+ taskinfo["auxiliary"] =
{"std:maniphest:external_reference":"Trello %s" % (card.shortLink)}
+
+ if self.args.conduit:
+ # This prints fields for maniphest.createtask
+ print '"%s"\n"%s"\n\n' %
(taskinfo["title"].encode('unicode-escape'),
+
taskinfo["description"].encode('unicode-escape'))
+ else:
+ if self.args.dry_run:
+ log("dry-run to create a task for Trello card
%s ('%s')" %
+ (card.shortLink, taskinfo["title"]))
+ else:
+ ticket = self.phab.maniphest.createtask(
+
title = taskinfo['title'],
+
description = taskinfo['description'],
+
projectPHIDs = taskinfo['projectPHIDs'],
+
ownerPHID = taskinfo['ownerPHID'],
+
ccPHIDs = taskinfo['ccPHIDs'],
+
auxiliary = taskinfo['auxiliary']
+ )
+
+ log("Created task: T%s (%s)" % (ticket['id'],
ticket['phid']))
+
+
+ # Here bugzilla_create goes on to log actual creating
user and view/edit policy,
+ # then set_task_ctime to creation_time.
+
+ # Should I add comments to the card here,
+ # or a separate step that goes through action in
self.board.blob["actions"]
+ # handling type="commentCard"?
+
+
+ # Here are the types of objects in the "actions" array.
+ # 20 "type": "addAttachmentToCard",
+ # 9 "type": "addChecklistToCard",
+ # 2 "type": "addMemberToBoard",
+ # 38 "type": "addMemberToCard",
+ # 69 "type": "commentCard",
+ # 1 "type": "copyCard",
+ # 25 "type": "createCard",
+ # 3 "type": "createList",
+ # 6 "type": "deleteAttachmentFromCard",
+ # 29 "type": "moveCardFromBoard",
+ # 18 "type": "moveCardToBoard",
+ # 4 "type": "moveListFromBoard",
+ # 2 "type": "moveListToBoard",
+ # 3 "type": "removeChecklistFromCard",
+ # 14 "type": "removeMemberFromCard",
+ # 3 "type": "updateBoard",
+ # 698 "type": "updateCard",
+ # 48 "type": "updateCheckItemStateOnCard",
+ # 8 "type": "updateList",
+ # def getCardCreationMeta(self, cardId):
+ # Look around in JSON for ["actions"] array for member with
type:"createCard"
+ # with ["card"]["id"] = cardId
+ # and use the siblings ["date"] and ["memberCreator"]["id"]
+
+ # def getCardComments(self, cardId):
+ # Look around in JSON ["actions"] for member with
type:""commentCard"
+ # with ["card"]["id"] = cardId
+ # and use the siblings ["date"] and ["memberCreator"]["id"]
+
+ def process_cards(self):
+ self.connect_to_phab()
+
+ # Load PHID Map
+ self.trelloUserPHIDMap = self.get_trelloUserPHIDMap()
+
+ scrubber = TrelloScrubber('conf/trello-scrub.yaml')
+ for j_card in self.board.blob["cards"]:
+ card = TrelloCard(j_card, scrubber)
+ card.figure_stuff_out(self.board)
+ if self.args.column and not card.column ==
self.args.column:
+ continue
+ # Skip archived cards ("closed" seems to correspond?)
+ # But I think archive all cards in column doesn't set
this.
+ if card.closed:
+ continue
+
+ # TODO: skip cards that are bugs
+ # TODO: skip cards that already exist in Phab.
+ self.createTask(card)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", action="store_true",
- help="increase output verbosity")
- parser.add_argument("-j", "--json",
- help="Trello board JSON export file" )
- parser.add_argument("-d", "--dump_conduit", action="store_true",
- help="dump lines for maniphest.createtask")
- parser.add_argument("-p", "--project",
- help="Phabricator project for imported tasks (NOT
IMPLEMENTED YET")
+ help="increase output verbosity")
+ parser.add_argument("-vv", "--verbose-logging", action="store_true",
+ help="wmfphablib verbose logging")
+ parser.add_argument("-j", "--json", required=True,
+ help="Trello board JSON export file" )
+ parser.add_argument("-c", "--conduit", action="store_true",
+ help="print out lines suitable for conduit
maniphest.createtask")
+ parser.add_argument("-d", "--dry-run", action="store_true",
+ help="don't actually add anything to Phabricator")
+ parser.add_argument("-l", "--column",
+ help="Name the one column to import")
+ parser.add_argument("-t", "--test-run", action="store_true",
+ help="prefix titles and description with 'TEST
trabuloust TEST' disclaimers")
+ parser.add_argument("-p", "--phab_project",
+ help="Phabricator project for imported tasks (NOT
IMPLEMENTED YET")
args = parser.parse_args()
- with open(args.json) as data_file:
- trello_json = json.load(data_file)
- sanity_check(trello_json, data_file);
-
- # From python-phabricator/README.rst
- phab = Phabricator(config.phab_user,
- config.phab_cert,
- config.phab_host)
-
- phab.update_interfaces()
- # DEBUG to verify the API connection worked: print phab.user.whoami()
- if args.verbose:
- elog('Looks like we connected to the phab API \o/')
-
- trelloBoard = get_board(trello_json, args.project);
- if args.verbose:
- elog('Trello board = %s' % (trelloBoard))
-
- projectPHID = get_projectPHID(args.project);
- if args.verbose:
- elog('Phabricator project PHID = %s' % (projectPHID))
-
- process_cards(trello_json, args.dump_conduit);
+ # As used in export_trello.py functions.
+ setup_logging('stdout', 'user', 'WARNING')
+ trell = TrelloImporter(args.json, args)
+ trell.process_cards()
if __name__ == '__main__':
- main()
+ main()
--
To view, visit https://gerrit.wikimedia.org/r/188287
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I70897def2a781191fe06213cf157284f4e29b1a6
Gerrit-PatchSet: 1
Gerrit-Project: phabricator/tools
Gerrit-Branch: master
Gerrit-Owner: Spage <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits