Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion atlassian/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def add_comment_to_object(self, comment, object_id, role):
raise NotImplementedError
params = {"comment": comment, "objectId": object_id, "role": role}
url = "rest/assets/1.0/comment/create"
return self.post(url, params=params)
return self.post(url, data=params)

def get_comment_of_object(self, object_id):
"""
Expand Down
31 changes: 24 additions & 7 deletions atlassian/confluence/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,28 +70,45 @@ def get_content_ancestors(self, content_id, **kwargs):

# Space Management
def get_spaces(self, **kwargs):
"""Get all spaces."""
return self.get("space", **kwargs)
"""
Get all spaces (single page).

Calls the Confluence Cloud v2 endpoint ``/wiki/api/v2/spaces``.
For paginated enumeration of every space, use :meth:`get_all_spaces`.
"""
return self.get("spaces", **kwargs)

def get_all_spaces(self, **kwargs):
"""
Get all spaces with full pagination.

Returns a generator yielding each space dict from the Confluence Cloud
v2 endpoint ``/wiki/api/v2/spaces``. Replaces the legacy v1
``get_all_spaces`` (which hit ``/rest/api/space``) — that endpoint is
not available on the OAuth API gateway and returns
``GoneException: This deprecated endpoint has been removed``.
"""
return self._get_paged("spaces", params=kwargs)

def get_space(self, space_id, **kwargs):
"""Get space by ID."""
return self.get(f"space/{space_id}", **kwargs)
return self.get(f"spaces/{space_id}", **kwargs)

def create_space(self, data, **kwargs):
"""Create new space."""
return self.post("space", data=data, **kwargs)
return self.post("spaces", data=data, **kwargs)

def update_space(self, space_id, data, **kwargs):
"""Update existing space."""
return self.put(f"space/{space_id}", data=data, **kwargs)
return self.put(f"spaces/{space_id}", data=data, **kwargs)

def delete_space(self, space_id, **kwargs):
"""Delete space."""
return self.delete(f"space/{space_id}", **kwargs)
return self.delete(f"spaces/{space_id}", **kwargs)

def get_space_content(self, space_id, **kwargs):
"""Get space content."""
return self.get(f"space/{space_id}/content", **kwargs)
return self.get(f"spaces/{space_id}/content", **kwargs)

# User Management
def get_users(self, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion atlassian/insight.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def add_comment_to_object(self, comment, object_id, role):
raise NotImplementedError
params = {"comment": comment, "objectId": object_id, "role": role}
url = "rest/insight/1.0/comment/create"
return self.post(url, params=params)
return self.post(url, data=params)

def get_comment_of_object(self, object_id):
"""
Expand Down
34 changes: 34 additions & 0 deletions atlassian/jira.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
"""

Expand Down
12 changes: 6 additions & 6 deletions atlassian/statuspage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2484,7 +2484,7 @@ def page_create_component_group(self, page_id, description, components_group):
-------
any
"""
url = f"v1/pages/{page_id}/component_groups"
url = f"v1/pages/{page_id}/component-groups"
return self.post(url, data={"description": description, "components_group": components_group})

def page_get_list_of_component_groups(self, page_id, per_page=100, page=1):
Expand Down Expand Up @@ -2513,7 +2513,7 @@ def page_get_list_of_component_groups(self, page_id, per_page=100, page=1):
-------
any
"""
url = f"v1/pages/{page_id}/component_groups"
url = f"v1/pages/{page_id}/component-groups"
return self.get(url, params={"per_page": per_page, "page": page})

def page_update_component_group(self, page_id, component_group_id, description, component_group):
Expand Down Expand Up @@ -2545,7 +2545,7 @@ def page_update_component_group(self, page_id, component_group_id, description,
-------
any
"""
url = f"v1/pages/{page_id}/component_groups/{component_group_id}"
url = f"v1/pages/{page_id}/component-groups/{component_group_id}"
return self.patch(url, data={"description": description, "component_group": component_group})

