~petersanchez/trello2redmine

53b0dc8fe9fb26686ef6041ca8e8469673e9684a — Peter Sanchez 2 years ago 4df925d
Many improvements to include more mapping options, attachments, checklists, assignees, watchers, etc.
1 files changed, 576 insertions(+), 163 deletions(-)

M trello2redmine.py
M trello2redmine.py => trello2redmine.py +576 -163
@@ 1,248 1,661 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

import trello2redmine_config as cfg

import sys
import json
import os
import re
import sys

import requests

import trello2redmine_config as cfg

try:
    from urlparse import urlparse
except ImportError:  # python 3
    from urllib.parse import urlparse


if len(sys.argv) > 3 or (len(sys.argv) == 2 and sys.argv[1] == '-h'):
	print('Usage: {0} [<options>])'.format(sys.argv[0]))
	print('See %s for configuration.' % cfg.__file__)
	print('Options:')
	print(' -h    Displays this information.')
	print(' -c    Commits the import. Otherwise does a dry run (prints resulting JSON to screen instead of sending it to Redmine).')
	sys.exit(0)
if len(sys.argv) > 3 or (len(sys.argv) == 2 and sys.argv[1] == "-h"):
    print("Usage: {0} [<options>])".format(sys.argv[0]))
    print("See %s for configuration." % cfg.__file__)
    print("Options:")
    print(" -h    Displays this information.")
    print(
        " -c    Commits the import. Otherwise does a dry run (prints resulting JSON to screen instead of sending it to Redmine)."
    )
    sys.exit(0)

# If a dry run, JSON is printed instead of submitted to Redmine.
#dry_run = len(sys.argv) < 2 or sys.argv[2] != '-c'
dry_run = False
dry_run = len(sys.argv) == 2 and sys.argv[1] == "-c"
dry_run = True
if dry_run:
	print('Making a dry run! Re-run with -c to commit the import into Redmine.')
    print("Making a dry run! Re-run with -c to commit the import into Redmine.")
else:
	print('Making a commit!')
    print("Making a commit!")

url = urlparse(cfg.trello_board_link)
if not url.netloc.endswith('trello.com'):
	print('URL does not seem to be a Trello board link. Are you sure this is correct?')
	sys.exit(1)
if not url.netloc.endswith("trello.com"):
    print("URL does not seem to be a Trello board link. Are you sure this is correct?")
    sys.exit(1)

# ============================================================================
# Trello JSON processing starts here.
# ============================================================================

print('Downloading board JSON from Trello...')
print("Downloading board JSON from Trello...")
sys.stdout.flush()

board = json.loads(open(sys.argv[1]).read())
#board = requests.get(cfg.trello_board_link + '.json').json()
# board = requests.get(cfg.trello_board_link + '.json').json()

print('Processing board JSON...')
print("Processing board JSON...")
sys.stdout.flush()

no_attachment_list = []
list_status_dict = {}
orig_lists_dict = {}
lists_dict = {}
for l in board["lists"]:
	name = l["name"]
	if name in cfg.list_map:
		name = cfg.list_map[name]
	lists_dict[l["id"]] = name
	orig_lists_dict[l["id"]] = l["name"]
	
    name = l["name"]
    if name in cfg.list_map:
        name = cfg.list_map[name]
    lists_dict[l["id"]] = name
    orig_lists_dict[l["id"]] = l["name"]
    list_status_dict[l["id"]] = l["closed"]

    if l["name"] in cfg.no_file_import:
        no_attachment_list.append(l["id"])

checklists_dict = {}
for c in board["checklists"]:
	checklists_dict[c["id"]] = c["checkItems"]
    checklists_dict[c["id"]] = c["checkItems"]

members_dict = {}
for m in board["members"]:
	name = m["fullName"]
	if name in cfg.username_map:
		name = cfg.username_map[name]
	members_dict[m["id"]] = name
    name = m["fullName"]
    if name in cfg.username_map:
        name = cfg.username_map[name]
    members_dict[m["id"]] = name

labels_dict = {}
for l in board["labels"]:
	label = l["name"]
	if label in cfg.label_map:
		label = cfg.label_map[label]
	labels_dict[l["id"]] = label
    label = l["name"]
    if label in cfg.label_map:
        label = cfg.label_map[label]
    labels_dict[l["id"]] = label

