From 12f64de5d0f298c800ee8bb39b4bc4db630ba67b Mon Sep 17 00:00:00 2001 From: tarasmatsyk Date: Tue, 27 Dec 2016 15:19:33 +0200 Subject: [PATCH 01/42] allow to define internal comments in service desk --- jira/client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/jira/client.py b/jira/client.py index fffca99b9..357d994c2 100644 --- a/jira/client.py +++ b/jira/client.py @@ -959,7 +959,7 @@ def comment(self, issue, comment): return self._find_for_resource(Comment, (issue, comment)) @translate_resource_args - def add_comment(self, issue, body, visibility=None): + def add_comment(self, issue, body, visibility=None, is_internal=False): """Add a comment from the current authenticated user on the specified issue and return a Resource for it. The issue identifier and comment body are required. @@ -970,15 +970,20 @@ def add_comment(self, issue, body, visibility=None): "type" is 'role' (or 'group' if the JIRA server has configured comment visibility for groups) and 'value' is the name of the role (or group) to which viewing of this comment will be restricted. + :param is_internal: defines whether a comment has to be marked as 'Internal' in Jira Service Desk """ data = { - 'body': body} + 'body': body, + 'properties':[{'key':'sd.public.comment','value':{'internal':is_internal}},] + } + if visibility is not None: data['visibility'] = visibility url = self._get_url('issue/' + str(issue) + '/comment') r = self._session.post( - url, data=json.dumps(data)) + url, data=json.dumps(data) + ) comment = Comment(self._options, self._session, raw=json_loads(r)) return comment From 7e96b77beff6c581e36a1b3b1923ac05e4aec5ff Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Fri, 6 Jan 2017 13:18:17 -0500 Subject: [PATCH 02/42] Flake8 cleanup --- jira/resources.py | 2 +- tests/tests.py | 27 ++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/jira/resources.py b/jira/resources.py index 001499564..8073fe0ed 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -336,7 +336,7 @@ def __init__(self, options, session, raw=None): def get(self): """Return the file content as a string.""" - r = self._session.get(self.content, headers={'Accept':'*/*'}) + r = self._session.get(self.content, headers={'Accept': '*/*'}) return r.content def iter_content(self, chunk_size=1024): diff --git a/tests/tests.py b/tests/tests.py index 0ba920687..c21079089 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -742,8 +742,7 @@ def test_create_issues(self): 'name': 'Bug'}, # 'customfield_10022': 'XSS', 'priority': { - 'name': 'Major'}}, - { + 'name': 'Major'}}, { 'project': { 'key': self.project_a}, 'issuetype': { @@ -763,7 +762,7 @@ def test_create_issues(self): self.assertEqual(issues[1]['issue'].fields.summary, 'Issue created via bulk create #2') self.assertEqual(issues[1]['issue'].fields.description, - "Another new issue for bulk test") + "Another new issue for bulk test") self.assertEqual(issues[1]['issue'].fields.issuetype.name, 'Bug') self.assertEqual(issues[1]['issue'].fields.project.key, self.project_a) self.assertEqual(issues[1]['issue'].fields.priority.name, 'Major') @@ -781,8 +780,7 @@ def test_create_issues_one_failure(self): 'name': 'Bug'}, # 'customfield_10022': 'XSS', 'priority': { - 'name': 'Major'}}, - { + 'name': 'Major'}}, { 'project': { 'key': self.project_a}, 'issuetype': { @@ -790,8 +788,7 @@ def test_create_issues_one_failure(self): 'summary': 'This issue will not succeed', 'description': "Should not be seen.", 'priority': { - 'name': 'Blah'}}, - { + 'name': 'Blah'}}, { 'project': { 'key': self.project_a}, 'issuetype': { @@ -827,14 +824,14 @@ def test_create_issues_one_failure(self): @not_on_custom_jira_instance def test_create_issues_without_prefetch(self): - field_list= [dict(project=self.project_b, - summary='Test issue created', - description='blahery', - issuetype={'name': 'Bug'}), - dict(project=self.project_a, - summary='Test issue #2', - description='fooery', - issuetype={'name': 'Bug'})] + field_list = [dict(project=self.project_b, + summary='Test issue created', + description='blahery', + issuetype={'name': 'Bug'}), + dict(project=self.project_a, + summary='Test issue #2', + description='fooery', + issuetype={'name': 'Bug'})] issues = self.jira.create_issues(field_list, prefetch=False) assert hasattr(issues[0]['issue'], 'self') From e6ac98ad5aed2491818e6ade8d5651e82b1292f0 Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Fri, 6 Jan 2017 17:31:42 -0500 Subject: [PATCH 03/42] Add support for customer, request_type and create_customer_requests --- jira/client.py | 101 ++++++++++++++++++++++++++++++++++++++++++++++ jira/resources.py | 33 ++++++++++++++- 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index edbb239ac..fbd2504c4 100644 --- a/jira/client.py +++ b/jira/client.py @@ -935,6 +935,88 @@ def create_issues(self, field_list, prefetch=True): 'error': None, 'input_fields': fields}) return issue_list + def create_customer(self, email, displayName): + """Create a new customer and return an issue Resource for it. + """ + url = self._options['server'] + '/rest/servicedeskapi/customer' + headers = {'X-ExperimentalApi': 'opt-in'} + r = self._session.post(url, headers=headers, data=json.dumps({ + 'email': email, + 'displayName': displayName + })) + + raw_customer_json = json_loads(r) + + if r.status_code != 201: + raise JIRAError(r.status_code, request=r) + return Customer(self._options, self._session, raw=raw_customer_json) + + def service_desks(self): + """Get a list of ServiceDesk Resources from the server visible to the current authenticated user.""" + url = self._options['server'] + '/rest/servicedeskapi/servicedesk' + headers = {'X-ExperimentalApi': 'opt-in'} + r_json = json_loads(self._session.get(url, headers=headers)) + projects = [ServiceDesk(self._options, self._session, raw_project_json) + for raw_project_json in r_json['values']] + return projects + + def service_desk(self, id): + """Get a Service Desk Resource from the server. + + :param id: ID or key of the Service Desk to get + """ + return self._find_for_resource(ServiceDesk, id) + + def create_customer_request(self, fields=None, prefetch=True, **fieldargs): + """Create a new customer request and return an issue Resource for it. + + Each keyword argument (other than the predefined ones) is treated as a field name and the argument's value + is treated as the intended value for that field -- if the fields argument is used, all other keyword arguments + will be ignored. + + By default, the client will immediately reload the issue Resource created by this method in order to return + a complete Issue object to the caller; this behavior can be controlled through the 'prefetch' argument. + + JIRA projects may contain many different issue types. Some issue screens have different requirements for + fields in a new issue. This information is available through the 'createmeta' method. Further examples are + available here: https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+Example+-+Create+Issue + + :param fields: a dict containing field names and the values to use. If present, all other keyword arguments + will be ignored + :param prefetch: whether to reload the created issue Resource so that all of its data is present in the value + returned from this method + """ + data = fields + + p = data['serviceDeskId'] + service_desk = None + + if isinstance(p, string_types) or isinstance(p, integer_types): + service_desk = self.service_desk(p) + elif isinstance(p, ServiceDesk): + service_desk = p + + data['serviceDeskId'] = service_desk.id + + p = data['requestTypeId'] + if isinstance(p, integer_types): + data['requestTypeId'] = p + elif isinstance(p, string_types): + data['requestTypeId'] = self.request_type_by_name( + service_desk, p).id + + url = self._options['server'] + '/rest/servicedeskapi/request' + headers = {'X-ExperimentalApi': 'opt-in'} + r = self._session.post(url, headers=headers, data=json.dumps(data)) + + raw_issue_json = json_loads(r) + if 'issueKey' not in raw_issue_json: + raise JIRAError(r.status_code, request=r) + if prefetch: + return self.issue(raw_issue_json['issueKey']) + else: + return Issue(self._options, self._session, raw=raw_issue_json) + def createmeta(self, projectKeys=None, projectIds=[], issuetypeIds=None, issuetypeNames=None, expand=None): """Get the metadata required to create issues, optionally filtered by projects and issue types. @@ -1468,6 +1550,25 @@ def issue_type_by_name(self, name): raise KeyError("Issue type '%s' is unknown." % name) return issue_type + def request_types(self, service_desk): + url = (self._options['server'] + + '/rest/servicedeskapi/servicedesk/%s/requesttype' + % service_desk.id) + headers = {'X-ExperimentalApi': 'opt-in'} + r_json = json_loads(self._session.get(url, headers=headers)) + request_types = [ + RequestType(self._options, self._session, raw_type_json) + for raw_type_json in r_json['values']] + return request_types + + def request_type_by_name(self, service_desk, name): + request_types = self.request_types(service_desk) + try: + request_type = [rt for rt in request_types if rt.name == name][0] + except IndexError: + raise KeyError("Request type '%s' is unknown." % name) + return request_type + # User permissions # non-resource diff --git a/jira/resources.py b/jira/resources.py index 8073fe0ed..23589a80e 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -48,7 +48,10 @@ def emit(self, record): 'Status', 'User', 'CustomFieldOption', - 'RemoteLink' + 'RemoteLink', + 'Customer', + 'ServiceDesk', + 'RequestType', ) logging.getLogger('jira').addHandler(NullHandler()) @@ -819,6 +822,34 @@ def delete(self, params=None): Resource.delete(self, params) +# Service Desk + +class Customer(Resource): + """A Service Desk customer.""" + + def __init__(self, options, session, raw=None): + Resource.__init__(self, 'customer', options, session, '{server}/rest/servicedeskapi/{path}') + if raw: + self._parse_raw(raw) + + +class ServiceDesk(Resource): + """A Service Desk.""" + + def __init__(self, options, session, raw=None): + Resource.__init__(self, 'servicedesk/{0}', options, session, '{server}/rest/servicedeskapi/{path}') + if raw: + self._parse_raw(raw) + + +class RequestType(Resource): + """A Service Desk Request Type.""" + + def __init__(self, options, session, raw=None): + Resource.__init__(self, 'servicedesk/{0}/requesttype', options, session, '{server}/rest/servicedeskapi/{path}') + if raw: + self._parse_raw(raw) + # Utilities From a57d7401b69607baafdc3a8d9b0c20d24acc8d1a Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 9 Jan 2017 10:45:04 -0500 Subject: [PATCH 04/42] Explicitly import resources --- jira/client.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index fbd2504c4..69f03f5c1 100644 --- a/jira/client.py +++ b/jira/client.py @@ -42,7 +42,34 @@ def emit(self, record): from six import iteritems from six.moves.urllib.parse import urlparse # JIRA specific resources -from jira.resources import * # NOQA +from jira.resources import ( + Resource, + Issue, + Comment, + Project, + Attachment, + Component, + Dashboard, + Filter, + Votes, + Watchers, + Worklog, + IssueLink, + IssueLinkType, + IssueType, + Priority, + Version, + Role, + Resolution, + SecurityLevel, + Status, + User, + CustomFieldOption, + RemoteLink, + Customer, + ServiceDesk, + RequestType, +) # GreenHopper specific resources from jira.exceptions import JIRAError From aee704328e28b4baa5fd2b134e0d226c627ddcb7 Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Fri, 17 Feb 2017 15:02:13 -0500 Subject: [PATCH 05/42] Allow service desk parameter to be an integer --- jira/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 69f03f5c1..756359f8a 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1578,9 +1578,11 @@ def issue_type_by_name(self, name): return issue_type def request_types(self, service_desk): + if hasattr(service_desk, 'id'): + service_desk = service_desk.id url = (self._options['server'] + '/rest/servicedeskapi/servicedesk/%s/requesttype' - % service_desk.id) + % service_desk) headers = {'X-ExperimentalApi': 'opt-in'} r_json = json_loads(self._session.get(url, headers=headers)) request_types = [ From 179feb6514b120a9460a2540794db9472533c8cd Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Fri, 17 Feb 2017 15:08:49 -0500 Subject: [PATCH 06/42] Only add Service Desk properties if is_internal is set --- jira/client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 357d994c2..4144745f5 100644 --- a/jira/client.py +++ b/jira/client.py @@ -974,9 +974,16 @@ def add_comment(self, issue, body, visibility=None, is_internal=False): """ data = { 'body': body, - 'properties':[{'key':'sd.public.comment','value':{'internal':is_internal}},] } + if is_internal: + data.update({ + 'properties': [ + {'key': 'sd.public.comment', + 'value': {'internal': is_internal}} + ] + }) + if visibility is not None: data['visibility'] = visibility From 1767b5c287ffbbf2362fdbc4380c4ddd64fa539c Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 18:51:20 -0500 Subject: [PATCH 07/42] Add supports_service_desk check --- jira/client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/jira/client.py b/jira/client.py index b42bb9aea..b1e3cc8fe 100644 --- a/jira/client.py +++ b/jira/client.py @@ -981,6 +981,15 @@ def create_issues(self, field_list, prefetch=True): 'error': None, 'input_fields': fields}) return issue_list + def supports_service_desk(self): + url = self._options['server'] + '/rest/servicedeskapi/info' + headers = {'X-ExperimentalApi': 'opt-in'} + try: + r = self._session.get(url, headers=headers) + return r.status_code == 200 + except JIRAError: + return False + def create_customer(self, email, displayName): """Create a new customer and return an issue Resource for it. """ From 4ef609fe36e62c2061813d9309437791b052a76b Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 18:56:50 -0500 Subject: [PATCH 08/42] PEP-8 --- jira/client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jira/client.py b/jira/client.py index b1e3cc8fe..e97ac83b0 100644 --- a/jira/client.py +++ b/jira/client.py @@ -991,8 +991,7 @@ def supports_service_desk(self): return False def create_customer(self, email, displayName): - """Create a new customer and return an issue Resource for it. - """ + """Create a new customer and return an issue Resource for it.""" url = self._options['server'] + '/rest/servicedeskapi/customer' headers = {'X-ExperimentalApi': 'opt-in'} r = self._session.post(url, headers=headers, data=json.dumps({ From 31767771e786c9375716a1bb9fa7df17c85201ab Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 19:01:36 -0500 Subject: [PATCH 09/42] PEP-8 H301 --- jira/client.py | 54 ++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/jira/client.py b/jira/client.py index e97ac83b0..844cd31f3 100644 --- a/jira/client.py +++ b/jira/client.py @@ -42,34 +42,32 @@ def emit(self, record): from six import iteritems from six.moves.urllib.parse import urlparse # JIRA specific resources -from jira.resources import ( - Resource, - Issue, - Comment, - Project, - Attachment, - Component, - Dashboard, - Filter, - Votes, - Watchers, - Worklog, - IssueLink, - IssueLinkType, - IssueType, - Priority, - Version, - Role, - Resolution, - SecurityLevel, - Status, - User, - CustomFieldOption, - RemoteLink, - Customer, - ServiceDesk, - RequestType, -) +from jira.resources import Resource +from jira.resources import Issue +from jira.resources import Comment +from jira.resources import Project +from jira.resources import Attachment +from jira.resources import Component +from jira.resources import Dashboard +from jira.resources import Filter +from jira.resources import Votes +from jira.resources import Watchers +from jira.resources import Worklog +from jira.resources import IssueLink +from jira.resources import IssueLinkType +from jira.resources import IssueType +from jira.resources import Priority +from jira.resources import Version +from jira.resources import Role +from jira.resources import Resolution +from jira.resources import SecurityLevel +from jira.resources import Status +from jira.resources import User +from jira.resources import CustomFieldOption +from jira.resources import RemoteLink +from jira.resources import Customer +from jira.resources import ServiceDesk +from jira.resources import RequestType # GreenHopper specific resources from jira.exceptions import JIRAError From 1c1a80a198c2fc54512c94abdc6873d19d0b737f Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 19:07:24 -0500 Subject: [PATCH 10/42] PEP-8 H306 --- jira/client.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/jira/client.py b/jira/client.py index 844cd31f3..5017492d9 100644 --- a/jira/client.py +++ b/jira/client.py @@ -41,41 +41,41 @@ def emit(self, record): from requests.utils import get_netrc_auth from six import iteritems from six.moves.urllib.parse import urlparse + +# GreenHopper specific resources +from jira.exceptions import JIRAError +from jira.resilientsession import raise_on_error +from jira.resilientsession import ResilientSession # JIRA specific resources -from jira.resources import Resource -from jira.resources import Issue -from jira.resources import Comment -from jira.resources import Project from jira.resources import Attachment +from jira.resources import Board +from jira.resources import Comment from jira.resources import Component +from jira.resources import Customer +from jira.resources import CustomFieldOption from jira.resources import Dashboard from jira.resources import Filter -from jira.resources import Votes -from jira.resources import Watchers -from jira.resources import Worklog +from jira.resources import GreenHopperResource +from jira.resources import Issue from jira.resources import IssueLink from jira.resources import IssueLinkType from jira.resources import IssueType from jira.resources import Priority -from jira.resources import Version -from jira.resources import Role +from jira.resources import Project +from jira.resources import RemoteLink +from jira.resources import RequestType from jira.resources import Resolution +from jira.resources import Resource +from jira.resources import Role from jira.resources import SecurityLevel -from jira.resources import Status -from jira.resources import User -from jira.resources import CustomFieldOption -from jira.resources import RemoteLink -from jira.resources import Customer from jira.resources import ServiceDesk -from jira.resources import RequestType - -# GreenHopper specific resources -from jira.exceptions import JIRAError -from jira.resilientsession import raise_on_error -from jira.resilientsession import ResilientSession -from jira.resources import Board -from jira.resources import GreenHopperResource from jira.resources import Sprint +from jira.resources import Status +from jira.resources import User +from jira.resources import Version +from jira.resources import Votes +from jira.resources import Watchers +from jira.resources import Worklog from jira import __version__ from jira.utils import CaseInsensitiveDict From 87855fda9b163a29fe6247c003e5e0bc64a6befb Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 19:50:06 -0500 Subject: [PATCH 11/42] Add a test for create_customer_request --- tests/tests.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index a69906e45..b9fbdb7b5 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -2104,6 +2104,34 @@ def test_jirashell_command_exists(self): self.assertEqual(result, 0) +class JiraServiceDeskTests(unittest.TestCase): + + def setUp(self): + self.jira = JiraTestManager().jira_admin + self.test_manager = JiraTestManager() + + @not_on_custom_jira_instance + def test_supports_service_desk(self): + self.assertTrue(self.jira.supports_service_desk()) + + @not_on_custom_jira_instance + def test_create_customer_request(self): + service_desk = self.jira.service_desks()[0] + request_type = self.jira.request_types(service_desk)[0] + + request = self.jira.create_customer_request(dict( + serviceDeskId=service_desk, + requestTypeId=request_type, + requestFieldValues=dict( + summary='Ticket title here', + description='Ticket body here' + ) + )) + + self.assertEqual(request.fields.summary, 'Ticket title here') + self.assertEqual(request.fields.description, 'Ticket body here') + + if __name__ == '__main__': # when running tests we expect various errors and we don't want to display them by default From 76aad99bfe2eeef43a943d3c690c31d6da22bed3 Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 20:29:47 -0500 Subject: [PATCH 12/42] Create Service Desk project during testing if it is missing --- jira/client.py | 7 ++++++- tests/tests.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 5017492d9..a436d56ac 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2734,12 +2734,17 @@ def create_project(self, key, name=None, assignee=None, type="Software", templat r = self._session.get(url) j = json_loads(r) + possible_templates = ['JIRA Classic', 'JIRA Default Schemes', 'Basic software development'] + + if template_name is not None: + possible_templates = [template_name] + # https://confluence.atlassian.com/jirakb/creating-a-project-via-rest-based-on-jira-default-schemes-744325852.html template_key = 'com.atlassian.jira-legacy-project-templates:jira-blank-item' templates = [] for template in _get_template_list(j): templates.append(template['name']) - if template['name'] in ['JIRA Classic', 'JIRA Default Schemes', 'Basic software development', template_name]: + if template['name'] in possible_templates: template_key = template['projectTemplateModuleCompleteKey'] break diff --git a/tests/tests.py b/tests/tests.py index b9fbdb7b5..f2520bc5d 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -2116,6 +2116,10 @@ def test_supports_service_desk(self): @not_on_custom_jira_instance def test_create_customer_request(self): + try: + self.jira.create_project('TESTSD', template_name='IT Service Desk') + except JIRAError: + pass service_desk = self.jira.service_desks()[0] request_type = self.jira.request_types(service_desk)[0] From 8d284b63ab28ae03eb0d0cd8958dea026749f467 Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 20:46:23 -0500 Subject: [PATCH 13/42] Pass ids when creating a customer request --- tests/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index f2520bc5d..56968f850 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -2124,8 +2124,8 @@ def test_create_customer_request(self): request_type = self.jira.request_types(service_desk)[0] request = self.jira.create_customer_request(dict( - serviceDeskId=service_desk, - requestTypeId=request_type, + serviceDeskId=service_desk.id, + requestTypeId=request_type.id, requestFieldValues=dict( summary='Ticket title here', description='Ticket body here' From 8e6675cdb82a81aa6c9eed46f3035ed613fc5c4e Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 20:46:42 -0500 Subject: [PATCH 14/42] Skip Service Desk tests on instances that don't support it --- tests/tests.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 56968f850..2cc93d42f 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -2110,12 +2110,10 @@ def setUp(self): self.jira = JiraTestManager().jira_admin self.test_manager = JiraTestManager() - @not_on_custom_jira_instance - def test_supports_service_desk(self): - self.assertTrue(self.jira.supports_service_desk()) - - @not_on_custom_jira_instance def test_create_customer_request(self): + if not self.jira.supports_service_desk(): + pytest.skip('Skipping Service Desk not enabled') + try: self.jira.create_project('TESTSD', template_name='IT Service Desk') except JIRAError: From 0e6a57f75acbeb31b3c58cddb26fc77d71704876 Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 21:00:23 -0500 Subject: [PATCH 15/42] Pass integer ids when creating customer requests --- tests/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 2cc93d42f..b831a23e5 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -2122,8 +2122,8 @@ def test_create_customer_request(self): request_type = self.jira.request_types(service_desk)[0] request = self.jira.create_customer_request(dict( - serviceDeskId=service_desk.id, - requestTypeId=request_type.id, + serviceDeskId=int(service_desk.id), + requestTypeId=int(request_type.id), requestFieldValues=dict( summary='Ticket title here', description='Ticket body here' From d6bdf357c379ea1b6ac7e59505173e7a823561ef Mon Sep 17 00:00:00 2001 From: Dan Bravender Date: Mon, 20 Feb 2017 21:13:46 -0500 Subject: [PATCH 16/42] Forcing a rebuild --- tests/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests.py b/tests/tests.py index b831a23e5..811549e7c 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -2122,7 +2122,7 @@ def test_create_customer_request(self): request_type = self.jira.request_types(service_desk)[0] request = self.jira.create_customer_request(dict( - serviceDeskId=int(service_desk.id), + serviceDeskId=service_desk.id, requestTypeId=int(request_type.id), requestFieldValues=dict( summary='Ticket title here', From 7934cde84d8286ce73435e1308c798de5de029ae Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Mon, 27 Feb 2017 17:00:25 +0000 Subject: [PATCH 17/42] linting fixes --- .editorconfig | 7 +++++++ jira/jirashell.py | 1 + jira/resources.py | 1 + setup.cfg | 6 +++++- setup.py | 1 + 5 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..924a7410a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +max_line_length = 160 diff --git a/jira/jirashell.py b/jira/jirashell.py index 307815ac0..578ddb88e 100644 --- a/jira/jirashell.py +++ b/jira/jirashell.py @@ -256,6 +256,7 @@ def main(): ipshell("*** JIRA shell active; client is in 'jira'." ' Press Ctrl-D to exit.') + if __name__ == '__main__': status = main() exit(status) diff --git a/jira/resources.py b/jira/resources.py index e6b14c618..7a0f6f197 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -898,6 +898,7 @@ def dict2resource(raw, top=None, options=None, session=None): setattr(top, i, j) return top + resource_class_map = { # JIRA specific resources r'attachment/[^/]+$': Attachment, diff --git a/setup.cfg b/setup.cfg index cf8fba8bf..a11a67dbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,4 +82,8 @@ statistics=yes ;pep8 --statistics -qq $PEP8_OPTS [pbr] -warnerrors = true \ No newline at end of file +warnerrors = true + +[pycodestyle] +max-line-length=160 +exclude = .eggs,.tox,build diff --git a/setup.py b/setup.py index caa98df2e..c62c0ee90 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ def run_tests(self): errno = pytest.main(self.pytest_args) sys.exit(errno) + setuptools.setup( setup_requires=['pbr>=1.9', 'setuptools>=17.1', 'pytest-runner'], pbr=True, From 5857a5d84fa7363a93802029f8b48052bd043125 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Mon, 13 Feb 2017 18:32:45 +0000 Subject: [PATCH 18/42] Fixed github deploy credentials --- .travis.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0579639b9..e7ed26ade 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,15 +30,19 @@ notifications: - pycontribs@googlegroups.com - sorin.sbarnea@gmail.com deploy: + +deploy: - provider: releases api_key: - secure: gr9iOcQjdoAyUAim6FWKzJI9MBaJo9XKfGQGu7wdPXUFhg80Rp6GLJsowP+aU94NjXM1UQlVHDAy627WtjBlLH2SvmVEIIr7+UKBopBYuXG5jJ1m3wOZE+4f1Pqe9bqFc1DxgucqE8qF0sC24fIbNM2ToeyYrxrS6RoL2gRrX2I= + secure: G19YtkGAX0aJ1oyd/7eRj1KYdsmAkjkfU2UISvsjh/68ec1+9qtPpN7BbkFYZYMjSx0BtS0SEEA7Vdl4F9DI9Zzqahbj7WzDLFe9/4aZKM/ztfKWR6CNAYaMazAKS5W7r9pPkBBDIIJ9zCqvV7FRzjewEpfTwFzwUdY+IpxEsAM= file: - - dist/$PACKAGE_NAME-$PACKAGE_VERSION* + - dist/$PACKAGE_NAME-$PACKAGE_VERSION.tar.gz + - dist/$PACKAGE_NAME-$PACKAGE_VERSION-py2.py3-none-any.whl - ChangeLog skip_cleanup: true on: - tags: false + repo: pycontribs/jira + tags: true python: 2.7 condition: $TOXENV != docs - provider: pypi From 608d7d1dde2f504ae6c22cf79d9614d7ee3db82f Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Sat, 4 Mar 2017 21:27:09 +0000 Subject: [PATCH 19/42] build logic and code cleanup Signed-off-by: Sorin Sbarnea --- .travis.yml | 8 +- README.rst | 18 ++--- jira/utils/lru_cache.py | 170 ---------------------------------------- jira/utils/version.py | 82 ------------------- tox.ini | 1 - 5 files changed, 8 insertions(+), 271 deletions(-) delete mode 100644 jira/utils/lru_cache.py delete mode 100644 jira/utils/version.py diff --git a/.travis.yml b/.travis.yml index e7ed26ade..39318d539 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,15 +12,15 @@ python: install: - pip -q --log dist/pip.log install --upgrade pip setuptools tox-travis py wheel - python setup.py sdist bdist_wheel install -- pip install ./dist/jira-*.whl +- pip install ./dist/*.whl - pip --version script: - export PACKAGE_NAME=$(python setup.py --name) - export PACKAGE_VERSION=$(python setup.py --version) - python setup.py --version -- tox --installpkg ./dist/jira-*.whl --travis-after +- tox --installpkg ./dist/*.whl --travis-after # validates that the build source distribution is installable using the old easy_install -- pip uninstall -y jira && easy_install ./dist/jira-*.tar.gz +- pip uninstall -y $PACKAGE_NAME && easy_install ./dist/$PACKAGE_NAME-*.tar.gz after_success: - coveralls - bash <(curl -s https://codecov.io/bash) @@ -30,8 +30,6 @@ notifications: - pycontribs@googlegroups.com - sorin.sbarnea@gmail.com deploy: - -deploy: - provider: releases api_key: secure: G19YtkGAX0aJ1oyd/7eRj1KYdsmAkjkfU2UISvsjh/68ec1+9qtPpN7BbkFYZYMjSx0BtS0SEEA7Vdl4F9DI9Zzqahbj7WzDLFe9/4aZKM/ztfKWR6CNAYaMazAKS5W7r9pPkBBDIIJ9zCqvV7FRzjewEpfTwFzwUdY+IpxEsAM= diff --git a/README.rst b/README.rst index 76586278b..4fc602c62 100644 --- a/README.rst +++ b/README.rst @@ -8,11 +8,11 @@ JIRA Python Library .. image:: https://img.shields.io/pypi/l/jira.svg :target: https://pypi.python.org/pypi/jira/ -.. image:: https://img.shields.io/pypi/dm/jira.svg +.. image:: https://img.shields.io/pypi/wheel/jira.svg :target: https://pypi.python.org/pypi/jira/ -.. image:: https://img.shields.io/pypi/wheel/Django.svg - :target: https://pypi.python.org/pypi/jira/ +.. image:: https://img.shields.io/codeclimate/issues/github/pycontribs/jira.svg + :target: https://github.com/pycontribs/jira/issues ------------ @@ -22,9 +22,6 @@ JIRA Python Library .. image:: https://api.travis-ci.org/pycontribs/jira.svg?branch=master :target: https://travis-ci.org/pycontribs/jira -.. image:: https://img.shields.io/pypi/status/jira.svg - :target: https://pypi.python.org/pypi/jira/ - .. image:: https://codecov.io/gh/pycontribs/jira/branch/develop/graph/badge.svg :target: https://codecov.io/gh/pycontribs/jira @@ -98,11 +95,10 @@ Credits In additions to all the contributors we would like to thank to these companies: -* Atlassian_ for developing such a powerful issue tracker and for providing a free on-demand JIRA_ instance that we can use for continous integration testing. +* Atlassian_ for developing such a powerful issue tracker and for providing a free on-demand JIRA_ instance that we can use for continuous integration testing. * JetBrains_ for providing us with free licenses of PyCharm_ -* Travis_ for hosting our continous integration +* Travis_ for hosting our continuous integration * Navicat_ for providing us free licenses of their powerful database client GUI tools. -* Citrix_ for providing maintenance of the library. .. _Atlassian: https://www.atlassian.com/ .. _JIRA: https://pycontribs.atlassian.net @@ -110,7 +106,6 @@ In additions to all the contributors we would like to thank to these companies: .. _PyCharm: http://www.jetbrains.com/pycharm/ .. _Travis: https://travis-ci.org/ .. _navicat: https://www.navicat.com/ -.. _Citrix: http://www.citrix.com/ .. image:: https://images1-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&refresh=3600&resize_h=50&url=https://www.atlassian.com/dms/wac/images/press/Atlassian-logos/logoAtlassianPNG.png :target: http://www.atlassian.com @@ -120,6 +115,3 @@ In additions to all the contributors we would like to thank to these companies: .. image:: https://images1-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&refresh=3600&resize_h=50&url=https://upload.wikimedia.org/wikipedia/en/9/90/PremiumSoft_Navicat_Premium_Logo.png :target: http://www.navicat.com/ - -.. image:: https://images1-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&refresh=3600&resize_h=50&url=https://www.citrix.com/content/dam/citrix/en_us/images/logos/citrix/citrix-logo-black.jpg - :target: http://www.citrix.com/ diff --git a/jira/utils/lru_cache.py b/jira/utils/lru_cache.py deleted file mode 100644 index 1b8237fcc..000000000 --- a/jira/utils/lru_cache.py +++ /dev/null @@ -1,170 +0,0 @@ -try: - from functools import lru_cache - -except ImportError: - # backport of Python's 3.3 lru_cache, written by Raymond Hettinger and - # licensed under MIT license, from: - # - # Should be removed when Django only supports Python 3.2 and above. - - from collections import namedtuple - from functools import update_wrapper - from threading import RLock - - _CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) - - class _HashedSeq(list): - __slots__ = 'hashvalue' - - def __init__(self, tup, hash=hash): - self[:] = tup - self.hashvalue = hash(tup) - - def __hash__(self): - return self.hashvalue - - def _make_key(args, kwds, typed, - kwd_mark=(object(),), - fasttypes=set([int, str, frozenset, type(None)]), - sorted=sorted, tuple=tuple, type=type, len=len): - """Make a cache key from optionally typed positional and keyword arguments.""" - key = args - if kwds: - sorted_items = sorted(kwds.items()) - key += kwd_mark - for item in sorted_items: - key += item - if typed: - key += tuple(type(v) for v in args) - if kwds: - key += tuple(type(v) for k, v in sorted_items) - elif len(key) == 1 and type(key[0]) in fasttypes: - return key[0] - return _HashedSeq(key) - - def lru_cache(maxsize=100, typed=False): - """Least-recently-used cache decorator. - - If *maxsize* is set to None, the LRU features are disabled and the cache - can grow without bound. - - If *typed* is True, arguments of different types will be cached separately. - For example, f(3.0) and f(3) will be treated as distinct calls with - distinct results. - - Arguments to the cached function must be hashable. - - View the cache statistics named tuple (hits, misses, maxsize, currsize) with - f.cache_info(). Clear the cache and statistics with f.cache_clear(). - Access the underlying function with f.__wrapped__. - - See: https://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used - """ - # Users should only access the lru_cache through its public API: - # cache_info, cache_clear, and f.__wrapped__ - # The internals of the lru_cache are encapsulated for thread safety and - # to allow the implementation to change (including a possible C version). - - def decorating_function(user_function): - - cache = dict() - stats = [0, 0] # make statistics updateable non-locally - HITS, MISSES = 0, 1 # names for the stats fields - make_key = _make_key - cache_get = cache.get # bound method to lookup key or return None - _len = len # localize the global len() function - lock = RLock() # because linkedlist updates aren't threadsafe - root = [] # root of the circular doubly linked list - root[:] = [root, root, None, None] # initialize by pointing to self - nonlocal_root = [root] # make updateable non-locally - PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields - - if maxsize == 0: - - def wrapper(*args, **kwds): - # no caching, just do a statistics update after a successful call - result = user_function(*args, **kwds) - stats[MISSES] += 1 - return result - - elif maxsize is None: - - def wrapper(*args, **kwds): - # simple caching without ordering or size limit - key = make_key(args, kwds, typed) - result = cache_get(key, root) # root used here as a unique not-found sentinel - if result is not root: - stats[HITS] += 1 - return result - result = user_function(*args, **kwds) - cache[key] = result - stats[MISSES] += 1 - return result - - else: - - def wrapper(*args, **kwds): - # size limited caching that tracks accesses by recency - key = make_key(args, kwds, typed) if kwds or typed else args - with lock: - link = cache_get(key) - if link is not None: - # record recent use of the key by moving it to the front of the list - root, = nonlocal_root - link_prev, link_next, key, result = link - link_prev[NEXT] = link_next - link_next[PREV] = link_prev - last = root[PREV] - last[NEXT] = root[PREV] = link - link[PREV] = last - link[NEXT] = root - stats[HITS] += 1 - return result - result = user_function(*args, **kwds) - with lock: - root, = nonlocal_root - if key in cache: - # getting here means that this same key was added to the - # cache while the lock was released. since the link - # update is already done, we need only return the - # computed result and update the count of misses. - pass - elif _len(cache) >= maxsize: - # use the old root to store the new key and result - oldroot = root - oldroot[KEY] = key - oldroot[RESULT] = result - # empty the oldest link and make it the new root - root = nonlocal_root[0] = oldroot[NEXT] - oldkey = root[KEY] - root[KEY] = root[RESULT] = None - # now update the cache dictionary for the new links - del cache[oldkey] - cache[key] = oldroot - else: - # put result in a new link at the front of the list - last = root[PREV] - link = [last, root, key, result] - last[NEXT] = root[PREV] = cache[key] = link - stats[MISSES] += 1 - return result - - def cache_info(): - """Report cache statistics.""" - with lock: - return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) - - def cache_clear(): - """Clear the cache and cache statistics.""" - with lock: - cache.clear() - root = nonlocal_root[0] - root[:] = [root, root, None, None] - stats[:] = [0, 0] - - wrapper.__wrapped__ = user_function - wrapper.cache_info = cache_info - wrapper.cache_clear = cache_clear - return update_wrapper(wrapper, user_function) - - return decorating_function diff --git a/jira/utils/version.py b/jira/utils/version.py deleted file mode 100644 index 60652adf4..000000000 --- a/jira/utils/version.py +++ /dev/null @@ -1,82 +0,0 @@ -from __future__ import unicode_literals - -import datetime -import os -import subprocess - -from jira.utils.lru_cache import lru_cache - - -def get_version(version=None): - """Return a PEP 440-compliant version number from VERSION.""" - version = get_complete_version(version) - - # Now build the two parts of the version number: - # main = X.Y[.Z] - # sub = .devN - for pre-alpha releases - # | {a|b|rc}N - for alpha, beta, and rc releases - - main = get_main_version(version) - - sub = '' - if version[3] == 'alpha' and version[4] == 0: - git_changeset = get_git_changeset() - if git_changeset: - sub = '.dev%s' % git_changeset - - elif version[3] != 'final': - mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'} - sub = mapping[version[3]] + str(version[4]) - - return str(main + sub) - - -def get_main_version(version=None): - """Return main version (X.Y[.Z]) from VERSION.""" - version = get_complete_version(version) - parts = 2 if version[2] == 0 else 3 - return '.'.join(str(x) for x in version[:parts]) - - -def get_complete_version(version=None): - """Return a tuple of the jira version. - - If version argument is non-empty, then checks for correctness of the tuple provided. - """ - if version is None: - from jira import VERSION as version - else: - assert len(version) == 5 - assert version[3] in ('alpha', 'beta', 'rc', 'final') - - return version - - -def get_docs_version(version=None): - version = get_complete_version(version) - if version[3] != 'final': - return 'dev' - else: - return '%d.%d' % version[:2] - - -@lru_cache() -def get_git_changeset(): - """Return a numeric identifier of the latest git changeset. - - The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format. - This value isn't guaranteed to be unique, but collisions are very unlikely, - so it's sufficient for generating the development version numbers. - """ - repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - git_log = subprocess.Popen( - 'git log --pretty=format:%ct --quiet -1 HEAD', - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True, cwd=repo_dir, universal_newlines=True, - ) - timestamp = git_log.communicate()[0] - try: - timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) - except ValueError: - return None - return timestamp.strftime('%Y%m%d%H%M%S') diff --git a/tox.ini b/tox.ini index 1de64cf62..3ee614c8a 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,6 @@ commands= python -m pip check python -m flake8 python -m pytest -# removed -n4 due to fixture failure -n4 setenv = PYTHONPATH = passenv = From 2a7b3f1890e5af62def115d66fb5ef810e144fa1 Mon Sep 17 00:00:00 2001 From: Emmanuel Sciara Date: Fri, 10 Mar 2017 02:04:41 +0100 Subject: [PATCH 20/42] added .idea/codeStyleSettings.xml to repo --- .gitignore | 6 +++++- .idea/codeStyleSettings.xml | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .idea/codeStyleSettings.xml diff --git a/.gitignore b/.gitignore index 73124ac82..a06f39777 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ -.idea/ +# See http://stackoverflow.com/questions/5533050/gitignore-exclude-folder-but-include-specific-subfolder +# to understand pattern used to include .idea/codeStyleSettings.xml but not the rest of .idea/ +!.idea/ +.idea/* +!.idea/codeStyleSettings.xml *.bak *.egg *.egg-info/ diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 000000000..f8ec6c977 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file From d4f0df2c3ff1fbec61592671f74f1337baba1597 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Fri, 24 Mar 2017 20:22:56 +0000 Subject: [PATCH 21/42] #361 Made ipython a direct dependency Signed-off-by: Sorin Sbarnea --- requirements-opt.txt | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-opt.txt b/requirements-opt.txt index 25089042f..e875a9514 100644 --- a/requirements-opt.txt +++ b/requirements-opt.txt @@ -2,7 +2,6 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -ipython>=4.0.0 PyJWT requests_jwt requests_kerberos diff --git a/requirements.txt b/requirements.txt index c7ca345b7..dc26a6d7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ requests_toolbelt setuptools>=20.10.1 six>=1.10.0 defusedxml +ipython>=4.0.0 From 72ed43789d89ae4e9cbd564089197a951baa4500 Mon Sep 17 00:00:00 2001 From: Pavol Babincak Date: Tue, 16 May 2017 15:53:02 +0200 Subject: [PATCH 22/42] Accept custom authentication URL If JIRA instance doesn't accept default URL for authentication (/rest/auth/1/session) one can set custom one. All following works: - key 'auth_url' in options keyword of JIRA() class - option name 'auth_url' in 'options' section of ~/.jira-python/jirashell.ini - --auth-url option of jirashell --- jira/client.py | 3 ++- jira/jirashell.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index a436d56ac..9c1c13490 100644 --- a/jira/client.py +++ b/jira/client.py @@ -195,6 +195,7 @@ class JIRA(object): DEFAULT_OPTIONS = { "server": "http://localhost:2990/jira", + "auth_url": '/rest/auth/1/session', "context_path": "/", "rest_path": "api", "rest_api_version": "2", @@ -2222,7 +2223,7 @@ def version_count_unresolved_issues(self, id): def session(self): """Get a dict of the current authenticated user's session information.""" - url = '{server}/rest/auth/1/session'.format(**self._options) + url = '{server}{auth_url}'.format(**self._options) if isinstance(self._session.auth, tuple): authentication_data = { diff --git a/jira/jirashell.py b/jira/jirashell.py index 578ddb88e..c5f891a62 100644 --- a/jira/jirashell.py +++ b/jira/jirashell.py @@ -134,6 +134,8 @@ def process_command_line(): help='The JIRA instance to connect to, including context path.') jira_group.add_argument('-r', '--rest-path', help='The root path of the REST API to use.') + jira_group.add_argument('--auth-url', + help='Path to URL to auth against.') jira_group.add_argument('-v', '--rest-api-version', help='The version of the API under the specified name.') @@ -175,6 +177,9 @@ def process_command_line(): if args.rest_path: options['rest_path'] = args.rest_path + if args.auth_url: + options['auth_url'] = args.auth_url + if args.rest_api_version: options['rest_api_version'] = args.rest_api_version From 3293203a1dd28b79ad690ea32b9977111cb7a8e7 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Tue, 23 May 2017 23:10:35 +0100 Subject: [PATCH 23/42] Move requirements extras to setup.cfg Made cli dependencies optional. Install them using using `pip install jira[cli]` fixes #361 Change-Id: I73ff68d0fbfd8fcd947b516e04ef6fa489d9da9d Signed-off-by: Sorin Sbarnea --- Makefile | 77 ++++++++++++++++---------------------------- README.rst | 4 +++ requirements-all.txt | 3 -- requirements-dev.txt | 27 ---------------- requirements.txt | 12 ++----- setup.cfg | 34 ++++++++++++++++++- setup.py | 26 ++------------- tox.ini | 17 ++++------ 8 files changed, 76 insertions(+), 124 deletions(-) delete mode 100644 requirements-all.txt delete mode 100644 requirements-dev.txt diff --git a/Makefile b/Makefile index 10afc3f71..56cfebc6d 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,20 @@ -all: info clean flake8 test docs upload release +all: info clean lint test docs upload release .PHONY: all docs upload info req PACKAGE_NAME := $(shell python setup.py --name) PACKAGE_VERSION := $(shell python setup.py --version) PYTHON_PATH := $(shell which python) -PLATFORM := $(shell uname -s | awk '{print tolower($0)}') +PLATFORM := $(shell uname -s | awk '{print tolower($$0)}') DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) PYTHON_VERSION := $(shell python3 -c "import sys; print('py%s%s' % sys.version_info[0:2] + ('-conda' if 'conda' in sys.version or 'Continuum' in sys.version else ''))") -PYENV_HOME := $(DIR)/.tox/$(PYTHON_VERSION)-$(PLATFORM)/ -ifneq (,$(findstring conda,$(PYTHON_VERSION))) -CONDA:=1 -endif - +PREFIX="" ifndef GIT_BRANCH GIT_BRANCH=$(shell git branch | sed -n '/\* /s///p') endif info: @echo "INFO: Building $(PACKAGE_NAME):$(PACKAGE_VERSION) on $(GIT_BRANCH) branch" - @echo "INFO: Python $(PYTHON_VERSION) from $(PYENV_HOME) [$(CONDA)]" + @echo "INFO: Python $(PYTHON_VERSION) from '$(PREFIX)' [$(CONDA)]" clean: @find . -name "*.pyc" -delete @@ -28,66 +24,49 @@ package: python setup.py sdist bdist_wheel build_sphinx req: - @$(PYENV_HOME)/bin/requires.io update-site -t ac3bbcca32ae03237a6aae2b02eb9411045489bb -r $(PACKAGE_NAME) + @$(PREFIX)requires.io update-site -t ac3bbcca32ae03237a6aae2b02eb9411045489bb -r $(PACKAGE_NAME) install: prepare - $(PYENV_HOME)/bin/pip install . + $(PREFIX)pip install . install-sdk: # https://developer.atlassian.com/docs/getting-started/set-up-the-atlassian-plugin-sdk-and-build-a-project/install-the-atlassian-sdk-on-a-linux-or-mac-system#InstalltheAtlassianSDKonaLinuxorMacsystem-Homebrew which atlas-run-standalone || brew tap atlassian/tap && brew install atlassian/tap/atlassian-plugin-sdk uninstall: - $(PYENV_HOME)/bin/pip uninstall -y $(PACKAGE_NAME) - -venv: $(PYENV_HOME)/bin/activate - -# virtual environment depends on requriements files -$(PYENV_HOME)/bin/activate: requirements*.txt - @echo "INFO: (Re)creating virtual environment..." -ifdef CONDA - test -e $(PYENV_HOME)/bin/activate || conda create -y --prefix $(PYENV_HOME) pip -else - test -e $(PYENV_HOME)/bin/activate || virtualenv --python=$(PYTHON_PATH) --system-site-packages $(PYENV_HOME) -endif - $(PYENV_HOME)/bin/pip install -q -r requirements.txt -r requirements-opt.txt -r requirements-dev.txt - touch $(PYENV_HOME)/bin/activate - -prepare: venv - pyenv install -s 2.7.13 - pyenv install -s 3.4.5 - pyenv install -s 3.5.2 - pyenv install -s 3.6.0 - pyenv local 2.7.13 3.4.5 3.5.2 3.6.0 + $(PREFIX)pip uninstall -y $(PACKAGE_NAME) + +prepare: + @pyenv install -s 2.7.13 + @pyenv install -s 3.4.5 + @pyenv install -s 3.5.2 + @pyenv install -s 3.6.0 + @pyenv local 2.7.13 3.4.5 3.5.2 3.6.0 @echo "INFO: === Prearing to run for package:$(PACKAGE_NAME) platform:$(PLATFORM) py:$(PYTHON_VERSION) dir:$(DIR) ===" - if [ -f ${HOME}/testspace/testspace ]; then ${HOME}/testspace/testspace config url ${TESTSPACE_TOKEN}@pycontribs.testspace.com/jira/tests ; fi; + #if [ -f ${HOME}/testspace/testspace ]; then ${HOME}/testspace/testspace config url ${TESTSPACE_TOKEN}@pycontribs.testspace.com/jira/tests ; fi; testspace: ${HOME}/testspace/testspace publish build/results.xml -flake8: venv - @echo "INFO: flake8" - $(PYENV_HOME)/bin/python -m flake8 - $(PYENV_HOME)/bin/python -m flake8 --install-hook 2>/dev/null || true +lint: + @echo "INFO: linting...." + $(PREFIX)python -m flake8 + @$(PREFIX)python -m flake8 --install-hook 2>/dev/null || true -test: prepare flake8 +test: prepare lint @echo "INFO: test" - $(PYENV_HOME)/bin/python setup.py build test build_sphinx sdist bdist_wheel check --restructuredtext --strict - -test-cli: - $(PYENV_HOME)/bin/ipython -c "import jira; j = jira.JIRA('https://pycontribs.atlassian.net'); j.server_info()" -i + $(PREFIX)python setup.py build test build_sphinx sdist bdist_wheel check --restructuredtext --strict test-all: @echo "INFO: test-all (extended/matrix tests)" # tox should not run inside virtualenv because it does create and use multiple virtualenvs pip install -q tox tox-pyenv - python -m tox --skip-missing-interpreters true - + python -m tox docs: @echo "INFO: Building the docs" - $(PYENV_HOME)/bin/pip install sphinx - $(PYENV_HOME)/bin/python setup.py build_sphinx + $(PREFIX)pip install sphinx + $(PREFIX)python setup.py build_sphinx @mkdir -p docs/build/docset @mkdir -p docs/build/html/docset # cannot put doc2dash into requirements.txt file because is using pinned requirements @@ -114,11 +93,11 @@ endif upload: ifeq ($(GIT_BRANCH),develop) @echo "INFO: Upload package to testpypi.python.org" - $(PYENV_HOME)/bin/python setup.py check --restructuredtext --strict - $(PYENV_HOME)/bin/python setup.py sdist bdist_wheel upload -r https://testpypi.python.org/pypi + $(PREFIX)python setup.py check --restructuredtext --strict + $(PREFIX)python setup.py sdist bdist_wheel upload -r https://testpypi.python.org/pypi endif ifeq ($(GIT_BRANCH),master) @echo "INFO: Upload package to pypi.python.org" - $(PYENV_HOME)/bin/python setup.py check --restructuredtext --strict - $(PYENV_HOME)/bin/python setup.py sdist bdist_wheel upload + $(PREFIX)python setup.py check --restructuredtext --strict + $(PREFIX)python setup.py sdist bdist_wheel upload endif diff --git a/README.rst b/README.rst index 4fc602c62..eaa90b798 100644 --- a/README.rst +++ b/README.rst @@ -66,6 +66,10 @@ You can also try ``pip install --user --upgrade jira`` which will install or upgrade jira to your user directory. Or maybe you ARE using a virtualenv_ right? +By default only the basic library dependencies are installed, so if you want +to use the ``cli`` tool or other optional dependencies do perform a full +installation using ``pip install jira[opt,cli,testing]`` + .. _virtualenv: http://www.virtualenv.org/en/latest/index.html diff --git a/requirements-all.txt b/requirements-all.txt deleted file mode 100644 index 4545bc8c4..000000000 --- a/requirements-all.txt +++ /dev/null @@ -1,3 +0,0 @@ --r requirements.txt --r requirements-opt.txt --r requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index ce9d1d347..000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,27 +0,0 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. - -py >= 1.4 - -hacking>=0.13 -MarkupSafe>=0.23 -coveralls>=1.1 -docutils>=0.12 -oauthlib -pytest-cache -pytest-cov -pytest-instafail -pytest-xdist>=1.14 -pytest>=2.9.1 -requires.io -sphinx>=1.3.5 -sphinx_rtd_theme -tox>=2.3.1 -tox-pyenv -wheel>=0.24.0 -xmlrunner>=1.7.7 -yanc>=0.3.3 -unittest2; python_version < '3.1' -flaky -tenacity diff --git a/requirements.txt b/requirements.txt index dc26a6d7d..673846c3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,8 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. -pbr - -ordereddict; python_version < '3.1' -argparse; python_version < '3.2' +argparse; python_version<'3.2' +defusedxml +ordereddict; python_version<'3.1' requests-oauthlib>=0.6.1 requests>=2.10.0 requests_toolbelt setuptools>=20.10.1 six>=1.10.0 -defusedxml -ipython>=4.0.0 diff --git a/setup.cfg b/setup.cfg index a11a67dbf..51287e506 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,6 @@ classifier = Programming Language :: Python :: 3.6 Topic :: Software Development :: Libraries :: Python Modules Topic :: Internet :: WWW/HTTP - keywords = api atlassian @@ -36,6 +35,39 @@ keywords = packages = jira +#[options.extras_require] +[extras] +cli = + ipython>=4.0.0 +opt = + filemagic>=1.6 + PyJWT + requests_jwt + requests_kerberos +testing = + coveralls>=1.1 + docutils>=0.12 + flaky + hacking>=0.13 + MarkupSafe>=0.23 + oauthlib + py >= 1.4 + pytest-cache + pytest-cov + pytest-instafail + pytest-xdist>=1.14 + pytest>=2.9.1 + requires.io + sphinx>=1.3.5 + sphinx_rtd_theme + tenacity + tox-pyenv + tox>=2.3.1 + unittest2:python_version<'3.1' + wheel>=0.24.0 + xmlrunner>=1.7.7 + yanc>=0.3.3 + [entry_points] console_scripts = jirashell = jira.jirashell:main diff --git a/setup.py b/setup.py index c62c0ee90..24f3bc5a8 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,6 @@ #!/usr/bin/env python import setuptools -from setuptools.command.test import test as TestCommand -import sys # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. @@ -13,26 +11,6 @@ pass -class PyTest(TestCommand): - - def initialize_options(self): - TestCommand.initialize_options(self) - self.pytest_args = [] - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - # import here, cause outside the eggs aren't loaded - import pytest - errno = pytest.main(self.pytest_args) - sys.exit(errno) - - setuptools.setup( - setup_requires=['pbr>=1.9', 'setuptools>=17.1', 'pytest-runner'], - pbr=True, - cmdclass={'test': PyTest}, - test_suite='tests') + setup_requires=['pbr>=3.0.0', 'setuptools>=17.1', 'pytest-runner'], + pbr=True) diff --git a/tox.ini b/tox.ini index 3ee614c8a..9a801439b 100644 --- a/tox.ini +++ b/tox.ini @@ -6,30 +6,25 @@ skip_missing_interpreters = true [testenv:docs] basepython=python changedir=docs -deps= - -rrequirements.txt - -rrequirements-dev.txt commands= sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html [testenv] +usedevelop = True +deps = -rrequirements.txt +extras = + cli + opt + testing sitepackages=False platform = win: windows linux: linux darwin: darwin - -deps= - -rrequirements.txt - -rrequirements-dev.txt - -rrequirements-opt.txt - commands= python -m pip check python -m flake8 python -m pytest -setenv = - PYTHONPATH = passenv = CI_JIRA_* From 8e4c0480b4f4c8d24a40ba9fc415b40162ba143f Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Fri, 14 Jul 2017 19:39:08 +0100 Subject: [PATCH 24/42] Include changelog in description Fixes #321 Change-Id: Ic3e7c08721cb257671adc5d3b5c87b439b86675e Signed-off-by: Sorin Sbarnea --- Makefile | 9 ++++++--- setup.cfg | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 56cfebc6d..92d0e4d13 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -all: info clean lint test docs upload release -.PHONY: all docs upload info req +all: info clean lint test docs dist upload release +.PHONY: all docs upload info req dist PACKAGE_NAME := $(shell python setup.py --name) PACKAGE_VERSION := $(shell python setup.py --version) @@ -18,7 +18,7 @@ info: clean: @find . -name "*.pyc" -delete - @rm -rf .tox/*-$(PLATFORM) .tox/docs dist/* .tox/dist .tox/log docs/build/* + @rm -rf .tox dist/* docs/build/* package: python setup.py sdist bdist_wheel build_sphinx @@ -36,6 +36,9 @@ install-sdk: uninstall: $(PREFIX)pip uninstall -y $(PACKAGE_NAME) +dist: + $(PREFIX)python setup.py sdist bdist_wheel + prepare: @pyenv install -s 2.7.13 @pyenv install -s 3.4.5 diff --git a/setup.cfg b/setup.cfg index 51287e506..3d53372e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,9 @@ author-email = ben.speakmon@gmail.com maintainer = Sorin Sbarnea maintainer-email = sorin.sbarnea@gmail.com summary = Python library for interacting with JIRA via REST APIs. -description-file = README.rst +description-file = + README.rst + ChangeLog home-page = https://github.com/pycontribs/jira license = BSD classifier = From eb4cb5fc5061752e7690fc0d292dd5072b121587 Mon Sep 17 00:00:00 2001 From: Daniel Fortunov Date: Sun, 30 Jul 2017 07:01:19 +0100 Subject: [PATCH 25/42] contributing.rst: Remove reference to BitBucket --- docs/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 1c7b3a49d..f76168e3c 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -5,7 +5,7 @@ The client is an open source project under the BSD license. Contributions of any https://github.com/pycontribs/jira/ -If you find a bug or have an idea for a useful feature, file it at that bitbucket project. Extra points for source +If you find a bug or have an idea for a useful feature, file it at the GitHub project. Extra points for source code patches -- fork and send a pull request. Discussion and support From 595e3db90ff0377d5ac339e6da6dc8a2f03496fb Mon Sep 17 00:00:00 2001 From: ybyang2 Date: Wed, 16 Aug 2017 17:10:29 +0800 Subject: [PATCH 26/42] =?UTF-8?q?=E5=A2=9E=E5=8A=A0jira=20=E4=B8=BA?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E6=B7=BB=E5=8A=A0=E8=A7=92=E8=89=B2=E6=88=90?= =?UTF-8?q?=E5=91=98=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jira/client.py | 12 ++++++++++-- jira/resources.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/jira/client.py b/jira/client.py index a436d56ac..15618290c 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1818,8 +1818,16 @@ def project_roles(self, project): :param project: ID or key of the project to get roles from """ - roles_dict = self._get_json('project/' + project + '/role') - return roles_dict + path = 'project/' + project + '/role' + _rolesdict = self._get_json(path) + rolesdict ={} + + for k,v in _rolesdict.items(): + tmp = {} + tmp['id'] = v.split("/")[-1] + tmp['url'] = v + rolesdict[k] = tmp + return rolesdict # TODO(ssbarnea): return a list of Roles() @translate_resource_args diff --git a/jira/resources.py b/jira/resources.py index 7a0f6f197..ee5c2f068 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -688,6 +688,23 @@ def update(self, users=None, groups=None): super(Role, self).update(**data) + def add_user(self, users=None, groups=None): + """Add by berlinsaint 2017/08/16 + Add the specified users or groups to this project role. One of ``users`` or ``groups`` must be specified. + + :param users: a user or users to add to the role + :type users: string, list or tuple + :param groups: a group or groups to add to the role + :type groups: string, list or tuple + """ + if users is not None and isinstance(users, string_types): + users = (users,) + if groups is not None and isinstance(groups, string_types): + groups = (groups,) + + data = { + 'user':users} + self._session.post(self.self, data=json.dumps(data)) class Resolution(Resource): """A resolution for an issue.""" From 2c862a9ddad79c792d050fab7975a333a4fd22f2 Mon Sep 17 00:00:00 2001 From: Daniel Fortunov Date: Thu, 31 Aug 2017 06:41:28 +0100 Subject: [PATCH 27/42] contributing.rst: Update JIRA community link --- docs/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index f76168e3c..8338729a0 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -11,7 +11,7 @@ code patches -- fork and send a pull request. Discussion and support ====================== -We encourage all who wish to discuss by using https://answers.atlassian.com/questions/topics/754366/jira-python +We encourage all who wish to discuss by using https://community.atlassian.com/t5/tag/jira-python/tg-p Keep in mind to use the jira-python tag when you add a new question. This will assure that the project mantainers will get notified about your question. From aa0102e0f6947985f725403bc88c0a568b14e0c2 Mon Sep 17 00:00:00 2001 From: Earl Chew Date: Mon, 17 Jul 2017 12:09:45 -0700 Subject: [PATCH 28/42] [client] Support JIRA.close() Signed-off-by: Earl Chew --- jira/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jira/client.py b/jira/client.py index 15618290c..6eb1f8f7e 100644 --- a/jira/client.py +++ b/jira/client.py @@ -376,8 +376,12 @@ def _check_update_(self): def __del__(self): """Destructor for JIRA instance.""" + self.close() + + def close(self): session = getattr(self, "_session", None) if session is not None: + self._session = None if self.sys_version_info < (3, 4, 0): # workaround for https://github.com/kennethreitz/requests/issues/2303 try: session.close() From e8d4858678ab9309b128612c8a0de55891a8dfac Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 14 Sep 2017 11:53:16 -0400 Subject: [PATCH 29/42] Fix bug --- jira/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 6eb1f8f7e..3b765b31d 100644 --- a/jira/client.py +++ b/jira/client.py @@ -437,7 +437,7 @@ def _fetch_pages(self, item_type, items_key, request_path, startAt=0, maxResults if isinstance(resource, dict): total = resource.get('total') # 'isLast' is the optional key added to responses in JIRA Agile 6.7.6. So far not used in basic JIRA API. - is_last = resource.get('isLast', True) + is_last = resource.get('isLast', False) start_at_from_response = resource.get('startAt', 0) max_results_from_response = resource.get('maxResults', 1) else: From dfe3cd792de6c5c31317dff77f30abc6317a6bdf Mon Sep 17 00:00:00 2001 From: peter Date: Mon, 25 Sep 2017 16:47:15 -0400 Subject: [PATCH 30/42] Fixing move_to_backlog() --- jira/client.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/jira/client.py b/jira/client.py index 6eb1f8f7e..d9fd32f4d 100644 --- a/jira/client.py +++ b/jira/client.py @@ -401,6 +401,12 @@ def _check_for_html_error(self, content): return False return True + def _get_sprint_field_id(self): + sprint_field_name = "Sprint" + sprint_field_id = [f['schema']['customId'] for f in self.fields() + if f['name'] == sprint_field_name][0] + return sprint_field_id + def _fetch_pages(self, item_type, items_key, request_path, startAt=0, maxResults=50, params=None, base=JIRA_BASE_URL): """Fetch pages. @@ -3146,10 +3152,7 @@ def add_issues_to_sprint(self, sprint_id, issue_keys): # issue.update() to perform this operation # Workaround based on https://answers.atlassian.com/questions/277651/jira-agile-rest-api-example - # Get the customFieldId for "Sprint" - sprint_field_name = "Sprint" - sprint_field_id = [f['schema']['customId'] for f in self.fields() - if f['name'] == sprint_field_name][0] + sprint_field_id = self._get_sprint_field_id() data = {'idOrKeys': issue_keys, 'customFieldId': sprint_field_id, 'sprintId': sprint_id, 'addToBacklog': False} @@ -3230,6 +3233,17 @@ def move_to_backlog(self, issue_keys): warnings.warn('Status code 404 may mean, that too old JIRA Agile version is installed.' ' At least version 6.7.10 is required.') raise + elif self._options['agile_rest_path'] == GreenHopperResource.GREENHOPPER_REST_PATH: + # In old, private API the function does not exist anymore and we need to use + # issue.update() to perform this operation + # Workaround based on https://answers.atlassian.com/questions/277651/jira-agile-rest-api-example + + sprint_field_id = self._get_sprint_field_id() + + data = {'idOrKeys': issue_keys, 'customFieldId': sprint_field_id, + 'addToBacklog': True} + url = self._get_url('sprint/rank', base=self.AGILE_BASE_URL) + return self._session.put(url, data=json.dumps(data)) else: raise NotImplementedError('No API for moving issues to backlog for agile_rest_path="%s"' % self._options['agile_rest_path']) From e9d9c245548b438ab3ef5b57817906384c9d4a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=91=D1=83=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2?= Date: Tue, 10 Oct 2017 16:30:14 +0300 Subject: [PATCH 31/42] Add worklog to transition_issue() --- jira/client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 6eb1f8f7e..588c78790 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1344,7 +1344,7 @@ def find_transitionid_by_name(self, issue, transition_name): return id @translate_resource_args - def transition_issue(self, issue, transition, fields=None, comment=None, **fieldargs): + def transition_issue(self, issue, transition, fields=None, comment=None, worklog=None, **fieldargs): """Perform a transition on an issue. Each keyword argument (other than the predefined ones) is treated as a field name and the argument's value @@ -1373,6 +1373,8 @@ def transition_issue(self, issue, transition, fields=None, comment=None, **field 'id': transitionId}} if comment: data['update'] = {'comment': [{'add': {'body': comment}}]} + if worklog: + data['update'] = {'worklog': [{'add': {'timeSpent': worklog}}]} if fields is not None: data['fields'] = fields else: From af962fe0dcf075b84539371b210bbe2903de3c3c Mon Sep 17 00:00:00 2001 From: guitarhero23 Date: Fri, 20 Oct 2017 15:33:41 -0400 Subject: [PATCH 32/42] Update client.py --- jira/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 6eb1f8f7e..8a16703c6 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2565,7 +2565,7 @@ def reindex(self, force=False, background=True): def backup(self, filename='backup.zip', attachments=False): """Will call jira export to backup as zipped xml. Returning with success does not mean that the backup process finished.""" if self.deploymentType == 'Cloud': - url = self._options['server'] + '/rest/obm/1.0/runbackup' + url = self._options['server'] + '/rest/backup/1/export/runbackup' payload = json.dumps({"cbAttachments": attachments}) self._options['headers']['X-Requested-With'] = 'XMLHttpRequest' else: From 7dd45b3479522cd2085442f79baf0dcd20acf814 Mon Sep 17 00:00:00 2001 From: Harold Dost Date: Thu, 9 Nov 2017 13:58:36 -0500 Subject: [PATCH 33/42] Allow through TRAVIS Environment Variables - Tox as a function of isolation will block most environment variables, so as a result they were causing some of the tests to not "know" that they were running inside of TRAVIS which they should. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9a801439b..32ec827a3 100644 --- a/tox.ini +++ b/tox.ini @@ -27,6 +27,7 @@ commands= python -m pytest passenv = CI_JIRA_* + TRAVIS* [travis:after] toxenv = py27 From 9d1697dd977833b307a271d6be7d350566b37393 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Mon, 13 Nov 2017 19:10:14 +0000 Subject: [PATCH 34/42] fixed linting errors Change-Id: Ia8321cc51d645a6118fd6928221da6df70cb9c8f Signed-off-by: Sorin Sbarnea --- jira/client.py | 4 ++-- jira/resources.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/jira/client.py b/jira/client.py index be21ed1d4..d1e374b5c 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1832,9 +1832,9 @@ def project_roles(self, project): """ path = 'project/' + project + '/role' _rolesdict = self._get_json(path) - rolesdict ={} + rolesdict = {} - for k,v in _rolesdict.items(): + for k, v in _rolesdict.items(): tmp = {} tmp['id'] = v.split("/")[-1] tmp['url'] = v diff --git a/jira/resources.py b/jira/resources.py index ee5c2f068..9d8389089 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -689,23 +689,26 @@ def update(self, users=None, groups=None): super(Role, self).update(**data) def add_user(self, users=None, groups=None): - """Add by berlinsaint 2017/08/16 - Add the specified users or groups to this project role. One of ``users`` or ``groups`` must be specified. + """Add the specified users or groups to this project role. + + One of ``users`` or ``groups`` must be specified. :param users: a user or users to add to the role :type users: string, list or tuple :param groups: a group or groups to add to the role :type groups: string, list or tuple """ + if users is not None and isinstance(users, string_types): users = (users,) if groups is not None and isinstance(groups, string_types): groups = (groups,) data = { - 'user':users} + 'user': users} self._session.post(self.self, data=json.dumps(data)) + class Resolution(Resource): """A resolution for an issue.""" From ef0ae3927f39c02c90b5d44f5086a5c387b89f4d Mon Sep 17 00:00:00 2001 From: Pavol Babincak Date: Tue, 14 Nov 2017 15:48:53 +0100 Subject: [PATCH 35/42] Make Kerberos option mutual_authentication configurable Before this change mutual_authentication was set to requests_kerberos.OPTIONAL. This option value doesn't change. --- docs/examples.rst | 4 ++++ jira/client.py | 22 +++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index e9f101ce8..8d396c300 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -91,6 +91,10 @@ To enable Kerberos auth, set ``kerberos=True``:: authed_jira = JIRA(kerberos=True) +To pass additional options to Kerberos auth use dict ``kerberos_options``, e.g.:: + + authed_jira = JIRA(kerberos=True, kerberos_options={'mutual_authentication': 'DISABLED'}) + .. _jirashell-label: Issues diff --git a/jira/client.py b/jira/client.py index 82a899709..518e10da3 100644 --- a/jira/client.py +++ b/jira/client.py @@ -221,7 +221,7 @@ class JIRA(object): JIRA_BASE_URL = Resource.JIRA_BASE_URL AGILE_BASE_URL = GreenHopperResource.AGILE_BASE_URL - def __init__(self, server=None, options=None, basic_auth=None, oauth=None, jwt=None, kerberos=False, + def __init__(self, server=None, options=None, basic_auth=None, oauth=None, jwt=None, kerberos=False, kerberos_options=None, validate=False, get_server_info=True, async=False, logging=True, max_retries=3, proxies=None, timeout=None): """Construct a JIRA client instance. @@ -257,6 +257,9 @@ def __init__(self, server=None, options=None, basic_auth=None, oauth=None, jwt=N * key_cert -- private key file to sign requests with (should be the pair of the public key supplied to JIRA in the OAuth application link) :param kerberos: If true it will enable Kerberos authentication. + :param kerberos_options: A dict of properties for Kerberos authentication. The following properties are possible: + * mutual_authentication -- string DISABLED or OPTIONAL. + Example kerberos_options structure: ``{'mutual_authentication': 'DISABLED'}`` :param jwt: A dict of properties for JWT authentication supported by Atlassian Connect. The following properties are required: * secret -- shared secret as delivered during 'installed' lifecycle event @@ -315,7 +318,7 @@ def __init__(self, server=None, options=None, basic_auth=None, oauth=None, jwt=N elif jwt: self._create_jwt_session(jwt, timeout) elif kerberos: - self._create_kerberos_session(timeout) + self._create_kerberos_session(timeout, kerberos_options=kerberos_options) else: verify = self._options['verify'] self._session = ResilientSession(timeout=timeout) @@ -2294,15 +2297,24 @@ def _create_oauth_session(self, oauth, timeout): self._session.verify = verify self._session.auth = oauth - def _create_kerberos_session(self, timeout): + def _create_kerberos_session(self, timeout, kerberos_options=None): verify = self._options['verify'] + if kerberos_options is None: + kerberos_options = {} from requests_kerberos import HTTPKerberosAuth - from requests_kerberos import OPTIONAL + from requests_kerberos import OPTIONAL, DISABLED + + mutual_authentication = OPTIONAL + if kerberos_options.get('mutual_authentication') == 'DISABLED': + mutual_authentication = DISABLED + else: + raise ValueError("Unknown value for mutual_authentication: %s" % + kerberos_options['mutual_authentication']) self._session = ResilientSession(timeout=timeout) self._session.verify = verify - self._session.auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL) + self._session.auth = HTTPKerberosAuth(mutual_authentication=mutual_authentication) @staticmethod def _timestamp(dt=None): From ba91e7f5c0b7ff399a7a0900321318ba8d6af7f3 Mon Sep 17 00:00:00 2001 From: Harold Dost Date: Tue, 14 Nov 2017 13:06:19 -0500 Subject: [PATCH 36/42] Fix Continuous Integration Testing --- tests/tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 811549e7c..03f27ed2e 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -305,7 +305,8 @@ def __init__(self): try: self.jira_admin.create_project(self.project_a, - self.project_a_name) + self.project_a_name, + template_name='Scrum software development') except Exception: # we care only for the project to exist pass @@ -319,7 +320,8 @@ def __init__(self): try: self.jira_admin.create_project(self.project_b, - self.project_b_name) + self.project_b_name, + template_name='Scrum software development') except Exception: # we care only for the project to exist pass From bfac4a8994769b782fcdb2062549f23d63e89e45 Mon Sep 17 00:00:00 2001 From: Harold Dost Date: Tue, 14 Nov 2017 13:41:24 -0500 Subject: [PATCH 37/42] Touch ChangeLog To Fix PR Checks --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 39318d539..94b31f7c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ python: - '3.6' install: - pip -q --log dist/pip.log install --upgrade pip setuptools tox-travis py wheel +- touch ChangeLog - python setup.py sdist bdist_wheel install - pip install ./dist/*.whl - pip --version From 03f5039520ef561dc7298c3659bb37071c93e370 Mon Sep 17 00:00:00 2001 From: Harold Dost Date: Tue, 14 Nov 2017 13:41:50 -0500 Subject: [PATCH 38/42] Place imports on separate lines --- jira/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index 518e10da3..1325026b0 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2302,8 +2302,9 @@ def _create_kerberos_session(self, timeout, kerberos_options=None): if kerberos_options is None: kerberos_options = {} + from requests_kerberos import DISABLED from requests_kerberos import HTTPKerberosAuth - from requests_kerberos import OPTIONAL, DISABLED + from requests_kerberos import OPTIONAL mutual_authentication = OPTIONAL if kerberos_options.get('mutual_authentication') == 'DISABLED': From ca799c15886fd8a584c4a15d967d0837e73b1e94 Mon Sep 17 00:00:00 2001 From: Harold Dost Date: Wed, 15 Nov 2017 11:59:43 -0500 Subject: [PATCH 39/42] Update Contribution Guidlines to be more explicit --- docs/contributing.rst | 96 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 8338729a0..3d34b2c9d 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1,3 +1,4 @@ +************ Contributing ************ @@ -8,10 +9,101 @@ https://github.com/pycontribs/jira/ If you find a bug or have an idea for a useful feature, file it at the GitHub project. Extra points for source code patches -- fork and send a pull request. + Discussion and support -====================== +********************** We encourage all who wish to discuss by using https://community.atlassian.com/t5/tag/jira-python/tg-p -Keep in mind to use the jira-python tag when you add a new question. This will assure that the project mantainers +Keep in mind to use the jira-python tag when you add a new question. This will assure that the project maintainers will get notified about your question. + + +Contributing Code +***************** + +* Patches should be: + * concise + * works across all supported versions of Python. + * follows the existing style of the code base (PEP-8). + * has comments included as needed. + +* Great Patch has: + * A test case that demonstrates the previous flaw that now passes with the included patch. + * Documentation for those changes to a public API + + +Issues and Feature Requests +*************************** + +* Check to see if there's an existing issue/pull request for the + bug/feature. All issues are at https://github.com/pycontribs/jira/issues + and pull requests are at https://github.com/pycontribs/jira/pulls. +* If there isn't an existing issue there, please file an issue. The ideal + report includes: + + * A description of the problem/suggestion. + * How to recreate the bug. + * If relevant, including the versions of your: + + * Python interpreter (2.7, 3.5, etc) + * jira-python + * Operating System and Version (Windows 7, OS X 10.10, Ubuntu 14.04, etc.) + * IPython if using jirashell + * Optionally of the other dependencies involved + + * If possible, create a pull request with a (failing) test case demonstrating + what's wrong. This makes the process for fixing bugs quicker & gets issues + resolved sooner. + * Here is an template:: + + Description: + + Python Intepreter: + jira-python: + OS: + IPython (Optional): + Other Dependencies: + + Steps To Reproduce: + 1. + 2. + 3. + ... + + Stack Trace: + + + +Issues +****** +Here are the best ways to help with open issues: + +* For issues without reproduction steps + * Try to reproduce the issue, comment with the minimal amount of steps to + reproduce the bug (a code snippet would be ideal). + * If there is not a set of steps that can be made to reproduce the issue, + at least make sure there are debug logs that capture the unexpected behavior. + +* Submit pull requests for open issues. + + +Pull Requests +************* +There are some key points that are needed to be met before a pull request +can be merged: + +* All tests must pass for all python versions. (Once the Test Framework is fixed) + * For now, no new failures should occur + +* All pull requests require tests that either test the new feature or test + that the specific bug is fixed. Pull requests for minor things like + adding a new region or fixing a typo do not need tests. +* Must follow PEP8 conventions. +* Within a major version changes must be backwards compatible. + +The best way to help with pull requests is to comment on pull requests by +noting if any of these key points are missing, it will both help get feedback +sooner to the issuer of the pull request and make it easier to determine for +an individual with write permissions to the repository if a pull request +is ready to be merged. From a8ca6944695cbe4d7190f2c5738980e16d77870e Mon Sep 17 00:00:00 2001 From: Pavol Babincak Date: Wed, 15 Nov 2017 17:51:59 +0100 Subject: [PATCH 40/42] Make OPTIONAL default and allowed value of mutual_authentication Before this change Kerberos authentication worked only if Kerberos option mutual_authentication was explicitly set to DISABLED. OPTIONAL was not default value anymore but it wasn't among allowed values of mutual_authentication. Therefore these authentications failed with raised exception: ValueError: Unknown value for mutual_authentication Relates: #424 Relates: #472 --- jira/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jira/client.py b/jira/client.py index 1325026b0..0cc9a0e13 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2306,8 +2306,9 @@ def _create_kerberos_session(self, timeout, kerberos_options=None): from requests_kerberos import HTTPKerberosAuth from requests_kerberos import OPTIONAL - mutual_authentication = OPTIONAL - if kerberos_options.get('mutual_authentication') == 'DISABLED': + if kerberos_options.get('mutual_authentication', 'OPTIONAL') == 'OPTIONAL': + mutual_authentication = OPTIONAL + elif kerberos_options.get('mutual_authentication') == 'DISABLED': mutual_authentication = DISABLED else: raise ValueError("Unknown value for mutual_authentication: %s" % From d2fdc9081abbe3bb536e647d3f028dd54547618b Mon Sep 17 00:00:00 2001 From: Harold Dost Date: Tue, 21 Nov 2017 09:47:06 -0500 Subject: [PATCH 41/42] Add testing section to contributions --- docs/contributing.rst | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index 3d34b2c9d..939bc8079 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -2,12 +2,13 @@ Contributing ************ -The client is an open source project under the BSD license. Contributions of any kind are welcome! +The client is an open source project under the BSD license. +Contributions of any kind are welcome! https://github.com/pycontribs/jira/ -If you find a bug or have an idea for a useful feature, file it at the GitHub project. Extra points for source -code patches -- fork and send a pull request. +If you find a bug or have an idea for a useful feature, file it at the GitHub +project. Extra points for source code patches -- fork and send a pull request. Discussion and support @@ -15,8 +16,8 @@ Discussion and support We encourage all who wish to discuss by using https://community.atlassian.com/t5/tag/jira-python/tg-p -Keep in mind to use the jira-python tag when you add a new question. This will assure that the project maintainers -will get notified about your question. +Keep in mind to use the jira-python tag when you add a new question. This will +assure that the project maintainers will get notified about your question. Contributing Code @@ -33,6 +34,22 @@ Contributing Code * Documentation for those changes to a public API +Testing +******* + +To test code run:: + + make test-all + +This will run the code in a virtual environment, and will test across the +versions of python which are installed. It will also install tox if it is +not already installed. + +Alternatively if you do not have make you can always run:: + + pip install tox + tox + Issues and Feature Requests *************************** From 30c042314d2af2e44a2e114ed3d23630e14e4da3 Mon Sep 17 00:00:00 2001 From: Pavol Babincak Date: Tue, 2 Jan 2018 16:17:59 +0100 Subject: [PATCH 42/42] jirashell: Import InteractiveShellEmbed depending on IPython version jirashell imported InteractiveShellEmbed from IPython.frontend. frontend subpackage has been removed in IPython 1.0. IPython keep backawards compatibility with old frondend imports but issues warning: ShimWarning: The top-level `frontend` package has been deprecated since IPython 1.0. All its subpackages have been moved to the top `IPython` level. For more information about frontend subpackage removal see: http://ipython.readthedocs.io/en/stable/whatsnew/version1.0.html#reorganization This should also fix jirashell on Python 2.7 on Windows. It seems that frontend shim module isn't present there (#251). --- jira/jirashell.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/jira/jirashell.py b/jira/jirashell.py index c5f891a62..239d6818c 100644 --- a/jira/jirashell.py +++ b/jira/jirashell.py @@ -254,7 +254,12 @@ def main(): jira = JIRA(options=options, basic_auth=basic_auth, oauth=oauth) - from IPython.frontend.terminal.embed import InteractiveShellEmbed + import IPython + # The top-level `frontend` package has been deprecated since IPython 1.0. + if IPython.version_info[0] >= 1: + from IPython.terminal.embed import InteractiveShellEmbed + else: + from IPython.frontend.terminal.embed import InteractiveShellEmbed ipshell = InteractiveShellEmbed( banner1='')