Skip to content

Commit 3286e99

Browse files
committed
1 parent 552132f commit 3286e99

9 files changed

Lines changed: 259 additions & 17 deletions

File tree

atlassian/bitbucket/cloud/repositories/commits.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@ def __init__(self, url, *args, **kwargs):
1515
def __get_object(self, data):
1616
return Commit(data, **self._new_session_args)
1717

18-
def each(self, top=None, q=None, sort=None):
18+
def each(self, top=None, q=None, sort=None, include=None, exclude=None, path=None):
1919
"""
2020
Return the list of commits in this repository.
2121
2222
:param top: string: Hash of commit to get the history for.
23-
:param q: string: Query string to narrow down the response.
24-
See https://developer.atlassian.com/bitbucket/api/2/reference/meta/filtering for details.
23+
:param q: string: Legacy query expression. Bitbucket Cloud does not
24+
support arbitrary ``q`` filtering for commits; it is
25+
retained for compatibility and passed to the API.
2526
:param sort: string: Name of a response property to sort results.
2627
See https://developer.atlassian.com/bitbucket/api/2/reference/meta/filtering for details.
28+
:param include: string: Commit or ref to include in the history.
29+
:param exclude: string: Commit or ref to exclude from the history.
30+
:param path: string: File path used to filter commits.
2731
2832
:return: A generator for the Commit objects
2933
@@ -35,6 +39,12 @@ def each(self, top=None, q=None, sort=None):
3539
params["sort"] = sort
3640
if q is not None:
3741
params["q"] = q
42+
if include is not None:
43+
params["include"] = include
44+
if exclude is not None:
45+
params["exclude"] = exclude
46+
if path is not None:
47+
params["path"] = path
3848
trailing = True
3949
if top is not None:
4050
trailing = False

atlassian/jira/jira_cloud.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,34 @@ def endpoint(self, resource: str = "") -> str:
209209
"""Return a JSM public REST endpoint path without issuing a request."""
210210
return self.url_joiner("rest/servicedeskapi", resource)
211211

212+
def get_sla_metrics(self, service_desk_id, start=None, limit=None):
213+
"""Return SLA metric configuration from Jira's agent endpoint.
214+
215+
This is an internal endpoint used by the JSM settings UI and may not
216+
be available on every Cloud or Data Center release.
217+
"""
218+
params = {key: value for key, value in {"start": start, "limit": limit}.items() if value is not None}
219+
return self.get(
220+
f"rest/servicedesk/1/servicedesk/agent/{service_desk_id}/sla/metrics",
221+
params=params or None,
222+
)
223+
224+
def update_sla_metric(self, service_desk_id, sla_id, data):
225+
"""Update an SLA metric configuration for a service desk.
226+
227+
Jira exposes this through an internal agent endpoint used by the JSM
228+
settings UI. The payload is passed through unchanged to support the
229+
metric schema used by the target Jira release.
230+
"""
231+
return self.put(
232+
f"rest/servicedesk/1/servicedesk/agent/{service_desk_id}/sla/metrics/{sla_id}",
233+
data=data,
234+
)
235+
236+
def set_sla_metric(self, service_desk_id, sla_id, data):
237+
"""Backward-compatible alias for :meth:`update_sla_metric`."""
238+
return self.update_sla_metric(service_desk_id, sla_id, data)
239+
212240

213241
def create_jira_cloud(url: str, *args: Any, api_version: Union[str, int] = 3, **kwargs: Any) -> JiraCloud:
214242
"""Create a versioned Jira Cloud Core client (v3 by default)."""

atlassian/service_desk.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,42 @@ def get_sla_by_id(self, issue_id_or_key, sla_id):
689689

690690
return self.get(url, headers=self.experimental_headers)
691691

692+
def get_sla_metrics(self, service_desk_id, start=None, limit=None):
693+
"""Return SLA metric configuration for a service desk.
694+
695+
Jira Service Management exposes this configuration through an internal
696+
agent endpoint used by the service-desk settings UI. It is not part of
697+
the public ``rest/servicedeskapi`` contract and may require an agent
698+
or Jira administrator permission.
699+
700+
:param service_desk_id: Service desk/project identifier used by Jira.
701+
:param start: Optional result offset, when supported by the instance.
702+
:param limit: Optional result limit, when supported by the instance.
703+
:return: SLA metric configuration response.
704+
"""
705+
params = {key: value for key, value in {"start": start, "limit": limit}.items() if value is not None}
706+
url = f"rest/servicedesk/1/servicedesk/agent/{service_desk_id}/sla/metrics"
707+
return self.get(url, params=params or None, headers=self.experimental_headers)
708+
709+
def update_sla_metric(self, service_desk_id, sla_id, data):
710+
"""Update an SLA metric configuration for a service desk.
711+
712+
This uses Jira Service Management's internal agent endpoint. The
713+
payload is passed through unchanged because its schema varies between
714+
Jira releases and contains the metric definition, goals and calendars.
715+
716+
:param service_desk_id: Service desk/project identifier used by Jira.
717+
:param sla_id: SLA metric identifier.
718+
:param data: Metric configuration payload accepted by Jira.
719+
:return: Updated SLA metric response.
720+
"""
721+
url = f"rest/servicedesk/1/servicedesk/agent/{service_desk_id}/sla/metrics/{sla_id}"
722+
return self.put(url, data=data, headers=self.experimental_headers)
723+
724+
def set_sla_metric(self, service_desk_id, sla_id, data):
725+
"""Backward-compatible alias for :meth:`update_sla_metric`."""
726+
return self.update_sla_metric(service_desk_id, sla_id, data)
727+
692728
def sla_rebuild(self, tickets=None):
693729
"""
694730
Fix corrupted or missing sla