comments_dict = {}
for a in board["actions"]:
	if a["type"] == "commentCard":
		card_id = a["data"]["card"]["id"]
		author_id = a["idMemberCreator"]
		comment_list = comments_dict[card_id] if card_id in comments_dict else []
		comment_list.append({
			"author":	members_dict[author_id] if author_id in members_dict else '',
			"text":		a["data"]["text"],
		})
		comments_dict[card_id] = comment_list

#print('Members:\n' + str(members_dict))
#print('Lists:\n' + str(lists_dict))
#print('Checklists:\n' + str(checklists_dict))
#print('Comments:\n' + str(comments_dict))
    if a["type"] == "commentCard":
        card_id = a["data"]["card"]["id"]
        author_id = a["idMemberCreator"]
        comment_list = comments_dict[card_id] if card_id in comments_dict else []
        comment_list.append(
            {
                "author": members_dict[author_id] if author_id in members_dict else "",
                "text": a["data"]["text"],
            }
        )
        comments_dict[card_id] = comment_list

# print('Members:\n' + str(members_dict))
# print('Lists:\n' + str(lists_dict))
# print('Checklists:\n' + str(checklists_dict))
# print('Comments:\n' + str(comments_dict))

# ============================================================================
# Redmine configuration processing starts here.
# ============================================================================

print('Querying Redmine configuration...')
print("Querying Redmine configuration...")
sys.stdout.flush()

redmine_projects = requests.get(cfg.redmine_root_url + '/projects.json?limit=200', verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key}).json()
fname = f"json/{cfg.redmine_project_identifier}-projects.json"
if not os.path.exists(fname):
    redmine_projects = requests.get(
        cfg.redmine_root_url + "/projects.json?limit=200",
        verify=cfg.redmine_verify_certificates,
        headers={"X-Redmine-API-Key": cfg.redmine_api_key},
    ).json()
    with open(fname, "w") as fp:
        json.dump(redmine_projects, fp)
else:
    redmine_projects = json.load(open(fname))

redmine_project_id = -1
for rp in redmine_projects["projects"]:
	if rp["identifier"] == cfg.redmine_project_identifier:
		redmine_project_id = rp["id"]
		break
    if rp["identifier"] == cfg.redmine_project_identifier:
        redmine_project_id = rp["id"]
        break

if redmine_project_id < 0:
	print('Project with identifier {0} does not exist in Redmine!\n\n{1}'.format(cfg.redmine_project_identifier, str(redmine_users_dict)))
	sys.exit(1)
#print('Redmine project ID: {0}'.format(redmine_project_id))
    print(
        "Project with identifier {0} does not exist in Redmine!\n\n".format(
            cfg.redmine_project_identifier
        )
    )
    sys.exit(1)
# print('Redmine project ID: {0}'.format(redmine_project_id))

fname = f"json/{cfg.redmine_project_identifier}-users.json"
if not os.path.exists(fname):
    redmine_users = requests.get(
        cfg.redmine_root_url + "/users.json?limit=200",
        verify=cfg.redmine_verify_certificates,
        headers={"X-Redmine-API-Key": cfg.redmine_api_key},
    ).json()
    with open(fname, "w") as fp:
        json.dump(redmine_users, fp)
else:
    redmine_users = json.load(open(fname))

# offset 2676 ensures 'Oliver Kurz' shows up
redmine_users = requests.get(cfg.redmine_root_url + '/users.json?offset=2676', verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key}).json()
redmine_users_dict = {}
for ru in redmine_users["users"]:
	fullname = u'{0} {1}'.format(ru["firstname"], ru["lastname"])
	redmine_users_dict[fullname] = ru["id"]

print('Redmine users:\n' + str(redmine_users_dict))

if not cfg.redmine_default_user in redmine_users_dict:
	print('Default user does not exist in Redmine!\n\n{0}'.format(str(redmine_users_dict)))
	sys.exit(1)
    fullname = "{0} {1}".format(ru["firstname"], ru["lastname"])
    redmine_users_dict[fullname] = ru["id"]

print("Redmine users:\n" + str(redmine_users_dict))

if cfg.redmine_default_user not in redmine_users_dict:
    print(
        "Default user does not exist in Redmine!\n\n{0}".format(str(redmine_users_dict))
    )
    sys.exit(1)

fname = f"json/{cfg.redmine_project_identifier}-priorities.json"
if not os.path.exists(fname):
    redmine_priorities = requests.get(
        cfg.redmine_root_url + "/enumerations/issue_priorities.json",
        verify=cfg.redmine_verify_certificates,
        headers={"X-Redmine-API-Key": cfg.redmine_api_key},
    ).json()
    with open(fname, "w") as fp:
        json.dump(redmine_priorities, fp)
else:
    redmine_priorities = json.load(open(fname))

redmine_priorities = requests.get(cfg.redmine_root_url + '/enumerations/issue_priorities.json', verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key}).json()
redmine_priorities_dict = {}
redmine_default_priority = "Normal"
for rp in redmine_priorities["issue_priorities"]:
	redmine_priorities_dict[rp["name"]] = rp["id"]
	if "is_default" in rp and rp["is_default"]:
		redmine_default_priority = rp["name"]
		
#print(u'Redmine priorities:\n' + str(redmine_priorities_dict))
    redmine_priorities_dict[rp["name"]] = rp["id"]
    if "is_default" in rp and rp["is_default"]:
        redmine_default_priority = rp["name"]

# print(u'Redmine priorities:\n' + str(redmine_priorities_dict))

fname = f"json/{cfg.redmine_project_identifier}-trackers.json"
if not os.path.exists(fname):
    redmine_trackers = requests.get(
        cfg.redmine_root_url + "/trackers.json",
        verify=cfg.redmine_verify_certificates,
        headers={"X-Redmine-API-Key": cfg.redmine_api_key},
    ).json()
    with open(fname, "w") as fp:
        json.dump(redmine_trackers, fp)
else:
    redmine_trackers = json.load(open(fname))

redmine_tracker_dict = {}
redmine_default_tracker = "Task"
for ts in redmine_trackers["trackers"]:
    redmine_tracker_dict[ts["name"]] = ts["id"]

fname = f"json/{cfg.redmine_project_identifier}-statuses.json"
if not os.path.exists(fname):
    redmine_statuses = requests.get(
        cfg.redmine_root_url + "/issue_statuses.json",
        verify=cfg.redmine_verify_certificates,
        headers={"X-Redmine-API-Key": cfg.redmine_api_key},
    ).json()
    with open(fname, "w") as fp:
        json.dump(redmine_statuses, fp)
else:
    redmine_statuses = json.load(open(fname))

redmine_statuses = requests.get(cfg.redmine_root_url + '/issue_statuses.json', verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key}).json()
redmine_statuses_dict = {}
redmine_default_status = "New"
for rs in redmine_statuses["issue_statuses"]:
	redmine_statuses_dict[rs["name"]] = rs["id"]
	if "is_default" in rs and rs["is_default"]:
		redmine_default_status = rs["name"]
		
#print(u'Redmine statuses:\n' + str(redmine_priorities_dict))
    redmine_statuses_dict[rs["name"]] = rs["id"]
    if "is_default" in rs and rs["is_default"]:
        redmine_default_status = rs["name"]


# GET VERSIONS
def get_redmine_versions(clean=False):
    fname = f"json/{cfg.redmine_project_identifier}-versions.json"
    if clean:
        try:
            os.unlink(fname)
        except FileNotFoundError:
            pass

    if not os.path.exists(fname):
        redmine_versions = requests.get(
            cfg.redmine_root_url + f"/projects/{redmine_project_id}/versions.json",
            verify=cfg.redmine_verify_certificates,
            headers={"X-Redmine-API-Key": cfg.redmine_api_key},
        ).json()
        with open(fname, "w") as fp:
            json.dump(redmine_versions, fp)
    else:
        redmine_versions = json.load(open(fname))

    return redmine_versions


redmine_versions = get_redmine_versions()

i_fname = f"json/{cfg.redmine_project_identifier}-import-history.json"
if not os.path.exists(i_fname):
    import_history = []
else:
    import_history = json.load(open(i_fname))


def write_import_history():
    if dry_run:
        return

    with open(i_fname, "w") as fp:
        json.dump(import_history, fp)


