Skip to content

Instantly share code, notes, and snippets.

@aepton
Created July 1, 2014 16:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aepton/cdab3da677a14c6abc6d to your computer and use it in GitHub Desktop.
Save aepton/cdab3da677a14c6abc6d to your computer and use it in GitHub Desktop.
Unfuddle ticket import script
#!/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