def page_delete_component_group(self, page_id, component_group_id):
Expand All @@ -2572,7 +2572,7 @@ def page_delete_component_group(self, page_id, component_group_id):
-------
any
"""
url = f"v1/pages/{page_id}/component_groups/{component_group_id}"
url = f"v1/pages/{page_id}/component-groups/{component_group_id}"
return self.delete(url)

def page_get_component_group(self, page_id, component_group_id):
Expand All @@ -2599,7 +2599,7 @@ def page_get_component_group(self, page_id, component_group_id):
-------
any
"""
url = f"v1/pages/{page_id}/component_groups/{component_group_id}"
url = f"v1/pages/{page_id}/component-groups/{component_group_id}"
return self.get(url)

def page_get_uptime_for_component_group(self, page_id, component_group_id, start=None, end=None):
Expand Down Expand Up @@ -2640,7 +2640,7 @@ def page_get_uptime_for_component_group(self, page_id, component_group_id, start
-------
any
"""
url = f"v1/pages/{page_id}/component_groups/{component_group_id}/uptime"
url = f"v1/pages/{page_id}/component-groups/{component_group_id}/uptime"

params = {}
if start is not None:
Expand Down
18 changes: 12 additions & 6 deletions atlassian/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,14 @@ def block_code_macro_confluence(code, lang=None):
"""
if not lang:
lang = ""
return ("""\
return (
"""\
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">{lang}</ac:parameter>
<ac:plain-text-body><![CDATA[{code}]]></ac:plain-text-body>
</ac:structured-macro>
""").format(lang=lang, code=code)
"""
).format(lang=lang, code=code)


def html_code__macro_confluence(text):
Expand All @@ -227,11 +229,13 @@ def html_code__macro_confluence(text):
:param text:
:return:
"""
return ("""\
return (
"""\
<ac:structured-macro ac:name="html" ac:schema-version="1">
<ac:plain-text-body><![CDATA[{text}]]></ac:plain-text-body>
</ac:structured-macro>
""").format(text=text)
"""
).format(text=text)


def noformat_code_macro_confluence(text, nopanel=None):
Expand All @@ -243,12 +247,14 @@ def noformat_code_macro_confluence(text, nopanel=None):
"""
if not nopanel:
nopanel = False
return ("""\
return (
"""\
<ac:structured-macro ac:name="noformat" ac:schema-version="1">
<ac:parameter ac:name="nopanel">{nopanel}</ac:parameter>
<ac:plain-text-body><![CDATA[{text}]]></ac:plain-text-body>
</ac:structured-macro>
""").format(nopanel=nopanel, text=text)
"""
).format(nopanel=nopanel, text=text)


def symbol_normalizer(text):
Expand Down
6 changes: 4 additions & 2 deletions examples/jira/jira_admins_confluence_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

confluence = Confluence(url="http://localhost:8090", username="admin", password="admin")

html = ["""<table>
html = [
"""<table>
<tr>
<th>Project Key</th>
<th>Project Name</th>
<th>Leader</th>
<th>Email</th>
</tr>"""]
</tr>"""
]

for data in jira.project_leaders():
log.info("{project_key} leader is {lead_name} <{lead_email}>".format(**data))
Expand Down
4 changes: 3 additions & 1 deletion examples/jira/jira_project_administrators.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
<td>{project_name}</td>
<td>{lead_name}</td>
<td><a href="mailto:{lead_email}">{lead_email}</a></td>
</tr>""".format(**data)
</tr>""".format(
**data
)

html += "</table>"

Expand Down
6 changes: 4 additions & 2 deletions examples/jira/jira_project_leaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
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

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 = '<a href="mailto:{lead_email}?subject={email_subject}&body={email_body}">{lead_name}</a>'

Expand Down
44 changes: 32 additions & 12 deletions tests/confluence/test_confluence_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,52 +150,72 @@ def test_get_content_ancestors(self, mock_get, confluence_cloud):
# Space Management Tests
@patch.object(ConfluenceCloud, "get")
def test_get_spaces(self, mock_get, confluence_cloud):
"""Test get_spaces method."""
"""get_spaces calls the v2 plural endpoint /wiki/api/v2/spaces."""
mock_get.return_value = {"results": [{"id": "TEST", "name": "Test Space"}]}
result = confluence_cloud.get_spaces()
mock_get.assert_called_once_with("space", **{})
mock_get.assert_called_once_with("spaces", **{})
assert result == {"results": [{"id": "TEST", "name": "Test Space"}]}

@patch.object(ConfluenceCloud, "get")
def test_get_all_spaces_paginates(self, mock_get, confluence_cloud):
"""get_all_spaces yields every space across paginated v2 responses."""
mock_get.side_effect = [
{
"results": [{"id": "1", "name": "A"}, {"id": "2", "name": "B"}],
"_links": {"next": "/wiki/api/v2/spaces?cursor=NEXT"},
},
{"results": [{"id": "3", "name": "C"}], "_links": {}},
]
result = list(confluence_cloud.get_all_spaces())
assert result == [
{"id": "1", "name": "A"},
{"id": "2", "name": "B"},
{"id": "3", "name": "C"},
]
# Entry-point URL is the v2 plural path; pagination URL handling is
# covered by existing _get_paged tests.
assert mock_get.call_args_list[0].args[0] == "spaces"

@patch.object(ConfluenceCloud, "get")
def test_get_space(self, mock_get, confluence_cloud):
"""Test get_space method."""
"""get_space calls the v2 plural endpoint."""
mock_get.return_value = {"id": "TEST", "name": "Test Space"}
result = confluence_cloud.get_space("TEST")
mock_get.assert_called_once_with("space/TEST", **{})
mock_get.assert_called_once_with("spaces/TEST", **{})
assert result == {"id": "TEST", "name": "Test Space"}

@patch.object(ConfluenceCloud, "post")
def test_create_space(self, mock_post, confluence_cloud):
"""Test create_space method."""
"""create_space calls the v2 plural endpoint."""
space_data = {"name": "New Space", "key": "NEW"}
mock_post.return_value = {"id": "NEW", "name": "New Space", "key": "NEW"}
result = confluence_cloud.create_space(space_data)
mock_post.assert_called_once_with("space", data=space_data, **{})
mock_post.assert_called_once_with("spaces", data=space_data, **{})
assert result == {"id": "NEW", "name": "New Space", "key": "NEW"}

@patch.object(ConfluenceCloud, "put")
def test_update_space(self, mock_put, confluence_cloud):
"""Test update_space method."""
"""update_space calls the v2 plural endpoint."""
space_data = {"name": "Updated Space"}
mock_put.return_value = {"id": "TEST", "name": "Updated Space"}
result = confluence_cloud.update_space("TEST", space_data)
mock_put.assert_called_once_with("space/TEST", data=space_data, **{})
mock_put.assert_called_once_with("spaces/TEST", data=space_data, **{})
assert result == {"id": "TEST", "name": "Updated Space"}

@patch.object(ConfluenceCloud, "delete")
def test_delete_space(self, mock_delete, confluence_cloud):
"""Test delete_space method."""
"""delete_space calls the v2 plural endpoint."""
mock_delete.return_value = {"success": True}
result = confluence_cloud.delete_space("TEST")
mock_delete.assert_called_once_with("space/TEST", **{})
mock_delete.assert_called_once_with("spaces/TEST", **{})
assert result == {"success": True}

@patch.object(ConfluenceCloud, "get")
def test_get_space_content(self, mock_get, confluence_cloud):
"""Test get_space_content method."""
"""get_space_content calls the v2 plural endpoint."""
mock_get.return_value = {"results": [{"id": "123", "title": "Page in Space"}]}
result = confluence_cloud.get_space_content("TEST")
mock_get.assert_called_once_with("space/TEST/content", **{})
mock_get.assert_called_once_with("spaces/TEST/content", **{})
assert result == {"results": [{"id": "123", "title": "Page in Space"}]}

# User Management Tests
Expand Down
Loading