# print(u'Redmine statuses:\n' + str(redmine_priorities_dict))

# ============================================================================
# Direct Trello-to-Redmine mappings are made here.
# ============================================================================

print('Generating configuration mappings...')
print("Generating configuration mappings...")
sys.stdout.flush()

user_map = {}
for id, fullname in members_dict.iteritems():
	if not fullname in redmine_users_dict:
		print(u'Warning: user {0} not found in Redmine, defaulting to {1}'.format(fullname, cfg.redmine_default_user))
		fullname = cfg.redmine_default_user
	redmine_id = redmine_users_dict[fullname]
	user_map[id] = redmine_id
	print(u'Matched user {0}, Trello ID {1}, Redmine ID {2}'.format(fullname, id, redmine_id).encode('utf-8'))

print(u'User ID map:\n{0}\nDefault user ID: {1}'.format(str(user_map), redmine_users_dict[cfg.redmine_default_user]).encode('utf-8'))
for id, fullname in members_dict.items():
    if fullname not in redmine_users_dict:
        print(
            "Warning: user {0} not found in Redmine, defaulting to {1}".format(
                fullname, cfg.redmine_default_user
            )
        )
        fullname = cfg.redmine_default_user
    redmine_id = redmine_users_dict[fullname]
    user_map[id] = redmine_id
    print(
        "Matched user {0}, Trello ID {1}, Redmine ID {2}".format(
            fullname, id, redmine_id
        ).encode("utf-8")
    )

print(
    "User ID map:\n{0}\nDefault user ID: {1}".format(
        str(user_map), redmine_users_dict[cfg.redmine_default_user]
    ).encode("utf-8")
)

priority_map = {}
for id, name in labels_dict.iteritems():
	if not name in redmine_priorities_dict:
		print(u'Warning: Trello label {0} is not mapped to a Redmine priority, defaulting to {1}'.format(name, redmine_default_priority).encode('utf-8'))
		name = redmine_default_priority
	redmine_id = redmine_priorities_dict[name]
	priority_map[id] = redmine_id
	#print(u'Matched label {0}, Trello ID {1}, Redmine ID {2}'.format(name, id, redmine_id).encode('utf-8'))

#print('Redmine priorities:\n' + str(redmine_priorities_dict))
for id, name in labels_dict.items():
    if name not in redmine_priorities_dict:
        name = redmine_default_priority
    redmine_id = redmine_priorities_dict[name]
    priority_map[id] = redmine_id
    # print(u'Matched label {0}, Trello ID {1}, Redmine ID {2}'.format(name, id, redmine_id).encode('utf-8'))

# print('Redmine priorities:\n' + str(redmine_priorities_dict))

tracker_map = {}
for id, name in labels_dict.items():
    if name not in redmine_tracker_dict:
        name = redmine_default_tracker
    redmine_id = redmine_tracker_dict[name]
    tracker_map[id] = redmine_id

status_map = {}
for id, name in lists_dict.iteritems():
	if not name in redmine_statuses_dict:
		print(u'Warning: Trello list {0} is not mapped to a Redmine status, defaulting to {1}'.format(name, redmine_default_status).encode('utf-8'))
		name = redmine_default_status
	redmine_id = redmine_statuses_dict[name]
	status_map[id] = redmine_id
	#print(u'Matched list {0}, Trello ID {1}, Redmine ID {2}'.format(name, id, redmine_id).encode('utf-8'))

#print('Redmine statuses:\n' + str(redmine_statuses_dict))
for id, name in lists_dict.items():
    if name not in redmine_statuses_dict:
        if not list_status_dict[id]:
            print(
                "Warning: Trello list {0} is not mapped to a Redmine status, defaulting to {1}".format(
                    name, redmine_default_status
                ).encode(
                    "utf-8"
                )
            )
        name = redmine_default_status
    redmine_id = redmine_statuses_dict[name]
    status_map[id] = redmine_id
    # print(u'Matched list {0}, Trello ID {1}, Redmine ID {2}'.format(name, id, redmine_id).encode('utf-8'))