docs/bitbucket.rst

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ so retain the results by pull request ID to avoid duplicates.
139139
See ``examples/bitbucket/bitbucket_server_release_report.py`` for a complete
140140
environment-variable-based script.
141141

142+
Commits belonging to an open pull request may not be reachable from the
143+
repository ref used by ``get_commits``. To retrieve the commits that belong to
144+
a specific pull request on Server/Data Center, query the pull-request commits
145+
endpoint instead:
146+
147+
.. code-block:: python
148+
149+
for commit in bitbucket.get_pull_requests_commits("PROJ", "repository", pull_request_id):
150+
print(commit["id"])
151+
152+
The Cloud object model provides the equivalent operation as
153+
``repository.pullrequests.get(pull_request_id).commits``. Use
154+
``get_pull_requests_contain_commit`` when starting from an individual commit
155+
and looking up its associated pull requests.
156+
142157
Manage projects
143158
---------------
144159

@@ -558,6 +573,20 @@ July 2026, so new integrations must use API tokens.
558573
# Get a repository
559574
repository = workspace.repositories.get(repository_slug)
560575
576+
# Commit history supports include/exclude refs and a file-path filter.
577+
commits = repository.commits.each(
578+
include="main",
579+
exclude="release",
580+
path="src/app.py",
581+
)
582+
for commit in commits:
583+
print(commit.hash)
584+
585+
The Cloud commits endpoint does not support arbitrary ``q`` expressions for
586+
filtering by author or date. Filter those fields in the returned commits, or
587+
use ``include``, ``exclude`` and ``path`` when those server-side filters fit
588+
the use case.
589+
561590
# Read raw bytes from a file at a branch, tag, or commit SHA
562591
readme = repository.get_source_file("main", "README.md")
563592

@@ -567,20 +596,26 @@ July 2026, so new integrations must use API tokens.
567596
# Get a list of deployment environments from a repository
568597
repository.deployment_environments.each()
569598

570-
# Get a single deployment environment from a repository by deployment environment key
599+
# Get a deployment environment by key
571600
deployment_environment = repository.deployment_environments.get(deployment_environment_key)
572601

573-
# Get a list of deployment environment variables from a deployment environment
602+
# Get deployment environment variables from an environment
574603
deployment_environment_variables = deployment_environment.deployment_environment_variables.each()
575604

576-
# Create a new deployment environment variable with a name of 'KEY', value of 'VALUE' and is not secured.
577-
new_deployment_environment_variable = deployment_environment.deployment_environment_variables.create("KEY", "VALUE", False)
605+
# Create a non-secured deployment environment variable.
606+
new_deployment_environment_variable = deployment_environment.deployment_environment_variables.create(
607+
"KEY", "VALUE", False
608+
)
578609

579610
# Update the 'key' field of repository_variable
580-
updated_deployment_environment_variable = new_deployment_environment_variable.update(key="UPDATED_DEPLOYMENT_ENVIRONMENT_VARIABLE_KEY")
611+
updated_deployment_environment_variable = new_deployment_environment_variable.update(
612+
key="UPDATED_DEPLOYMENT_ENVIRONMENT_VARIABLE_KEY"
613+
)
581614

582615
# Update the 'value' field of repository_variable
583-
updated_deployment_environment_variable = new_deployment_environment_variable.update(value="UPDATED_DEPLOYMENT_ENVIRONMENT_VARIABLE_VALUE")
616+
updated_deployment_environment_variable = new_deployment_environment_variable.update(
617+
value="UPDATED_DEPLOYMENT_ENVIRONMENT_VARIABLE_VALUE"
618+
)
584619

