diff --git a/atlassian/confluence/__init__.py b/atlassian/confluence/__init__.py index 5ac9fb324..e26099383 100644 --- a/atlassian/confluence/__init__.py +++ b/atlassian/confluence/__init__.py @@ -25,9 +25,12 @@ def __init__(self, url, *args, **kwargs): if is_cloud is None: hostname = urlparse(url).hostname or "" is_cloud = ( - hostname == "atlassian.net" or hostname.endswith(".atlassian.net") - or hostname == "jira.com" or hostname.endswith(".jira.com") - or hostname == "api.atlassian.com" or hostname.endswith(".api.atlassian.com") + hostname == "atlassian.net" + or hostname.endswith(".atlassian.net") + or hostname == "jira.com" + or hostname.endswith(".jira.com") + or hostname == "api.atlassian.com" + or hostname.endswith(".api.atlassian.com") ) if is_cloud: impl = ConfluenceCloud(url, *args, **kwargs) diff --git a/atlassian/confluence/cloud/base.py b/atlassian/confluence/cloud/base.py index ec2a081b0..f90f43f5f 100644 --- a/atlassian/confluence/cloud/base.py +++ b/atlassian/confluence/cloud/base.py @@ -23,5 +23,3 @@ def __init__(self, url, *args, **kwargs): :return: nothing """ super(ConfluenceCloudBase, self).__init__(url, *args, **kwargs) - - diff --git a/atlassian/confluence/server/__init__.py b/atlassian/confluence/server/__init__.py index 50ccc9978..65199e083 100644 --- a/atlassian/confluence/server/__init__.py +++ b/atlassian/confluence/server/__init__.py @@ -297,7 +297,9 @@ def get_all_draft_pages_from_space(self, space_key, **kwargs): def get_all_draft_blog_posts_from_space(self, space_key, **kwargs): """Get all draft blog posts from space.""" - return self._get_paged("content", params={"spaceKey": space_key, "type": "blogpost", "status": "draft", **kwargs}) + return self._get_paged( + "content", params={"spaceKey": space_key, "type": "blogpost", "status": "draft", **kwargs} + ) # Trash Management def get_trash_content(self, space_key, **kwargs): @@ -310,7 +312,9 @@ def get_all_pages_from_space_trash(self, space_key, **kwargs): def get_all_blog_posts_from_space_trash(self, space_key, **kwargs): """Get all blog posts from space trash.""" - return self._get_paged("content", params={"spaceKey": space_key, "type": "blogpost", "status": "trashed", **kwargs}) + return self._get_paged( + "content", params={"spaceKey": space_key, "type": "blogpost", "status": "trashed", **kwargs} + ) # Export def export_content(self, content_id, **kwargs): diff --git a/atlassian/confluence/server/base.py b/atlassian/confluence/server/base.py index f7451db93..0bfd53963 100644 --- a/atlassian/confluence/server/base.py +++ b/atlassian/confluence/server/base.py @@ -23,5 +23,3 @@ def __init__(self, url, *args, **kwargs): :return: nothing """ super(ConfluenceServerBase, self).__init__(url, *args, **kwargs) - - diff --git a/atlassian/jira.py b/atlassian/jira.py index 454c57da7..6dad5c309 100644 --- a/atlassian/jira.py +++ b/atlassian/jira.py @@ -1457,6 +1457,40 @@ def get_issue_changelog(self, issue_key: str, start: Optional[int] = None, limit url = f"{base_url}/{issue_key}?expand=changelog" return self._get_response_content(url, fields=[("changelog", params)]) + def get_changelogs_bulk( + self, + issue_ids_or_keys: List[str], + fields_by: Optional[str] = None, + next_page_token: Optional[str] = None, + max_results: Optional[int] = None, + ) -> T_resp_json: + """ + Returns changelogs for multiple issues in bulk. + Only Jira Cloud platform. + + Reference: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-changelog-bulkfetch-post + + :param issue_ids_or_keys: List of issue IDs or keys to fetch changelogs for. Required. + :param fields_by: OPTIONAL: Whether to filter changelog entries by field ID or field name. + Valid values: "id", "name". + :param next_page_token: OPTIONAL: Token for the next page of results (pagination). + :param max_results: OPTIONAL: Maximum number of results to return. + :return: Paginated list of changelogs for the given issues. + """ + if not self.cloud: + raise ValueError("``get_changelogs_bulk`` method is only available for Jira Cloud platform") + url = self.resource_url("changelog/bulkfetch", api_version=3) + data: dict = {"issueIdsOrKeys": issue_ids_or_keys} + if fields_by is not None: + if fields_by not in ("id", "name"): + raise ValueError("``fields_by`` must be either 'id' or 'name'") + data["fieldsByKeys"] = fields_by == "name" + if next_page_token is not None: + data["nextPageToken"] = next_page_token + if max_results is not None: + data["maxResults"] = int(max_results) + return self.post(url, data=data) + def issue_add_json_worklog(self, key: str, worklog: Union[dict, str]): """ diff --git a/atlassian/utils.py b/atlassian/utils.py index 631d0111d..24a479993 100644 --- a/atlassian/utils.py +++ b/atlassian/utils.py @@ -213,12 +213,14 @@ def block_code_macro_confluence(code, lang=None): """ if not lang: lang = "" - return ("""\ + return ( + """\ {lang} - """).format(lang=lang, code=code) + """ + ).format(lang=lang, code=code) def html_code__macro_confluence(text): @@ -227,11 +229,13 @@ def html_code__macro_confluence(text): :param text: :return: """ - return ("""\ + return ( + """\ - """).format(text=text) + """ + ).format(text=text) def noformat_code_macro_confluence(text, nopanel=None): @@ -243,12 +247,14 @@ def noformat_code_macro_confluence(text, nopanel=None): """ if not nopanel: nopanel = False - return ("""\ + return ( + """\ {nopanel} - """).format(nopanel=nopanel, text=text) + """ + ).format(nopanel=nopanel, text=text) def symbol_normalizer(text): diff --git a/examples/jira/jira_admins_confluence_page.py b/examples/jira/jira_admins_confluence_page.py index 0232cf1ae..5703ad0d0 100644 --- a/examples/jira/jira_admins_confluence_page.py +++ b/examples/jira/jira_admins_confluence_page.py @@ -12,13 +12,15 @@ confluence = Confluence(url="http://localhost:8090", username="admin", password="admin") -html = [""" +html = [ + """
- """] + """ +] for data in jira.project_leaders(): log.info("{project_key} leader is {lead_name} <{lead_email}>".format(**data)) diff --git a/examples/jira/jira_project_administrators.py b/examples/jira/jira_project_administrators.py index a25b248dc..de3af90e8 100644 --- a/examples/jira/jira_project_administrators.py +++ b/examples/jira/jira_project_administrators.py @@ -17,7 +17,9 @@ - """.format(**data) + """.format( + **data + ) html += "
Project Key Project Name Leader Email
{project_name} {lead_name} {lead_email}
" diff --git a/examples/jira/jira_project_leaders.py b/examples/jira/jira_project_leaders.py index 709fcc61c..4fdff24a4 100644 --- a/examples/jira/jira_project_leaders.py +++ b/examples/jira/jira_project_leaders.py @@ -7,7 +7,8 @@ jira = Jira(url="http://localhost:8080", username="admin", password="admin") EMAIL_SUBJECT = quote("Jira access to project {project_key}") -EMAIL_BODY = quote("""I am asking for access to the {project_key} project in Jira. +EMAIL_BODY = quote( + """I am asking for access to the {project_key} project in Jira. To give me the appropriate permissions, assign me to a role on the page: http://localhost:8080/plugins/servlet/project-config/{project_key}/roles @@ -15,7 +16,8 @@ Role: Users - read-only access + commenting Developers - work on tasks, editing, etc. -Admin - Change of configuration and the possibility of starting sprints""") +Admin - Change of configuration and the possibility of starting sprints""" +) MAILTO = '{lead_name}' diff --git a/tests/confluence/test_confluence_cloud.py b/tests/confluence/test_confluence_cloud.py index b7f874147..55af107e1 100644 --- a/tests/confluence/test_confluence_cloud.py +++ b/tests/confluence/test_confluence_cloud.py @@ -579,7 +579,7 @@ def test_pagination_with_relative_next_link_and_base(self, mock_get, confluence_ assert result == [{"id": "1", "title": "Page 1"}, {"id": "2", "title": "Page 2"}] assert mock_get.call_count == 2 - + # Verify the second call used scheme+host from self.url (preserving API gateway routing) args, kwargs = mock_get.call_args_list[1] assert args[0] == "https://test.atlassian.net/rest/api/content?cursor=1"