if cfg.closed_list_status not in redmine_statuses_dict:
    print(
        f"Warning: 'closed_list_status' value not found in Redmine statuses. Defaulting to {redmine_default_status}"
    )
    id = redmine_statuses_dict[redmine_default_status]
    redmine_statuses_dict[cfg.closed_list_status] = id

# print('Redmine statuses:\n' + str(redmine_statuses_dict))

refetch_versions = False
version_map = {}
list_version_map = {}
_versions = [v for _, v in cfg.version_map.items()]

for l_id, l_name in orig_lists_dict.items():
    v_name = l_name.split()[-1].replace("v.1", "v1")
    if re.match(r"v\d\.\d\.\d$", v_name) or re.match(r"v\d\.\d$", v_name):
        v_name = v_name[1:]  # remove leading "v"
        _versions.append(v_name)
        list_version_map[l_id] = v_name

_versions = list(set(_versions))

for v in redmine_versions["versions"]:
    version_map[v["name"]] = v["id"]

for v in _versions:
    if v not in version_map:
        refetch_versions = True
        version = {
            "version": {"name": v},
        }

        if dry_run:
            print(f"Creating FAKE VERSION {v}")
            version_map[v] = -1
        else:
            result = requests.post(
                cfg.redmine_root_url + f"/projects/{redmine_project_id}/versions.json",
                data=json.dumps(version),
                verify=cfg.redmine_verify_certificates,
                headers={
                    "X-Redmine-API-Key": cfg.redmine_api_key,
                    "Content-Type": "application/json",
                },
            )
            if result.status_code >= 400:
                print(
                    "Error on post {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}".format(
                        str(result),
                        result.request.headers,
                        result.headers,
                        result.content,
                    )
                )
                print(
                    json.dumps(
                        version, sort_keys=False, indent=4, separators=(",", ": ")
                    )
                )
                sys.exit(1)
            else:
                vjson = result.json()
                version_map[v] = vjson["version"]["id"]
                print(f"Created version '{v}'")

if refetch_versions:
    redmine_versions = get_redmine_versions(clean=True)

# ============================================================================
# Finally, cards processing.
# ============================================================================

print ('Processing cards...')
print("Processing cards...")
sys.stdout.flush()


def _set_tracker_or_status(c):
    result = {}
    if c["idLabels"]:
        label_id = c["idLabels"][0]
        result["tracker_id"] = tracker_map[label_id]
        label_name = labels_dict[label_id]
        if label_name.startswith("Status:"):
            lbname = label_name[7:]
            if lbname in redmine_statuses_dict:
                result["status_id"] = redmine_statuses_dict[lbname]

    # List to status and version mapping
    if list_status_dict[c["idList"]]:
        # List is archived
        result["status_id"] = redmine_statuses_dict[cfg.closed_list_status]

    lname = orig_lists_dict[c["idList"]]
    if lname in cfg.version_map:
        result["fixed_version_id"] = version_map[cfg.version_map[lname]]
    elif c["idList"] in list_version_map:
        v_name = list_version_map[c["idList"]]
        result["fixed_version_id"] = version_map[v_name]

    # User assignee and watchers
    a_id = None
    w_ids = []
    for mid in c["idMembers"]:
        if a_id is None:
            a_id = mid
        else:
            w_ids.append(mid)

    if a_id is not None:
        result["assigned_to_id"] = a_id
        if len(w_ids):
            result["watcher_user_ids"] = w_ids

    return result