585620
# Delete deployment environment variable
586621
updated_deployment_environment_variable.delete()
@@ -594,17 +629,25 @@ July 2026, so new integrations must use API tokens.
594629
# Get a list of repository variables from a repository
595630
repository.repository_variables.each()
596631

597-
# Get a single repository variable from a repository by repository variable key
598-
repository_variable = repository.repository_variables.get(repository_variable_key)
632+
# Get a repository variable by key
633+
repository_variable = repository.repository_variables.get(
634+
repository_variable_key
635+
)
599636

600-
# Create a new repository variable with a name of 'KEY', value of 'VALUE' and is not secured.
601-
new_repository_variable = repository.repository_variables.create("KEY", "VALUE", False)
637+
# Create a non-secured repository variable.
638+
new_repository_variable = repository.repository_variables.create(
639+
"KEY", "VALUE", False
640+
)
602641

603642
# Update the 'key' field of repository_variable
604-
updated_repository_variable = repository_variable.update(key="UPDATED_REPOSITORY_VARIABLE_KEY")
643+
updated_repository_variable = repository_variable.update(
644+
key="UPDATED_REPOSITORY_VARIABLE_KEY"
645+
)
605646

606647
# Update the 'value' field of repository_variable
607-
updated_repository_variable = repository_variable.update(value="UPDATED_REPOSITORY_VARIABLE_VALUE")
648+
updated_repository_variable = repository_variable.update(
649+
value="UPDATED_REPOSITORY_VARIABLE_VALUE"
650+
)
608651

609652
# Delete repository_variable
610653
repository_variable.delete()
@@ -613,13 +656,19 @@ July 2026, so new integrations must use API tokens.
613656
repository.hooks.each()
614657

615658
# Create a hook for a repository
616-
hook = repo.hooks.create(url="endpoint-url", description="description", active=True, events=["a-repository-event"])
659+
hook = repo.hooks.create(
660+
url="endpoint-url", description="description", active=True,
661+
events=["a-repository-event"]
662+
)
617663

618664
# Get a single hook for a repository
619665
hook = repo.hooks.get("a-webhook-id")
620666

621667
# Update a specific hook for a repository
622-
hook.update(url="endpoint-url", description="description", active=True, events=["a-repository-event"])
668+
hook.update(
669+
url="endpoint-url", description="description", active=True,
670+
events=["a-repository-event"]
671+
)
623672

624673
# Delete a speicifc hook for a repository
625674
hook.delete()

docs/jira_cloud.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ documented root/version. Supported API names are ``agile``, ``software``,
5959
``security``, ``operations``, and ``devopscomponents``. The separate roots
6060
avoid incorrectly treating Jira Software as Core v1.
6161

62+
Issue ranking
63+
-------------
64+
65+
Jira Software exposes ranking through the Agile API. ``rank_issues`` accepts
66+
multiple issue keys or IDs and placement fields supported by Jira:
67+
68+
.. code-block:: python
69+
70+
software.rank_issues({
71+
"issues": ["EXAMPLE-1", "EXAMPLE-2"],
72+
"rankAfterIssue": "EXAMPLE-10",
73+
})
74+
75+
Use ``rank_epics`` for ranking epics. These methods are available on Cloud and
76+
on Jira Software Server/Data Center installations exposing the Agile REST API.
77+
6278
Generated OpenAPI operations
6379
----------------------------
6480

docs/service_desk.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ SLA actions
196196
# IMPORTANT: The calling user must be an agent
197197
sd.get_sla_by_id(issue_id_or_key, sla_id)
198198
199+
# Get SLA metric configuration for a service desk/project.
200+
# This uses an internal agent endpoint and may vary by Jira release.
201+
sd.get_sla_metrics(service_desk_id)
202+
203+
# Update one SLA metric. Pass the metric definition/goals payload required
204+
# by your Jira release; this is an internal agent endpoint.
205+
sd.update_sla_metric(service_desk_id, sla_id, metric_payload)
206+
199207
Approvals
200208
---------
201209

tests/test_bitbucket_cloud_oo.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,28 @@ def fail_get(*args, **kwargs):
137137
assert commit.hash == "1fbd047cd99a"
138138
assert commit.message == "src created online with Bitbucket"
139139

