Created
July 1, 2014 16:56
-
-
Save aepton/cdab3da677a14c6abc6d to your computer and use it in GitHub Desktop.
Unfuddle ticket import script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# Takes a markdown formatted sked of milestones and tickets and | |
# pushes it all into unfuddle. Tries to prevent duplicates, but | |
# doesn't do a great job because Unfuddle's API acts strangely. | |
# This version is heavily indebted to https://gist.github.com/ryanmark/2661823 | |
# | |
# TODO: Figure out why new tickets don't show up in the get tickets API request | |
# | |
# usage: import_tix.py sked.txt | |
# | |
# File format: | |
# | |
# # Project Name | |
# | |
# ## Iteration 1 (Nov. 18) | |
# * Write a script to import all these tasks (aepton) [2] | |
# * build a website (ryanmark) [4] | |
# - this is a description of the website to build | |
# | |
# ## Iteration 2 (Nov. 25) | |
# * Launch the website (aepton) [2] | |
# - Don't forget to tell people | |
# * Profit!!! (aepton) [8] | |
import sys | |
import requests | |
import json | |
import re | |
from dateutil.parser import parse | |
auth = ("UNFUDDLE_USERNAME", "UNFUDDLE_PASSWORD") | |
unfuddle_domain = 'example.unfuddle.com' | |
def get_project(name): | |
# return the dictionary of a project with a name like the one given | |
r = requests.get( | |
'https://%s/api/v1/projects' % unfuddle_domain, | |
auth=auth, | |
headers={'Accept': 'application/json'} | |
) | |
if r.ok: | |
projects = json.loads(r.content) | |
lowername = name.strip().lower() | |
for p in projects: | |
if lowername == p['title'].lower() or lowername == p['short_name']: | |
return p | |
return None | |
else: | |
raise Exception('API call failed') | |
def get_components(project_id): | |
# return the dictionary of components for this project | |
r = requests.get( | |
'https://%s/api/v1/projects/%i/components' % (unfuddle_domain, project_id), | |
auth=auth, | |
headers={'Accept': 'application/json'} | |
) | |
if r.ok: | |
return json.loads(r.content) | |
else: | |
raise Exception('API call failed') | |
def get_component_name_from_id(project_id, name): | |
# return the ID of a component with the given name and project id | |
components = get_components(project_id) | |
for comp in components: | |
if comp['name'].lower() == name.lower(): | |
return comp | |
def get_people(): | |
# return the dictionary of all people unfuddle knows about | |
r = requests.get( | |
'https://%s/api/v1/people' % unfuddle_domain, | |
auth=auth, | |
headers={'Accept': 'application/json'} | |
) | |
if r.ok: | |
return json.loads(r.content) | |
else: | |
raise Exception('API call failed') | |
def get_person_from_username(username): | |
# return the dictionary corresponding to a specific username | |
people = get_people() | |
for person in people: | |
if person['username'].lower() == username.lower(): | |
return person | |
def get_or_create_milestone(project_id, title, due_date): | |
# return the dictionary of the milestone with the given title. | |
# Create it if a matching one can't be found | |
milestone = get_milestone(project_id, title) | |
if not milestone: | |
# didn't find a match, create it | |
requests.post( | |
'https://%s/api/v1/projects/%i/milestones' % (unfuddle_domain, project_id), | |
auth=auth, | |
headers={'Accept': 'application/json', 'Content-Type': 'application/xml'}, | |
data="<milestone><due-on>%s</due-on><title>%s</title></milestone>" % | |
(due_date.strftime('%Y/%m/%d'), title)) | |
milestone = get_milestone(project_id, title) | |
if not milestone: | |
raise Exception('API call failed') | |
return milestone | |
def get_milestone(project_id, title): | |
r = requests.get( | |
'https://%s/api/v1/projects/%i/milestones' % (unfuddle_domain, project_id), | |
auth=auth, | |
headers={'Accept': 'application/json'} | |
) | |
if r.ok: | |
milestones = json.loads(r.content) | |
for m in milestones: | |
if title.strip().lower() == m['title'].lower(): | |
return m | |
return None | |
else: | |
raise Exception('API call failed') | |
def get_or_create_ticket( | |
project_id, milestone_id, component_id, assignee_id, est, title, description, priority=3): | |
# return the dictionary of the milestone with the given title. | |
# Create it if a matching one can't be found | |
ticket = get_ticket(project_id, milestone_id, title) | |
if not ticket: | |
# didn't find a match, create it | |
component_str = '' | |
assignee_str = '' | |
estimate_str = '' | |
if component_id: | |
component_str = '<component-id type="integer">%i</component-id>' % component_id | |
if assignee_id: | |
assignee_str = '<assignee-id type="integer">%i</assignee-id>' % assignee_id | |
if est: | |
estimate_str = '<hours-estimate-initial type="float">%s</hours-estimate-initial>' % est | |
r = requests.post( | |
'https://%s/api/v1/projects/%i/tickets' % (unfuddle_domain, project_id), | |
auth=auth, | |
headers={'Accept': 'application/json', 'Content-Type': 'application/xml'}, | |
data="""<ticket> | |
<milestone-id type="integer">%i</milestone-id> | |
<priority>%i</priority> | |
<description>%s</description> | |
<summary>%s</summary> | |
%s | |
%s | |
%s | |
</ticket>""" % ( | |
milestone_id, priority, description, title, component_str, assignee_str, estimate_str) | |
) | |
if not r.ok: | |
raise Exception('API call failed') | |
ticket = get_ticket(project_id, milestone_id, title) | |
if not ticket: | |
print("WARNING: Ticket was created, but I couldn't retrieve it.") | |
return ticket | |
def get_ticket(project_id, milestone_id, summary): | |
r = requests.get( | |
'https://%s/api/v1/projects/%i/tickets' % (unfuddle_domain, project_id), | |
auth=auth, | |
headers={'Accept': 'application/json'} | |
) | |
if r.ok: | |
tickets = json.loads(r.content) | |
for t in tickets: | |
if summary.strip().lower() == t['summary'].lower(): | |
return t | |
return None | |
else: | |
raise Exception('API call failed') | |
if __name__ == "__main__": | |
# A line that begins with # is the name of the project | |
# A line that begins with ## is the name of the milestone | |
# Milestones need due dates: ## Milestone 1 (due date) | |
# A line that begins with an * is a ticket | |
# A line that begins with an - and follows a line that starts | |
# with a * is a ticket description | |
# Any other line is ignored | |
datafile = sys.argv[1] | |
f = open(datafile, "U") | |
project = None | |
milestone = None | |
component = {'id': ''} | |
assignee = {'id': None} | |
estimate = None | |
lines = f.readlines() | |
for idx, line in enumerate(lines): | |
if line.startswith('###'): | |
# component name | |
name = line.strip().replace('### ', '') | |
if project and name: | |
component = get_component_name_from_id(project['id'], name) | |
print('Adding tickets to component "%s"...' % component['name']) | |
elif line.startswith('##'): | |
# milestone and due date | |
if not project: | |
raise Exception("Don't know which project to put this stuff in!") | |
milestone_title, due_date = re.match( | |
r'##\s+([\w\s]+)\s+\(([^\)]+)\)', | |
line.strip()).group(1, 2) | |
print('Adding tickets to milestone "%s"...' % milestone_title) | |
milestone = get_or_create_milestone(project['id'], milestone_title, parse(due_date)) | |
elif line.startswith('#'): | |
# project name | |
project_name = line.strip()[1:].strip() | |
print('Switching to project "%s"...' % project_name) | |
project = get_project(project_name) | |
if not project: | |
raise Exception("Couldn't find project '%s'" % project_name) | |
elif line.startswith('*'): | |
# ticket | |
if not project or not milestone: | |
raise Exception("Don't know which project or milestone to put this stuff in!") | |
# title | |
ticket_title = line.strip()[1:].strip() | |
# assignee | |
text, username, estimate = re.match( | |
r'\*\s+([\w\s\-\.,&*%$#@!\?]+)\s+(\([^\)]+\)){0,1}\s+(\[[^\]]\])', | |
line.strip()).group(1, 2, 3) | |
if username: | |
assignee = get_person_from_username(username[1:-1]) | |
if estimate: | |
estimate = estimate[1:-1] | |
if text: | |
ticket_title = text | |
# description | |
position = idx + 1 | |
ticket_description = [] | |
try: | |
while lines[position].strip().startswith('-'): | |
ticket_description.append(lines[position].strip()) | |
if position + 1 < len(lines): | |
position += 1 | |
else: | |
break | |
except IndexError: | |
pass | |
print 'Adding ticket "%s", assigning to %s, estimating at %s...' % ( | |
ticket_title, assignee['username'], estimate) | |
ticket = get_or_create_ticket( | |
project['id'], | |
milestone['id'], | |
component['id'], | |
assignee['id'], | |
estimate, | |
ticket_title, | |
"\n".join(ticket_description) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment