From 12b19b5147262a65c34d8857242dd91dc02301c7 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Thu, 3 Oct 2019 19:57:20 +0300 Subject: [PATCH 01/16] A new Tempo method --- atlassian/bitbucket.py | 4 +++- atlassian/confluence.py | 14 ++++++++++++++ atlassian/jira.py | 26 +++++++++++++++++++++----- requirements.txt | 4 ++-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/atlassian/bitbucket.py b/atlassian/bitbucket.py index 1dd7df5ac..c0f014518 100644 --- a/atlassian/bitbucket.py +++ b/atlassian/bitbucket.py @@ -527,7 +527,7 @@ def delete_branch(self, project, repository, name, end_point): data = {"name": str(name), "endPoint": str(end_point)} return self.delete(url, data=data) - def get_pull_requests(self, project, repository, state='OPEN', order='newest', limit=100, start=0): + def get_pull_requests(self, project, repository, state='OPEN', order='newest', limit=100, start=0, at=None): """ Get pull requests :param project: @@ -550,6 +550,8 @@ def get_pull_requests(self, project, repository, state='OPEN', order='newest', l params['start'] = start if order: params['order'] = order + if at: + params['at'] = at response = self.get(url, params=params) if 'values' not in response: return [] diff --git a/atlassian/confluence.py b/atlassian/confluence.py index 5a70a68fc..118b68fb4 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -1295,3 +1295,17 @@ def avatar_set_default_for_user(self, user_key): """ url = 'rest/user-profile/1.0/{}/avatar/default'.format(user_key) return self.get(url) + + def add_user_to_group(self, username, group_name): + """ + Add given user to a group + + :param username: str + :param group_name: str + :return: Current state of the group + """ + url = 'rest/api/2/group/user' + params = {'groupname': group_name} + data = {'name': username} + + return self.post(url, params=params, data=data) \ No newline at end of file diff --git a/atlassian/jira.py b/atlassian/jira.py index efa399e94..4a84781b1 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -386,8 +386,9 @@ def add_version(self, project_key, project_id, version, is_archived=False, is_re :is_released: :return: """ - payload = {'name': version, 'archived': is_archived, 'released': is_released, 'project': project_key, 'projectId': project_id} - return self.post("rest/api/2/version", data = payload) + payload = {'name': version, 'archived': is_archived, 'released': is_released, 'project': project_key, + 'projectId': project_id} + return self.post("rest/api/2/version", data=payload) def get_project_roles(self, project_key): """ @@ -1044,7 +1045,7 @@ def set_issue_status_by_transition_id(self, issue_key, transition_id): def get_issue_status(self, issue_key): url = 'rest/api/2/issue/{issue_key}?fields=status'.format(issue_key=issue_key) - return (self.get(url) or {}).get('fields').get('status').get('name') + return (((self.get(url) or {}).get('fields') or {}).get('status') or {}).get('name') or {} def get_issue_status_id(self, issue_key): url = 'rest/api/2/issue/{issue_key}?fields=status'.format(issue_key=issue_key) @@ -1711,12 +1712,12 @@ def tempo_timesheets_get_worklogs(self, date_from=None, date_to=None, username=N def tempo_timesheets_write_worklog(self, worker, started, time_spend_in_seconds, issue_id, comment=None): """ - - :param comment: + Log work for user :param worker: :param started: :param time_spend_in_seconds: :param issue_id: + :param comment: :return: """ data = {"worker": worker, @@ -1728,6 +1729,21 @@ def tempo_timesheets_write_worklog(self, worker, started, time_spend_in_seconds, url = 'rest/tempo-timesheets/4/worklogs/' return self.post(url, data=data) + def tempo_timesheets_approval_worklog_report(self, user_key, period_start_date): + """ + Return timesheets for approval + :param user_key: + :param period_start_date: + :return: + """ + url = "rest/tempo-timesheets/4/timesheet-approval/current" + params = {} + if period_start_date: + params['periodStartDate'] = period_start_date + if user_key: + params['userKey'] = user_key + return self.get(url, params=params) + def tempo_get_links_to_project(self, project_id): """ Gets all links to a specific project diff --git a/requirements.txt b/requirements.txt index 34fe49703..2ad9cd06c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ six requests oauthlib requests_oauthlib -kerberos; platform_system != 'Windows' -kerberos-sspi; platform_system == 'Windows' +kerberos ; platform_system != 'Windows' +kerberos-sspi ; platform_system == 'Windows' \ No newline at end of file From 81932839013d18bd7a843dabc4009c3de68ce4ea Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Thu, 3 Oct 2019 20:08:40 +0300 Subject: [PATCH 02/16] Tempo: add team member --- atlassian/jira.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index af8f3bbab..a6b70aab1 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1720,6 +1720,13 @@ def tempo_get_default_link_to_project(self, project_id): url = 'rest/tempo-accounts/1/link/project/{}/default/'.format(project_id) return self.get(url) + def tempo_teams_get_all_teams(self, expand=None): + url = "rest/tempo-teams/2/team" + params = {} + if expand: + params['expand'] = expand + return self.get(url, params=params) + def tempo_teams_add_member(self, team_id, member_key): """ Add team member @@ -1727,12 +1734,37 @@ def tempo_teams_add_member(self, team_id, member_key): :param member_key: :return: """ - url = 'rest/tempo-teams/2/team/{}/member/'.format(team_id) - data = {"member": {"key": member_key, "type": "USER"}, + data = {"member": {"key": str(member_key), "type": "USER"}, "membership": { "availability": "100", "role": {"id": 1} }} + return self.tempo_teams_add_member_raw(team_id, member_data=data) + + def tempo_teams_add_membership(self, team_id, member_id): + """ + Add team member + :param team_id: + :param member_id: + :return: + """ + data = {"teamMemberId": member_id, + "teamId": team_id, + "availability": "100", + "role": {"id": 1} + } + url = "rest/tempo-teams/2/team/{}/member/{}/membership".format(team_id, member_id) + return self.post(url, data=data) + + def tempo_teams_add_member_raw(self, team_id, member_data): + """ + Add team member + :param team_id: + :param member_data: + :return: + """ + url = 'rest/tempo-teams/2/team/{}/member/'.format(team_id) + data = member_data return self.post(url, data=data) def tempo_teams_get_members(self, team_id): From 16beefa11183eafb606380d150bec45e18ddfb5c Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Fri, 4 Oct 2019 12:55:49 +0300 Subject: [PATCH 03/16] Bugfix --- atlassian/confluence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlassian/confluence.py b/atlassian/confluence.py index 118b68fb4..f372c62ac 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -1266,7 +1266,7 @@ def team_calendar_events(self, sub_calendar_id, start, end, user_time_zone_id=No if start: params['start'] = start if end: - params['start'] = end + params['end'] = end return self.get(url, params=params) def get_mobile_parameters(self, username): From fd0cf22c5b2fe72a9c997bfd46dcd5c56834c4cc Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Thu, 24 Oct 2019 00:51:26 +0300 Subject: [PATCH 04/16] Set workload method --- atlassian/jira.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/atlassian/jira.py b/atlassian/jira.py index 075bc2313..0dbb8ae73 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1657,6 +1657,17 @@ def tempo_workload_scheme_get_members(self, scheme_id): url = 'rest/tempo-core/1/workloadscheme/users/{}'.format(scheme_id) return self.get(url) + def tempo_workload_scheme_set_member(self, scheme_id, member): + """ + Provide a workload scheme members + :param member: user name of user + :param scheme_id: + :return: + """ + url = 'rest/tempo-core/1/workloadscheme/user/{}'.format(member) + data = {'id': scheme_id} + return self.put(url, data=data) + def tempo_timesheets_get_configuration(self): """ Provide the configs of timesheets From f190a274da54a83c8b1f7e4de76209bc2ae0747a Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Thu, 24 Oct 2019 00:52:20 +0300 Subject: [PATCH 05/16] Bump version --- atlassian/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlassian/VERSION b/atlassian/VERSION index 850e74240..63e799cf4 100644 --- a/atlassian/VERSION +++ b/atlassian/VERSION @@ -1 +1 @@ -1.14.0 +1.14.1 From 359c1ee4ceb9b26573245405674376e7cf5bc581 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 28 Oct 2019 22:16:55 +0300 Subject: [PATCH 06/16] Organize the imports --- atlassian/VERSION | 2 +- atlassian/__init__.py | 12 ++++++------ atlassian/bamboo.py | 2 ++ atlassian/bitbucket.py | 1 + atlassian/confluence.py | 8 +++++--- atlassian/crowd.py | 1 + atlassian/jira.py | 2 ++ atlassian/jira8.py | 1 + atlassian/marketplace.py | 1 + atlassian/portfolio.py | 1 + atlassian/request_utils.py | 1 + atlassian/rest_client.py | 4 +++- atlassian/service_desk.py | 1 + 13 files changed, 26 insertions(+), 11 deletions(-) diff --git a/atlassian/VERSION b/atlassian/VERSION index 63e799cf4..a4cc55716 100644 --- a/atlassian/VERSION +++ b/atlassian/VERSION @@ -1 +1 @@ -1.14.1 +1.14.2 diff --git a/atlassian/__init__.py b/atlassian/__init__.py index 2c6308697..cf793bbdc 100644 --- a/atlassian/__init__.py +++ b/atlassian/__init__.py @@ -1,13 +1,13 @@ -from .confluence import Confluence -from .jira import Jira +from .bamboo import Bamboo from .bitbucket import Bitbucket from .bitbucket import Bitbucket as Stash -from .portfolio import Portfolio -from .bamboo import Bamboo +from .confluence import Confluence from .crowd import Crowd -from .service_desk import ServiceDesk -from .marketplace import MarketPlace +from .jira import Jira from .jira8 import Jira8 +from .marketplace import MarketPlace +from .portfolio import Portfolio +from .service_desk import ServiceDesk __all__ = [ 'Confluence', diff --git a/atlassian/bamboo.py b/atlassian/bamboo.py index 8a40153b9..2189ea088 100755 --- a/atlassian/bamboo.py +++ b/atlassian/bamboo.py @@ -1,6 +1,8 @@ # coding=utf-8 import logging + from requests.exceptions import HTTPError + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) diff --git a/atlassian/bitbucket.py b/atlassian/bitbucket.py index 89418a115..896ac737e 100644 --- a/atlassian/bitbucket.py +++ b/atlassian/bitbucket.py @@ -1,5 +1,6 @@ # coding=utf-8 import logging + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) diff --git a/atlassian/confluence.py b/atlassian/confluence.py index 60fd5bab8..9a44b5233 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -1,11 +1,13 @@ # coding=utf-8 -from atlassian import utils -from .rest_client import AtlassianRestAPI -from requests import HTTPError import logging import os import time +from requests import HTTPError + +from atlassian import utils +from .rest_client import AtlassianRestAPI + log = logging.getLogger(__name__) diff --git a/atlassian/crowd.py b/atlassian/crowd.py index 0f3583a48..e7b2f91fa 100644 --- a/atlassian/crowd.py +++ b/atlassian/crowd.py @@ -1,5 +1,6 @@ # coding=utf-8 import logging + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) diff --git a/atlassian/jira.py b/atlassian/jira.py index 14d7808b3..ce0972fb8 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1,6 +1,8 @@ # coding=utf-8 import logging + from requests.exceptions import HTTPError + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) diff --git a/atlassian/jira8.py b/atlassian/jira8.py index 87bd538f7..960592678 100644 --- a/atlassian/jira8.py +++ b/atlassian/jira8.py @@ -1,5 +1,6 @@ # coding=utf-8 import logging + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) diff --git a/atlassian/marketplace.py b/atlassian/marketplace.py index 47b70cb56..0f983e1b9 100644 --- a/atlassian/marketplace.py +++ b/atlassian/marketplace.py @@ -1,5 +1,6 @@ # coding=utf-8 import logging + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) diff --git a/atlassian/portfolio.py b/atlassian/portfolio.py index 4907be073..a1f127db5 100644 --- a/atlassian/portfolio.py +++ b/atlassian/portfolio.py @@ -1,5 +1,6 @@ # coding=utf-8 import logging + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) diff --git a/atlassian/request_utils.py b/atlassian/request_utils.py index e968fa404..f1e4027cb 100644 --- a/atlassian/request_utils.py +++ b/atlassian/request_utils.py @@ -1,4 +1,5 @@ import logging + from six import PY3 diff --git a/atlassian/rest_client.py b/atlassian/rest_client.py index a44dd11af..3761ed9eb 100644 --- a/atlassian/rest_client.py +++ b/atlassian/rest_client.py @@ -1,10 +1,12 @@ # coding=utf-8 import json import logging -from six.moves.urllib.parse import urlencode + import requests from oauthlib.oauth1 import SIGNATURE_RSA from requests_oauthlib import OAuth1 +from six.moves.urllib.parse import urlencode + from atlassian.request_utils import get_default_logger log = get_default_logger(__name__) diff --git a/atlassian/service_desk.py b/atlassian/service_desk.py index 56046b833..a7b30df79 100644 --- a/atlassian/service_desk.py +++ b/atlassian/service_desk.py @@ -1,5 +1,6 @@ # coding=utf-8 import logging + from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) From 0e4e922d6e8bcbe0b02180b9f85b42a7a724f2dd Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 28 Oct 2019 22:20:50 +0300 Subject: [PATCH 07/16] Polish --- atlassian/bamboo.py | 2 ++ atlassian/bitbucket.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/atlassian/bamboo.py b/atlassian/bamboo.py index 2189ea088..41db89577 100755 --- a/atlassian/bamboo.py +++ b/atlassian/bamboo.py @@ -198,6 +198,7 @@ def get_vcs_branches(self, plan_key, max_results=25): """ Get all vcs names for the current plan :param plan_key: str TST-BLD + :param max_results :return: """ resource = 'plan/{plan_key}/vcsBranches'.format(plan_key=plan_key) @@ -304,6 +305,7 @@ def build_result(self, build_key, expand=None, include_all_states=False): using stages.stage.results.result. All expand parameters should contain results.result prefix. :param build_key: Should be in the form XX-YY[-ZZ]-99, that is, the last token should be an integer representing the build number + :param include_all_states """ try: int(build_key.split('-')[-1]) diff --git a/atlassian/bitbucket.py b/atlassian/bitbucket.py index 896ac737e..5f42aef86 100644 --- a/atlassian/bitbucket.py +++ b/atlassian/bitbucket.py @@ -561,7 +561,8 @@ def get_pull_request_settings(self, project, repository): :param repository: :return: """ - url = 'rest/api/1.0/projects/{project}/repos/{repository}/settings/pull-requests'.format(project=project,repository=repository) + url = 'rest/api/1.0/projects/{project}/repos/{repository}/settings/pull-requests'.format(project=project, + repository=repository) return self.get(url) def set_pull_request_settings(self, project, repository, data): @@ -572,7 +573,8 @@ def set_pull_request_settings(self, project, repository, data): :param data: json body :return: """ - url = 'rest/api/1.0/projects/{project}/repos/{repository}/settings/pull-requests'.format(project=project,repository=repository) + url = 'rest/api/1.0/projects/{project}/repos/{repository}/settings/pull-requests'.format(project=project, + repository=repository) return self.post(url, data=data) def get_pull_requests(self, project, repository, state='OPEN', order='newest', limit=100, start=0, at=None): @@ -1075,7 +1077,7 @@ def get_branches_permissions(self, project, repository=None, start=0, limit=25): :param limit: :return: """ - if repository != None: + if repository is not None: url = 'rest/branch-permissions/2.0/projects/{project}/repos/{repository}/restrictions'.format( project=project, repository=repository) @@ -1099,12 +1101,12 @@ def all_branches_permissions(self, project, repository=None): """ start = 0 branches_permissions = [] - response = self.get_branches_permissions(project=project, repository=repository, start=start) - branches_permissions += response.get('values') + response = self.get_branches_permissions(project=project, repository=repository, start=start) + branches_permissions += response.get('values') while not response.get('isLastPage'): start = response.get('nextPageStart') - response = self.get_branches_permissions(project=project, repository=repository, start=start) - branches_permissions += response.get('values') + response = self.get_branches_permissions(project=project, repository=repository, start=start) + branches_permissions += response.get('values') return branches_permissions def reindex(self): From 1fbfcc9744704744cb77b2178e81f95acfa467f2 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Wed, 13 Nov 2019 20:50:19 +0700 Subject: [PATCH 08/16] Bump version --- atlassian/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlassian/VERSION b/atlassian/VERSION index a4cc55716..cf28a128f 100644 --- a/atlassian/VERSION +++ b/atlassian/VERSION @@ -1 +1 @@ -1.14.2 +2.14.3 From 91bf3025ffbc7d61a613530d1a0c2bf4e2213101 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Wed, 13 Nov 2019 21:09:31 +0700 Subject: [PATCH 09/16] Add LFS info fetcher --- atlassian/bitbucket.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/atlassian/bitbucket.py b/atlassian/bitbucket.py index 5f42aef86..638c6ef24 100644 --- a/atlassian/bitbucket.py +++ b/atlassian/bitbucket.py @@ -1408,3 +1408,9 @@ def create_code_insights_report(self, project_key, repository_slug, commit_id, r data = {"title": report_title} data.update(report_params) return self.put(url, data=data) + + def get_lfs_repo_status(self, project_key, repo): + url = 'rest/git-lfs/git-lfs/admin/projects/{projectKey}/repos/{repositorySlug}/enabled'.format( + projectKey=project_key, + repositorySlug=repo) + return self.get(url) From 0aacfc16acbcd44f9991483c0f914d991ffa2c54 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sat, 23 Nov 2019 17:44:07 +0300 Subject: [PATCH 10/16] Typo --- atlassian/jira.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 75b83a520..97a635e9b 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -2,7 +2,6 @@ import logging from requests.exceptions import HTTPError - from .rest_client import AtlassianRestAPI log = logging.getLogger(__name__) @@ -62,7 +61,7 @@ def jql(self, jql, fields='*all', start=0, limit=None, expand=None): :param start: OPTIONAL: The start point of the collection to return. Default: 0. :param limit: OPTIONAL: The limit of the number of issues to return, this may be restricted by fixed system limits. Default by built-in method: 50 - :param expand: OPTIONAL: expland the search result + :param expand: OPTIONAL: expand the search result :return: """ params = {} From ce26890ed47f62834ad1052791e9f34e5f287000 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sat, 23 Nov 2019 18:32:05 +0300 Subject: [PATCH 11/16] Consolidate the Jira8 module into main --- atlassian/jira.py | 236 +++++++++++++++++++++++++++++++++++++++++--- atlassian/jira8.py | 239 +-------------------------------------------- docs/jira8.rst | 4 - 3 files changed, 226 insertions(+), 253 deletions(-) delete mode 100644 docs/jira8.rst diff --git a/atlassian/jira.py b/atlassian/jira.py index 97a635e9b..ff41feae2 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -305,7 +305,7 @@ def user_find_by_user_string(self, username, start=0, limit=50, include_inactive 'maxResults': limit } return self.get(url, params=params) - + def is_user_in_application(self, username, applicationKey): """ Utility function to test whether a user has an application role @@ -313,10 +313,10 @@ def is_user_in_application(self, username, applicationKey): :param applicationKey: The application key of the application :return: True if the user has the application, else False """ - user = self.user(username, 'applicationRoles') # Get applications roles of the user + user = self.user(username, 'applicationRoles') # Get applications roles of the user if 'self' in user: for applicationRole in user.get('applicationRoles').get('items'): - if applicationRole.get('key')==applicationKey: + if applicationRole.get('key') == applicationKey: return True return False @@ -333,8 +333,28 @@ def add_user_to_application(self, username, applicationKey): 'username': username, 'applicationKey': applicationKey } - return self.post('rest/api/2/user/application', params=params)==None - + return self.post('rest/api/2/user/application', params=params) == None + + # Application roles + def get_all_application_roles(self): + """ + Returns all ApplicationRoles in the system + :return: + """ + url = 'rest/api/2/applicationrole' + + return self.get(url) + + def get_application_role(self, role_key): + """ + Returns the ApplicationRole with passed key if it exists + :param role_key: str + :return: + """ + url = 'rest/api/2/applicationrole/{}'.format(role_key) + + return self.get(url) + def projects(self, included_archived=None): """Returns all projects which are visible for the currently logged in user. If no user is logged in, it returns the list of projects that are visible when using anonymous access. @@ -526,6 +546,31 @@ def create_issue_type(self, name, description='', type='standard'): def issue(self, key, fields='*all'): return self.get('rest/api/2/issue/{0}?fields={1}'.format(key, fields)) + def get_issue(self, issue_id_or_key, fields=None, properties=None, update_history=True): + """ + Returns a full representation of the issue for the given issue key + By default, all fields are returned in this get-issue resource + + :param issue_id_or_key: str + :param fields: str + :param properties: str + :param update_history: bool + :return: issue + """ + url = 'rest/api/2/issue/{}'.format(issue_id_or_key) + params = {} + + if fields is not None: + params['fields'] = fields + if properties is not None: + params['properties'] = properties + if update_history is True: + params['updateHistory'] = 'true' + if update_history is False: + params['updateHistory'] = 'false' + + return self.get(url, params=params) + def bulk_issue(self, issue_list, fields='*all'): """ :param fields: @@ -885,11 +930,54 @@ def issue_deleted(self, issue_key): log.info('Issue "{issue_key}" is deleted'.format(issue_key=issue_key)) return True + def delete_issue(self, issue_id_or_key, delete_subtasks=True): + """ + Delete an issue + If the issue has subtasks you must set the parameter delete_subtasks = True to delete the issue + You cannot delete an issue without its subtasks also being deleted + :param issue_id_or_key: + :param delete_subtasks: + :return: + """ + url = 'rest/api/2/issue/{}'.format(issue_id_or_key) + params = {} + + if delete_subtasks is True: + params['deleteSubtasks'] = 'true' + else: + params['deleteSubtasks'] = 'false' + + log.warning('Removing issue {}...'.format(issue_id_or_key)) + + return self.delete(url, params=params) + + # @todo merge with edit_issue method def issue_update(self, issue_key, fields): log.warning('Updating issue "{issue_key}" with "{fields}"'.format(issue_key=issue_key, fields=fields)) url = 'rest/api/2/issue/{0}'.format(issue_key) return self.put(url, data={'fields': fields}) + def edit_issue(self, issue_id_or_key, fields, notify_users=True): + """ + Edits an issue from a JSON representation + The issue can either be updated by setting explicit the field + value(s) or by using an operation to change the field value + + :param issue_id_or_key: str + :param fields: JSON + :param notify_users: bool + :return: + """ + url = 'rest/api/2/issue/{}'.format(issue_id_or_key) + params = {} + data = {'update': fields} + + if notify_users is True: + params['notifyUsers'] = 'true' + else: + params['notifyUsers'] = 'false' + return self.put(url, data=data, params=params) + def issue_add_watcher(self, issue_key, user): """ Start watching issue @@ -914,9 +1002,38 @@ def assign_issue(self, issue, assignee=None): return self.put(url, data=data) + def create_issue(self, fields, update_history=False): + """ + Creates an issue or a sub-task from a JSON representation + :param fields: JSON data + :param update_history: bool (if true then the user's project history is updated) + :return: + """ + url = 'rest/api/2/issue' + data = {'fields': fields} + params = {} + + if update_history is True: + params['updateHistory'] = 'true' + else: + params['updateHistory'] = 'false' + return self.post(url, params=params, data=data) + + def create_issues(self, list_of_issues_data): + """ + Creates issues or sub-tasks from a JSON representation + Creates many issues in one bulk operation + :param list_of_issues_data: list of JSON data + :return: + """ + url = 'rest/api/2/issue/bulk' + data = {'issueUpdates': list_of_issues_data} + return self.post(url, data=data) + + # @todo refactor and merge with create_issue method def issue_create(self, fields): log.warning('Creating issue "{summary}"'.format(summary=fields['summary'])) - url = 'rest/api/2/issue/' + url = 'rest/api/2/issue' return self.post(url, data={'fields': fields}) def issue_create_or_update(self, fields): @@ -949,20 +1066,49 @@ def issue_add_comment(self, issue_key, comment, visibility=None): data['visibility'] = visibility return self.post(url, data=data) + # Attachments + def get_attachment(self, attachment_id): + """ + Returns the meta-data for an attachment, including the URI of the actual attached file + :param attachment_id: int + :return: + """ + url = 'rest/api/2/attachment/{}'.format(attachment_id) + + return self.get(url) + + def remove_attachment(self, attachment_id): + """ + Remove an attachment from an issue + :param attachment_id: int + :return: if success, return None + """ + url = 'rest/api/2/attachment/{}'.format(attachment_id) + + return self.delete(url) + + def get_attachment_meta(self): + """ + Returns the meta information for an attachments, + specifically if they are enabled and the maximum upload size allowed + :return: + """ + url = 'rest/api/2/attachment/meta' + + return self.get(url) + def add_attachment(self, issue_key, filename): """ Add attachment to Issue - :param issue_key: str :param filename: str, name, if file in current directory or full path to file """ log.warning('Adding attachment...') headers = {'X-Atlassian-Token': 'no-check'} - with open(filename, 'rb') as file: - files = {'file': file} - url = 'rest/api/2/issue/{}/attachments'.format(issue_key) - - return self.post(url, headers=headers, files=files) + url = 'rest/api/2/issue/{}/attachments'.format(issue_key) + with open(filename, 'rb') as attachment: + files = {'file': attachment} + return self.post(url, headers=headers, files=files) def get_issue_remotelinks(self, issue_key, global_id=None, internal_id=None): """ @@ -1344,11 +1490,38 @@ def check_plugin_manager_status(self): url = 'rest/plugins/latest/safe-mode' return self.request(method='GET', path=url, headers=headers) + # API/2 Get permissions + def get_permissions(self, project_id=None, project_key=None, issue_id=None, issue_key=None): + """ + Returns all permissions in the system and whether the currently logged in user has them. + You can optionally provide a specific context + to get permissions for (projectKey OR projectId OR issueKey OR issueId) + + :param project_id: str + :param project_key: str + :param issue_id: str + :param issue_key: str + :return: + """ + url = 'rest/api/2/mypermissions' + params = {} + + if project_id: + params['projectId'] = project_id + if project_key: + params['projectKey'] = project_key + if issue_id: + params['issueId'] = issue_id + if issue_key: + params['issueKey'] = issue_key + + return self.get(url, params=params) + def get_all_permissions(self): """ Returns all permissions that are present in the Jira instance - Global, Project and the global ones added by plugins - :return: + :return: All permissions """ url = 'rest/api/2/permissions' return self.get(url) @@ -1454,6 +1627,43 @@ def get_project_issue_security_scheme(self, project_id_or_key, only_levels=False else: return self.get(url) + # Application properties + def get_property(self, key=None, permission_level=None, key_filter=None): + """ + Returns an application property + + :param key: str + :param permission_level: str + :param key_filter: str + :return: list or item + """ + url = 'rest/api/2/application-properties' + params = {} + + if key: + params['key'] = key + if permission_level: + params['permissionLevel'] = permission_level + if key_filter: + params['keyFilter'] = key_filter + + return self.get(url, params=params) + + def set_property(self, property_id, value): + url = 'rest/api/2/application-properties/{}'.format(property_id) + data = {'id': property_id, 'value': value} + + return self.put(url, data=data) + + def get_advanced_settings(self): + """ + Returns the properties that are displayed on the "General Configuration > Advanced Settings" page + :return: + """ + url = 'rest/api/2/application-properties/advanced-settings' + + return self.get(url) + """ ####################################################################### # Tempo Account REST API implements # diff --git a/atlassian/jira8.py b/atlassian/jira8.py index 960592678..17cd3c60a 100644 --- a/atlassian/jira8.py +++ b/atlassian/jira8.py @@ -1,243 +1,10 @@ # coding=utf-8 import logging -from .rest_client import AtlassianRestAPI +from jira import Jira log = logging.getLogger(__name__) -class Jira8(AtlassianRestAPI): - - # API/2 Get permissions - def get_permissions(self, project_id=None, project_key=None, issue_id=None, issue_key=None): - """ - Returns all permissions in the system and whether the currently logged in user has them. - You can optionally provide a specific context - to get permissions for (projectKey OR projectId OR issueKey OR issueId) - - :param project_id: str - :param project_key: str - :param issue_id: str - :param issue_key: str - :return: - """ - url = 'rest/api/2/mypermissions' - params = {} - - if project_id: - params['projectId'] = project_id - if project_key: - params['projectKey'] = project_key - if issue_id: - params['issueId'] = issue_id - if issue_key: - params['issueKey'] = issue_key - - return self.get(url, params=params) - - def get_all_permissions(self): - """ - Returns all permissions that are present in the JIRA instance - Global, Project - and the global ones added by plugins - - :return: All permissions - """ - url = 'rest/api/2/permissions' - - return self.get(url) - - # Application properties - def get_property(self, key=None, permission_level=None, key_filter=None): - """ - Returns an application property - - :param key: str - :param permission_level: str - :param key_filter: str - :return: list or item - """ - url = 'rest/api/2/application-properties' - params = {} - - if key: - params['key'] = key - if permission_level: - params['permissionLevel'] = permission_level - if key_filter: - params['keyFilter'] = key_filter - - return self.get(url, params=params) - - def set_property(self, property_id, value): - url = 'rest/api/2/application-properties/{}'.format(property_id) - data = {'id': property_id, 'value': value} - - return self.put(url, data=data) - - def get_advanced_settings(self): - """ - Returns the properties that are displayed on the "General Configuration > Advanced Settings" page - - :return: - """ - url = 'rest/api/2/application-properties/advanced-settings' - - return self.get(url) - - # Application roles - def get_all_application_roles(self): - """ - Returns all ApplicationRoles in the system - - :return: - """ - url = 'rest/api/2/applicationrole' - - return self.get(url) - - def get_application_role(self, role_key): - """ - Returns the ApplicationRole with passed key if it exists - - :param role_key: str - :return: - """ - url = 'rest/api/2/applicationrole/{}'.format(role_key) - - return self.get(url) - - # Attachments - def get_attachment(self, attachment_id): - """ - Returns the meta-data for an attachment, including the URI of the actual attached file - - :param attachment_id: int - :return: - """ - url = 'rest/api/2/attachment/{}'.format(attachment_id) - - return self.get(url) - - def remove_attachment(self, attachment_id): - """ - Remove an attachment from an issue - - :param attachment_id: int - :return: if success, return None - """ - url = 'rest/api/2/attachment/{}'.format(attachment_id) - - return self.delete(url) - - def get_attachment_meta(self): - """ - Returns the meta information for an attachments, - specifically if they are enabled and the maximum upload size allowed - - :return: - """ - url = 'rest/api/2/attachment/meta' - - return self.get(url) - - # Issues - def create_issue(self, fields, update_history=False): - """ - Creates an issue or a sub-task from a JSON representation - - :param fields: JSON data - :param update_history: bool (if true then the user's project history is updated) - :return: - """ - url = 'rest/api/2/issue' - data = {'fields': fields} - params = {} - - if update_history is True: - params['updateHistory'] = 'true' - else: - params['updateHistory'] = 'false' - - return self.post(url, params=params, data=data) - - def create_issues(self, list_of_issues_data): - """ - Creates issues or sub-tasks from a JSON representation - Creates many issues in one bulk operation - - :param list_of_issues_data: list of JSON data - :return: - """ - url = 'rest/api/2/issue/bulk' - data = {'issueUpdates': list_of_issues_data} - - return self.post(url, data=data) - - def delete_issue(self, issue_id_or_key, delete_subtasks=True): - """ - Delete an issue - If the issue has subtasks you must set the parameter delete_subtasks = True to delete the issue - You cannot delete an issue without its subtasks also being deleted - - :param issue_id_or_key: - :param delete_subtasks: - :return: - """ - url = 'rest/api/2/issue/{}'.format(issue_id_or_key) - params = {} - - if delete_subtasks is True: - params['deleteSubtasks'] = 'true' - else: - params['deleteSubtasks'] = 'false' - - log.warning('Removing issue {}...'.format(issue_id_or_key)) - - return self.delete(url, params=params) - - def edit_issue(self, issue_id_or_key, fields, notify_users=True): - """ - Edits an issue from a JSON representation - The issue can either be updated by setting explicit the field - value(s) or by using an operation to change the field value - - :param issue_id_or_key: str - :param fields: JSON - :param notify_users: bool - :return: - """ - url = 'rest/api/2/issue/{}'.format(issue_id_or_key) - params = {} - data = {'update': fields} - - if notify_users is True: - params['notifyUsers'] = 'true' - else: - params['notifyUsers'] = 'false' - - return self.put(url, data=data, params=params) - - def get_issue(self, issue_id_or_key, fields=None, properties=None, update_history=True): - """ - Returns a full representation of the issue for the given issue key - By default, all fields are returned in this get-issue resource - - :param issue_id_or_key: str - :param fields: str - :param properties: str - :param update_history: bool - :return: issue - """ - url = 'rest/api/2/issue/{}'.format(issue_id_or_key) - params = {} - - if fields is not None: - params['fields'] = fields - if properties is not None: - params['properties'] = properties - if update_history is True: - params['updateHistory'] = 'true' - if update_history is False: - params['updateHistory'] = 'false' - - return self.get(url, params=params) +class Jira8(Jira): + # methods migrated into main module \ No newline at end of file diff --git a/docs/jira8.rst b/docs/jira8.rst deleted file mode 100644 index 59034ef5a..000000000 --- a/docs/jira8.rst +++ /dev/null @@ -1,4 +0,0 @@ -Jira 8 module -============= - -This is the test module. \ No newline at end of file From 61b8fbabe5b1f7069ac83be9345eca9ab763102d Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sat, 23 Nov 2019 22:21:24 +0300 Subject: [PATCH 12/16] Add reindex methods --- atlassian/bamboo.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/atlassian/bamboo.py b/atlassian/bamboo.py index dd9a156e4..171308987 100755 --- a/atlassian/bamboo.py +++ b/atlassian/bamboo.py @@ -590,6 +590,20 @@ def chart(self, report_key, build_keys, group_by_period, date_filter=None, date_ params['height'] = height return self.get(self.resource_url('chart'), params=params) + def reindex(self): + """ + Returns status of the current indexing operation. + reindexInProgress - reindex is currently performed in background reindexPending - reindex is required + (i.e. it failed before or some upgrade task asked for it) + """ + return self.get(self.resource_url('reindex')) + + def stop_reindex(self): + """ + Kicks off a reindex. Requires system admin permissions to perform this reindex. + """ + return self.post(self.resource_url('reindex')) + def health_check(self): """ Get health status From cf3e80e0be8b6a49005d57dcc570e3d9ae2cccfd Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sat, 23 Nov 2019 23:41:14 +0300 Subject: [PATCH 13/16] Project related methods --- atlassian/bamboo.py | 19 +++++++++++++++++++ atlassian/jira8.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/atlassian/bamboo.py b/atlassian/bamboo.py index 171308987..2e20d7094 100755 --- a/atlassian/bamboo.py +++ b/atlassian/bamboo.py @@ -406,6 +406,25 @@ def delete_label(self, project_key, plan_key, build_number, label): resource = "result/{}-{}-{}/label/{}".format(project_key, plan_key, build_number, label) return self.delete(self.resource_url(resource)) + def get_projects(self): + """Method used to list all projects defined in Bamboo. + Projects without any plan are not listed by default, unless showEmpty query param is set to true.""" + resource = 'project?showEmpty' + for project in self.get(self.resource_url(resource)): + yield project + + def get_project(self, project_key): + """Method used to retrieve information for project specified as project key. + Possible expand parameters: plans, list of plans for project. plans.plan, list of plans with plan details + (only plans visible - READ permission for user)""" + resource = 'project/{}?showEmpty'.format(project_key) + return self.get(self.resource_url(resource)) + + def delete_project(self, project_key): + """Marks project for deletion. Project will be deleted by a batch job.""" + resource = 'project/{}'.format(project_key) + return self.delete(self.resource_url(resource)) + """ Deployments """ def deployment_projects(self): diff --git a/atlassian/jira8.py b/atlassian/jira8.py index 17cd3c60a..9f8a78a1e 100644 --- a/atlassian/jira8.py +++ b/atlassian/jira8.py @@ -1,7 +1,7 @@ # coding=utf-8 import logging -from jira import Jira +from .jira import Jira log = logging.getLogger(__name__) From a950ffbe3e801e79490f40701e8b39c915c02a7c Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sat, 23 Nov 2019 23:50:01 +0300 Subject: [PATCH 14/16] Add update setup.py --- atlassian/VERSION | 2 +- setup.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/atlassian/VERSION b/atlassian/VERSION index 90b5837b1..c97038c79 100644 --- a/atlassian/VERSION +++ b/atlassian/VERSION @@ -1,2 +1,2 @@ -1.14.3 +1.14.4 diff --git a/setup.py b/setup.py index 3cc3bf3d4..6170f4beb 100644 --- a/setup.py +++ b/setup.py @@ -15,11 +15,12 @@ license='Apache License 2.0', version=version, download_url='https://github.com/atlassian-api/atlassian-python-api', - author='Matt Harasymczuk', author_email='matt@astrotech.io', + maintainer='Gonchik Tsymzhitov', + maintainer_email='gonchik.tsymzhitov@gmail.com', url='https://github.com/atlassian-api/atlassian-python-api', - keywords='atlassian jira confluence bitbucket bamboo crowd portfolio tempo teams servicedesk jsd rest api', + keywords='atlassian jira core software confluence bitbucket bamboo crowd portfolio tempo servicedesk jsd rest api', packages=find_packages(), package_dir={'atlassian': 'atlassian'}, From 952ca9ef2c6750e3af71781f7319e80784f8f1f3 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 24 Nov 2019 00:02:01 +0300 Subject: [PATCH 15/16] Rename variables and polish to be in PEP-8 --- atlassian/bamboo.py | 3 ++- atlassian/bitbucket.py | 8 ++++---- atlassian/confluence.py | 9 +++++---- atlassian/jira.py | 22 ++++++++++------------ atlassian/jira8.py | 2 +- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/atlassian/bamboo.py b/atlassian/bamboo.py index 2e20d7094..b3e2ed124 100755 --- a/atlassian/bamboo.py +++ b/atlassian/bamboo.py @@ -201,7 +201,8 @@ def get_vcs_branches(self, plan_key, max_results=25): :return: """ resource = 'plan/{plan_key}/vcsBranches'.format(plan_key=plan_key) - return self.base_list_call(resource, start_index=0, max_results=max_results) + return self.base_list_call(resource, start_index=0, max_results=max_results, clover_enabled=None, expand=None, + favourite=None) """ Build results """ diff --git a/atlassian/bitbucket.py b/atlassian/bitbucket.py index 2185d3c72..740da636d 100644 --- a/atlassian/bitbucket.py +++ b/atlassian/bitbucket.py @@ -815,8 +815,8 @@ def update_pull_request_comment(self, project, repository, pull_request_id, comm that must match the server's version of the comment or the update will fail. """ - url = "rest/api/1.0/projects/{project}/repos/{repository}/pull-requests/{pull_request_id}/comments/{comment_id}".format( - project=project, repository=repository, pull_request_id=pull_request_id, comment_id=comment_id + url = 'rest/api/1.0/projects/{project}/repos/{repo}/pull-requests/{pull_request}/comments/{comment_id}'.format( + project=project, repo=repository, pull_request=pull_request_id, comment_id=comment_id ) payload = { "version": comment_version, @@ -852,8 +852,8 @@ def change_reviewed_status(self, project_key, repository_slug, pull_request_id, :param user_slug: :return: """ - url = "rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/participants/{userSlug}".format( - projectKey=project_key, repositorySlug=repository_slug, pullRequestId=pull_request_id, userSlug=user_slug, + url = 'rest/api/1.0/projects/{projectKey}/repos/{repo}/pull-requests/{pull_request}/participants/{userSlug}'.format( + projectKey=project_key, repo=repository_slug, pull_request=pull_request_id, userSlug=user_slug, ) approved = True if status == "APPROVED" else False data = { diff --git a/atlassian/confluence.py b/atlassian/confluence.py index 8ea0efd9b..1af555718 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -895,10 +895,11 @@ def get_group_members(self, group_name='confluence-users', start=0, limit=1000, :param expand: OPTIONAL: A comma separated list of properties to expand on the content. status :return: """ - url = 'rest/api/group/{group_name}/member?limit={limit}&start={start}&expand={expand}'.format(group_name=group_name, - limit=limit, - start=start, - expand=expand) + url = 'rest/api/group/{group_name}/member?limit={limit}&start={start}&expand={expand}'.format( + group_name=group_name, + limit=limit, + start=start, + expand=expand) return (self.get(url) or {}).get('results') def get_space(self, space_key, expand='description.plain,homepage'): diff --git a/atlassian/jira.py b/atlassian/jira.py index ff41feae2..9fc459da2 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -306,34 +306,33 @@ def user_find_by_user_string(self, username, start=0, limit=50, include_inactive } return self.get(url, params=params) - def is_user_in_application(self, username, applicationKey): + def is_user_in_application(self, username, application_key): """ Utility function to test whether a user has an application role :param username: The username of the user to test. - :param applicationKey: The application key of the application + :param application_key: The application key of the application :return: True if the user has the application, else False """ user = self.user(username, 'applicationRoles') # Get applications roles of the user if 'self' in user: for applicationRole in user.get('applicationRoles').get('items'): - if applicationRole.get('key') == applicationKey: + if applicationRole.get('key') == application_key: return True - return False - def add_user_to_application(self, username, applicationKey): + def add_user_to_application(self, username, application_key): """ Add a user to an application :param username: The username of the user to add. - :param applicationKey: The application key of the application + :param application_key: The application key of the application :return: True if the user was added to the application, else False :see: https://docs.atlassian.com/software/jira/docs/api/REST/7.5.3/#api/2/user-addUserToApplication """ params = { 'username': username, - 'applicationKey': applicationKey + 'applicationKey': application_key } - return self.post('rest/api/2/user/application', params=params) == None + return self.post('rest/api/2/user/application', params=params) is None # Application roles def get_all_application_roles(self): @@ -2071,10 +2070,9 @@ def tempo_teams_add_member(self, team_id, member_key): :return: """ data = {"member": {"key": str(member_key), "type": "USER"}, - "membership": { - "availability": "100", - "role": {"id": 1} - }} + "membership": {"availability": "100", + "role": {"id": 1}} + } return self.tempo_teams_add_member_raw(team_id, member_data=data) def tempo_teams_add_membership(self, team_id, member_id): diff --git a/atlassian/jira8.py b/atlassian/jira8.py index 9f8a78a1e..b5073b037 100644 --- a/atlassian/jira8.py +++ b/atlassian/jira8.py @@ -7,4 +7,4 @@ class Jira8(Jira): - # methods migrated into main module \ No newline at end of file +# methods migrated into main module From f83adac0d1acb58a67a901ebcbe55635c58cb34f Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 24 Nov 2019 00:25:34 +0300 Subject: [PATCH 16/16] Add Jira issue regex matcher --- atlassian/jira.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 75933bc77..e6c650bf7 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1,6 +1,6 @@ # coding=utf-8 import logging - +import re from requests.exceptions import HTTPError from .rest_client import AtlassianRestAPI @@ -576,8 +576,13 @@ def bulk_issue(self, issue_list, fields='*all'): :param list issue_list: :return: """ + jira_issue_regex = re.compile('[A-Z]{1,10}-\d+') missing_issues = list() - jql = 'key in ({})'.format(', '.join(['"{}"'.format(key) for key in issue_list])) + matched_issue_keys = list() + for key in issue_list: + if re.match(jira_issue_regex, key): + matched_issue_keys.append(key) + jql = 'key in ({})'.format(', '.join(matched_issue_keys)) query_result = self.jql(jql, fields=fields) if 'errorMessages' in query_result.keys(): for message in query_result['errorMessages']: