From 12b19b5147262a65c34d8857242dd91dc02301c7 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Thu, 3 Oct 2019 19:57:20 +0300 Subject: [PATCH 01/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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/31] 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']: From 9f1ef7c51104c080ed7219cec9fa42d8415e1ec6 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 24 Nov 2019 00:36:07 +0300 Subject: [PATCH 17/31] Add static method --- atlassian/jira8.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/atlassian/jira8.py b/atlassian/jira8.py index caa3b600a..d677fd748 100644 --- a/atlassian/jira8.py +++ b/atlassian/jira8.py @@ -8,3 +8,6 @@ class Jira8(Jira): # methods migrated into main module + @staticmethod + def print_module_name(): + print(__name__) From 2dbdffdc624ddb37af47fda33525018eb957a737 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 24 Nov 2019 21:56:36 +0300 Subject: [PATCH 18/31] [Jira] fix upload attachment methods --- atlassian/jira.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index e6c650bf7..68487b68d 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1078,7 +1078,6 @@ def get_attachment(self, attachment_id): :return: """ url = 'rest/api/2/attachment/{}'.format(attachment_id) - return self.get(url) def remove_attachment(self, attachment_id): @@ -1088,7 +1087,6 @@ def remove_attachment(self, attachment_id): :return: if success, return None """ url = 'rest/api/2/attachment/{}'.format(attachment_id) - return self.delete(url) def get_attachment_meta(self): @@ -1098,7 +1096,6 @@ def get_attachment_meta(self): :return: """ url = 'rest/api/2/attachment/meta' - return self.get(url) def add_attachment(self, issue_key, filename): @@ -1112,7 +1109,7 @@ def add_attachment(self, issue_key, filename): 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) + return self.post(url, headers=headers, files=files) def get_issue_remotelinks(self, issue_key, global_id=None, internal_id=None): """ From df1f8b06ffaeae03cd5a8d6c8734d5421b0cc17c Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 25 Nov 2019 10:33:53 +0300 Subject: [PATCH 19/31] Hotfix for jira8 class --- atlassian/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlassian/VERSION b/atlassian/VERSION index c97038c79..c8c23e55b 100644 --- a/atlassian/VERSION +++ b/atlassian/VERSION @@ -1,2 +1,2 @@ -1.14.4 +1.14.4.1 From b38682104d32574e39700eaf249d390c1545b498 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 25 Nov 2019 10:36:49 +0300 Subject: [PATCH 20/31] Hotfix for jira8 class --- atlassian/jira8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlassian/jira8.py b/atlassian/jira8.py index d677fd748..f1adff952 100644 --- a/atlassian/jira8.py +++ b/atlassian/jira8.py @@ -10,4 +10,4 @@ class Jira8(Jira): # methods migrated into main module @staticmethod def print_module_name(): - print(__name__) + print("The class {} merged into Jira Class".format(__name__)) \ No newline at end of file From 9452aaa055eff63d82f58a239d2a360a9f1632a9 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 22 Dec 2019 11:33:36 +0300 Subject: [PATCH 21/31] Add the cookie file supports --- atlassian/rest_client.py | 2 ++ atlassian/utils.py | 17 +++++++++++++++++ docs/index.rst | 22 ++++++++++++++++++++++ requirements.txt | 2 +- 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/atlassian/rest_client.py b/atlassian/rest_client.py index a8f6be1bd..7dd83f006 100644 --- a/atlassian/rest_client.py +++ b/atlassian/rest_client.py @@ -46,6 +46,8 @@ def __init__(self, url, username=None, password=None, timeout=60, api_root='rest self._create_oauth_session(oauth) elif kerberos is not None: self._create_kerberos_session(kerberos) + elif cookies is not None: + self._session.cookies.update(cookies) def _create_basic_session(self, username, password): self._session.auth = (username, password) diff --git a/atlassian/utils.py b/atlassian/utils.py index 1e7976349..b91b5d95b 100644 --- a/atlassian/utils.py +++ b/atlassian/utils.py @@ -259,3 +259,20 @@ def symbol_normalizer(text): result = result.replace('å', u'å') result = result.replace('°', u'°') return result + + +def parse_cookie_file(cookie_file): + """ + Parse a cookies.txt file (Netscape HTTP Cookie File) + return a dictionary of key value pairs + compatible with requests. + :param cookie_file: a cookie file + :return dict of cookies pair + """ + cookies = {} + with open(cookie_file, 'r') as fp: + for line in fp: + if not re.match(r'^\#', line): + line_fields = line.strip().split('\t') + cookies[line_fields[5]] = line_fields[6] + return cookies diff --git a/docs/index.rst b/docs/index.rst index d7797e44c..c62c379ac 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -124,6 +124,28 @@ Or Kerberos *(installation with kerberos extra necessary)*: url='http://localhost:8080', kerberos=kerberos_service) +Or reuse cookie file: + +.. code-block:: python + + from atlassian import utils + cookie_dict = utils.parse_cookie_file("cookie.txt") + + jira = Jira( + url='http://localhost:8080', + cookie=cookie_dict) + + confluence = Confluence( + url='http://localhost:8090', + cookie=cookie_dict) + + bitbucket = Bitbucket( + url='http://localhost:7990', + cookie=cookie_dict) + + service_desk = ServiceDesk( + url='http://localhost:8080', + cookie=cookie_dict) .. toctree:: :maxdept:2 diff --git a/requirements.txt b/requirements.txt index 8f7f01869..13169a458 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ requests oauthlib requests_oauthlib kerberos; platform_system!='Windows' -kerberos-sspi; platform_system=='Windows' +kerberos-sspi; platform_system=='Windows' \ No newline at end of file From 2bfd7246c0d8cda548fd73de58bd8de0644c3c67 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sun, 22 Dec 2019 11:55:58 +0300 Subject: [PATCH 22/31] Add methods for Confluence docs --- atlassian/confluence.py | 18 +++++++++++++++++ docs/confluence.rst | 43 +++++++++++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/atlassian/confluence.py b/atlassian/confluence.py index ad62a9198..2474553ea 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -329,6 +329,11 @@ def get_all_draft_pages_from_space_through_cql(self, space, start=0, limit=500, return (self.get(url, params=params) or {}).get('results') def get_all_restictions_for_content(self, content_id): + """keep typo method""" + log.warning("Please, be informed that is deprecated as typo naming") + return self.get_all_restrictions_for_content(content_id=content_id) + + def get_all_restrictions_for_content(self, content_id): """ Returns info about all restrictions by operation. :param content_id: @@ -552,6 +557,13 @@ def remove_page_attachment_keep_version(self, page_id, filename, keep_last_versi log.info("Kept versions {} for {}".format(keep_last_versions, attachment.get('title'))) def get_attachment_history(self, attachment_id, limit=200, start=0): + """ + Get attachment history + :param attachment_id + :param limit + :param start + :return + """ params = {'limit': limit, 'start': start} url = 'rest/experimental/content/{}/version'.format(attachment_id) return (self.get(url, params=params) or {}).get("results") @@ -614,6 +626,12 @@ def get_content_history(self, content_id): return self.history(content_id) def get_content_history_by_version_number(self, content_id, version_number): + """ + Get content history by version number + :param content_id: + :param version_number: + :return: + """ url = 'rest/experimental/content/{0}/version/{1}'.format(content_id, version_number) return self.get(url) diff --git a/docs/confluence.rst b/docs/confluence.rst index 34ad8286f..457dd3b29 100644 --- a/docs/confluence.rst +++ b/docs/confluence.rst @@ -34,7 +34,7 @@ Get page info confluence.get_all_pages_by_label(label, start=0, limit=50) # Get all pages from Space - # contet_type can be 'page' or 'blogpost'. Defaults to 'page' + # content_type can be 'page' or 'blogpost'. Defaults to 'page' # expand is a comma separated list of properties to expand on the content. # max limit is 100. For more you have to loop over start values. confluence.get_all_pages_from_space(space, start=0, limit=100, status=None, expand=None, content_type='page') @@ -51,7 +51,7 @@ Get page info confluence.get_all_draft_pages_from_space_through_cql(space, start=0, limit=500, status='draft') # Info about all restrictions by operation - confluence.get_all_restictions_for_content(content_id) + confluence.get_all_restrictions_for_content(content_id) Page actions ------------ @@ -61,8 +61,11 @@ Page actions # Create page from scratch confluence.create_page(space, title, body, parent_id=None, type='page', representation='storage') - # Remove page - confluence.remove_page(page_id, status=None) + # This method removes a page, if it has recursive flag, method removes including child pages + confluence.remove_page(page_id, status=None, recursive=False) + + # Remove any content + confluence.remove_content(content_id): # Remove page from trash confluence.remove_page_from_trash(page_id) @@ -99,6 +102,24 @@ Page actions # automatically version the new file and keep the old one confluence.attach_content(content, name=None, content_type=None, page_id=None, title=None, space=None, comment=None) + # Remove completely a file if version is None or delete version + confluence.delete_attachment(page_id, filename, version=None) + + # Remove completely a file if version is None or delete version + confluence.delete_attachment_by_id(attachment_id, version) + + # Keep last versions + confluence.remove_page_attachment_keep_version(page_id, filename, keep_last_versions) + + # Get attachment history + confluence.get_attachment_history(attachment_id, limit=200, start=0) + + # Get attachment for content + confluence.get_attachments_from_content(page_id, start=0, limit=50, expand=None, filename=None, media_type=None) + + # Check has unknown attachment error on page + confluence.has_unknown_attachment_error(page_id) + # Export page as PDF # api_version needs to be set to 'cloud' when exporting from Confluence Cloud. confluence.export_page(page_id) @@ -108,14 +129,18 @@ Page actions # Delete Confluence page label confluence.remove_page_label(page_id, label) - + + # Add comment into page + confluence.add_comment(page_id, text) + Get spaces info --------------- .. code-block:: python # Get all spaces with provided limit - confluence.get_all_spaces(start=0, limit=500) + # additional info, e.g. metadata, icon, description, homepage + confluence.get_all_spaces(start=0, limit=500, expand=None) # Get information about a space through space key confluence.get_space(space_key, expand='description.plain,homepage') @@ -165,6 +190,12 @@ Other actions # Get page history confluence.history(page_id) + # Get content history by version number. It works as experimental method + confluence.get_content_history_by_version_number(content_id, version_number) + + # Remove content history. It works as experimental method + confluence.remove_content_history(page_id, version_number) + # Compare content and check is already updated or not confluence.is_page_content_is_already_updated(page_id, body) From 6a3cee2a2d0bf0dd533890b157b5882bd073ca27 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Wed, 8 Jan 2020 19:42:51 +0300 Subject: [PATCH 23/31] Polish comment --- atlassian/jira.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 68487b68d..ab6986c6e 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -2068,7 +2068,7 @@ def tempo_teams_add_member(self, team_id, member_key): """ Add team member :param team_id: - :param member_key: + :param member_key: user_name or user_key of Jira :return: """ data = {"member": {"key": str(member_key), "type": "USER"}, From 497da1c0895e12425acb0e4262de80cc9bb69e47 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Fri, 10 Jan 2020 19:39:16 +0300 Subject: [PATCH 24/31] Add priority scheme configs --- atlassian/jira.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/atlassian/jira.py b/atlassian/jira.py index 8f388a20b..3da93e1f0 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1628,6 +1628,72 @@ def get_project_issue_security_scheme(self, project_id_or_key, only_levels=False else: return self.get(url) + # Priority Schemes + def get_all_priority_schemes(self, start=0, limit=100, expand=None): + """ + Returns all priority schemes. + All project keys associated with the priority scheme will only be returned + if additional query parameter is provided expand=schemes.projectKeys. + :param start: the page offset, if not specified then defaults to 0 + :param limit: how many results on the page should be included. Defaults to 100, maximum is 1000. + :param expand: can be 'schemes.projectKeys' + :return: + """ + url = 'rest/api/2/priorityschemes' + params = {} + if start: + params['startAt'] = int(start) + if limit: + params['maxResults'] = int(limit) + if expand: + params['expand'] = expand + return self.get(url, params=params) + + def create_priority_scheme(self, data): + """ + Creates new priority scheme. + :param data: + {"name": "New priority scheme", + "description": "Priority scheme for very important projects", + "defaultOptionId": "3", + "optionIds": [ + "1", + "2", + "3", + "4", + "5" + ]} + :return: Returned if the priority scheme was created. + """ + return self.post(path="rest/api/2/priorityschemes", data=data) + + # api/2/project/{projectKeyOrId}/priorityscheme + # Resource for associating priority schemes and projects. + def get_priority_scheme_of_project(self, project_key_or_id): + """ + Gets a full representation of a priority scheme in JSON format used by specified project. + User must be global administrator or project administrator. + :param project_key_or_id: + :return: + """ + url = 'rest/api/2/project/{}/priorityscheme'.format(project_key_or_id) + return self.get(url) + + def assign_priority_scheme_for_project(self, project_key_or_id, priority_scheme_id): + """ + Assigns project with priority scheme. Priority scheme assign with migration is possible from the UI. + Operation will fail if migration is needed as a result of operation + eg. there are issues with priorities invalid in the destination scheme. + All project keys associated with the priority scheme will only be returned + if additional query parameter is provided expand=projectKeys. + :param project_key_or_id: + :param priority_scheme_id: + :return: + """ + url = "rest/api/2/project/{projectKeyOrId}/priorityscheme".format(projectKeyOrId=project_key_or_id) + data = {"id": priority_scheme_id} + return self.put(url, data=data) + # Application properties def get_property(self, key=None, permission_level=None, key_filter=None): """ From 319e6aa0c7822433fa53628c5b33dd5247e10b39 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 20 Jan 2020 17:38:02 +0300 Subject: [PATCH 25/31] Bitbucket: example for clean branches --- atlassian/jira.py | 20 +++--- examples/stash-clean-jira-branches.py | 95 +++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 examples/stash-clean-jira-branches.py diff --git a/atlassian/jira.py b/atlassian/jira.py index 8e332f0f7..e8aea3aa6 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -139,6 +139,16 @@ def user_update_username(self, old_username, new_username): data = {"name": new_username} return self.user_update(old_username, data=data) + def user_update_email(self, username, email): + """ + Update user email for new domain changes + :param username: + :param email: + :return: + """ + data = {'name': username, 'emailAddress': email} + return self.user_update(username, data=data) + def user_create(self, username, email, display_name, password=None, notification=None): """ Create a user in Jira @@ -219,16 +229,6 @@ def user_update_or_create_property_through_rest_point(self, username, key, value params = {'username': username, 'property': key, 'value': value} return self.get(url, params=params) - def user_update_email(self, username, email): - """ - Update user email for new domain changes - :param username: - :param email: - :return: - """ - data = {'name': username, 'emailAddress': email} - return self.user_update(username, data=data) - def user_deactivate(self, username): """ Disable user diff --git a/examples/stash-clean-jira-branches.py b/examples/stash-clean-jira-branches.py new file mode 100644 index 000000000..1bccca5a0 --- /dev/null +++ b/examples/stash-clean-jira-branches.py @@ -0,0 +1,95 @@ +from atlassian import Jira +from atlassian import Stash +from var import config +import logging +import time + +PROJECT_KEY = 'PROJ' +REPOS = ['repo1', 'repo2'] +ACCEPTED_ISSUE_STATUSES = ["Closed", "Verified"] +EXCLUDE_REPO_RULES = config.exclude_parameters +LAST_COMMIT_CONDITION_IN_DAYS = 75 +ATLASSIAN_USER = config.JIRA_LOGIN +ATLASSIAN_PASSWORD = config.JIRA_PASSWORD + +logging.basicConfig(level=logging.ERROR) +jira = Jira( + url=config.JIRA_URL, + username=ATLASSIAN_USER, + password=ATLASSIAN_PASSWORD) + +stash = Stash( + url=config.STASH_URL, + username=ATLASSIAN_USER, + password=ATLASSIAN_PASSWORD +) + +flag = True +time_now = int(time.time()) * 1000 +delta_for_time_ms = LAST_COMMIT_CONDITION_IN_DAYS * 24 * 60 * 60 * 1000 +commit_info_key = "com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata" +out_going_pull_request = "com.atlassian.bitbucket.server.bitbucket-ref-metadata:outgoing-pull-request-metadata" +branch_related_issues = "com.atlassian.bitbucket.server.bitbucket-jira:branch-list-jira-issues" + + +def is_can_removed_branch(branch_candidate): + branch_id_name = branch_candidate.get('id') + # Just exclude exist mainstream branches + if any(x in branch_id_name for x in EXCLUDE_REPO_RULES): + print(branch.get('displayId') + " in exclusion list") + return False + # skip default branch maybe DevOps made configs in ui + if branch_candidate.get('isDefault'): + print(branch.get('displayId') + " is default") + return False + pull_request_info = ((branch_candidate.get('metadata') or {}).get(out_going_pull_request) or {}) + if pull_request_info.get('pullRequest') is not None or (pull_request_info.get('open') or 0) > 0: + print(branch.get('displayId') + " has open PR") + return False + # skip branches without pull request info + if pull_request_info is None or len(pull_request_info) == 0: + print(branch.get('displayId') + " without pull request info") + # return False + + author_time_stamp = branch_candidate.get('metadata').get(commit_info_key).get('authorTimestamp') + # check latest commit info + if time_now - author_time_stamp < delta_for_time_ms: + print(branch.get('displayId') + " is too early to remove") + return False + + # check issues statuses + exclude = False + issues_in_metadata = branch_candidate.get('metadata').get(branch_related_issues) + for issue in issues_in_metadata: + if jira.get_issue_status(issue.get('key')) not in ACCEPTED_ISSUE_STATUSES: + print(branch.get('displayId') + " related issue has not Resolution ") + return False + # so branch can be removed + return True + + +if __name__ == '__main__': + DRY_RUN = False + log = open("candidate_to_remove.csv", "w") + log.write("'Branch name', 'Latest commit', 'Related issues has Resolution'\n") + for repository in REPOS: + step = 0 + limit = 10 + while flag: + branches = stash.get_branches(PROJECT_KEY, repository, start=step * limit, limit=limit, + order_by='ALPHABETICAL') + if len(branches) == 0: + flag = False + break + for branch in branches: + display_id = branch['displayId'] + committer_time_stamp = branch.get('metadata').get(commit_info_key).get('committerTimestamp') / 1000 + last_date_commit = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(committer_time_stamp)) + if is_can_removed_branch(branch): + if not DRY_RUN: + stash.delete_branch(project=PROJECT_KEY, repository=repository, name=display_id, + end_point=branch['latestCommit']) + log.write("{},{},{}\n".format(display_id, last_date_commit, True)) + step += 1 + log.close() + print("Done") From 1bceaca616af7596c230bbdf1efb2f95e94147c9 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 27 Jan 2020 12:12:21 +0300 Subject: [PATCH 26/31] [Confluence] Evaluate the move page method --- atlassian/confluence.py | 34 +++++++++++++++++++++++----------- atlassian/rest_client.py | 2 +- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/atlassian/confluence.py b/atlassian/confluence.py index e6a790273..93a38d23f 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -416,6 +416,15 @@ def create_page(self, space, title, body, parent_id=None, type='page', data['ancestors'] = [{'type': type, 'id': parent_id}] return self.post(url, data=data) + def move_page(self, space_key, page_id, target_title, position="append"): + url = "/pages/movepage.action" + params = {"spaceKey": space_key, "pageId": page_id} + if target_title: + params["targetTitle"] = target_title + if position: + params["position"] = position + return self.get(url, params=params, headers=self.no_check_headers) + def get_all_spaces(self, start=0, limit=500, expand=None): """ Get all spaces with provided limit @@ -714,12 +723,12 @@ def is_page_content_is_already_updated(self, page_id, body, title=None): """ if self.advanced_mode: confluence_content = (((self.get_page_by_id(page_id, expand='body.storage').json() or {}) - .get('body') or {}) - .get('storage') or {}) + .get('body') or {}) + .get('storage') or {}) else: confluence_content = (((self.get_page_by_id(page_id, expand='body.storage') or {}) - .get('body') or {}) - .get('storage') or {}) + .get('body') or {}) + .get('storage') or {}) if title: current_title = confluence_content.get('title', None) @@ -793,8 +802,9 @@ def update_page(self, page_id, title, body, parent_id=None, type='page', represe return self.put('rest/api/content/{0}'.format(page_id), data=data) - def _insert_to_existing_page(self, page_id, title, insert_body, parent_id=None, type='page', representation='storage', - minor_edit=False, top_of_page=False): + def _insert_to_existing_page(self, page_id, title, insert_body, parent_id=None, type='page', + representation='storage', + minor_edit=False, top_of_page=False): """ Insert body to a page if already exist :param parent_id: @@ -849,11 +859,12 @@ def append_page(self, page_id, title, append_body, parent_id=None, type='page', """ log.info('Updating {type} "{title}"'.format(title=title, type=type)) - return self._insert_to_existing_page(page_id, title, append_body, parent_id=parent_id, type=type, representation=representation, - minor_edit=minor_edit, top_of_page=False) + return self._insert_to_existing_page(page_id, title, append_body, parent_id=parent_id, type=type, + representation=representation, + minor_edit=minor_edit, top_of_page=False) def prepend_page(self, page_id, title, prepend_body, parent_id=None, type='page', representation='storage', - minor_edit=False): + minor_edit=False): """ Append body to page if already exist :param parent_id: @@ -868,8 +879,9 @@ def prepend_page(self, page_id, title, prepend_body, parent_id=None, type='page' """ log.info('Updating {type} "{title}"'.format(title=title, type=type)) - return self._insert_to_existing_page(page_id, title, prepend_body, parent_id=parent_id, type=type, representation=representation, - minor_edit=minor_edit, top_of_page=True) + return self._insert_to_existing_page(page_id, title, prepend_body, parent_id=parent_id, type=type, + representation=representation, + minor_edit=minor_edit, top_of_page=True) def update_or_create(self, parent_id, title, body, representation='storage', minor_edit=False): """ diff --git a/atlassian/rest_client.py b/atlassian/rest_client.py index d808f431f..0274867a7 100644 --- a/atlassian/rest_client.py +++ b/atlassian/rest_client.py @@ -18,7 +18,7 @@ class AtlassianRestAPI(object): 'X-ExperimentalApi': 'opt-in'} form_token_headers = {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Atlassian-Token': 'no-check'} - + no_check_headers = {'X-Atlassian-Token': 'no-check'} response = None def __init__(self, url, username=None, password=None, timeout=60, api_root='rest/api', api_version='latest', From a3946d8a95c00e80cf84531b72484f7ef6f9f910 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 27 Jan 2020 12:17:13 +0300 Subject: [PATCH 27/31] Add docs --- docs/confluence.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/confluence.rst b/docs/confluence.rst index 6efe62b3e..63595aadc 100644 --- a/docs/confluence.rst +++ b/docs/confluence.rst @@ -99,6 +99,9 @@ Page actions # Delete the page (content) property e.g. delete key of hash confluence.delete_page_property(page_id, page_property) + # Move page + confluence.move_page(space_key, page_id, target_title, position="append") + # Get the page (content) property e.g. get key of hash confluence.get_page_property(page_id, page_property_key) From c015aef80aff808d5eaf707d2d796dfaf5355073 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 27 Jan 2020 12:19:35 +0300 Subject: [PATCH 28/31] Add docs --- atlassian/confluence.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/atlassian/confluence.py b/atlassian/confluence.py index 93a38d23f..7b572028f 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -417,6 +417,14 @@ def create_page(self, space, title, body, parent_id=None, type='page', return self.post(url, data=data) def move_page(self, space_key, page_id, target_title, position="append"): + """ + Move page method + :param space_key: + :param page_id: + :param target_title: + :param position: topLevel or append + :return: + """ url = "/pages/movepage.action" params = {"spaceKey": space_key, "pageId": page_id} if target_title: From eb416071807f74de00cd57217e90cda3406e7286 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Mon, 27 Jan 2020 13:30:47 +0300 Subject: [PATCH 29/31] [JIRA] Add method disable through REST api instead of crawling way --- atlassian/bitbucket.py | 11 +++++++++++ atlassian/jira.py | 27 +++++---------------------- docs/jira.rst | 2 +- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/atlassian/bitbucket.py b/atlassian/bitbucket.py index 4cb2e017b..05a6b05a4 100644 --- a/atlassian/bitbucket.py +++ b/atlassian/bitbucket.py @@ -1239,6 +1239,17 @@ def delete_tag(self, project, repository, tag_name): return self.delete(url) def get_diff(self, project, repository, path, hash_oldest, hash_newest): + """ + Gets a diff of the changes available in the {@code from} commit but not in the {@code to} commit. + If either the {@code from} or {@code to} commit are not specified, + they will be replaced by the default branch of their containing repository. + :param project: + :param repository: + :param path: + :param hash_oldest: the source commit (can be a partial/full commit ID or qualified/unqualified ref name) + :param hash_newest: the target commit (can be a partial/full commit ID or qualified/unqualified ref name) + :return: + """ if not self.cloud: url = 'rest/api/1.0/projects/{project}/repos/{repository}/compare/diff/{path}'.format(project=project, repository=repository, diff --git a/atlassian/jira.py b/atlassian/jira.py index 2607873ce..f66baf000 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -231,30 +231,13 @@ def user_update_or_create_property_through_rest_point(self, username, key, value def user_deactivate(self, username): """ - Disable user + Disable user. Works from 8.3.0 Release + https://docs.atlassian.com/software/jira/docs/api/REST/8.3.0/#api/2/user-updateUser :param username: :return: """ - url = 'secure/admin/user/EditUser.jspa' - headers = self.form_token_headers - user = self.user(username) - data = { - 'inline': 'true', - 'decorator': 'dialog', - 'username': user['name'], - 'fullName': user['displayName'], - 'email': user['emailAddress'], - 'editName': user['name'] - } - answer = self.get('secure/admin/WebSudoAuthenticate.jspa', self.form_token_headers) - atl_token = None - if answer: - atl_token = \ - answer.split(' Date: Mon, 27 Jan 2020 18:47:36 +0300 Subject: [PATCH 30/31] Add get worklog method --- atlassian/jira.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/atlassian/jira.py b/atlassian/jira.py index f66baf000..345560025 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -611,6 +611,16 @@ def issue_worklog(self, key, started, time_sec, comment=None): data['comment'] = comment return self.issue_add_json_worklog(key=key, worklog=data) + def issue_get_worklog(self, issue_id_or_key): + """ + Returns all work logs for an issue. + Note: Work logs won't be returned if the Log work field is hidden for the project. + :param issue_id_or_key: + :return: + """ + url = "rest/api/2/issue/{issueIdOrKey}/worklog".format(issueIdOrKey=issue_id_or_key) + return self.get(url) + def issue_field_value(self, key, field): issue = self.get('rest/api/2/issue/{0}?fields={1}'.format(key, field)) return issue['fields'][field] From 6d45f5117dacf098ed1be81bdf3e8622d2850c39 Mon Sep 17 00:00:00 2001 From: Gonchik Tsymzhitov Date: Sat, 8 Feb 2020 10:27:21 +0200 Subject: [PATCH 31/31] Add move methods --- atlassian/confluence.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/atlassian/confluence.py b/atlassian/confluence.py index fa8e6b6ce..0b56eb032 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -416,19 +416,22 @@ def create_page(self, space, title, body, parent_id=None, type='page', data['ancestors'] = [{'type': type, 'id': parent_id}] return self.post(url, data=data) - def move_page(self, space_key, page_id, target_title, position="append"): + def move_page(self, space_key, page_id, target_id=None, target_title=None, position="append"): """ Move page method :param space_key: :param page_id: :param target_title: - :param position: topLevel or append + :param target_id: + :param position: topLevel or append , above :return: """ url = "/pages/movepage.action" params = {"spaceKey": space_key, "pageId": page_id} if target_title: params["targetTitle"] = target_title + if target_id: + params["targetId"] = target_id if position: params["position"] = position return self.get(url, params=params, headers=self.no_check_headers)