Alexander Dudko avatar Alexander Dudko committed e6e2240

Initial commit

Comments (0)

Files changed (10)

+*.pyc
+dist
+build
+*.egg-info
+include *.rst
+Introduction
+============
+
+I used to waste hours cloning tasks in JIRA, editing their summaries, description and etc. Step by step I came to the idea that I need a template of my frequently used tasks so that I can re-create them very easy and effortless.
+
+The key idea of jira-bulk-loader is an activity template.
+
+The template is written in human language with a few markup rules. jira-bulk-loader.py uses the prepared template to create the corresponding set of tasks in less than one minute.
+
+
+
+Installation
+============
+
+Download and install using pip:
+
+    pip install jira-bulk-loader
+
+
+
+Very simple case
+================
+
+Template:
+
+    | 	h5. First task summary \*assignee\*
+    |	=description line 1
+    | 	=description line 2
+    |
+    | 	h5. Second task summary \*assignee\*
+    | 	=description line 3
+    | 	=description line 4
+
+command: 
+
+	./jira-bulk-loader.py -U <your_username> -P <your_password> -H jira.your_domain.org -W PRKEY template_file
+
+two tasks will be created and assigned to *assignee* in the project with a project key *PRKEY*.
+
+
+
+One more simple case
+====================
+
+Template:
+
+    | 	h5. Task summary \*assignee\*
+    |	=description line 1
+    | 	=description line 2
+    |
+    | 	# First sub-task summary \*assignee1\* 
+    | 	=description line 3
+    |
+    |	# Second sub-task summary \*assignee2\* %2012-09-18%
+    | 	=description line 3
+
+and the command:
+
+	./jira-bulk-loader.py -U <your_username> -P <your_password> -H jira.your_domain.org -D 2012-09-20 -W PRKEY template_file
+
+It will create a task with two subtasks. Moreover it also sets due date 2012-09-18 (YYYY-mm-DD) to 2nd sub-task, and 2012-09-20 to the task and its first sub-task.
+
+
+
+Dry run option
+==============
+
+jira-bulk-loader.py has an option *--dry*. If it is specified in command line, jira-bulk-loader checks template syntax, verifies project name and assignees but doesn't create tasks.
+
+I would strongly recommend using it every time.
+
+
+
+User story and 'included in' tasks
+==================================
+
+Sometime an activity is too complex and it is much easier and appropriate to create several tasks with sub-tasks and link them to a user story.
+
+    | 	h4. User story summary \*assignee\*
+    |	=description
+    |
+    | 	h5. First task summary \*assignee1\*
+    |	=description
+    | 	# Sub-task summary \*assignee1\* 
+    | 	=description
+    |
+    | 	h5. Second task summary \*assignee2\*
+    |	=description
+    | 	# Sub-task summary \*assignee2\* 
+    | 	=description
+
+In this case h5 tasks will be linked to h4 user story.
+
+
+
+A short summary
+===============
+
+Let me summarize what are the possible markups to begin a line with:
+
+- a user story: h4. summary \*assignee\*
+- a task: h5. summary \*assignee\*
+- a sub-task: # summary \*assignee\*  
+- one more sub-task: #* summary \*assignee\*
+- description: = 
+
+
+
+Task parameters
+===============
+
+It is possible to define task attributes in template:
+
+    |	{"project":{"key":"PRKEY"}
+    |	{"priority": {"name": "High"}}
+    |	{"duedate": "2012-09-20"}
+    |	{"components": [{"name": "Production"}]}
+    |
+    | 	h5. First task summary \*assignee1\*
+    |	=description
+    |
+    | 	h5. Second task summary \*assignee2\* {"components": [{"name": "Localizations"}]}
+    |	=description
+    |
+    | 	h5. Third task summary \*assignee3\*
+    |	=description
+
+It the example *project*, *priority* and *duedate* will be applied to both tasks by default. The *component* 'Production' will be applied to task 1 and 3. However, the second task will use the *component* 'Localizations'.
+
+`This part <http://docs.atlassian.com/jira/REST/latest/#id200060>`_ of Jira documentation could give a clue how to find out relevant parameters in your project and their format.
+
+
+
+Template variables
+==================
+
+    |	[REVISION=194567]
+    |	[QA=John]
+    |
+    | 	h5. First task summary \*$QA\*
+    |	=description $REVISION
+    |
+    | 	h5. Second task summary \*$QA\*
+    |	=description $REVISION
+
+is equivalent to 
+
+    | 	h5. First task summary \*John\*
+    |	=description 194567
+    |
+    | 	h5. Second task summary \*John\*
+    |	=description 194567
+
+the important difference is that you don't need to change assignee or description of each task in your template. You change variable value instead and it is applied to every line in the template. 
+
+
+
+Issues and new ideas
+====================
+
+If you found an issue or if you have an idea of improvement please visit `http://bitbucket.org/oktopuz/jira-bulk-loader/issues <http://bitbucket.org/oktopuz/jira-bulk-loader/issues>`_
+
+

bin/jira-bulk-loader.py

+#!/usr/bin/python
+#-*- coding: UTF-8 -*-
+
+import argparse
+from jirabulkloader.task_extractor import TaskExtractor
+from jirabulkloader.task_extractor_exceptions import TaskExtractorTemplateErrorProject, TaskExtractorJiraValidationError, TaskExtractorTemplateErrorJson, TaskExtractorJiraCreationError
+from requests.exceptions import ConnectionError
+
+prg_description="""Uses template file to create many tasks in Jira at once.
+For more information about template format please visit http://bitbucket.org/oktopuz/jira-bulk-loader"""
+
+parser = argparse.ArgumentParser(description=prg_description, formatter_class=argparse.RawDescriptionHelpFormatter)
+
+parser.add_argument('template_file', type=argparse.FileType('rU'), help='file containing tasks definition')
+parser.add_argument('-W', dest='project', help='Project key')
+parser.add_argument('-R', dest='priority', help='Task priority. "Medium" by default', default="Medium")
+parser.add_argument('-D', dest='dueDate', help='dueDate  (YYYY-mm-DD). For example: 2012-05-31')
+parser.add_argument('--dry', dest='dry_run', action='store_true', help='Make a dry run. It checks everything but does not create tasks', default=False)
+
+mandatory = parser.add_argument_group('mandatory arguments')
+mandatory.add_argument('-H', dest='hostname', required=True, help='Jira hostname. Without http://')
+mandatory.add_argument('-U', dest='username', required=True, help='your Jira username')
+mandatory.add_argument('-P', dest='password', required=True, help='your Jira password')
+
+args = parser.parse_args()
+
+
+##############################################################
+# open input file, parse and create tasks
+
+input_text = args.template_file.read()
+
+options = {}
+if args.dueDate: options['duedate'] = args.dueDate
+if args.priority: options['priority'] = {'name':args.priority}
+if args.project: options['project'] = {'key':args.project}
+
+jira_url = "http://" + args.hostname
+
+task_ext = TaskExtractor(jira_url, args.username, args.password, options, dry_run = args.dry_run)
+
+try:
+    print "Parsing task list.."
+    tasks =  task_ext.load(input_text)
+
+    print "Validating tasks.."
+    task_ext.validate_load(tasks)
+
+    print "Creating tasks.."
+    breakdown = task_ext.create_tasks(tasks)
+except TaskExtractorTemplateErrorProject, e:
+    print e.message
+    exit(1)
+except TaskExtractorJiraValidationError, e:
+    print e.message
+    exit(1)
+except TaskExtractorJiraCreationError, e:
+    print e.message
+    exit(1)
+except TaskExtractorTemplateErrorJson, e:
+    print "ERROR: The following line in template is not valid:", e.error_element
+    print "A correct JSON structure expected."
+    exit(1)
+
+print '===  The following structure will be created ===' + '\n\n' + breakdown
+
+print "\nDone."
+

jirabulkloader/__init__.py

+__author__ = 'alex.dudko@gmail.com'

jirabulkloader/task_extractor.py

+#-*- coding: UTF-8 -*-
+
+import re
+import base64
+from urllib2 import Request, urlopen, URLError
+import simplejson as json
+from task_extractor_exceptions import TaskExtractorTemplateErrorProject, TaskExtractorTemplateErrorJson, TaskExtractorJiraValidationError, TaskExtractorJiraCreationError
+
+
+class TaskExtractor:
+
+    def __init__(self, jira_url, username, password, options, dry_run = False):
+        self.h5_tasks_to_link_to_h4_task = [] # will be used to link h5-tasks to the root task
+        self.tmpl_vars = {} # template variables dict
+        self.tmpl_json = {}
+
+        self.username = username
+        self.password = password
+
+        self.default_params = options
+        self.dry_run = dry_run
+        self.jira_url = jira_url
+
+
+    def validate_load(self, task_list):
+        """
+        It takes the task_list prepared by load() and validate list of assignees and projects.
+        """
+        assignees = []
+
+        for line in task_list:
+            if 'assignee' in line:
+                if line['assignee'] not in assignees:
+                    assignees.append(line['assignee'])
+                    self._validate_user(line['assignee'], self._get_project_or_raise_exception(line))
+
+
+#####################################################################################
+# helpers for validate_load()
+
+    def _get_project_or_raise_exception(self, input_line):
+        try:
+            return input_line['tmpl_ext']['project']['key']
+        except KeyError:
+            if 'project' in self.default_params:
+                return self.default_params['project']['key']
+            else:
+                raise TaskExtractorTemplateErrorProject('Missing project key in line: ' + input_line['summary'])
+
+    def _validate_user(self, user, project):
+        """
+        Checks if a new issue of the project can be assigned to the user.
+        http://docs.atlassian.com/jira/REST/latest/#id120417
+        """
+
+        full_url = "%s/rest/api/2/user/assignable/search?username=%s&project=%s" % (self.jira_url, user, project)
+        try:
+            result = json.load(self._jira_request(full_url, None, 'GET'))
+        except URLError, e:
+            if hasattr(e, 'code'):
+                if e.code == 403 or e.code == 401:
+                    error_message = "Your username and password are not accepted by Jira."
+                    raise TaskExtractorJiraValidationError(error_message)
+                else:
+                    error_message = "The username '%s' and the project '%s' can not be validated.\nJira response: Error %s, %s" % (user, project, e.code, e.read())
+                    raise TaskExtractorJiraValidationError(error_message)
+        if len(result) == 0: # the project is okay but username is missing n Jira
+            error_message = "ERROR: the username '%s' specified in template can not be validated." % user
+            raise TaskExtractorJiraValidationError(error_message)
+
+
+# end of load() helpers
+#####################################################################################
+
+
+    def load(self, input_text):
+        """
+        Parse and convert the input_text to a list of tasks
+        """
+        result = []
+        input_text = input_text.lstrip('\n');
+
+        pattern_task = re.compile('^(h5\.|h4\.|#[*#]?)\s+(.+)\s+\*(\w+)\*(?:\s+%(\d{4}-\d\d-\d\d)%)?(?:\s+({.+}))?')
+        pattern_description = re.compile('=')
+        pattern_vars = re.compile('^\[(\w+)=(.+)\]$')
+        pattern_json = re.compile('^{.+}$')
+
+        for line in input_text.splitlines():
+                if self.tmpl_vars:
+                    line = self._replace_template_vars(line)
+                line = line.rstrip()
+                match_task = pattern_task.search(line)
+                if match_task:
+                    result.append(self._make_json_task(match_task))
+                elif pattern_description.match(line): # if description
+                    result[-1] = self._add_task_description(result[-1], line[1:])
+                else:
+                    match_vars = pattern_vars.search(line)
+                    if match_vars:
+                        self.tmpl_vars[match_vars.group(1)] = match_vars.group(2)
+                    else:
+                        if pattern_json.match(line): # if json
+                            self.tmpl_json.update(self._validated_json_loads(line))
+                        else:
+                            result.append({'text':line})
+        return result
+
+#####################################################################################
+# several helpers for load()
+
+    def _make_json_task(self, match):
+        task_json = {'markup':match.group(1), 'summary':match.group(2), 'assignee':match.group(3)}
+        if match.group(4): task_json['duedate'] = match.group(4)
+        if not len(self.tmpl_json) == 0:
+            task_json['tmpl_ext'] = self.tmpl_json.copy()
+        if match.group(5):
+             if not 'tmpl_ext' in task_json: task_json['tmpl_ext'] = {}
+             task_json['tmpl_ext'].update(self._validated_json_loads(match.group(5)))
+        return task_json
+
+    def _add_task_description(self, task_json, input_line):
+        if 'description' in task_json:
+            task_json['description'] = '\n'.join([task_json['description'], input_line])
+        else:
+            task_json['description'] = input_line
+        return task_json
+
+    def _replace_template_vars(self, input_line):
+        for key in self.tmpl_vars:
+            input_line = re.sub('\$' + key, self.tmpl_vars[key], input_line)
+        return input_line
+
+    def _validated_json_loads(self, input_line):
+        result = ''
+        try:
+            result = json.loads(input_line)
+        except json.JSONDecodeError, e:
+            raise TaskExtractorTemplateErrorJson(input_line)
+        return result
+
+# end of load() helpers
+#####################################################################################
+
+    def jira_format(self, task):
+        fields = {}
+
+        fields.update(self.default_params)
+        if 'tmpl_ext' in task: fields.update(task['tmpl_ext'])
+        if 'duedate' in task: fields['duedate'] = task['duedate']
+        fields['summary'] = task['summary']
+        if 'description' in task: fields['description'] = task['description']
+        fields['issuetype'] = {'name':task['issuetype']}
+        fields['assignee'] = {'name':task['assignee']}
+        if 'parent' in task: fields['parent'] = {'key':task['parent']}
+
+        return {'fields':fields}
+
+
+    def create_tasks(self, task_list):
+        """
+        It takes the task_list prepared by load(), creates all tasks
+        and compose created tasks summary.
+        """
+
+        summary = ''
+        h5_task_ext = ''
+
+        for line in task_list:
+            if 'markup' in line:
+                if line['markup'] == 'h5.':
+                    if 'h5_task_key' in vars(): # if new h5 task begins
+                        h5_summary_list = self._h5_task_completion(h5_task_key, h5_task_caption, h5_task_desc, h5_task_ext)
+                        summary = '\n'.join([summary, h5_summary_list]) if summary else h5_summary_list
+                        h5_task_ext = ''
+                    h5_task_key, h5_task_caption, h5_task_desc = self._create_h5_task_and_return_key_caption_description(line)
+                elif line['markup'][0] == '#':
+                    sub_task_caption = self._create_sub_task_and_return_caption(line, h5_task_key)
+                    h5_task_ext = '\n'.join([h5_task_ext, sub_task_caption]) if h5_task_ext else sub_task_caption
+                elif line['markup'] == 'h4.':
+                    h4_task = line
+            elif 'text' in line:
+                h5_task_ext = '\n'.join([h5_task_ext, line['text']]) if h5_task_ext else line['text']
+
+        if 'h5_task_key' in vars():
+            h5_summary_list = self._h5_task_completion(h5_task_key, h5_task_caption, h5_task_desc, h5_task_ext)
+            summary = '\n'.join([summary, h5_summary_list]) if summary else h5_summary_list
+
+        if 'h4_task' in vars():
+            h4_task_key, h4_task_caption = self._create_h4_task_and_return_key_caption(h4_task)
+            summary = ('\n'.join([h4_task_caption, summary]) if summary else h4_task_caption)
+
+        return summary
+
+#####################################################################################
+# several helpers for create_tasks()
+
+    def _make_task_caption(self, task_json, task_key):
+        return ' '.join([task_json['markup'], task_json['summary'], '(' + task_key + ')'])
+
+    def _h5_task_completion(self, key, caption, desc, ext):
+        summary_list = [caption]
+        if ext:
+            desc = '\n'.join([desc, ext]) if desc else ext
+            self.update_issue_desc(key, desc)
+        if desc:
+            summary_list.append(desc)
+        return '\n'.join(summary_list)
+
+    def _create_sub_task_and_return_caption(self, sub_task_json, parent_task_key):
+        sub_task_json['parent'] = parent_task_key
+        sub_task_json['issuetype'] = 'Sub-task'
+        sub_task_key = self.create_issue(sub_task_json)
+        return self._make_task_caption(sub_task_json,  sub_task_key)
+
+    def _create_h5_task_and_return_key_caption_description(self, h5_task_json):
+        h5_task_json['issuetype'] = 'Task'
+        h5_task_key = self.create_issue(h5_task_json)
+        self.h5_tasks_to_link_to_h4_task.append(h5_task_key)
+        h5_task_caption = self._make_task_caption(h5_task_json,  h5_task_key)
+        h5_task_desc = h5_task_json['description'] if 'description' in h5_task_json else None
+        return (h5_task_key, h5_task_caption, h5_task_desc)
+
+    def _create_h4_task_and_return_key_caption(self, h4_task_json):
+        h4_task_json['issuetype'] = 'Task'
+        h4_task_key = self.create_issue(h4_task_json)
+        for key in self.h5_tasks_to_link_to_h4_task:
+            self.create_link(h4_task_key, key)
+        return (h4_task_key, self._make_task_caption(h4_task_json,  h4_task_key))
+
+# end of create_tasks() helpers
+#####################################################################################
+
+    def create_issue(self, issue):
+        """
+        """
+
+        if not self.dry_run:
+            try:
+                full_url = self.jira_url + '/rest/api/2/issue'
+                jira_response = self._jira_request(full_url, json.dumps(self.jira_format(issue)))
+                issueID = json.load(jira_response)
+                return issueID['key']
+            except URLError, e:
+                if hasattr(e, 'code'):
+                    if e.code == 403 or e.code == 401:
+                        error_message = "Your username and password are not accepted by Jira."
+                        raise TaskExtractorJiraValidationError(error_message)
+                    else:
+                        error_message = "ERROR: The task cannot be created: %s\nJira response: Error %s, %s" % (issue['summary'], e.code, e.read())
+                        raise TaskExtractorJiraCreationError(error_message)
+        else:
+            return 'DRY-RUN-XXXX'
+
+
+    def create_link(self, inward_issue, outward_issue, link_type = 'Inclusion'):
+        """Creates an issue link between two issues.
+
+        The specified link type in the request is used to create the link 
+        and will create a link from the first issue to the second issue using the outward description.
+        The list of issue types can be retrieved using rest/api/2/issueLinkType
+        For now possible types are Block, Duplicate, Gantt Dependency, Inclusion, Reference
+        """
+
+        if not self.dry_run:
+            jira_link = {"type":{"name":link_type},"inwardIssue":{"key":inward_issue},"outwardIssue": {"key": outward_issue}}
+            full_url = self.jira_url + '/rest/api/2/issueLink'
+            return self._jira_request(full_url, json.dumps(jira_link))
+        else:
+          return 'dry run'
+
+
+    def update_issue_desc(self, issue_key, issue_desc):
+        if not self.dry_run:
+            full_url = self.jira_url + '/rest/api/2/issue/' + issue_key
+            jira_data = {'update':{'description':[{'set':issue_desc}]}}
+            return self._jira_request(full_url, json.dumps(jira_data), 'PUT')
+        else:
+            return 'dry run'
+
+
+    def _jira_request(self, url, data, method = 'POST', headers = {'Content-Type': 'application/json'}):
+        """Compose and make HTTP request to JIRA.
+
+        url should be a string containing a valid URL.
+        data is a str. headers is dict of HTTP headers.
+        Supported method are POST (for creating and linking) and PUT (for updating).
+        It expects also self.username and self.password to be set to perform basic HTTP authentication.
+        """
+        request = Request(url, data, headers)
+
+        # basic HTTP authentication
+        base64string = base64.encodestring('%s:%s' % (self.username, self.password)).replace('\n', '')
+        request.add_header("Authorization", "Basic %s" % base64string)
+        request.get_method = lambda : method
+
+        return urlopen(request)
+
+

jirabulkloader/task_extractor_exceptions.py

+
+#############################################################################################
+## Exception
+class TaskExtractorJiraCreationError(Exception):
+    def __init__(self, arg):
+        self.message = arg
+
+class TaskExtractorJiraValidationError(Exception):
+    def __init__(self, arg):
+        self.message = arg
+
+class TaskExtractorTemplateErrorProject(Exception):
+    def __init__(self, arg):
+        self.message = arg
+
+class TaskExtractorTemplateErrorJson(Exception):
+    def __init__(self, arg):
+        self.error_element = arg
+
+
Add a comment to this file

jirabulkloader/test/__init__.py

Empty file added.

jirabulkloader/test/test_task_extractor.py

+#!/usr/bin/python
+
+#-*- coding: UTF-8 -*-
+
+from jirabulkloader.task_extractor import TaskExtractor
+from jirabulkloader.task_extractor_exceptions import TaskExtractorTemplateErrorProject, TaskExtractorJiraValidationError, TaskExtractorTemplateErrorJson, TaskExtractorJiraCreationError
+
+import unittest
+from mock import MagicMock, call
+import simplejson as json
+
+class TestTaskExtractor(unittest.TestCase):
+  
+  def setUp(self):
+    self.jira_url = "http://jira.atlassian.com" # MUST BE CHANGED
+    options = {}
+    self.te = TaskExtractor(self.jira_url, "", "", options, dry_run = True)
+
+  def tearDown(self):
+    self.te = None
+
+##########################################################
+### validate_load
+
+  def test_validate_load_call_validate_user_with_correct_parameters(self):
+    input_task_list = [{'assignee': 'user1', 'markup': 'h5.', 'summary': 'h5 task', 'tmpl_ext':{"project": {"key": "project1"}}}, \
+        {'text':'sample text'}, \
+        {'assignee': 'user2', 'markup': 'h5.', 'summary': 'h5 task', 'tmpl_ext':{"project": {"key": "project2"}}}]
+    self.te._validate_user = MagicMock()
+    expected_result = [call('user1', 'project1'), call('user2', 'project2')]
+    self.te.validate_load(input_task_list)
+    self.assertEquals(self.te._validate_user.call_args_list, expected_result)
+
+  def test_validate_load_raise_error_if_no_project(self):
+    input_task_list = [{'assignee': 'user1', 'markup': 'h5.', 'summary': 'h5 task'}]
+    self.te._validate_user = MagicMock()
+    self.assertRaises(TaskExtractorTemplateErrorProject, self.te.validate_load, input_task_list)
+
+##########################################################
+### load
+
+  def test_load_Using_simple_text(self):
+    input_text = """
+h4. h4 task *assignee*
+=h4 task description
+h5. h5 task *assignee*
+# sub-task *assignee*
+=line1 description
+=line2 description
+"""
+    excpected_result = [{'assignee': 'assignee', 'markup': 'h4.', 'description': 'h4 task description', 'summary': 'h4 task'}, \
+        {'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task'}, \
+        {'assignee': 'assignee', 'markup': '#', 'description': 'line1 description\nline2 description', 'summary': 'sub-task'}]
+    self.assertEquals(excpected_result, self.te.load(input_text))
+
+  def test_load_Text_h4_and_h5_with_empty_line(self):
+    input_text = "\n\nh4. h4 task *assignee*\n\nh5. h5 task *assignee*"
+    excpected_result = [{'assignee': 'assignee', 'markup': 'h4.', 'summary': 'h4 task'}, \
+        {'text':''}, {'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task'}]
+    self.assertEquals(excpected_result, self.te.load(input_text))
+
+  def test_load_Text_h5_and_sub_task_with_empty_line(self):
+    input_text = "h5. h5 task *assignee*\n\n#* Sub-task 1 *assignee*\n\n#* Sub-task 2 *assignee*"
+    excpected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task'}, {'text':''}, \
+        {'assignee': 'assignee', 'markup': '#*', 'summary': 'Sub-task 1'}, {'text':''}, \
+        {'assignee': 'assignee', 'markup': '#*', 'summary': 'Sub-task 2'}]
+    self.assertEquals(excpected_result, self.te.load(input_text))
+
+  def test_load_Check_dueDate(self):
+    input_text = "h5. h5 task *assignee* %2012-04-01%\n=line1 description"
+    expected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task', 'description':'line1 description', 'duedate':'2012-04-01'}]
+    self.assertEquals(expected_result, self.te.load(input_text))
+
+  def test_load_Recognize_template_variables(self):
+    input_text = """
+[VAR1=1]
+[VAR2=2]
+h5. h5 task *assignee* %2012-04-01%
+=line1 description
+"""
+    expected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task', 'description':'line1 description', 'duedate':'2012-04-01'}]
+    self.assertEquals(expected_result, self.te.load(input_text))
+    self.assertEquals({'VAR1':'1', 'VAR2':'2'}, self.te.tmpl_vars)
+
+  def test_load_Text_replacement(self):
+    input_text = """
+[VAR1=h5.]
+[VAR2= h5 task]
+$VAR1$VAR2 *assignee* 
+"""
+    expected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task'}]
+    self.assertEquals(expected_result, self.te.load(input_text))
+
+  def test_load_Recognize_template_json(self):
+    input_text = '{"item1":{"name":"test"}}'
+    self.te.load(input_text)
+    self.assertEquals({'item1':{'name':'test'}}, self.te.tmpl_json)
+
+  def test_load_JSON_variable_must_be_replaced_by_new_value(self):
+    input_text = '{"item1":{"name":"test"}}\n{"item1":{"name":"newtest"}}'
+    self.te.load(input_text)
+    self.assertEquals({'item1':{'name':'newtest'}}, self.te.tmpl_json)
+
+  def test_load_json(self):
+    input_text = '{"item1":{"name":"test"}}\nh5. h5 task *assignee*'
+    expected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task', 'tmpl_ext':{'item1':{'name':'test'}}}]
+    self.assertEquals(expected_result, self.te.load(input_text))
+
+  def test_load_json_if_it_is_not_valid(self):
+    input_text = '{fail test}'
+    self.assertRaises(TaskExtractorTemplateErrorJson, self.te.load, input_text)
+
+  def test_load_Check_dueDate_and_JSON_in_one_line(self):
+    input_text = 'h5. h5 task1 *assignee* %2012-04-01% {"item2":"test2"}\nh5. h5 task2 *assignee*'
+    expected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task1', 'duedate':'2012-04-01', 'tmpl_ext':{"item2":"test2"}}, \
+        {'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task2'}]
+    self.assertEquals(expected_result, self.te.load(input_text))
+
+  def test_load_Check_JSON_inline(self):
+    input_text = '{"item1":{"name":"test"}}\nh5. h5 task *assignee* {"item2":"test2"}'
+    expected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task', 'tmpl_ext':{"item1":{"name":"test"}, "item2":"test2"}}]
+    self.assertEquals(expected_result, self.te.load(input_text))
+
+  def test_load_Check_JSON_inline_replacement(self):
+    input_text = '{"item1":{"name":"test"}}\n{"item2":"test2"}\nh5. h5 task *assignee* {"item1":"test1"}\n#* Sub-task 1 *assignee*'
+    expected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task', 'tmpl_ext':{"item1":"test1","item2":"test2"}}, \
+            {'assignee': 'assignee', 'markup': '#*', 'summary': 'Sub-task 1', 'tmpl_ext':{"item1":{"name":"test"},"item2":"test2"}}]
+    self.assertEquals(expected_result, self.te.load(input_text))
+
+
+##########################################################
+### create_tasks
+
+  def test_create_tasks_Single_h4_task(self):
+    self.te.create_issue = MagicMock()
+    self.te.create_issue.return_value = 'DRY-RUN-XXXX'
+    input_list = [{'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h4.', 'description': 'h4 task description', 'summary': 'h4 task'}]
+    expected_result = {'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h4.', 'description': 'h4 task description', 'summary': 'h4 task'}
+    expected_output = 'h4. h4 task (DRY-RUN-XXXX)'
+    self.assertEquals(self.te.create_tasks(input_list), expected_output)
+    self.te.create_issue.assert_called_once_with(expected_result)
+
+  def test_create_tasks_Single_h5_task(self):
+    self.te.create_issue = MagicMock()
+    self.te.create_issue.return_value = 'DRY-RUN-XXXX'
+    self.te.update_issue_desc = MagicMock()
+    input_list = [{'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'description': 'h5 task description', 'summary': 'h5 task'}]
+    expected_result = {'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'description': 'h5 task description', 'summary': 'h5 task'}
+    expected_output = 'h5. h5 task (DRY-RUN-XXXX)\nh5 task description'
+    self.assertEquals(self.te.create_tasks(input_list), expected_output)
+    self.te.create_issue.assert_called_once_with(expected_result)
+    self.assertEquals(self.te.update_issue_desc.call_count, 0)
+
+  def test_create_tasks_Several_tasks(self):
+    self.te.create_issue = MagicMock()
+    self.te.create_issue.return_value = 'DRY-RUN-XXXX'
+    self.te.update_issue_desc = MagicMock()
+    input_list = [{'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h4.', 'description': 'h4 task description', 'summary': 'h4 task'}, \
+      {'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task', 'description': 'h5 task description'}, \
+      {'issuetype': 'Sub-task', 'assignee': 'assignee', 'markup': '#', 'description': 'line1 description\nline2 description', 'summary': 'sub-task'}]
+    expected_result = [call({'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'description': 'h5 task description', 'summary': 'h5 task'}),
+        call({'description': 'line1 description\nline2 description', 'parent': 'DRY-RUN-XXXX', 'markup': '#', 'summary': 'sub-task', 'assignee': 'assignee', \
+            'issuetype': 'Sub-task'}),
+        call({'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h4.', 'description': 'h4 task description', 'summary': 'h4 task'})]
+    self.te.create_tasks(input_list)
+    self.assertEquals(self.te.create_issue.call_args_list, expected_result)
+    MagicMock.assert_called_once_with(self.te.update_issue_desc, 'DRY-RUN-XXXX', 'h5 task description\n# sub-task (DRY-RUN-XXXX)')
+
+  def test_create_tasks_Several_tasks_without_description(self):
+    self.te.create_issue = MagicMock()
+    self.te.create_issue.return_value = 'DRY-RUN-XXXX'
+    self.te.update_issue_desc = MagicMock()
+    input_list = [{'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h4.', 'summary': 'h4 task'}, \
+      {'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task'}, \
+      {'issuetype': 'Sub-task', 'assignee': 'assignee', 'markup': '#', 'summary': 'sub-task'}]
+    expected_result = [call({'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task'}),
+        call({'parent': 'DRY-RUN-XXXX', 'markup': '#', 'summary': 'sub-task', 'assignee': 'assignee', 'issuetype': 'Sub-task'}),
+        call({'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h4.', 'summary': 'h4 task'})]
+    expected_output = 'h4. h4 task (DRY-RUN-XXXX)\nh5. h5 task (DRY-RUN-XXXX)\n# sub-task (DRY-RUN-XXXX)'
+    self.assertEquals(self.te.create_tasks(input_list), expected_output)
+    self.assertEquals(self.te.create_issue.call_args_list, expected_result)
+    MagicMock.assert_called_once_with(self.te.update_issue_desc, 'DRY-RUN-XXXX', '# sub-task (DRY-RUN-XXXX)')
+
+  def test_create_tasks_Tasks_with_text(self):
+    self.te.create_issue = MagicMock()
+    self.te.create_issue.return_value = 'DRY-RUN-XXXX'
+    input_list = [{'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h4.', 'summary': 'h4 task'}, \
+        {'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task', 'description':'h5 desc'}, {'text':'text line'}, \
+        {'issuetype': 'Sub-task', 'assignee': 'assignee', 'markup': '#', 'summary': 'sub-task'}]
+    expected_result = 'h4. h4 task (DRY-RUN-XXXX)\nh5. h5 task (DRY-RUN-XXXX)\nh5 desc\ntext line\n# sub-task (DRY-RUN-XXXX)'
+    self.assertEquals(self.te.create_tasks(input_list), expected_result)
+
+  def test_create_tasks_Tasks_with_no_h4_task(self):
+    self.te.create_issue = MagicMock()
+    self.te.create_issue.return_value = 'DRY-RUN-XXXX'
+    input_list = [{'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task 1'}, \
+        {'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task 2'}, \
+        {'issuetype': 'Sub-task', 'assignee': 'assignee', 'markup': '#', 'summary': 'sub-task of h5 task 2'}]
+    expected_result = [call({'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task 1'}), \
+        call({'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task 2'}), \
+        call({'parent': 'DRY-RUN-XXXX', 'markup': '#', 'summary': 'sub-task of h5 task 2', 'assignee': 'assignee', 'issuetype': 'Sub-task'})]
+    expected_output = 'h5. h5 task 1 (DRY-RUN-XXXX)\nh5. h5 task 2 (DRY-RUN-XXXX)\n# sub-task of h5 task 2 (DRY-RUN-XXXX)'
+    self.assertEquals(self.te.create_tasks(input_list), expected_output)
+    self.assertEquals(self.te.create_issue.call_args_list, expected_result)
+
+##########################################################
+### jira_format
+
+  def test_jira_format_Simple_case(self):
+    input_dict = {'parent': 'DRY-RUN-XXXX', 'markup': '#', 'summary': 'sub-task', 'assignee': 'assignee', 'issuetype': 'Sub-task'}
+    expected_result = {'fields': { 'parent': {'key': 'DRY-RUN-XXXX'}, \
+        'summary': 'sub-task', 'assignee': {'name': 'assignee'}, 'issuetype': {'name': 'Sub-task'}}}
+    self.assertEquals(self.te.jira_format(input_dict), expected_result)
+
+  def test_jira_format_If_additional_fields_provided(self): 
+    options = {'project': {'key':'TestProject'}, 'item1':['subitem1', 'subitem2']}
+    self.te = TaskExtractor(self.jira_url, "", "", options, dry_run = True)
+    input_dict = {'parent': 'DRY-RUN-XXXX', 'markup': '#', 'summary': 'sub-task', 'assignee': 'assignee', 'issuetype': 'Sub-task'}
+    expected_result = {'fields': {'parent': {'key': 'DRY-RUN-XXXX'}, 'summary': 'sub-task', 'project': {'key': 'TestProject'}, \
+         'assignee': {'name': 'assignee'}, 'issuetype': {'name': 'Sub-task'}, 'item1':['subitem1', 'subitem2']}}
+    self.assertEquals(self.te.jira_format(input_dict), expected_result)
+
+  def test_jira_format_Replaces_default_params_by_tmpl_json(self):
+    options = {'project': {'key':'TestProject'}, 'duedate':'2012-03-01', 'item1':'default_value'}
+    self.te = TaskExtractor(self.jira_url, "", "", options, dry_run = True)
+    input_json = {'duedate':'2012-04-01', 'issuetype': 'Task', 'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task', 'tmpl_ext':{'item1':'template_value'}}
+    expected_result = {'fields': {'summary': 'h5 task', 'project': {'key': 'TestProject'}, 'duedate':'2012-04-01', \
+          'assignee': {'name': 'assignee'}, 'issuetype': {'name': 'Task'}, 'item1':'template_value'}}
+    self.assertEquals(self.te.jira_format(input_json), expected_result)
+
+
+##########################################################
+### integration tests
+
+#  def test_full_run(self):
+#    input_text = """
+#{"item":"value"}
+#[VAR1=h5.]
+#[VAR2= h5 task]
+#$VAR1$VAR2 *assignee*
+#h5. h5 task2  *assignee* {"item":"new_value"}
+#"""
+#    excpected_result = [{'assignee': 'assignee', 'markup': 'h5.', 'summary': 'h5 task'}, \
+#            {'assignee': 'assignee', 'markup': 'h5', 'summary': 'h5 task2', 'tmpl_ext': {'item': 'new_value'}}]
+#    load_result = self.te.load(input_text)
+#    self.assertEquals(excpected_result, load_result)
+
+
+
+#if __name__ == "__main__":
+#      unittest.main()
+
+import nose
+nose.run(argv=["", "test_task_extractor", "--verbosity=2"])
+
+from setuptools import setup, find_packages
+
+setup(
+    name='jira-bulk-loader',
+    version='0.1',
+    packages=['jirabulkloader','jirabulkloader.test',],
+    author='Alexander Dudko',
+    author_email='alex.dudko@gmail.com',
+    license='GPLv3',
+    url='http://bitbucket.org/oktopuz/jira-bulk-loader',
+    scripts=['bin/jira-bulk-loader.py'],
+    description='An automation tool for creating tasks in Jira via RESTful API',
+    long_description=open('README.rst').read(),
+    install_requires=[
+        "simplejson >= 2.6.1",
+        "requests >= 0.14.0",
+        "argparse >= 1.2.1",
+    ],
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Console',
+        'Intended Audience :: Information Technology',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Topic :: Utilities',
+        'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
+    ],
+)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.