Skip to content

Commit 170176d

Browse files
committed
Jira: object has no attribute 'enhanced_jql'
#1631
1 parent 08c5f82 commit 170176d

3 files changed

Lines changed: 113 additions & 1 deletion

File tree

‎atlassian/jira/jira_cloud.py‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
the compatibility clients for Server, Data Center, and existing Cloud users.
66
"""
77

8-
from typing import Any, Optional, Union
8+
from typing import Any, Dict, List, Optional, Union
99

1010
from ..rest_client import AtlassianRestAPI
1111
from .core_methods import JiraCloudCoreMethods
@@ -38,6 +38,62 @@ def endpoint(self, resource: str, api_version: Optional[Union[str, int]] = None)
3838
raise ValueError("Jira Cloud Core api_version must be 2 or 3")
3939
return self.resource_url(resource, api_root="rest/api", api_version=version)
4040

41+
def enhanced_jql(
42+
self,
43+
jql: str,
44+
fields: Union[str, List[str]] = "*all",
45+
nextPageToken: Optional[str] = None,
46+
limit: Optional[int] = None,
47+
expand: Optional[str] = None,
48+
):
49+
"""Search Jira Cloud issues through the v3 enhanced JQL endpoint.
50+
51+
Use ``nextPageToken`` from a previous response to retrieve the next
52+
page. Unlike the older search API, enhanced JQL is cursor-paginated.
53+
"""
54+
params: Dict[str, Union[str, int]] = {"jql": jql}
55+
if nextPageToken is not None:
56+
params["nextPageToken"] = str(nextPageToken)
57+
if limit is not None:
58+
params["maxResults"] = int(limit)
59+
if fields is not None:
60+
params["fields"] = ",".join(fields) if isinstance(fields, (list, tuple, set)) else fields
61+
if expand is not None:
62+
params["expand"] = expand
63+
return self.get(self.endpoint("search/jql", api_version=3), params=params)
64+
65+
def enhanced_jql_get_list_of_tickets(
66+
self,
67+
jql: str,
68+
fields: Union[str, List[str]] = "*all",
69+
limit: Optional[int] = None,
70+
expand: Optional[str] = None,
71+
) -> list:
72+
"""Return all cursor-paginated enhanced JQL issues up to ``limit``.
73+
74+
When ``limit`` is omitted, iteration continues until Jira marks the
75+
result set as final.
76+
"""
77+
results = []
78+
next_page_token = None
79+
while True:
80+
response = self.enhanced_jql(
81+
jql,
82+
fields=fields,
83+
nextPageToken=next_page_token,
84+
limit=limit,
85+
expand=expand,
86+
)
87+
if not response:
88+
break
89+
results.extend(response.get("issues", []))
90+
if limit is not None and len(results) >= limit:
91+
return results[:limit]
92+
next_page_token = response.get("nextPageToken")
93+
if response.get("isLast", False) or not next_page_token:
94+
break
95+
return results
96+
4197

4298
class JiraSoftware(JiraSoftwareMethods, AtlassianRestAPI):
4399
"""Jira Software Cloud REST APIs.

‎docs/jira_cloud.rst‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ existing contract. ``Jira`` still defaults to its historical string version
3232
``"2"`` and does not automatically enable Cloud mode; this is intentional
3333
backward compatibility.
3434

35+
Enhanced JQL search
36+
-------------------
37+
38+
Both ``JiraCloud`` and the legacy ``Jira(..., cloud=True)`` client provide
39+
``enhanced_jql()``. The method always uses Jira Cloud's v3
40+
``/search/jql`` endpoint, regardless of the Core client's selected version.
41+
Pass the returned ``nextPageToken`` to fetch another page, or use
42+
``enhanced_jql_get_list_of_tickets()`` to collect cursor-paginated results.
43+
44+
.. code-block:: python
45+
46+
issues = core.enhanced_jql_get_list_of_tickets(
47+
'project = EXAMPLE ORDER BY updated DESC',
48+
fields=["summary", "description"],
49+
limit=100,
50+
expand="names",
51+
)
52+
3553
Software API roots
3654
------------------
3755

‎tests/test_jira_cloud_clients.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,44 @@ def test_core_methods_honor_the_selected_v2_route(self):
4848
jira.get_issue("ABC-1")
4949
get.assert_called_once_with("rest/api/2/issue/ABC-1", params=None, data=None)
5050

51+
def test_core_client_supports_enhanced_jql_at_v3(self):
52+
jira = JiraCloud("https://example.atlassian.net", api_version=2)
53+
54+
with patch.object(jira, "get", return_value={"issues": [], "isLast": True}) as get:
55+
jira.enhanced_jql(
56+
"project = EXAMPLE ORDER BY key DESC",
57+
fields=["summary", "description"],
58+
nextPageToken="next-token",
59+
limit=100,
60+
expand="names",
61+
)
62+
63+
get.assert_called_once_with(
64+
"rest/api/3/search/jql",
65+
params={
66+
"jql": "project = EXAMPLE ORDER BY key DESC",
67+
"fields": "summary,description",
68+
"nextPageToken": "next-token",
69+
"maxResults": 100,
70+
"expand": "names",
71+
},
72+
)
73+
74+
def test_core_client_enhanced_jql_collects_cursor_pages(self):
75+
jira = JiraCloud("https://example.atlassian.net")
76+
77+
with patch.object(
78+
jira,
79+
"enhanced_jql",
80+
side_effect=[
81+
{"issues": [{"key": "EXAMPLE-1"}], "nextPageToken": "next", "isLast": False},
82+
{"issues": [{"key": "EXAMPLE-2"}], "isLast": True},
83+
],
84+
):
85+
issues = jira.enhanced_jql_get_list_of_tickets("project = EXAMPLE")
86+
87+
self.assertEqual([issue["key"] for issue in issues], ["EXAMPLE-1", "EXAMPLE-2"])
88+
5189
def test_software_client_builds_each_documented_api_root(self):
5290
jira = JiraSoftware("https://example.atlassian.net", cloud=False)
5391

0 commit comments

Comments
 (0)