Skip to content

Adding teams API #1794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
06a5d52
added team_members function
Maxim-Durand Nov 18, 2023
8e385a7
changes to 'post_create.sh' so the container doesn't fail at startup
Maxim-Durand Nov 18, 2023
20fe576
Added Team Resource and added all Team's methods skeletons
Maxim-Durand Nov 18, 2023
1d25e22
Added 'test_add_team' but without the correct org_id (don't know how …
Maxim-Durand Nov 18, 2023
24d6030
removed sudo chmod in post_create.sh as it was ruining the git changelog
Maxim-Durand Nov 18, 2023
7e8c583
lint
Maxim-Durand Nov 18, 2023
1d71f6b
more lint
Maxim-Durand Nov 18, 2023
76968c1
Started adding Organization API too
Maxim-Durand Nov 27, 2023
0ffae35
Started adding tests
Maxim-Durand Nov 27, 2023
bf37718
added all api routes for org.
Maxim-Durand Nov 28, 2023
24efc5c
added install of libkrb5-dev in Dockerfile as it's a needed dependenc…
Maxim-Durand Dec 9, 2023
f7a8235
added _get_service_desk_url helping function to make sure all methods…
Maxim-Durand Dec 9, 2023
8569141
added team_members function
Maxim-Durand Nov 18, 2023
cdfd11a
changes to 'post_create.sh' so the container doesn't fail at startup
Maxim-Durand Nov 18, 2023
f561715
Added Team Resource and added all Team's methods skeletons
Maxim-Durand Nov 18, 2023
224505b
Added 'test_add_team' but without the correct org_id (don't know how …
Maxim-Durand Nov 18, 2023
4780ce9
removed sudo chmod in post_create.sh as it was ruining the git changelog
Maxim-Durand Nov 18, 2023
7fdf0b8
lint
Maxim-Durand Nov 18, 2023
d740987
more lint
Maxim-Durand Nov 18, 2023
d8a08d5
Started adding Organization API too
Maxim-Durand Nov 27, 2023
d2b5072
Started adding tests
Maxim-Durand Nov 27, 2023
37f34d0
added all api routes for org.
Maxim-Durand Nov 28, 2023
67aa439
added _get_service_desk_url helping function to make sure all methods…
Maxim-Durand Dec 9, 2023
09cdd50
Merge branch 'pycontribs-main'
Maxim-Durand Jan 7, 2024
09a08f7
lint
Maxim-Durand Jan 7, 2024
b3237f5
added docstring to all member methods
Maxim-Durand Jan 7, 2024
df5fbf7
removed all code paths about orgs both in client.py and tests.py
Maxim-Durand Jan 7, 2024
cf3c276
removed last code path for orgs
Maxim-Durand Jan 7, 2024
1c3e965
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 7, 2024
7c57de8
removed last code path for orgs
Maxim-Durand Jan 7, 2024
8cbe77e
Merge remote-tracking branch 'origin/only_team_api' into only_team_api
Maxim-Durand Jan 7, 2024
f879605
mypy typing
Maxim-Durand Jan 7, 2024
0fe0f3c
using siteId in params instead of passing it in url
Maxim-Durand Jan 19, 2024
d4880e4
minor refactoring to client.py teams api
Maxim-Durand Jan 19, 2024
39a1ced
moved teams API tests into tests/resources/test_teams.py and improved…
Maxim-Durand Jan 19, 2024
33ecea3
created function to retrieve paginated results
Maxim-Durand Jan 19, 2024
37b33d1
moved TEAM_API_BASE_URL outside of init as in AgileResource
Maxim-Durand Jan 19, 2024
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
1 change: 1 addition & 0 deletions .devcontainer/post_create.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/bin/bash
# This file is run from the .vscode folder
WORKSPACE_FOLDER=/workspaces/jira
git config --global --add safe.directory /workspaces/jira

# Start the Jira Server docker instance first so can be running while we initialise everything else
# Need to ensure this --version matches what is in CI
Expand Down
189 changes: 188 additions & 1 deletion jira/client.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
Sprint,
Status,
StatusCategory,
Team,
User,
Version,
Votes,
Expand Down Expand Up @@ -1309,6 +1310,192 @@ def update_filter(
raw_filter_json = json.loads(r.text)
return Filter(self._options, self._session, raw=raw_filter_json)

# Teams

def create_team(
self,
org_id: str,
description: str,
display_name: str,
team_type: str,
site_id: str = None,
) -> Team:
"""Creates a team, and adds the requesting user as the initial member.

Args:
org_id (str): organization identifier
description (str): description field of the team to be created
display_name (str): name of the team to be created
team_type (str): either 'OPEN' or 'MEMBER_INVITE'
site_id (Optional[str])

Returns:
Team
"""
url = f"gateway/api/public/teams/v1/org/{org_id}/teams/"
payload = {
"description": description,
"displayName": display_name,
"teamType": team_type,
}
if site_id is not None:
payload["siteId"] = site_id
r = self._session.post(url, data=json.dumps(payload))
raw_team_json: dict[str, Any] = json_loads(r)
return Team(self._options, self._session, raw=raw_team_json)

def get_team(self, org_id: str, team_id: str, site_id: str = None) -> Team:
"""Get the specified team.

Args:
org_id (str): organization identifier
team_id (str): team identifier
site_id (Optional[str])

Returns:
Team
"""
url = f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}"
params = {}
if site_id is not None:
params = {"siteId": site_id}
r = self._session.get(url, params=params)
raw_team_json: dict[str, Any] = json_loads(r)
return Team(self._options, self._session, raw=raw_team_json)

def remove_team(
self,
org_id: str,
team_id: str,
):
"""Delete the specified team.

Args:
org_id (str): organization identifier
team_id (str): team identifier

Returns:
bool
"""
url = f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}"
r = self._session.delete(url)
return r.ok

def update_team(
self,
org_id: str,
team_id: str,
description: str,
displayName: str,
) -> Team:
"""Modifies the specified team with new values.

Args:
org_id (str): organization identifier
team_id (str): team identifier

Returns:
Team
"""
url = f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}"