def handle_attachments(c):
    uploads = []
    if not c["attachments"]:
        return uploads

    # Don't upload files in specific lists
    if c["idList"] in no_attachment_list:
        print("Skipping upload for card in prohibited upload list.")
        return uploads

    for a in c["attachments"]:
        if not a["isUpload"]:
            continue

        filename = a["fileName"].strip().replace(" ", "_")
        filename = re.sub(r"(?u)[^-\w.]", "", filename)  # from django
        file_path = f"attachments/{filename}"
        upload = {
            "filename": filename,
            "content_type": a["mimeType"],
        }
        try:
            r = requests.get(
                a["url"],
                stream=True,
                headers={
                    "Authorization": f'OAuth oauth_consumer_key="{cfg.trello_apikey}", oauth_token="{cfg.trello_token}"',
                },
            )
        except Exception as e:
            print(f"Failuring downloading file {a['url']}, error: {e}")
            continue

        with open(file_path, "wb") as fp:
            for chunk in r.iter_content(chunk_size=8192):
                fp.write(chunk)

        print(f"Downloaded {a['url']}...")

        # Upload file to Redmine
        try:
            with open(file_path, "rb") as fp:
                r = requests.post(
                    cfg.redmine_root_url + f"/uploads.json?filename={filename}",
                    data=fp,
                    verify=cfg.redmine_verify_certificates,
                    headers={
                        "X-Redmine-API-Key": cfg.redmine_api_key,
                        "Content-Type": "application/octet-stream",
                    },
                )
                if r.status_code >= 400:
                    print(
                        "Error on post {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}".format(
                            str(r), r.request.headers, r.headers, r.content
                        )
                    )
                    raise Exception("Redmine upload failed")
                else:
                    rmfile = r.json()
                    upload["token"] = rmfile["upload"]["token"]

        except Exception as e:
            print(f"Failuring uploading file to Redmine {file_path}, error: {e}")
            continue

        # Delete file locally
        os.unlink(file_path)

        # Wrap up this attachment
        uploads.append(upload)

    return uploads


dry_run_counter = 0
import_counter = 0
for c in board["cards"]:
	desc = c["desc"]
	if c["idChecklists"]:
		desc += u'\n'
		for id in c["idChecklists"]:
			for item in checklists_dict[id]:
				desc += u'\n[{0}] {1}'.format(u'x' if item["state"] == u'complete' else u' ', item["name"])
	card = {
		"issue": {
			"subject": u'[{0}][{1}] {2}'.format(board["name"], orig_lists_dict[c["idList"]], c["name"]),
			"description": desc,
			"project_id": redmine_project_id,
			"assigned_to_id": user_map[c["idMembers"][0]] if c["idMembers"] else redmine_users_dict[cfg.redmine_default_user],
			"status_id": status_map[c["idList"]] if c["idList"] else redmine_statuses_dict[redmine_default_status],
			"priority_id": priority_map[c["idLabels"][0]] if c["idLabels"] else redmine_priorities_dict[redmine_default_priority],
			"is_private": False
		}
	}
	print(u'Importing {0}...'.format(card["issue"]["subject"]).encode('utf-8'))
	issue_id = -1
	if dry_run:
		dry_run_counter += 1
		issue_id = dry_run_counter
		print(json.dumps(card, sort_keys=False, indent=4, separators=(',', ': ')))
	else:
		result = requests.post(cfg.redmine_root_url + '/issues.json', data=json.dumps(card), verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key, "Content-Type": "application/json"})
		if result.status_code >= 400:
			print('Error on post {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}'.format(str(result), result.request.headers, result.headers, result.content))
		        print(json.dumps(card, sort_keys=False, indent=4, separators=(',', ': ')))
			sys.exit(1)
		else:
			print(str(result))
			issue = result.json()
			issue_id = issue["issue"]["id"]
			#print(u'Response JSON:\n{0}\nIssue ID: {1}'.format(str(issue), issue_id).encode('utf-8'))
	if issue_id >= 0 and c["id"] in comments_dict:
		for index, comment in enumerate(reversed(comments_dict[c["id"]])):
			update = {
				"issue": {
					"notes": u'{0}:\n{1}'.format(comment["author"], comment["text"]).format('utf-8') if len(comment["author"]) > 0 else comment["text"]
				}
			}
			print(u'Importing comment {0} of {1}...'.format(index + 1, len(comments_dict[c["id"]])).encode('utf-8'))
			if dry_run:
				print(json.dumps(update, sort_keys=False, indent=4, separators=(',', ': ')))
			else:
				result = requests.put(cfg.redmine_root_url + '/issues/{0}.json'.format(issue_id), data=json.dumps(update), verify=cfg.redmine_verify_certificates, headers={'X-Redmine-API-Key': cfg.redmine_api_key, "Content-Type": "application/json"})
				if result.status_code >= 400:
					print('Error for comments {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}'.format(str(result), result.request.headers, result.headers, result.content))
					sys.exit(1)
				else:
					print(str(result))
        #print("Aborting after one for testing")
        #sys.exit(0)