140+
def test_repository_commits_each_forwards_supported_filters(self, monkeypatch):
141+
repository = CLOUD.workspaces.get("TestWorkspace1").repositories.get("testrepository1")
142+
calls = []
143+
144+
def get_paged(*args, **kwargs):
145+
calls.append((args, kwargs))
146+
return iter([])
147+
148+
monkeypatch.setattr(repository.commits, "_get_paged", get_paged)
149+
150+
list(repository.commits.each(include="main", exclude="release", path="src/app.py"))
151+
152+
assert calls == [
153+
(
154+
(None,),
155+
{
156+
"trailing": True,
157+
"params": {"include": "main", "exclude": "release", "path": "src/app.py"},
158+
},
159+
)
160+
]
161+
140162
def test_not_exists_repository(self):
141163
assert not CLOUD.workspaces.get("TestWorkspace1").repositories.exists(
142164
"testrepository1xxx"

tests/test_jira_cloud_clients.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ def test_core_methods_cover_the_supplied_v3_document(self):
5757
jira.get_issue("ABC-1", fields="summary")
5858
get.assert_called_once_with("rest/api/3/issue/ABC-1", params={"fields": "summary"}, data=None)
5959

60+
def test_software_rank_issues_supports_multiple_issue_keys(self):
61+
jira = JiraSoftware("https://example.atlassian.net")
62+
payload = {"issues": ["EXAMPLE-1", "EXAMPLE-2"], "rankAfterIssue": "EXAMPLE-10"}
63+
64+
with patch.object(jira, "put", return_value=None) as put:
65+
jira.rank_issues(payload)
66+
67+
put.assert_called_once_with("rest/agile/1.0/issue/rank", params=None, data=payload)
68+
69+
def test_software_rank_epics_uses_epic_endpoint(self):
70+
jira = JiraSoftware("https://example.atlassian.net")
71+
payload = {"epic": "EXAMPLE-1", "rankBeforeEpic": "EXAMPLE-2"}
72+
73+
with patch.object(jira, "put", return_value=None) as put:
74+
jira.rank_epics("EXAMPLE-1", payload)
75+
76+
put.assert_called_once_with("rest/agile/1.0/epic/EXAMPLE-1/rank", params=None, data=payload)
77+
6078
def test_core_methods_honor_the_selected_v2_route(self):
6179
jira = JiraCloud("https://example.atlassian.net", api_version=2)
6280

tests/test_sla_metrics.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from unittest.mock import patch
2+
3+
from atlassian import JiraServiceManagement, ServiceDesk
4+
5+
6+
def test_legacy_service_desk_get_sla_metrics_uses_agent_endpoint():
7+
service_desk = ServiceDesk("https://example.atlassian.net")
8+
9+
with patch.object(service_desk, "get", return_value={"metrics": []}) as get:
10+
result = service_desk.get_sla_metrics("10", start=0, limit=25)
11+
12+
assert result == {"metrics": []}
13+
get.assert_called_once_with(
14+
"rest/servicedesk/1/servicedesk/agent/10/sla/metrics",
15+
params={"start": 0, "limit": 25},
16+
headers=service_desk.experimental_headers,
17+
)
18+
19+
20+
def test_jsm_cloud_get_sla_metrics_uses_agent_endpoint():
21+
service_management = JiraServiceManagement("https://example.atlassian.net")
22+
23+
with patch.object(service_management, "get", return_value={"metrics": []}) as get:
24+
service_management.get_sla_metrics("10")
25+
26+
get.assert_called_once_with("rest/servicedesk/1/servicedesk/agent/10/sla/metrics", params=None)
27+
28+
29+
def test_legacy_service_desk_update_sla_metric_uses_agent_endpoint():
30+
service_desk = ServiceDesk("https://example.atlassian.net")
31+
payload = {"id": 42, "name": "Time to resolution", "config": {"goals": []}}
32+
33+
with patch.object(service_desk, "put", return_value=payload) as put:
34+
result = service_desk.update_sla_metric("10", "42", payload)
35+
36+
assert result == payload
37+
put.assert_called_once_with(
38+
"rest/servicedesk/1/servicedesk/agent/10/sla/metrics/42",
39+
data=payload,
40+
headers=service_desk.experimental_headers,
41+
)
42+
43+
44+
def test_jsm_cloud_update_sla_metric_uses_agent_endpoint():
45+
service_management = JiraServiceManagement("https://example.atlassian.net")
46+
payload = {"id": 42, "config": {"goals": []}}
47+
48+
with patch.object(service_management, "put", return_value=payload) as put:
49+
result = service_management.update_sla_metric("10", "42", payload)
50+
51+
assert result == payload
52+
put.assert_called_once_with(
53+
"rest/servicedesk/1/servicedesk/agent/10/sla/metrics/42",
54+
data=payload,
55+
)

0 commit comments

Comments
 (0)