headers = {"Accept": "application/json", "Content-Type": "application/json"}

payload = {}
if description != "":
payload["description"] = description
if displayName != "":
payload["displayName"] = displayName

response = self._session.request(
"PATCH", url, data=json.dumps(payload), headers=headers
)
raw_team_json: dict[str, Any] = json_loads(response)
return Team(self._options, self._session, raw=raw_team_json)

def _fetch_paginated(self, url, payload):
result_response = self._session.get(url, data=json.dumps(payload)).json()
has_next_page = result_response["pageInfo"]["hasNextPage"]
end_index = result_response["pageInfo"]["endCursor"]

while has_next_page:
payload["after"] = end_index
r2 = self._session.get(url, data=json.dumps(payload)).json()
for res in r2["results"]:
result_response["results"].append(res)
end_index = r2["pageInfo"]["endCursor"]
has_next_page = r2["pageInfo"]["hasNextPage"]
return result_response

def team_members(
self,
org_id: str,
team_id: str,
) -> list[str]:
"""Return the list of account Ids corresponding to the team members.

Args:
org_id (str): Id of the org.
team_id (str): Id of the team.

Returns:
list[str]
"""
url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members"
payload = {"first": 50}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pagination implementation should be moved into a helper method, if is is specific to the Teams API I would make it a static method of the Teams Resource

Then you would call it like:

Teams._fetch_pages(session=session, url=url, ...)