print('Done!')
    if dry_run and dry_run_counter > 0:
        print("ENDING DRY RUN")
        sys.exit(0)

    if c["id"] in import_history:
        # Already imported this card, continue
        continue

    desc = c["desc"]
    desc += "\n\n"
    desc += "Imported from Trello: [{0}][{1}]".format(
        board["name"], orig_lists_dict[c["idList"]]
    )
    all_checklists = []
    if c["idChecklists"]:
        for id in c["idChecklists"]:
            for item in checklists_dict[id]:
                all_checklists.append(
                    {
                        "subject": item["name"],
                        "is_done": 1 if item["state"] == "complete" else 0,
                    }
                )
    card = {
        "issue": {
            "subject": "{0}".format(c["name"]),
            "description": desc,
            "project_id": redmine_project_id,
            "assigned_to_id": user_map[c["idMembers"][0]]
            if c["idMembers"]
            else redmine_users_dict[cfg.redmine_default_user],
            "status_id": status_map[c["idList"]]
            if c["idList"]
            else redmine_statuses_dict[redmine_default_status],
            "priority_id": priority_map[c["idLabels"][0]]
            if c["idLabels"]
            else redmine_priorities_dict[redmine_default_priority],
            "is_private": False,
        }
    }

    trk = _set_tracker_or_status(c)
    if trk:
        card["issue"].update(trk)

    if len(all_checklists):
        card["issue"]["checklists_attributes"] = all_checklists

    uploads = handle_attachments(c)
    if len(uploads):
        card["issue"]["uploads"] = uploads

    print("Importing {0}...".format(card["issue"]["subject"]).encode("utf-8"))
    issue_id = -1
    if dry_run:
        dry_run_counter += 1
        issue_id = dry_run_counter
        print(json.dumps(card, sort_keys=False, indent=4, separators=(",", ": ")))
    else:
        result = requests.post(
            cfg.redmine_root_url + "/issues.json",
            data=json.dumps(card),
            verify=cfg.redmine_verify_certificates,
            headers={
                "X-Redmine-API-Key": cfg.redmine_api_key,
                "Content-Type": "application/json",
            },
        )
        if result.status_code >= 400:
            print(
                "Error on post {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}".format(
                    str(result), result.request.headers, result.headers, result.content
                )
            )
            print(json.dumps(card, sort_keys=False, indent=4, separators=(",", ": ")))
            sys.exit(1)
        else:
            print(str(result))
            issue = result.json()
            issue_id = issue["issue"]["id"]
            # print(u'Response JSON:\n{0}\nIssue ID: {1}'.format(str(issue), issue_id).encode('utf-8'))
    if issue_id >= 0 and c["id"] in comments_dict:
        for index, comment in enumerate(reversed(comments_dict[c["id"]])):
            try:
                update = {
                    "issue": {
                        "notes": "{0}:\n{1}".format(
                            comment["author"], comment["text"]
                        ).format("utf-8")
                        if len(comment["author"]) > 0
                        else comment["text"]
                    }
                }
            except KeyError as err:
                print(f"ERR: {err}, data: {comment}")

            print(
                "Importing comment {0} of {1}...".format(
                    index + 1, len(comments_dict[c["id"]])
                ).encode("utf-8")
            )
            if dry_run:
                print(
                    json.dumps(
                        update, sort_keys=False, indent=4, separators=(",", ": ")
                    )
                )
            else:
                result = requests.put(
                    cfg.redmine_root_url + "/issues/{0}.json".format(issue_id),
                    data=json.dumps(update),
                    verify=cfg.redmine_verify_certificates,
                    headers={
                        "X-Redmine-API-Key": cfg.redmine_api_key,
                        "Content-Type": "application/json",
                    },
                )
                if result.status_code >= 400:
                    print(
                        "Error for comments {0}: Request headers:\n{1}\nResponse headers:\n{2}\nContent: {3}".format(
                            str(result),
                            result.request.headers,
                            result.headers,
                            result.content,
                        )
                    )
                    sys.exit(1)
                else:
                    print(str(result))

        import_history.append(c["id"])
        import_counter += 1
        if import_counter >= 5:
            write_import_history()
        # print("Aborting after one for testing")
        # sys.exit(0)

print("Done!")