From 2e8e1b004c795891a08d2b610d5c6bab333665aa Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Sat, 3 Feb 2024 21:07:48 +0100 Subject: [PATCH 01/21] fixing minor issue in scrap_regex_from_issue method --- atlassian/jira.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 6a1250779..b195908ac 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1548,15 +1548,16 @@ def scrap_regex_from_issue(self, issue, regex): comments = issue_output["fields"]["comment"]["comments"] try: - description_matches = [x.group(0) for x in re.finditer(regex, description)] - if description_matches: - regex_output.extend(description_matches) - - for comment in comments: - comment_html = comment["body"] - comment_matches = [x.group(0) for x in re.finditer(regex, comment_html)] - if comment_matches: - regex_output.extend(comment_matches) + if description is not None: + description_matches = [x.group(0) for x in re.finditer(regex, description)] + if description_matches: + regex_output.extend(description_matches) + + for comment in comments: + comment_html = comment["body"] + comment_matches = [x.group(0) for x in re.finditer(regex, comment_html)] + if comment_matches: + regex_output.extend(comment_matches) return regex_output except HTTPError as e: From 715463b82e5642ba969d333dc9b89e747b4220d4 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Sun, 4 Feb 2024 17:31:52 +0100 Subject: [PATCH 02/21] new Confluence method scrap_regex_from_page+ docs + examples --- atlassian/confluence.py | 28 ++++++++++++++++++- docs/confluence.rst | 5 +++- .../confluence_scrap_regex_from_page.py | 13 +++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 examples/confluence/confluence_scrap_regex_from_page.py diff --git a/atlassian/confluence.py b/atlassian/confluence.py index ed0137841..f8d102f4b 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -3,7 +3,7 @@ import os import time import json - +import re from requests import HTTPError import requests from deprecated import deprecated @@ -397,6 +397,32 @@ def get_tables_from_page(self, page_id): except Exception as e: log.error("Error occured", e) + def scrap_regex_from_page(self, page_id, regex): + """ + Method scraps regex patterns from a Confluence page_id. + + :param page_id: The ID of the Confluence page. + :param regex: The regex pattern to scrape. + :return: A list of regex matches. + """ + regex_output = [] + page_output = self.get_page_by_id(page_id, expand="body.storage")["body"]["storage"]["value"] + try: + if page_output is not None: + description_matches = [x.group(0) for x in re.finditer(regex, page_output)] + if description_matches: + regex_output.extend(description_matches) + return regex_output + except HTTPError as e: + if e.response.status_code == 404: + # Raise ApiError as the documented reason is ambiguous + log.error("couldn't find page_id : ", page_id) + raise ApiNotFoundError( + "There is no content with the given page id," + "or the calling user does not have permission to view the page", + reason=e, + ) + def get_page_labels(self, page_id, prefix=None, start=None, limit=None): """ Returns the list of labels on a piece of Content. diff --git a/docs/confluence.rst b/docs/confluence.rst index 8b01cd136..00fb3d90e 100644 --- a/docs/confluence.rst +++ b/docs/confluence.rst @@ -156,7 +156,10 @@ Page actions confluence.add_comment(page_id, text) # Fetch tables from Confluence page - confluence.get_page_tables(page_id) + confluence.get_tables_from_page(page_id) + + # Get regex matches from Confluence page + confluence.scrap_regex_from_page(page_id, regex) Template actions ---------------- diff --git a/examples/confluence/confluence_scrap_regex_from_page.py b/examples/confluence/confluence_scrap_regex_from_page.py new file mode 100644 index 000000000..03225875b --- /dev/null +++ b/examples/confluence/confluence_scrap_regex_from_page.py @@ -0,0 +1,13 @@ +from atlassian import Confluence + + +confluence = Confluence( + url="", + username="", + password="api_key", +) +page_id = 393464 +ipv4_regex = r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" +confluence.scrap_regex_from_page( + page_id, ipv4_regex +) # method returns list of matches of ipv4 addresses from page content. From 9fd7ae4cec5ded8a7364e967c18d6b57e13c1d84 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Thu, 8 Feb 2024 16:26:56 +0100 Subject: [PATCH 03/21] added method get_attachments_ids_from_page to jira.py --- atlassian/jira.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 958a5e19b..9b80a881c 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -162,7 +162,17 @@ def get_application_role(self, role_key): Attachments Reference: https://docs.atlassian.com/software/jira/docs/api/REST/8.5.0/#api/2/attachment """ - + def get_attachments_ids_from_page(self, issue_id): + """ + Get attachments from page + :param issue_id: int + :return: list of attachments + """ + test = self.get_issue(issue_id)['fields']['attachment'] + output = [] + for i in test: + output.append({"filename": i['filename'], "attachment_id": i['id']}) + return output def get_attachment(self, attachment_id): """ Returns the meta-data for an attachment, including the URI of the actual attached file From f69534626b821d39bc35f7e3fdac4bd53adfc911 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Thu, 8 Feb 2024 22:54:22 +0100 Subject: [PATCH 04/21] added method download_attachments_from_issue --- atlassian/jira.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/atlassian/jira.py b/atlassian/jira.py index 9b80a881c..414434f60 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1,6 +1,7 @@ # coding=utf-8 import logging import re +import os from warnings import warn from deprecated import deprecated from requests import HTTPError @@ -183,6 +184,30 @@ def get_attachment(self, attachment_id): url = "{base_url}/{attachment_id}".format(base_url=base_url, attachment_id=attachment_id) return self.get(url) + def downlaod_all_attachments_from_page(self, page_id, path=None): + + """ + Downloads all attachments from a page + :param page_id: + :param path: path to directory where attachments will be saved. If None, current working directory will be used. + :return info message: number of saved attachments + path to directory where attachments were saved: + """ + if path is None: + path = os.getcwd() + + issue_id = self.issue(page_id, fields='id')['id'] + # test_ur https://gregstestinginstance666.atlassian.net/secure/issueAttachments/10003.zip + url = self.url + f"/secure/issueAttachments/{issue_id}.zip" + r = self._session.get(url) + attachment_name = f"{page_id}_attachments.zip" + print(path) + file_path = os.path.join(path, attachment_name) + + with open(file_path, "wb") as f: + f.write(r.content) + + return "test" + def get_attachment_content(self, attachment_id): """ Returns the content for an attachment From 6f3c1f60b1c9608f41425edcf69c24f0a48eee82 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Thu, 8 Feb 2024 23:35:26 +0100 Subject: [PATCH 05/21] refactoring download_all_attachments_from_page method --- atlassian/jira.py | 54 +++++++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 414434f60..c23386f10 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -163,17 +163,19 @@ def get_application_role(self, role_key): Attachments Reference: https://docs.atlassian.com/software/jira/docs/api/REST/8.5.0/#api/2/attachment """ + def get_attachments_ids_from_page(self, issue_id): """ Get attachments from page :param issue_id: int :return: list of attachments """ - test = self.get_issue(issue_id)['fields']['attachment'] + test = self.get_issue(issue_id)["fields"]["attachment"] output = [] for i in test: - output.append({"filename": i['filename'], "attachment_id": i['id']}) + output.append({"filename": i["filename"], "attachment_id": i["id"]}) return output + def get_attachment(self, attachment_id): """ Returns the meta-data for an attachment, including the URI of the actual attached file @@ -184,29 +186,41 @@ def get_attachment(self, attachment_id): url = "{base_url}/{attachment_id}".format(base_url=base_url, attachment_id=attachment_id) return self.get(url) - def downlaod_all_attachments_from_page(self, page_id, path=None): - - """ - Downloads all attachments from a page - :param page_id: - :param path: path to directory where attachments will be saved. If None, current working directory will be used. - :return info message: number of saved attachments + path to directory where attachments were saved: - """ - if path is None: - path = os.getcwd() - - issue_id = self.issue(page_id, fields='id')['id'] - # test_ur https://gregstestinginstance666.atlassian.net/secure/issueAttachments/10003.zip + def download_all_attachments_from_page(self, issue_id, path=None): + """ + Downloads all attachments from a Jira issue. + :param issue_id: The ID of the Jira issue. + :param path: Path to directory where attachments will be saved. If None, current working directory will be used. + :return: A message indicating the result of the download operation. + """ + try: + issue_id = self.issue(issue_id, fields="id")["id"] url = self.url + f"/secure/issueAttachments/{issue_id}.zip" - r = self._session.get(url) - attachment_name = f"{page_id}_attachments.zip" - print(path) + response = self._session.get(url) + attachment_name = f"{issue_id}_attachments.zip" file_path = os.path.join(path, attachment_name) + file_size = sum(len(chunk) for chunk in response.iter_content(8196)) + + # if Jira issue doesn't have any attachments _session.get request response will return 22 bytes of PK zip format + if file_size == 22: + return "No attachments found on the Jira issue" + + if os.path.isfile(file_path): + return "File already exists" with open(file_path, "wb") as f: - f.write(r.content) + f.write(response.content) - return "test" + return "Attachments downloaded successfully" + + except FileNotFoundError: + raise FileNotFoundError("Verify if directory path is correct and/or if directory exists") + except PermissionError: + raise PermissionError( + "Directory found, but there is a problem with saving file to this directory. Check directory permissions" + ) + except Exception as e: + raise e def get_attachment_content(self, attachment_id): """ From 7fd73d85daf0dbdc32d1400d0960323ca5b3011b Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Fri, 9 Feb 2024 15:02:21 +0100 Subject: [PATCH 06/21] finished download_attachments_from_issue --- atlassian/jira.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index c23386f10..2a3d22e97 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -164,17 +164,17 @@ def get_application_role(self, role_key): Reference: https://docs.atlassian.com/software/jira/docs/api/REST/8.5.0/#api/2/attachment """ - def get_attachments_ids_from_page(self, issue_id): + def get_attachments_ids_from_issue(self, issue): """ - Get attachments from page - :param issue_id: int - :return: list of attachments + Get attachments IDs from jira issue + :param jira issue key: str + :return: list of integers attachment IDs """ - test = self.get_issue(issue_id)["fields"]["attachment"] - output = [] - for i in test: - output.append({"filename": i["filename"], "attachment_id": i["id"]}) - return output + issue_id = self.get_issue(issue)["fields"]["attachment"] + list_attachments_id = [] + for attachment in issue_id: + list_attachments_id.append({"filename": attachment["filename"], "attachment_id": attachment["id"]}) + return list_attachments_id def get_attachment(self, attachment_id): """ @@ -186,31 +186,33 @@ def get_attachment(self, attachment_id): url = "{base_url}/{attachment_id}".format(base_url=base_url, attachment_id=attachment_id) return self.get(url) - def download_all_attachments_from_page(self, issue_id, path=None): + def download_attachments_from_issue(self, issue, path=None, cloud=True): """ Downloads all attachments from a Jira issue. - :param issue_id: The ID of the Jira issue. + :param issue: The issue-key of the Jira issue :param path: Path to directory where attachments will be saved. If None, current working directory will be used. + :param cloud: Use True for Jira Cloud, false when using Jira Data Center or Server :return: A message indicating the result of the download operation. """ try: - issue_id = self.issue(issue_id, fields="id")["id"] - url = self.url + f"/secure/issueAttachments/{issue_id}.zip" + if path is None: + path = os.getcwd() + issue_id = self.issue(issue, fields="id")["id"] + if cloud: + url = self.url + f"/secure/issueAttachments/{issue_id}.zip" + else: + url = self.url + f"/secure/attachmentzip/{issue_id}.zip" response = self._session.get(url) attachment_name = f"{issue_id}_attachments.zip" file_path = os.path.join(path, attachment_name) + # if Jira issue doesn't have any attachments _session.get request response will return 22 bytes of PKzip format file_size = sum(len(chunk) for chunk in response.iter_content(8196)) - - # if Jira issue doesn't have any attachments _session.get request response will return 22 bytes of PK zip format if file_size == 22: return "No attachments found on the Jira issue" - if os.path.isfile(file_path): return "File already exists" - with open(file_path, "wb") as f: f.write(response.content) - return "Attachments downloaded successfully" except FileNotFoundError: From ef752abc5ec665fbc5af331d33413c5837e47bb5 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Fri, 9 Feb 2024 15:58:57 +0100 Subject: [PATCH 07/21] added two new methods: download_attachments.from_issue and get_attachments_ids_from_issue --- docs/jira.rst | 6 ++++++ examples/jira/jira_download_attachments.from_issue.py | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 examples/jira/jira_download_attachments.from_issue.py diff --git a/docs/jira.rst b/docs/jira.rst index 5e8e3be05..e6c761b5b 100644 --- a/docs/jira.rst +++ b/docs/jira.rst @@ -489,6 +489,12 @@ Attachments actions # Add attachment (IO Object) to issue jira.add_attachment_object(issue_key, attachment) + # Download attachments from the issue + jira.download_attachments_from_issue(issue, path=None, cloud=True): + + # Get list of attachments ids from issue + jira.get_attachments_ids_from_issue(issue_key) + Manage components ----------------- diff --git a/examples/jira/jira_download_attachments.from_issue.py b/examples/jira/jira_download_attachments.from_issue.py new file mode 100644 index 000000000..6dc6f6cdf --- /dev/null +++ b/examples/jira/jira_download_attachments.from_issue.py @@ -0,0 +1,9 @@ +from atlassian import Jira + +jira_cloud = Jira(url="", username="username", password="password") +jira_dc = Jira(url="url", token=">") +path = "/Users/>/PycharmProjects/api_python_atlassian_features/api_python_atlassian_features/atlassian-python-api/attachments" +# JIRA DC using custom directory path +jira_dc.download_attachments_from_issue("TEST-1", path=path, cloud=False) +# Jira cloud. Attachemtns will be saved to same director where script is being executed. +jira_cloud.get_attachments_ids_from_page("SC-1", cloud=True) From a56b1be2b3b232a55dd996ca459bf0f17a706d71 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Wed, 13 Mar 2024 23:15:10 +0100 Subject: [PATCH 08/21] added fix to the infinitive loop --- atlassian/jira.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 140a7fe89..dccd95f35 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1704,7 +1704,27 @@ def get_issue_remote_links(self, issue_key, global_id=None, internal_id=None): if internal_id: url += "/" + internal_id return self.get(url, params=params) - + def get_issue_tree(self, issue_key, tree=[]): + + issue = self.get_issue(issue_key) + issue_links = issue['fields']['issuelinks'] + for i in issue_links: + if i.get('inwardIssue') is not None: + issue_key3 = issue['key'] + if not [d for d in tree if i['inwardIssue']['key'] in d.keys()]: + print(issue_key3) + tree.append({issue_key3: i['inwardIssue']['key']}) + self.get_issue_tree(i['inwardIssue']['key'], tree) + subtasks = issue['fields']['subtasks'] + + for j in subtasks: + if j.get('key') is not None: + issue_key3 = issue['key'] + if not [d for d in tree if j['key'] in d.keys()]: + print(issue_key3) + tree.append({issue_key3: j['key']}) + self.get_issue_tree(j['key'], tree) + return tree def create_or_update_issue_remote_links( self, issue_key, From c23e7cda115c1b96ef43bd09d52c1173c80fecd5 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Tue, 19 Mar 2024 15:01:58 +0100 Subject: [PATCH 09/21] adding reursion depth condition --- atlassian/jira.py | 48 +++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index dccd95f35..858751769 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1704,27 +1704,35 @@ def get_issue_remote_links(self, issue_key, global_id=None, internal_id=None): if internal_id: url += "/" + internal_id return self.get(url, params=params) - def get_issue_tree(self, issue_key, tree=[]): - - issue = self.get_issue(issue_key) - issue_links = issue['fields']['issuelinks'] - for i in issue_links: - if i.get('inwardIssue') is not None: - issue_key3 = issue['key'] - if not [d for d in tree if i['inwardIssue']['key'] in d.keys()]: - print(issue_key3) - tree.append({issue_key3: i['inwardIssue']['key']}) - self.get_issue_tree(i['inwardIssue']['key'], tree) - subtasks = issue['fields']['subtasks'] - - for j in subtasks: - if j.get('key') is not None: - issue_key3 = issue['key'] - if not [d for d in tree if j['key'] in d.keys()]: - print(issue_key3) - tree.append({issue_key3: j['key']}) - self.get_issue_tree(j['key'], tree) + + def get_issue_tree(self, issue_key, tree=[], depth=0, max_depth=50): + ''' + :param jira issue_key: + :param tree: blank parameter used for recursion. Don't add anything to it when calling the function. + :return: list that contains the tree of the issue, with all subtasks and inward linked issues + example of the output get_issue_tree(INTEGRTEST-2): [{'INTEGRTEST-2': 'TEST-1'}, {'INTEGRTEST-2': 'INTEGRTEST-3'}, {'INTEGRTEST-2': 'INTEGRTEST-4'}, {'INTEGRTEST-4': 'INTEGRTEST-6'}] + ''' + # Check the recursion depth + if depth > max_depth: return tree + issue = self.get_issue(issue_key) + issue_links = issue['fields']['issuelinks'] + subtasks = issue['fields']['subtasks'] + + for issue_link in issue_links: + if issue_link.get('inwardIssue') is not None: + parent_issue_key = issue['key'] + if not [x for x in tree if issue_link['inwardIssue']['key'] in x.keys()]: # condition to avoid infinite recursion + tree.append({parent_issue_key: issue_link['inwardIssue']['key']}) + self.get_issue_tree(issue_link['inwardIssue']['key'], tree, depth+1) # recursive call of the function + + for subtask in subtasks: + if subtask.get('key') is not None: + parent_issue_key = issue['key'] + if not [x for x in tree if subtask['key'] in x.keys()]: # condition to avoid infinite recursion + tree.append({parent_issue_key: subtask['key']}) + self.get_issue_tree(subtask['key'], tree, depth+1) # recursive call of the function + return tree def create_or_update_issue_remote_links( self, issue_key, From e8e895978b31450368696a21f2c253478fb815cf Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Tue, 19 Mar 2024 17:43:46 +0100 Subject: [PATCH 10/21] fixed reursion depth condition --- atlassian/jira.py | 56 ++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 858751769..db455df1a 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -2,6 +2,7 @@ import logging import re import os +from sys import setrecursionlimit from warnings import warn from deprecated import deprecated from requests import HTTPError @@ -1705,34 +1706,35 @@ def get_issue_remote_links(self, issue_key, global_id=None, internal_id=None): url += "/" + internal_id return self.get(url, params=params) - def get_issue_tree(self, issue_key, tree=[], depth=0, max_depth=50): - ''' - :param jira issue_key: - :param tree: blank parameter used for recursion. Don't add anything to it when calling the function. - :return: list that contains the tree of the issue, with all subtasks and inward linked issues - example of the output get_issue_tree(INTEGRTEST-2): [{'INTEGRTEST-2': 'TEST-1'}, {'INTEGRTEST-2': 'INTEGRTEST-3'}, {'INTEGRTEST-2': 'INTEGRTEST-4'}, {'INTEGRTEST-4': 'INTEGRTEST-6'}] - ''' - # Check the recursion depth - if depth > max_depth: + def get_issue_tree_recursive(self, issue_key, tree=[], depth=0, depth_max=50): + ''' + :param jira issue_key: + :param tree: blank parameter used for recursion. Don't add anything to it when calling the function. + :param depth: blank parameter used for recursion. Don't add anything to it when calling the function. + :param depth_max: maximum depth of recursion if the tree is too big + :return: list that contains the tree of the issue, with all subtasks and inward linked issues + example of the output get_issue_tree(INTEGRTEST-2): [{'INTEGRTEST-2': 'TEST-1'}, {'INTEGRTEST-2': 'INTEGRTEST-3'}, {'INTEGRTEST-2': 'INTEGRTEST-4'}, {'INTEGRTEST-4': 'INTEGRTEST-6'}] + ''' + # Check the recursion depth + if depth > depth_max: + raise Exception("Recursion depth exceeded") + issue = self.get_issue(issue_key) + issue_links = issue['fields']['issuelinks'] + subtasks = issue['fields']['subtasks'] + for issue_link in issue_links: + if issue_link.get('inwardIssue') is not None: + parent_issue_key = issue['key'] + if not [x for x in tree if issue_link['inwardIssue']['key'] in x.keys()]: # condition to avoid infinite recursion + tree.append({parent_issue_key: issue_link['inwardIssue']['key']}) + self.get_issue_tree(issue_link['inwardIssue']['key'], tree, depth + 1) # recursive call of the function + for subtask in subtasks: + if subtask.get('key') is not None: + parent_issue_key = issue['key'] + if not [x for x in tree if subtask['key'] in x.keys()]: # condition to avoid infinite recursion + tree.append({parent_issue_key: subtask['key']}) + self.get_issue_tree(subtask['key'], tree, depth + 1) # recursive call of the function return tree - issue = self.get_issue(issue_key) - issue_links = issue['fields']['issuelinks'] - subtasks = issue['fields']['subtasks'] - - for issue_link in issue_links: - if issue_link.get('inwardIssue') is not None: - parent_issue_key = issue['key'] - if not [x for x in tree if issue_link['inwardIssue']['key'] in x.keys()]: # condition to avoid infinite recursion - tree.append({parent_issue_key: issue_link['inwardIssue']['key']}) - self.get_issue_tree(issue_link['inwardIssue']['key'], tree, depth+1) # recursive call of the function - - for subtask in subtasks: - if subtask.get('key') is not None: - parent_issue_key = issue['key'] - if not [x for x in tree if subtask['key'] in x.keys()]: # condition to avoid infinite recursion - tree.append({parent_issue_key: subtask['key']}) - self.get_issue_tree(subtask['key'], tree, depth+1) # recursive call of the function - return tree + def create_or_update_issue_remote_links( self, issue_key, From 8c2824b4dd5f751cc5c291606ee36625d489ca69 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Tue, 19 Mar 2024 22:32:30 +0100 Subject: [PATCH 11/21] added update4d jira.py with new method + docs +example --- atlassian/jira.py | 64 ++++++++++-------- docs/jira.rst | 2 + .../jira/jira_get_issue_tree_recursive.py | 66 +++++++++++++++++++ 3 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 examples/jira/jira_get_issue_tree_recursive.py diff --git a/atlassian/jira.py b/atlassian/jira.py index db455df1a..c65b78302 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -2,7 +2,6 @@ import logging import re import os -from sys import setrecursionlimit from warnings import warn from deprecated import deprecated from requests import HTTPError @@ -1706,34 +1705,41 @@ def get_issue_remote_links(self, issue_key, global_id=None, internal_id=None): url += "/" + internal_id return self.get(url, params=params) - def get_issue_tree_recursive(self, issue_key, tree=[], depth=0, depth_max=50): - ''' - :param jira issue_key: - :param tree: blank parameter used for recursion. Don't add anything to it when calling the function. - :param depth: blank parameter used for recursion. Don't add anything to it when calling the function. - :param depth_max: maximum depth of recursion if the tree is too big - :return: list that contains the tree of the issue, with all subtasks and inward linked issues - example of the output get_issue_tree(INTEGRTEST-2): [{'INTEGRTEST-2': 'TEST-1'}, {'INTEGRTEST-2': 'INTEGRTEST-3'}, {'INTEGRTEST-2': 'INTEGRTEST-4'}, {'INTEGRTEST-4': 'INTEGRTEST-6'}] - ''' - # Check the recursion depth - if depth > depth_max: - raise Exception("Recursion depth exceeded") - issue = self.get_issue(issue_key) - issue_links = issue['fields']['issuelinks'] - subtasks = issue['fields']['subtasks'] - for issue_link in issue_links: - if issue_link.get('inwardIssue') is not None: - parent_issue_key = issue['key'] - if not [x for x in tree if issue_link['inwardIssue']['key'] in x.keys()]: # condition to avoid infinite recursion - tree.append({parent_issue_key: issue_link['inwardIssue']['key']}) - self.get_issue_tree(issue_link['inwardIssue']['key'], tree, depth + 1) # recursive call of the function - for subtask in subtasks: - if subtask.get('key') is not None: - parent_issue_key = issue['key'] - if not [x for x in tree if subtask['key'] in x.keys()]: # condition to avoid infinite recursion - tree.append({parent_issue_key: subtask['key']}) - self.get_issue_tree(subtask['key'], tree, depth + 1) # recursive call of the function - return tree + def get_issue_tree_recursive(self, issue_key, tree=[], depth=0): + """ + Return list that contains the tree of the issue, with all subtasks and inward linked issues. + (!) Function only returns child issues from the same jira instance or from instance to which api key has access to. + (!) User asssociated with API key must have access to the all child issues in order to get them. + :param jira issue_key: + :param tree: blank parameter used for recursion. Don't change it. + :param depth: blank parameter used for recursion. Don't change it. + :return: list of dictioanries, key is the parent issue key, value is the child/linked issue key + + """ + + # Check the recursion depth. In case of any bugs that would result in infinite recursion, this will prevent the function from crashing your app. Python default for REcursionError is 1000 + if depth > 50: + raise Exception("Recursion depth exceeded") + issue = self.get_issue(issue_key) + issue_links = issue["fields"]["issuelinks"] + subtasks = issue["fields"]["subtasks"] + for issue_link in issue_links: + if issue_link.get("inwardIssue") is not None: + parent_issue_key = issue["key"] + if not [ + x for x in tree if issue_link["inwardIssue"]["key"] in x.keys() + ]: # condition to avoid infinite recursion + tree.append({parent_issue_key: issue_link["inwardIssue"]["key"]}) + self.get_issue_tree( + issue_link["inwardIssue"]["key"], tree, depth + 1 + ) # recursive call of the function + for subtask in subtasks: + if subtask.get("key") is not None: + parent_issue_key = issue["key"] + if not [x for x in tree if subtask["key"] in x.keys()]: # condition to avoid infinite recursion + tree.append({parent_issue_key: subtask["key"]}) + self.get_issue_tree(subtask["key"], tree, depth + 1) # recursive call of the function + return tree def create_or_update_issue_remote_links( self, diff --git a/docs/jira.rst b/docs/jira.rst index e6c761b5b..ec961e466 100644 --- a/docs/jira.rst +++ b/docs/jira.rst @@ -366,6 +366,8 @@ Manage issues # Scrap regex matches from issue description and comments: jira.scrap_regex_from_issue(issue_key, regex) + # Get tree representation of issue and its subtasks + inward issue links + jira.get_issue_tree(issue_key) Epic Issues ------------- diff --git a/examples/jira/jira_get_issue_tree_recursive.py b/examples/jira/jira_get_issue_tree_recursive.py new file mode 100644 index 000000000..2d2c8dd34 --- /dev/null +++ b/examples/jira/jira_get_issue_tree_recursive.py @@ -0,0 +1,66 @@ +from atlassian import Jira +import networkx as nx # for visualisation of the tree +import matplotlib.pyplot as plt # for visualisation of the tree + +# use one of above objects depending on your instance type cloud or DC +jira_cloud = Jira(url="", username="username", password="password") +jira_dc = Jira(url="url", token=">") + +""" + + Return list that contains the tree of the issue, with all subtasks and inward linked issues. + be aware of following limitations: + (!) Function only returns child issues from the same jira instance or from instance to which api key has access to. + (!) User asssociated with API key must have access to the all child issues in order to get them. + """ +""" + Let's say we have a tree of issues: + INTEGRTEST-2 is the root issue and it has 1 subtask from project TEST - TEST1 + and also two linked issues from project INTEGRTEST - INTEGRTEST-3 and INTEGRTEST-4. + INTEGRTEST-4 has a subtask INTEGRTEST-6 + -------------- graph representation of the tree ---------------- +INTEGRTEST-2 + TEST-1 + INTEGRTEST-3 + INTEGRTEST-4 + INTEGRTEST-6 + ---------------------------------------------------------------- +""" +output = jira_cloud.get_issue_tree_recursive("INTEGRTEST-2") + + +# print(output) will return: +# [{'INTEGRTEST-2': 'TEST-1'}, {'INTEGRTEST-2': 'INTEGRTEST-3'}, {'INTEGRTEST-2': 'INTEGRTEST-4'}, {'INTEGRTEST-4': 'INTEGRTEST-6'}] +# now we can use this output to create a graph representation of the tree: +def print_tree(node, dict_list, level=0): + children = [value for dict_item in dict_list for key, value in dict_item.items() if key == node] + print(" " * level + node) + for child in children: + print_tree(child, dict_list, level + 1) + + +# or use this input to create a visualisation using networkx and matplotlib librarries or some js library like recharts or vis.js +def make_graph(dict_list): + # Create a new directed graph + G = nx.DiGraph() + # Add an edge to the graph for each key-value pair in each dictionary + for d in dict_list: + for key, value in d.items(): + G.add_edge(key, value) + + # Generate a layout for the nodes + pos = nx.spring_layout(G) + + # Define a color map for the nodes + color_map = [] + for node in G: + if node.startswith("CYBER"): + color_map.append("blue") + else: + color_map.append("red") + + # Draw the graph + nx.draw(G, pos, node_color=color_map, with_labels=True, node_size=1500) + + # Display the graph + plt.show() From 9b1c9c44fe08785469e8262859cba94dff9b82c1 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Tue, 19 Mar 2024 22:35:01 +0100 Subject: [PATCH 12/21] added update4d jira.py with new method + docs +exampl --- atlassian/jira.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index c65b78302..068dc366c 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1707,7 +1707,7 @@ def get_issue_remote_links(self, issue_key, global_id=None, internal_id=None): def get_issue_tree_recursive(self, issue_key, tree=[], depth=0): """ - Return list that contains the tree of the issue, with all subtasks and inward linked issues. + Returns list that contains the tree structure of the root issue, with all subtasks and inward linked issues. (!) Function only returns child issues from the same jira instance or from instance to which api key has access to. (!) User asssociated with API key must have access to the all child issues in order to get them. :param jira issue_key: From 08d4acb87f823a22348ff1af4e29a0a103104137 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Tue, 19 Mar 2024 23:05:39 +0100 Subject: [PATCH 13/21] fix flake8 issue --- examples/jira/jira_get_issue_tree_recursive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/jira/jira_get_issue_tree_recursive.py b/examples/jira/jira_get_issue_tree_recursive.py index 2d2c8dd34..b8a971afa 100644 --- a/examples/jira/jira_get_issue_tree_recursive.py +++ b/examples/jira/jira_get_issue_tree_recursive.py @@ -11,8 +11,8 @@ Return list that contains the tree of the issue, with all subtasks and inward linked issues. be aware of following limitations: (!) Function only returns child issues from the same jira instance or from instance to which api key has access to. - (!) User asssociated with API key must have access to the all child issues in order to get them. - """ + (!) User asssociated with API key must have access to the all child issues in order to get them. +""" """ Let's say we have a tree of issues: INTEGRTEST-2 is the root issue and it has 1 subtask from project TEST - TEST1 From 26cf6911729dd21bab22551358987015549d28d3 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Wed, 20 Mar 2024 10:01:23 +0100 Subject: [PATCH 14/21] hotfix get_issue_tree_recursive --- atlassian/jira.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 068dc366c..776bf3f2a 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1730,7 +1730,7 @@ def get_issue_tree_recursive(self, issue_key, tree=[], depth=0): x for x in tree if issue_link["inwardIssue"]["key"] in x.keys() ]: # condition to avoid infinite recursion tree.append({parent_issue_key: issue_link["inwardIssue"]["key"]}) - self.get_issue_tree( + self.get_issue_tree_recursive( issue_link["inwardIssue"]["key"], tree, depth + 1 ) # recursive call of the function for subtask in subtasks: @@ -1738,7 +1738,7 @@ def get_issue_tree_recursive(self, issue_key, tree=[], depth=0): parent_issue_key = issue["key"] if not [x for x in tree if subtask["key"] in x.keys()]: # condition to avoid infinite recursion tree.append({parent_issue_key: subtask["key"]}) - self.get_issue_tree(subtask["key"], tree, depth + 1) # recursive call of the function + self.get_issue_tree_recursive(subtask["key"], tree, depth + 1) # recursive call of the function return tree def create_or_update_issue_remote_links( From 10ee67e2140d8de5c4621734361c87a0355a11ad Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Fri, 22 Mar 2024 19:55:16 +0100 Subject: [PATCH 15/21] added expand to get_issue method, added new method --- atlassian/jira.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index 776bf3f2a..47a2fc5a5 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1088,6 +1088,7 @@ def get_issue( fields=None, properties=None, update_history=True, + expand=None ): """ Returns a full representation of the issue for the given issue key @@ -1109,6 +1110,8 @@ def get_issue( params["fields"] = fields if properties is not None: params["properties"] = properties + if expand: + params["expand"] = expand if update_history is True: params["updateHistory"] = "true" if update_history is False: @@ -1866,7 +1869,21 @@ def set_issue_status(self, issue_key, status_name, fields=None, update=None): if update is not None: data["update"] = update return self.post(url, data=data) - + def get_issue_status_changlog(self, issue): + # Get the issue details with changelog + issue = self.get_issue(issue, expand="changelog") + status_change_history = [] + for history in issue['changelog']['histories']: + for item in history['items']: + # Check if the item is a status change + if item['field'] == 'status': + status_change_history.append({ + 'from': item['fromString'], + 'to': item['toString'], + 'date': history['created'] + }) + + return status_change_history def set_issue_status_by_transition_id(self, issue_key, transition_id): """ Setting status by transition_id From 273401b295bbdde09f5748e9618d0c1dd32ab2d5 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Fri, 22 Mar 2024 20:05:35 +0100 Subject: [PATCH 16/21] get_issue_status_changelog method --- atlassian/jira.py | 29 +++++++------------ docs/jira.rst | 3 ++ .../jira/jira_get_issue_status_changelog.py | 9 ++++++ 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 examples/jira/jira_get_issue_status_changelog.py diff --git a/atlassian/jira.py b/atlassian/jira.py index 47a2fc5a5..e49eaa9d4 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1082,14 +1082,7 @@ def issue(self, key, fields="*all", expand=None): params["expand"] = expand return self.get(url, params=params) - def get_issue( - self, - issue_id_or_key, - fields=None, - properties=None, - update_history=True, - expand=None - ): + def get_issue(self, issue_id_or_key, fields=None, properties=None, update_history=True, expand=None): """ Returns a full representation of the issue for the given issue key By default, all fields are returned in this get-issue resource @@ -1869,21 +1862,21 @@ def set_issue_status(self, issue_key, status_name, fields=None, update=None): if update is not None: data["update"] = update return self.post(url, data=data) - def get_issue_status_changlog(self, issue): + + def get_issue_status_changelog(self, issue_id): # Get the issue details with changelog - issue = self.get_issue(issue, expand="changelog") + issue_id = self.get_issue(issue_id, expand="changelog") status_change_history = [] - for history in issue['changelog']['histories']: - for item in history['items']: + for history in issue_id["changelog"]["histories"]: + for item in history["items"]: # Check if the item is a status change - if item['field'] == 'status': - status_change_history.append({ - 'from': item['fromString'], - 'to': item['toString'], - 'date': history['created'] - }) + if item["field"] == "status": + status_change_history.append( + {"from": item["fromString"], "to": item["toString"], "date": history["created"]} + ) return status_change_history + def set_issue_status_by_transition_id(self, issue_key, transition_id): """ Setting status by transition_id diff --git a/docs/jira.rst b/docs/jira.rst index ec961e466..c5af95be4 100644 --- a/docs/jira.rst +++ b/docs/jira.rst @@ -248,6 +248,9 @@ Manage issues # Get issue transitions jira.get_issue_transitions(issue_key) + # Get issue status change log + jira.get_issue_status_changelog(issue_key) + # Get status ID from name jira.get_status_id_from_name(status_name) diff --git a/examples/jira/jira_get_issue_status_changelog.py b/examples/jira/jira_get_issue_status_changelog.py new file mode 100644 index 000000000..94fcff55d --- /dev/null +++ b/examples/jira/jira_get_issue_status_changelog.py @@ -0,0 +1,9 @@ +from atlassian import Jira + +jira_cloud = Jira(url="", username="username", password="password") +jira_dc = Jira(url="url", token=">") + +# example use +jira_cloud.get_issue_status_changelog("TEST-1") +# example output: +# [{'from': 'Closed', 'to': 'In Progress', 'date': '2024-03-17T17:22:29.524-0500'}, {'from': 'In Progress', 'to': 'Closed', 'date': '2024-03-17T14:33:07.317-0500'}, {'from': 'In Progress', 'to': 'In Progress', 'date': '2024-03-16T09:25:52.033-0500'}, {'from': 'To Do', 'to': 'In Progress', 'date': '2024-03-14T19:25:02.511-0500'}] From b70f6185da76439f378b0458640293978703ec64 Mon Sep 17 00:00:00 2001 From: gkowalc <> Date: Fri, 22 Mar 2024 20:11:35 +0100 Subject: [PATCH 17/21] included param expand in get_issue method decription --- atlassian/jira.py | 1 + 1 file changed, 1 insertion(+) diff --git a/atlassian/jira.py b/atlassian/jira.py index e49eaa9d4..be587da62 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1091,6 +1091,7 @@ def get_issue(self, issue_id_or_key, fields=None, properties=None, update_histor :param fields: str :param properties: str :param update_history: bool + :param expand: str :return: issue """ base_url = self.resource_url("issue") From 6a83b0b2d611b7dcdcdd4cfc88d238316dd127d1 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 29 Mar 2024 13:24:57 +0100 Subject: [PATCH 18/21] WIP PR changes - https://github.com/atlassian-api/atlassian-python-api/pull/1357 --- atlassian/jira.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/atlassian/jira.py b/atlassian/jira.py index be587da62..bd24798a6 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1106,11 +1106,7 @@ def get_issue(self, issue_id_or_key, fields=None, properties=None, update_histor params["properties"] = properties if expand: params["expand"] = expand - if update_history is True: - params["updateHistory"] = "true" - if update_history is False: - params["updateHistory"] = "false" - + params["updateHistory"] = str(update_history).lower() return self.get(url, params=params) def epic_issues(self, epic, fields="*all", expand=None): @@ -1866,9 +1862,9 @@ def set_issue_status(self, issue_key, status_name, fields=None, update=None): def get_issue_status_changelog(self, issue_id): # Get the issue details with changelog - issue_id = self.get_issue(issue_id, expand="changelog") + response_get_issue = self.get_issue(issue_id, expand="changelog") status_change_history = [] - for history in issue_id["changelog"]["histories"]: + for history in response_get_issue["changelog"]["histories"]: for item in history["items"]: # Check if the item is a status change if item["field"] == "status": From 5596fd53d0c19eb51b377f332923bf8adf822a05 Mon Sep 17 00:00:00 2001 From: Greg Date: Fri, 12 Apr 2024 11:08:56 +0200 Subject: [PATCH 19/21] improving index.rst docs https://github.com/atlassian-api/atlassian-python-api/issues/1365 --- docs/index.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 0704ce2c3..c7cc920e5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -187,20 +187,20 @@ To authenticate to the Atlassian Cloud APIs Jira, Confluence, ServiceDesk: jira = Jira( url='https://your-domain.atlassian.net', - username=jira_username, - password=jira_api_token, + username=atlassian_username, + password=atlassian_api_token, cloud=True) confluence = Confluence( url='https://your-domain.atlassian.net', - username=jira_username, - password=jira_api_token, + username=atlassian_username, + password=atlassian_api_token, cloud=True) service_desk = ServiceDesk( url='https://your-domain.atlassian.net', - username=jira_username, - password=jira_api_token, + username=atlassian_username, + password=atlassian_api_token, cloud=True) And to Bitbucket Cloud: From 19aaf298c9257c79e49540f8b3ab15c9ce1d8946 Mon Sep 17 00:00:00 2001 From: gkowalc Date: Sun, 21 Apr 2024 13:36:36 +0200 Subject: [PATCH 20/21] added whiteboard methods --- atlassian/confluence.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/atlassian/confluence.py b/atlassian/confluence.py index f8d102f4b..4c29cf978 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -2877,6 +2877,47 @@ def audit( params["searchString"] = search_string return self.get(url, params=params) + """ + ############################################################################################## + # Confluence whiteboards (cloud only!) # + ############################################################################################## + """ + + def create_whiteboard(self, spaceId, title=None, parentId=None): + # Use spaceId, not space key. + url = '/api/v2/whiteboards' + data = {"spaceId": spaceId} + if title is not None: + data["title"] = title + if parentId is not None: + data["parentId"] = parentId + return self.post(url, data=data) + + def get_whiteboard(self, whiteboard_id): + try: + url = f'/api/v2/whiteboards/{whiteboard_id}' + return self.get(url) + except HTTPError as e: + # Default 404 error handling is ambiguous + if e.response.status_code == 404: + raise ApiValueError("Whiteboard not found. Check confluence instance url and/or if whiteboard id exists", reason=e) + + raise + + + def delete_whiteboard(self, whiteboard_id): + # Deleting a whiteboard moves the whiteboard to the trash, where it can be restored later + try: + url = f'/api/v2/whiteboards/{whiteboard_id}' + return self.delete(url) + except HTTPError as e: + # # Default 404 error handling is ambiguous + if e.response.status_code == 404: + raise ApiValueError( + "Whiteboard not found. Check confluence instance url and/or if whiteboard id exists", reason=e) + + raise + """ ############################################################################################## # Team Calendars REST API implements (https://jira.atlassian.com/browse/CONFSERVER-51003) # From fd3c1b606b047877a7f07a45853b1e59d1801bd8 Mon Sep 17 00:00:00 2001 From: gkowalc Date: Sun, 21 Apr 2024 14:34:06 +0200 Subject: [PATCH 21/21] added confluence whiteboard endpoints --- atlassian/confluence.py | 24 ++++++++++---------- docs/confluence.rst | 15 ++++++++++++ examples/confluence/confluence_whiteboard.py | 20 ++++++++++++++++ 3 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 examples/confluence/confluence_whiteboard.py diff --git a/atlassian/confluence.py b/atlassian/confluence.py index 4c29cf978..f195ae2fd 100644 --- a/atlassian/confluence.py +++ b/atlassian/confluence.py @@ -2878,14 +2878,13 @@ def audit( return self.get(url, params=params) """ - ############################################################################################## - # Confluence whiteboards (cloud only!) # - ############################################################################################## - """ + ############################################################################################## + # Confluence whiteboards (cloud only!) # + ############################################################################################## + """ def create_whiteboard(self, spaceId, title=None, parentId=None): - # Use spaceId, not space key. - url = '/api/v2/whiteboards' + url = "/api/v2/whiteboards" data = {"spaceId": spaceId} if title is not None: data["title"] = title @@ -2895,26 +2894,27 @@ def create_whiteboard(self, spaceId, title=None, parentId=None): def get_whiteboard(self, whiteboard_id): try: - url = f'/api/v2/whiteboards/{whiteboard_id}' + url = f"/api/v2/whiteboards/{whiteboard_id}" return self.get(url) except HTTPError as e: # Default 404 error handling is ambiguous if e.response.status_code == 404: - raise ApiValueError("Whiteboard not found. Check confluence instance url and/or if whiteboard id exists", reason=e) + raise ApiValueError( + "Whiteboard not found. Check confluence instance url and/or if whiteboard id exists", reason=e + ) raise - def delete_whiteboard(self, whiteboard_id): - # Deleting a whiteboard moves the whiteboard to the trash, where it can be restored later try: - url = f'/api/v2/whiteboards/{whiteboard_id}' + url = f"/api/v2/whiteboards/{whiteboard_id}" return self.delete(url) except HTTPError as e: # # Default 404 error handling is ambiguous if e.response.status_code == 404: raise ApiValueError( - "Whiteboard not found. Check confluence instance url and/or if whiteboard id exists", reason=e) + "Whiteboard not found. Check confluence instance url and/or if whiteboard id exists", reason=e + ) raise diff --git a/docs/confluence.rst b/docs/confluence.rst index 4858a8f6e..3d375e5b7 100644 --- a/docs/confluence.rst +++ b/docs/confluence.rst @@ -161,6 +161,21 @@ Page actions # Get regex matches from Confluence page confluence.scrap_regex_from_page(page_id, regex) +Confluence Whiteboards +---------------------- + +.. code-block:: python + + # Create new whiteboard - cloud only + confluence.create_whiteboard(spaceId, title=None, parentId=None) + + # Delete existing whiteboard - cloud only + confluence.delete_whiteboard(whiteboard_id) + + # Get whiteboard by id - cloud only! + confluence.get_whiteboard(whiteboard_id) + + Template actions ---------------- diff --git a/examples/confluence/confluence_whiteboard.py b/examples/confluence/confluence_whiteboard.py new file mode 100644 index 000000000..71695a707 --- /dev/null +++ b/examples/confluence/confluence_whiteboard.py @@ -0,0 +1,20 @@ +from atlassian import Confluence + +confluence = Confluence( + url="", + username="", + password="api_key", +) +""" +This is example on how to use confluence whiteboard endponds +Currently only available on confluence cloud +""" +# create whiteboard. First parameter is a spaceID (not spacekey!), +# second param is a name of whiteboard (optional), third one is a parent pageid (optional) +confluence.create_whiteboard("42342", "My whiteboard", "545463") + +# To delete of get whiteboard, use whiteboard id +# https:///wiki/spaces//whiteboard/ +# Deleting a whiteboard moves the whiteboard to the trash, where it can be restored later +confluence.delete_whiteboard("42342") +confluence.get_whiteboard("42342")