I would aim to match as many of the parameters to the function JIRA._fetch_pages() if possible

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory it's not specific to Teams API, but isn't exactly like JIRA._fetch_pages() neither as it's using different identifiers for Jira's latest API (why they didn't stick to the same variable names between API idk 🙄 )

For now I simply created it as a helper function but if it it's used outside of Teams it's bound to change.

r = self._fetch_paginated(url, payload)
result = []
for accounts in r["results"]:
result.append(accounts.get("accountId"))
return result

def add_team_members(
self,
org_id: str,
team_id: str,
members: list[str],
) -> tuple[list[str], list[str]]:
"""Adds a list of members (accountIds) to the team members.

Args:
org_id (str): Id of the org.
team_id (str): Id of the team.
members (list[str]): Account Ids of the new members.

Returns:
(list[str], list[str]): (list of successful addition, list of failure)
"""
url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members/add"
payload_members_list = [{"accountId": accountId} for accountId in members]
payload = {"members": payload_members_list}
r = self._session.post(url, data=json.dumps(payload))
response_json = r.json()
return response_json["members"], response_json["errors"]

def remove_team_members(
self,
org_id: str,
team_id: str,
members: list[str],
) -> bool:
"""Removes the specified members from the team.

Args:
team_id (str): Id of the team.
org_id (str): Id of the org.
members (list[str]): Account Ids of the new members.

Returns:
bool
"""
url = (
f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members/remove"
)
payload_members_list = [{"accountId": accountId} for accountId in members]
payload = {"members": payload_members_list}
r = self._session.post(url, data=json.dumps(payload))
return r.ok

# Groups

def group(self, id: str, expand: Any = None) -> Group:
Expand Down Expand Up @@ -2719,7 +2906,7 @@ def request_types(self, service_desk: ServiceDesk) -> list[RequestType]:
service_desk = service_desk.id
url = (
self.server_url
+ f"/rest/servicedeskapi/servicedesk/{service_desk}/requesttype"
+ "/rest/servicedeskapi/servicedesk/{service_desk}/requesttype"
)
headers = {"X-ExperimentalApi": "opt-in"}
r_json = json_loads(self._session.get(url, headers=headers))
Expand Down
21 changes: 21 additions & 0 deletions jira/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class AnyLike:
"Resolution",
"SecurityLevel",
"Status",
"Team",
"User",
"Group",
"CustomFieldOption",
Expand Down Expand Up @@ -1234,6 +1235,25 @@ def __init__(
self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw)


class Team(Resource):
"""A Jira team."""

TEAM_API_BASE_URL = "{server}/gateway/api/public/teams/v1/"

def __init__(
self,
options: dict[str, str],
session: ResilientSession,
raw: dict[str, Any] = None,
):
Resource.__init__(
self, "org/{0}/teams/{1}", options, session, base_url=self.TEAM_API_BASE_URL
)
if raw:
self._parse_raw(raw)
self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw)


class Group(Resource):
"""A Jira user group."""

Expand Down Expand Up @@ -1521,6 +1541,7 @@ def dict2resource(
# Agile specific resources
r"sprints/[^/]+$": Sprint,
r"views/[^/]+$": Board,
r"org\?(accountId)/teams\?(accountId).+$": Team,
}


Expand Down
80 changes: 80 additions & 0 deletions tests/resources/test_teams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from __future__ import annotations

import os
from contextlib import contextmanager

from tests.conftest import JiraTestCase, allow_on_cloud


@allow_on_cloud
class TeamsTests(JiraTestCase):
def setUp(self):
JiraTestCase.setUp(self)
self.test_team_name = f"testTeamFor_{self.test_manager.project_a}"
self.test_team_type = "OPEN"
self.org_id = os.environ["CI_JIRA_ORG_ID"]
Copy link
Author

@Maxim-Durand Maxim-Durand Jan 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to get the org_id of the testing organisation 🤔

I guess the only real solution is to implement the organisation API and to create a new one before running the tests.
I'll get to work 😉

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a new PR (#1803) based on this one but also adding Organisations API so that I'm able to test the Team API.

self.test_team_description = "test Description"

@contextmanager
def make_team(self, **kwargs):
try:
new_team = self.jira.create_team(
self.org_id,
self.test_team_description,
self.test_team_name,
self.test_team_type,
)

if len(kwargs):
raise ValueError("Incorrect kwarg used !")
yield new_team
finally:
new_team.delete()

def test_team_creation(self):
with self.make_team() as test_team:
self.assertEqual(
self.test_team_name,
test_team["displayName"],
)
self.assertEqual(self.test_team_description, test_team["description"])
self.assertEqual(self.test_team_type, test_team["teamType"])

def test_team_get(self):
with self.make_team() as test_team:
fetched_team = self.jira.get_team(self.org_id, test_team.id)
self.assertEqual(
self.test_team_name,
fetched_team["displayName"],
)

def test_team_deletion(self):
with self.make_team() as test_team:
ok = self.jira.remove_team(self.org_id, test_team.id)
self.assertTrue(ok)

def test_updating_team(self):
new_desc = "Fake new description"
new_name = "Fake new Name"
with self.make_team() as test_team:
updated_team = self.jira.update_team(
self.org_id, test_team.id, description=new_desc, displayName=new_name
)
self.assertEqual(new_name, updated_team["displayName"])
self.assertEqual(new_desc, updated_team["description"])

def test_adding_team_members(self):
with self.make_team() as test_team:
self.jira.add_team_members(
self.org_id, test_team.id, members=[self.user_admin["accountId"]]
)

def test_get_team_members(self):
expected_accounts_id = [self.user_admin["accountId"]]
with self.make_team() as test_team:
self.jira.add_team_members(
self.org_id, test_team.id, members=expected_accounts_id
)

fetched_account_ids = self.jira.team_members(self.org_id, test_team.id)
self.assertEqual(expected_accounts_id, fetched_account_ids)