From 06a5d524366e744287715abb1d607383d43da3a7 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 09:47:31 -0500 Subject: [PATCH 01/38] added team_members function --- jira/client.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/jira/client.py b/jira/client.py index 64c3e7406..7a00c831f 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1293,6 +1293,41 @@ def update_filter( raw_filter_json = json.loads(r.text) return Filter(self._options, self._session, raw=raw_filter_json) + # Teams + + def team_members(self, team_id: str, org_id: str) -> list[str]: + """Return list of account Ids in the team. Requires Jira 6.0 or will raise NotImplemented. + + Args: + team_id (str): Id of the team. + org_id (str): Id of the org. + """ + + url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" + headers = { + "Accept": "*/*", + "Content-Type": "application/json" + } + payload = { + "first": 50 + } + r = self._session.get(url,headers=headers,data=json.dumps(payload)) + has_next_page = r["pageInfo"]["hasNextPage"] + end_index = r["pageInfo"]["endCursor"] + + while has_next_page: + payload["after"]=str(end_index) + r2 = self._session.get(url,headers=headers,data=json.dumps(payload)) + for user in r2["results"]: + r["results"].append(user) + end_index = r2["pageInfo"]["endCursor"] + has_next_page = r2["pageInfo"]["hasNextPage"] + + result = [] + for accounts in r["results"]: + result.append(accounts.get("accountId")) + return result + # Groups def group(self, id: str, expand: Any = None) -> Group: From 8e385a756c760a474089d5ee96c25a99926283a5 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 09:48:45 -0500 Subject: [PATCH 02/38] changes to 'post_create.sh' so the container doesn't fail at startup --- .devcontainer/post_create.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh index 5882eb626..5743d5a3f 100755 --- a/.devcontainer/post_create.sh +++ b/.devcontainer/post_create.sh @@ -1,6 +1,8 @@ #!/bin/bash # This file is run from the .vscode folder WORKSPACE_FOLDER=/workspaces/jira +sudo chmod -R 777 . +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 From 20fe5767f6b10815c242c6d5e2251ece58ba7495 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 11:53:37 -0500 Subject: [PATCH 03/38] Added Team Resource and added all Team's methods skeletons --- jira/client.py | 74 +++++++++++++++++++++++++++++++++++++++++++---- jira/resources.py | 17 +++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/jira/client.py b/jira/client.py index 7a00c831f..277374338 100644 --- a/jira/client.py +++ b/jira/client.py @@ -84,6 +84,7 @@ Sprint, Status, StatusCategory, + Team, User, Version, Votes, @@ -1295,6 +1296,55 @@ def update_filter( # Teams + def create_team( + self, + org_id: str, + description: str, + display_name: str, + team_type: str, + site_id: str = None, + ) -> 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: + url=f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" + if site_id is not None: + url+=f"?siteId={site_id}" + r=self._session.get(url) + 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, + ): + pass + + def update_team( + self, + org_id: str, + team_id: str, + description: str, + displayName: str, + ) -> Team: + pass + def team_members(self, team_id: str, org_id: str) -> list[str]: """Return list of account Ids in the team. Requires Jira 6.0 or will raise NotImplemented. @@ -1304,20 +1354,16 @@ def team_members(self, team_id: str, org_id: str) -> list[str]: """ url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" - headers = { - "Accept": "*/*", - "Content-Type": "application/json" - } payload = { "first": 50 } - r = self._session.get(url,headers=headers,data=json.dumps(payload)) + r = self._session.get(url,data=json.dumps(payload)) has_next_page = r["pageInfo"]["hasNextPage"] end_index = r["pageInfo"]["endCursor"] while has_next_page: payload["after"]=str(end_index) - r2 = self._session.get(url,headers=headers,data=json.dumps(payload)) + r2 = self._session.get(url,data=json.dumps(payload)) for user in r2["results"]: r["results"].append(user) end_index = r2["pageInfo"]["endCursor"] @@ -1328,6 +1374,22 @@ def team_members(self, team_id: str, org_id: str) -> list[str]: result.append(accounts.get("accountId")) return result + def add_team_members( + self, + org_id: str, + team_id: str, + members: list[str], + ): + pass + + def remove_team_members( + self, + org_id: str, + team_id: str, + members: list[str], + ): + pass + # Groups def group(self, id: str, expand: Any = None) -> Group: diff --git a/jira/resources.py b/jira/resources.py index c0ba80a10..6a05d2d1d 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1215,6 +1215,22 @@ def __init__( self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) +class Team(Resource): + """A Jira team.""" + + def __init__( + self, + options: dict[str, str], + session: ResilientSession, + raw: dict[str, Any] = None, + ): + TEAM_API_BASE_URL = "{server}/gateway/api/public/teams/v1/" #rest/{rest_path}/{rest_api_version}/{path}" + + Resource.__init__(self, "org/{0}/teams/{1}", options, session, base_url=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.""" @@ -1501,6 +1517,7 @@ def dict2resource( # Agile specific resources r"sprints/[^/]+$": Sprint, r"views/[^/]+$": Board, + r"org\?(accountId)/teams\?(accountId).+$": Team, } From 1d25e220454fe98dd016fcb8f01db45888a48459 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 11:58:20 -0500 Subject: [PATCH 04/38] Added 'test_add_team' but without the correct org_id (don't know how to get it yet) --- tests/tests.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index b032f43e6..a186c092e 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -546,6 +546,10 @@ def setUp(self): self.test_email = f"{self.test_username}@example.com" self.test_password = rndpassword() self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" + self.test_team_name= f"testTeamFor_{self.test_manager.project_a}" + self.org_id="" + self.test_team_type="OPEN" + self.test_team_description="test Description" def _skip_pycontribs_instance(self): pytest.skip( @@ -610,6 +614,27 @@ def test_add_and_remove_user(self): result = self.jira.delete_user(self.test_username) assert result, True + def test_add_team(self): + if self._should_skip_for_pycontribs_instance(): + self._skip_pycontribs_instance() + try: + self.jira.remove_team(self.test_team_name) + except JIRAError: + pass + + sleep(2) # avoid 500 errors + result_team = self.jira.create_team( + self.org_id, + self.test_team_description, + self.test_team_name, + self.test_team_type + ) + self.assertEqual( + self.test_team_name, + result_team.__getattr__("displayName"), + "Did not find expected group after trying to add" " it. Test Fails.", + ) + def test_add_group(self): if self._should_skip_for_pycontribs_instance(): self._skip_pycontribs_instance() From 24d60301356b2ca97526dc27627fbbf464c96b70 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:14:24 -0500 Subject: [PATCH 05/38] removed sudo chmod in post_create.sh as it was ruining the git changelog --- .devcontainer/post_create.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh index 5743d5a3f..5176abfa8 100755 --- a/.devcontainer/post_create.sh +++ b/.devcontainer/post_create.sh @@ -1,7 +1,6 @@ #!/bin/bash # This file is run from the .vscode folder WORKSPACE_FOLDER=/workspaces/jira -sudo chmod -R 777 . git config --global --add safe.directory /workspaces/jira # Start the Jira Server docker instance first so can be running while we initialise everything else From 7e8c583347872ddc995864e6055eb8cd941a9629 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:15:30 -0500 Subject: [PATCH 06/38] lint --- jira/client.py | 82 +++++++++++++++++++++-------------------------- jira/resources.py | 9 ++++-- tests/tests.py | 12 +++---- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/jira/client.py b/jira/client.py index 277374338..2757039b6 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1297,51 +1297,46 @@ def update_filter( # Teams def create_team( - self, - org_id: str, - description: str, - display_name: str, - team_type: str, - site_id: str = None, + self, + org_id: str, + description: str, + display_name: str, + team_type: str, + site_id: str = None, ) -> Team: - url=f"gateway/api/public/teams/v1/org/{org_id}/teams/" + url = f"gateway/api/public/teams/v1/org/{org_id}/teams/" payload = { "description": description, "displayName": display_name, - "teamType": team_type + "teamType": team_type, } if site_id is not None: - payload["siteId"]=site_id - r=self._session.post(url,data=json.dumps(payload)) + 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) + 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: - url=f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" + def get_team(self, org_id: str, team_id: str, site_id: str = None) -> Team: + url = f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" if site_id is not None: - url+=f"?siteId={site_id}" - r=self._session.get(url) + url += f"?siteId={site_id}" + r = self._session.get(url) raw_team_json: dict[str, Any] = json_loads(r) - return Team(self._options,self._session,raw=raw_team_json) + return Team(self._options, self._session, raw=raw_team_json) def remove_team( - self, - org_id: str, - team_id: str, + self, + org_id: str, + team_id: str, ): pass def update_team( - self, - org_id: str, - team_id: str, - description: str, - displayName: str, + self, + org_id: str, + team_id: str, + description: str, + displayName: str, ) -> Team: pass @@ -1352,41 +1347,38 @@ def team_members(self, team_id: str, org_id: str) -> list[str]: team_id (str): Id of the team. org_id (str): Id of the org. """ - url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" - payload = { - "first": 50 - } - r = self._session.get(url,data=json.dumps(payload)) + payload = {"first": 50} + r = self._session.get(url, data=json.dumps(payload)) has_next_page = r["pageInfo"]["hasNextPage"] end_index = r["pageInfo"]["endCursor"] while has_next_page: - payload["after"]=str(end_index) - r2 = self._session.get(url,data=json.dumps(payload)) + payload["after"] = str(end_index) + r2 = self._session.get(url, data=json.dumps(payload)) for user in r2["results"]: r["results"].append(user) end_index = r2["pageInfo"]["endCursor"] has_next_page = r2["pageInfo"]["hasNextPage"] - + 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], + self, + org_id: str, + team_id: str, + members: list[str], ): pass def remove_team_members( - self, - org_id: str, - team_id: str, - members: list[str], + self, + org_id: str, + team_id: str, + members: list[str], ): pass diff --git a/jira/resources.py b/jira/resources.py index 6a05d2d1d..992980b13 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1215,6 +1215,7 @@ def __init__( self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + class Team(Resource): """A Jira team.""" @@ -1224,9 +1225,11 @@ def __init__( session: ResilientSession, raw: dict[str, Any] = None, ): - TEAM_API_BASE_URL = "{server}/gateway/api/public/teams/v1/" #rest/{rest_path}/{rest_api_version}/{path}" - - Resource.__init__(self, "org/{0}/teams/{1}", options, session, base_url=TEAM_API_BASE_URL) + TEAM_API_BASE_URL = "{server}/gateway/api/public/teams/v1/" # rest/{rest_path}/{rest_api_version}/{path}" + + Resource.__init__( + self, "org/{0}/teams/{1}", options, session, base_url=TEAM_API_BASE_URL + ) if raw: self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) diff --git a/tests/tests.py b/tests/tests.py index a186c092e..446c17f45 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -546,10 +546,10 @@ def setUp(self): self.test_email = f"{self.test_username}@example.com" self.test_password = rndpassword() self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" - self.test_team_name= f"testTeamFor_{self.test_manager.project_a}" - self.org_id="" - self.test_team_type="OPEN" - self.test_team_description="test Description" + self.test_team_name = f"testTeamFor_{self.test_manager.project_a}" + self.org_id = "" + self.test_team_type = "OPEN" + self.test_team_description = "test Description" def _skip_pycontribs_instance(self): pytest.skip( @@ -626,8 +626,8 @@ def test_add_team(self): result_team = self.jira.create_team( self.org_id, self.test_team_description, - self.test_team_name, - self.test_team_type + self.test_team_name, + self.test_team_type, ) self.assertEqual( self.test_team_name, From 1d71f6b72bc30863c043c83fe7dea06019e8b8d9 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:18:18 -0500 Subject: [PATCH 07/38] more lint --- jira/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jira/client.py b/jira/client.py index 2757039b6..1a8a8406a 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1349,13 +1349,13 @@ def team_members(self, team_id: str, org_id: str) -> list[str]: """ url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" payload = {"first": 50} - r = self._session.get(url, data=json.dumps(payload)) + r = self._session.get(url, data=json.dumps(payload)).json() has_next_page = r["pageInfo"]["hasNextPage"] end_index = r["pageInfo"]["endCursor"] while has_next_page: - payload["after"] = str(end_index) - r2 = self._session.get(url, data=json.dumps(payload)) + payload["after"] = end_index + r2 = self._session.get(url, data=json.dumps(payload)).json() for user in r2["results"]: r["results"].append(user) end_index = r2["pageInfo"]["endCursor"] From 76968c1761bfe0fe11afbb010152b48fc97c409b Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Mon, 27 Nov 2023 05:46:37 -0500 Subject: [PATCH 08/38] Started adding Organization API too --- jira/client.py | 30 ++++++++++++++++++++++++++++++ jira/resources.py | 18 ++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/jira/client.py b/jira/client.py index 1a8a8406a..3b68ae12a 100644 --- a/jira/client.py +++ b/jira/client.py @@ -84,6 +84,7 @@ Sprint, Status, StatusCategory, + Organization, Team, User, Version, @@ -1294,6 +1295,35 @@ def update_filter( raw_filter_json = json.loads(r.text) return Filter(self._options, self._session, raw=raw_filter_json) + # Organisations + + def create_org(self, org_name: str) -> Organization: + url = f"rest/servicedeskapi/organization" + payload = { + "name": org_name + } + r = self._session.post(url, data=json.dumps(payload)) + raw_org_json: dict[str, Any] = json_loads(r) + return Organization(self._options, self._session, raw=raw_org_json) + + def remove_org(self,org_id:str) -> bool: + url = f"rest/servicedeskapi/organization/{org_id}" + r = self._session.delete(url) + if r.status_code == 204: + return True + return False + + def get_org(self, org_id: str) -> Organization: + url = f"rest/servicedeskapi/organization/{org_id}" + r = self._session.get(url) + raw_org_json: dict[str, Any] = json_loads(r) + if r.status_code == 200: + return Organization(self._options, self._session, raw=raw_org_json) + return None + + def get_orgs(self, start, limit, account_id): + pass + # Teams def create_team( diff --git a/jira/resources.py b/jira/resources.py index 992980b13..9da72567a 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -57,6 +57,8 @@ class AnyLike: "Resolution", "SecurityLevel", "Status", + "Organization", + "Team", "User", "Group", "CustomFieldOption", @@ -1215,6 +1217,22 @@ def __init__( self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) +class Organization(Resource): + """A JIRA Organization.""" + + def __init__( + self, + options: dict[str, str], + session: ResilientSession, + raw: dict[str, Any] = None, + ): + + Resource.__init__( + self, "organization/{0}", options, session, "{server}/rest/servicedeskapi/{path}" + ) + if raw: + self._parse_raw(raw) + self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) class Team(Resource): """A Jira team.""" From 0ffae3568925ab37cc32bfbb78d117273909abca Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Mon, 27 Nov 2023 05:46:48 -0500 Subject: [PATCH 09/38] Started adding tests --- tests/tests.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 446c17f45..ed4e00f79 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -547,7 +547,7 @@ def setUp(self): self.test_password = rndpassword() self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" self.test_team_name = f"testTeamFor_{self.test_manager.project_a}" - self.org_id = "" + self.test_org_name = "testOrgFor_{self.test_manager.project_a}" self.test_team_type = "OPEN" self.test_team_description = "test Description" @@ -614,10 +614,22 @@ def test_add_and_remove_user(self): result = self.jira.delete_user(self.test_username) assert result, True - def test_add_team(self): + def test_create_org(self): + result_org = self.jira.create_org(self.test_org_name) + self.assertEqual( + self.test_org_name, + result_org.__getattr__("name") + ) + + def test_remove_org(self): + pass + + def test_create_team(self): if self._should_skip_for_pycontribs_instance(): self._skip_pycontribs_instance() try: + new_org = self.jira.create_org(self.test_org_name) + self.jira.remove_team(self.test_team_name) except JIRAError: pass From bf3771870b9ee063c92d56afe01fdf8ce5681fb2 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:38:23 -0500 Subject: [PATCH 10/38] added all api routes for org. Started adding the tests for those org routes --- jira/client.py | 53 +++++++++++++++++++++++++++++++++++++++++++------- tests/tests.py | 51 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/jira/client.py b/jira/client.py index 3b68ae12a..8b17af888 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1298,7 +1298,7 @@ def update_filter( # Organisations def create_org(self, org_name: str) -> Organization: - url = f"rest/servicedeskapi/organization" + url = f"/rest/servicedeskapi/organization" payload = { "name": org_name } @@ -1307,22 +1307,61 @@ def create_org(self, org_name: str) -> Organization: return Organization(self._options, self._session, raw=raw_org_json) def remove_org(self,org_id:str) -> bool: - url = f"rest/servicedeskapi/organization/{org_id}" + url = f"/rest/servicedeskapi/organization/{org_id}" r = self._session.delete(url) if r.status_code == 204: return True return False - def get_org(self, org_id: str) -> Organization: - url = f"rest/servicedeskapi/organization/{org_id}" + def org(self, org_id: str) -> Organization: + url = f"/rest/servicedeskapi/organization/{org_id}" r = self._session.get(url) raw_org_json: dict[str, Any] = json_loads(r) if r.status_code == 200: return Organization(self._options, self._session, raw=raw_org_json) return None - def get_orgs(self, start, limit, account_id): - pass + def orgs(self, start=0, limit=50) -> ResultList[Organization]: + url = f"/rest/servicedeskapi/organization" + return self._fetch_pages( + Organization, + "values", + url, + start, + limit, + base=self.server_url + ) + + def org_users(self, org_id, start, limit) -> ResultList[User]: + url = f"/rest/servicedeskapi/organization/{org_id}/user" + return self._fetch_pages( + User, + None, + url, + start, + limit, + base=self.server_url + ) + + def add_users_to_org(self, org_id: str, users: list[str]) -> bool: + url=f"/rest/servicedeskapi/organization/{org_id}/user" + payload={ + "usernames": users + } + r= self._session.post(url, data=json.dumps(payload)) + if r.status_code == 204: + return True + return False + + def remove_users_from_org(self, org_id: str, users: list[str]) -> bool: + url=f"/rest/servicedeskapi/organization/{org_id}/user" + payload={ + "usernames": users + } + r= self._session.delete(url, data=json.dumps(payload)) + if r.status_code == 204: + return True + return False # Teams @@ -1371,7 +1410,7 @@ def update_team( pass def team_members(self, team_id: str, org_id: str) -> list[str]: - """Return list of account Ids in the team. Requires Jira 6.0 or will raise NotImplemented. + """Return list of account Ids in the team. Args: team_id (str): Id of the team. diff --git a/tests/tests.py b/tests/tests.py index ed4e00f79..1d8faddd7 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -615,6 +615,13 @@ def test_add_and_remove_user(self): assert result, True def test_create_org(self): + if self._should_skip_for_pycontribs_instance(): + self._skip_pycontribs_instance() + try: + self.jira.remove_org(self.test_org_name) + except JIRAError: + pass + result_org = self.jira.create_org(self.test_org_name) self.assertEqual( self.test_org_name, @@ -622,14 +629,52 @@ def test_create_org(self): ) def test_remove_org(self): - pass + if self._should_skip_for_pycontribs_instance(): + self._skip_pycontribs_instance() + try: + self.jira.create_org(self.test_org_name) + sleep(1) # avoid 400 + except JIRAError: + pass + result = self.jira.remove_org(self.test_org_name) + assert result, True + + def test_fetching_orgs(self): + try: + self.jira.create_org(self.test_org_name) + sleep(1) # avoid 400 + except JIRAError: + pass + all_orgs=self.jira.orgs() + assert len(all_orgs) != 0 + + def test_fetching_org(self): + self.jira.remove_org(self.test_org_name) + org = self.jira.create_org(self.test_org_name) + org_id= org.id + response_org = self.jira.org(org_id) + assert response_org.id == org_id + + def test_adding_and_fetching_users_to_org(self): + try: + self.jira.add_user( + self.test_username, self.test_email, password=self.test_password + ) + org = self.jira.create_org(self.test_org_name) + except JIRAError: + pass + assert True, self.jira.add_users_to_org(org.id,[self.test_username]) + users = self.jira.org_users(org.id) + is_test_user_in_org = False + for u in users: + if u.username == self.test_username: + is_test_user_in_org = True + assert is_test_user_in_org, True def test_create_team(self): if self._should_skip_for_pycontribs_instance(): self._skip_pycontribs_instance() try: - new_org = self.jira.create_org(self.test_org_name) - self.jira.remove_team(self.test_team_name) except JIRAError: pass From 24efc5c6ff7a5bf304f6fce8709b1a8f0fbbacb4 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 9 Dec 2023 09:15:11 -0500 Subject: [PATCH 11/38] added install of libkrb5-dev in Dockerfile as it's a needed dependency of gssapi python package --- .devcontainer/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9b8963240..5022dd923 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -7,3 +7,6 @@ FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends \ + +# Required dependency of python module: gssapi +RUN apt install libkrb5-dev \ No newline at end of file From f7a82355472745a39779c3cc1325d1187b80f1ba Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 9 Dec 2023 14:33:18 -0500 Subject: [PATCH 12/38] added _get_service_desk_url helping function to make sure all methods calling service desk do it from the same root api endpoint --- jira/client.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/jira/client.py b/jira/client.py index 8b17af888..be8e497aa 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1295,10 +1295,18 @@ def update_filter( raw_filter_json = json.loads(r.text) return Filter(self._options, self._session, raw=raw_filter_json) + def _get_service_desk_url(self) -> str: + """Returns the service desk root url. + + Returns: + str: service desk api url + """ + return f"{self.server_url}/rest/servicedeskapi" + # Organisations def create_org(self, org_name: str) -> Organization: - url = f"/rest/servicedeskapi/organization" + url = f"{self._get_service_desk_url()}/organization" payload = { "name": org_name } @@ -1307,14 +1315,14 @@ def create_org(self, org_name: str) -> Organization: return Organization(self._options, self._session, raw=raw_org_json) def remove_org(self,org_id:str) -> bool: - url = f"/rest/servicedeskapi/organization/{org_id}" + url = f"{self._get_service_desk_url()}/organization/{org_id}" r = self._session.delete(url) if r.status_code == 204: return True return False def org(self, org_id: str) -> Organization: - url = f"/rest/servicedeskapi/organization/{org_id}" + url = f"{self._get_service_desk_url()}/organization/{org_id}" r = self._session.get(url) raw_org_json: dict[str, Any] = json_loads(r) if r.status_code == 200: @@ -1322,7 +1330,7 @@ def org(self, org_id: str) -> Organization: return None def orgs(self, start=0, limit=50) -> ResultList[Organization]: - url = f"/rest/servicedeskapi/organization" + url = f"{self._get_service_desk_url()}/organization" return self._fetch_pages( Organization, "values", @@ -1333,7 +1341,7 @@ def orgs(self, start=0, limit=50) -> ResultList[Organization]: ) def org_users(self, org_id, start, limit) -> ResultList[User]: - url = f"/rest/servicedeskapi/organization/{org_id}/user" + url = f"{self._get_service_desk_url()}/organization/{org_id}/user" return self._fetch_pages( User, None, @@ -1344,7 +1352,7 @@ def org_users(self, org_id, start, limit) -> ResultList[User]: ) def add_users_to_org(self, org_id: str, users: list[str]) -> bool: - url=f"/rest/servicedeskapi/organization/{org_id}/user" + url=f"{self._get_service_desk_url()}/organization/{org_id}/user" payload={ "usernames": users } @@ -1354,7 +1362,7 @@ def add_users_to_org(self, org_id: str, users: list[str]) -> bool: return False def remove_users_from_org(self, org_id: str, users: list[str]) -> bool: - url=f"/rest/servicedeskapi/organization/{org_id}/user" + url=f"{self._get_service_desk_url()}/organization/{org_id}/user" payload={ "usernames": users } @@ -1764,7 +1772,7 @@ def supports_service_desk(self): Returns: bool """ - url = self.server_url + "/rest/servicedeskapi/info" + url = f"{self._get_service_desk_url()}/info" headers = {"X-ExperimentalApi": "opt-in"} try: r = self._session.get(url, headers=headers) @@ -1782,7 +1790,7 @@ def create_customer(self, email: str, displayName: str) -> Customer: Returns: Customer """ - url = self.server_url + "/rest/servicedeskapi/customer" + url = f"{self._get_service_desk_url()}/customer" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post( url, @@ -1802,7 +1810,7 @@ def service_desks(self) -> list[ServiceDesk]: Returns: List[ServiceDesk] """ - url = self.server_url + "/rest/servicedeskapi/servicedesk" + url = f"{self._get_service_desk_url()}/servicedesk" headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) projects = [ @@ -1863,7 +1871,7 @@ def create_customer_request( elif isinstance(p, str): data["requestTypeId"] = self.request_type_by_name(service_desk, p).id - url = self.server_url + "/rest/servicedeskapi/request" + url = f"{self._get_service_desk_url()}/request" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post(url, headers=headers, data=json.dumps(data)) @@ -2820,8 +2828,7 @@ def request_types(self, service_desk: ServiceDesk) -> list[RequestType]: if hasattr(service_desk, "id"): service_desk = service_desk.id url = ( - self.server_url - + f"/rest/servicedeskapi/servicedesk/{service_desk}/requesttype" + f"{self._get_service_desk_url()}/servicedesk/{service_desk}/requesttype" ) headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) From 8569141880075c8bcdb3a1730cf0832bd8c283d3 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 09:47:31 -0500 Subject: [PATCH 13/38] added team_members function --- jira/client.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/jira/client.py b/jira/client.py index 1c8534869..64686cbec 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1309,6 +1309,41 @@ def update_filter( raw_filter_json = json.loads(r.text) return Filter(self._options, self._session, raw=raw_filter_json) + # Teams + + def team_members(self, team_id: str, org_id: str) -> list[str]: + """Return list of account Ids in the team. Requires Jira 6.0 or will raise NotImplemented. + + Args: + team_id (str): Id of the team. + org_id (str): Id of the org. + """ + + url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" + headers = { + "Accept": "*/*", + "Content-Type": "application/json" + } + payload = { + "first": 50 + } + r = self._session.get(url,headers=headers,data=json.dumps(payload)) + has_next_page = r["pageInfo"]["hasNextPage"] + end_index = r["pageInfo"]["endCursor"] + + while has_next_page: + payload["after"]=str(end_index) + r2 = self._session.get(url,headers=headers,data=json.dumps(payload)) + for user in r2["results"]: + r["results"].append(user) + end_index = r2["pageInfo"]["endCursor"] + has_next_page = r2["pageInfo"]["hasNextPage"] + + result = [] + for accounts in r["results"]: + result.append(accounts.get("accountId")) + return result + # Groups def group(self, id: str, expand: Any = None) -> Group: From cdfd11aa3c1333259e8262cb96b71cd4bd12c46e Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 09:48:45 -0500 Subject: [PATCH 14/38] changes to 'post_create.sh' so the container doesn't fail at startup --- .devcontainer/post_create.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh index 272b6ba23..fef566521 100755 --- a/.devcontainer/post_create.sh +++ b/.devcontainer/post_create.sh @@ -1,6 +1,8 @@ #!/bin/bash # This file is run from the .vscode folder WORKSPACE_FOLDER=/workspaces/jira +sudo chmod -R 777 . +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 From f5617150761a3a3a62d29aa7866e5f2149609f8f Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 11:53:37 -0500 Subject: [PATCH 15/38] Added Team Resource and added all Team's methods skeletons --- jira/client.py | 74 +++++++++++++++++++++++++++++++++++++++++++---- jira/resources.py | 17 +++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/jira/client.py b/jira/client.py index 64686cbec..a22c8b032 100644 --- a/jira/client.py +++ b/jira/client.py @@ -85,6 +85,7 @@ Sprint, Status, StatusCategory, + Team, User, Version, Votes, @@ -1311,6 +1312,55 @@ def update_filter( # Teams + def create_team( + self, + org_id: str, + description: str, + display_name: str, + team_type: str, + site_id: str = None, + ) -> 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: + url=f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" + if site_id is not None: + url+=f"?siteId={site_id}" + r=self._session.get(url) + 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, + ): + pass + + def update_team( + self, + org_id: str, + team_id: str, + description: str, + displayName: str, + ) -> Team: + pass + def team_members(self, team_id: str, org_id: str) -> list[str]: """Return list of account Ids in the team. Requires Jira 6.0 or will raise NotImplemented. @@ -1320,20 +1370,16 @@ def team_members(self, team_id: str, org_id: str) -> list[str]: """ url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" - headers = { - "Accept": "*/*", - "Content-Type": "application/json" - } payload = { "first": 50 } - r = self._session.get(url,headers=headers,data=json.dumps(payload)) + r = self._session.get(url,data=json.dumps(payload)) has_next_page = r["pageInfo"]["hasNextPage"] end_index = r["pageInfo"]["endCursor"] while has_next_page: payload["after"]=str(end_index) - r2 = self._session.get(url,headers=headers,data=json.dumps(payload)) + r2 = self._session.get(url,data=json.dumps(payload)) for user in r2["results"]: r["results"].append(user) end_index = r2["pageInfo"]["endCursor"] @@ -1344,6 +1390,22 @@ def team_members(self, team_id: str, org_id: str) -> list[str]: result.append(accounts.get("accountId")) return result + def add_team_members( + self, + org_id: str, + team_id: str, + members: list[str], + ): + pass + + def remove_team_members( + self, + org_id: str, + team_id: str, + members: list[str], + ): + pass + # Groups def group(self, id: str, expand: Any = None) -> Group: diff --git a/jira/resources.py b/jira/resources.py index 57ec31bbc..efdce3b06 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1233,6 +1233,22 @@ def __init__( self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) +class Team(Resource): + """A Jira team.""" + + def __init__( + self, + options: dict[str, str], + session: ResilientSession, + raw: dict[str, Any] = None, + ): + TEAM_API_BASE_URL = "{server}/gateway/api/public/teams/v1/" #rest/{rest_path}/{rest_api_version}/{path}" + + Resource.__init__(self, "org/{0}/teams/{1}", options, session, base_url=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.""" @@ -1521,6 +1537,7 @@ def dict2resource( # Agile specific resources r"sprints/[^/]+$": Sprint, r"views/[^/]+$": Board, + r"org\?(accountId)/teams\?(accountId).+$": Team, } From 224505b9cc954a55a4fdddd0f41c19b1c705e2fc Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 11:58:20 -0500 Subject: [PATCH 16/38] Added 'test_add_team' but without the correct org_id (don't know how to get it yet) --- tests/tests.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index fc101844a..84bc12cdb 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -555,6 +555,10 @@ def setUp(self): self.test_email = f"{self.test_username}@example.com" self.test_password = rndpassword() self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" + self.test_team_name= f"testTeamFor_{self.test_manager.project_a}" + self.org_id="" + self.test_team_type="OPEN" + self.test_team_description="test Description" def _skip_pycontribs_instance(self): pytest.skip( @@ -619,6 +623,27 @@ def test_add_and_remove_user(self): result = self.jira.delete_user(self.test_username) assert result, True + def test_add_team(self): + if self._should_skip_for_pycontribs_instance(): + self._skip_pycontribs_instance() + try: + self.jira.remove_team(self.test_team_name) + except JIRAError: + pass + + sleep(2) # avoid 500 errors + result_team = self.jira.create_team( + self.org_id, + self.test_team_description, + self.test_team_name, + self.test_team_type + ) + self.assertEqual( + self.test_team_name, + result_team.__getattr__("displayName"), + "Did not find expected group after trying to add" " it. Test Fails.", + ) + def test_add_group(self): if self._should_skip_for_pycontribs_instance(): self._skip_pycontribs_instance() From 4780ce970fd705a30dbed3e9431e083b3c891acd Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:14:24 -0500 Subject: [PATCH 17/38] removed sudo chmod in post_create.sh as it was ruining the git changelog --- .devcontainer/post_create.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh index fef566521..424884396 100755 --- a/.devcontainer/post_create.sh +++ b/.devcontainer/post_create.sh @@ -1,7 +1,6 @@ #!/bin/bash # This file is run from the .vscode folder WORKSPACE_FOLDER=/workspaces/jira -sudo chmod -R 777 . git config --global --add safe.directory /workspaces/jira # Start the Jira Server docker instance first so can be running while we initialise everything else From 7fdf0b848fb59e6a61ab79ba95ae31a3d074994a Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:15:30 -0500 Subject: [PATCH 18/38] lint --- jira/client.py | 82 +++++++++++++++++++++-------------------------- jira/resources.py | 9 ++++-- tests/tests.py | 12 +++---- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/jira/client.py b/jira/client.py index a22c8b032..237692cc2 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1313,51 +1313,46 @@ def update_filter( # Teams def create_team( - self, - org_id: str, - description: str, - display_name: str, - team_type: str, - site_id: str = None, + self, + org_id: str, + description: str, + display_name: str, + team_type: str, + site_id: str = None, ) -> Team: - url=f"gateway/api/public/teams/v1/org/{org_id}/teams/" + url = f"gateway/api/public/teams/v1/org/{org_id}/teams/" payload = { "description": description, "displayName": display_name, - "teamType": team_type + "teamType": team_type, } if site_id is not None: - payload["siteId"]=site_id - r=self._session.post(url,data=json.dumps(payload)) + 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) + 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: - url=f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" + def get_team(self, org_id: str, team_id: str, site_id: str = None) -> Team: + url = f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" if site_id is not None: - url+=f"?siteId={site_id}" - r=self._session.get(url) + url += f"?siteId={site_id}" + r = self._session.get(url) raw_team_json: dict[str, Any] = json_loads(r) - return Team(self._options,self._session,raw=raw_team_json) + return Team(self._options, self._session, raw=raw_team_json) def remove_team( - self, - org_id: str, - team_id: str, + self, + org_id: str, + team_id: str, ): pass def update_team( - self, - org_id: str, - team_id: str, - description: str, - displayName: str, + self, + org_id: str, + team_id: str, + description: str, + displayName: str, ) -> Team: pass @@ -1368,41 +1363,38 @@ def team_members(self, team_id: str, org_id: str) -> list[str]: team_id (str): Id of the team. org_id (str): Id of the org. """ - url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" - payload = { - "first": 50 - } - r = self._session.get(url,data=json.dumps(payload)) + payload = {"first": 50} + r = self._session.get(url, data=json.dumps(payload)) has_next_page = r["pageInfo"]["hasNextPage"] end_index = r["pageInfo"]["endCursor"] while has_next_page: - payload["after"]=str(end_index) - r2 = self._session.get(url,data=json.dumps(payload)) + payload["after"] = str(end_index) + r2 = self._session.get(url, data=json.dumps(payload)) for user in r2["results"]: r["results"].append(user) end_index = r2["pageInfo"]["endCursor"] has_next_page = r2["pageInfo"]["hasNextPage"] - + 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], + self, + org_id: str, + team_id: str, + members: list[str], ): pass def remove_team_members( - self, - org_id: str, - team_id: str, - members: list[str], + self, + org_id: str, + team_id: str, + members: list[str], ): pass diff --git a/jira/resources.py b/jira/resources.py index efdce3b06..20cda17af 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1233,6 +1233,7 @@ def __init__( self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + class Team(Resource): """A Jira team.""" @@ -1242,9 +1243,11 @@ def __init__( session: ResilientSession, raw: dict[str, Any] = None, ): - TEAM_API_BASE_URL = "{server}/gateway/api/public/teams/v1/" #rest/{rest_path}/{rest_api_version}/{path}" - - Resource.__init__(self, "org/{0}/teams/{1}", options, session, base_url=TEAM_API_BASE_URL) + TEAM_API_BASE_URL = "{server}/gateway/api/public/teams/v1/" # rest/{rest_path}/{rest_api_version}/{path}" + + Resource.__init__( + self, "org/{0}/teams/{1}", options, session, base_url=TEAM_API_BASE_URL + ) if raw: self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) diff --git a/tests/tests.py b/tests/tests.py index 84bc12cdb..a44406f3c 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -555,10 +555,10 @@ def setUp(self): self.test_email = f"{self.test_username}@example.com" self.test_password = rndpassword() self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" - self.test_team_name= f"testTeamFor_{self.test_manager.project_a}" - self.org_id="" - self.test_team_type="OPEN" - self.test_team_description="test Description" + self.test_team_name = f"testTeamFor_{self.test_manager.project_a}" + self.org_id = "" + self.test_team_type = "OPEN" + self.test_team_description = "test Description" def _skip_pycontribs_instance(self): pytest.skip( @@ -635,8 +635,8 @@ def test_add_team(self): result_team = self.jira.create_team( self.org_id, self.test_team_description, - self.test_team_name, - self.test_team_type + self.test_team_name, + self.test_team_type, ) self.assertEqual( self.test_team_name, From d740987c666fccd441703a75b5b9d952db4de0cc Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 18 Nov 2023 12:18:18 -0500 Subject: [PATCH 19/38] more lint --- jira/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jira/client.py b/jira/client.py index 237692cc2..0bdb65774 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1365,13 +1365,13 @@ def team_members(self, team_id: str, org_id: str) -> list[str]: """ url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" payload = {"first": 50} - r = self._session.get(url, data=json.dumps(payload)) + r = self._session.get(url, data=json.dumps(payload)).json() has_next_page = r["pageInfo"]["hasNextPage"] end_index = r["pageInfo"]["endCursor"] while has_next_page: - payload["after"] = str(end_index) - r2 = self._session.get(url, data=json.dumps(payload)) + payload["after"] = end_index + r2 = self._session.get(url, data=json.dumps(payload)).json() for user in r2["results"]: r["results"].append(user) end_index = r2["pageInfo"]["endCursor"] From d8a08d56fe0302f14dde647383032de50909c3e3 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Mon, 27 Nov 2023 05:46:37 -0500 Subject: [PATCH 20/38] Started adding Organization API too --- jira/client.py | 30 ++++++++++++++++++++++++++++++ jira/resources.py | 18 ++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/jira/client.py b/jira/client.py index 0bdb65774..20567eb91 100644 --- a/jira/client.py +++ b/jira/client.py @@ -85,6 +85,7 @@ Sprint, Status, StatusCategory, + Organization, Team, User, Version, @@ -1310,6 +1311,35 @@ def update_filter( raw_filter_json = json.loads(r.text) return Filter(self._options, self._session, raw=raw_filter_json) + # Organisations + + def create_org(self, org_name: str) -> Organization: + url = f"rest/servicedeskapi/organization" + payload = { + "name": org_name + } + r = self._session.post(url, data=json.dumps(payload)) + raw_org_json: dict[str, Any] = json_loads(r) + return Organization(self._options, self._session, raw=raw_org_json) + + def remove_org(self,org_id:str) -> bool: + url = f"rest/servicedeskapi/organization/{org_id}" + r = self._session.delete(url) + if r.status_code == 204: + return True + return False + + def get_org(self, org_id: str) -> Organization: + url = f"rest/servicedeskapi/organization/{org_id}" + r = self._session.get(url) + raw_org_json: dict[str, Any] = json_loads(r) + if r.status_code == 200: + return Organization(self._options, self._session, raw=raw_org_json) + return None + + def get_orgs(self, start, limit, account_id): + pass + # Teams def create_team( diff --git a/jira/resources.py b/jira/resources.py index 20cda17af..90fff8726 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -57,6 +57,8 @@ class AnyLike: "Resolution", "SecurityLevel", "Status", + "Organization", + "Team", "User", "Group", "CustomFieldOption", @@ -1233,6 +1235,22 @@ def __init__( self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) +class Organization(Resource): + """A JIRA Organization.""" + + def __init__( + self, + options: dict[str, str], + session: ResilientSession, + raw: dict[str, Any] = None, + ): + + Resource.__init__( + self, "organization/{0}", options, session, "{server}/rest/servicedeskapi/{path}" + ) + if raw: + self._parse_raw(raw) + self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) class Team(Resource): """A Jira team.""" From d2b50722948cbd0db98e5f3ed7ec9402b5c69cac Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Mon, 27 Nov 2023 05:46:48 -0500 Subject: [PATCH 21/38] Started adding tests --- tests/tests.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index a44406f3c..abd7bf8d9 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -556,7 +556,7 @@ def setUp(self): self.test_password = rndpassword() self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" self.test_team_name = f"testTeamFor_{self.test_manager.project_a}" - self.org_id = "" + self.test_org_name = "testOrgFor_{self.test_manager.project_a}" self.test_team_type = "OPEN" self.test_team_description = "test Description" @@ -623,10 +623,22 @@ def test_add_and_remove_user(self): result = self.jira.delete_user(self.test_username) assert result, True - def test_add_team(self): + def test_create_org(self): + result_org = self.jira.create_org(self.test_org_name) + self.assertEqual( + self.test_org_name, + result_org.__getattr__("name") + ) + + def test_remove_org(self): + pass + + def test_create_team(self): if self._should_skip_for_pycontribs_instance(): self._skip_pycontribs_instance() try: + new_org = self.jira.create_org(self.test_org_name) + self.jira.remove_team(self.test_team_name) except JIRAError: pass From 37f34d05095e0427dfe0d027b47d29ead663086c Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Tue, 28 Nov 2023 14:38:23 -0500 Subject: [PATCH 22/38] added all api routes for org. Started adding the tests for those org routes --- jira/client.py | 53 +++++++++++++++++++++++++++++++++++++++++++------- tests/tests.py | 51 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/jira/client.py b/jira/client.py index 20567eb91..60cc27282 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1314,7 +1314,7 @@ def update_filter( # Organisations def create_org(self, org_name: str) -> Organization: - url = f"rest/servicedeskapi/organization" + url = f"/rest/servicedeskapi/organization" payload = { "name": org_name } @@ -1323,22 +1323,61 @@ def create_org(self, org_name: str) -> Organization: return Organization(self._options, self._session, raw=raw_org_json) def remove_org(self,org_id:str) -> bool: - url = f"rest/servicedeskapi/organization/{org_id}" + url = f"/rest/servicedeskapi/organization/{org_id}" r = self._session.delete(url) if r.status_code == 204: return True return False - def get_org(self, org_id: str) -> Organization: - url = f"rest/servicedeskapi/organization/{org_id}" + def org(self, org_id: str) -> Organization: + url = f"/rest/servicedeskapi/organization/{org_id}" r = self._session.get(url) raw_org_json: dict[str, Any] = json_loads(r) if r.status_code == 200: return Organization(self._options, self._session, raw=raw_org_json) return None - def get_orgs(self, start, limit, account_id): - pass + def orgs(self, start=0, limit=50) -> ResultList[Organization]: + url = f"/rest/servicedeskapi/organization" + return self._fetch_pages( + Organization, + "values", + url, + start, + limit, + base=self.server_url + ) + + def org_users(self, org_id, start, limit) -> ResultList[User]: + url = f"/rest/servicedeskapi/organization/{org_id}/user" + return self._fetch_pages( + User, + None, + url, + start, + limit, + base=self.server_url + ) + + def add_users_to_org(self, org_id: str, users: list[str]) -> bool: + url=f"/rest/servicedeskapi/organization/{org_id}/user" + payload={ + "usernames": users + } + r= self._session.post(url, data=json.dumps(payload)) + if r.status_code == 204: + return True + return False + + def remove_users_from_org(self, org_id: str, users: list[str]) -> bool: + url=f"/rest/servicedeskapi/organization/{org_id}/user" + payload={ + "usernames": users + } + r= self._session.delete(url, data=json.dumps(payload)) + if r.status_code == 204: + return True + return False # Teams @@ -1387,7 +1426,7 @@ def update_team( pass def team_members(self, team_id: str, org_id: str) -> list[str]: - """Return list of account Ids in the team. Requires Jira 6.0 or will raise NotImplemented. + """Return list of account Ids in the team. Args: team_id (str): Id of the team. diff --git a/tests/tests.py b/tests/tests.py index abd7bf8d9..1f070f94e 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -624,6 +624,13 @@ def test_add_and_remove_user(self): assert result, True def test_create_org(self): + if self._should_skip_for_pycontribs_instance(): + self._skip_pycontribs_instance() + try: + self.jira.remove_org(self.test_org_name) + except JIRAError: + pass + result_org = self.jira.create_org(self.test_org_name) self.assertEqual( self.test_org_name, @@ -631,14 +638,52 @@ def test_create_org(self): ) def test_remove_org(self): - pass + if self._should_skip_for_pycontribs_instance(): + self._skip_pycontribs_instance() + try: + self.jira.create_org(self.test_org_name) + sleep(1) # avoid 400 + except JIRAError: + pass + result = self.jira.remove_org(self.test_org_name) + assert result, True + + def test_fetching_orgs(self): + try: + self.jira.create_org(self.test_org_name) + sleep(1) # avoid 400 + except JIRAError: + pass + all_orgs=self.jira.orgs() + assert len(all_orgs) != 0 + + def test_fetching_org(self): + self.jira.remove_org(self.test_org_name) + org = self.jira.create_org(self.test_org_name) + org_id= org.id + response_org = self.jira.org(org_id) + assert response_org.id == org_id + + def test_adding_and_fetching_users_to_org(self): + try: + self.jira.add_user( + self.test_username, self.test_email, password=self.test_password + ) + org = self.jira.create_org(self.test_org_name) + except JIRAError: + pass + assert True, self.jira.add_users_to_org(org.id,[self.test_username]) + users = self.jira.org_users(org.id) + is_test_user_in_org = False + for u in users: + if u.username == self.test_username: + is_test_user_in_org = True + assert is_test_user_in_org, True def test_create_team(self): if self._should_skip_for_pycontribs_instance(): self._skip_pycontribs_instance() try: - new_org = self.jira.create_org(self.test_org_name) - self.jira.remove_team(self.test_team_name) except JIRAError: pass From 67aa43936891a4b99ccb4352cebccc464ec0e741 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 9 Dec 2023 14:33:18 -0500 Subject: [PATCH 23/38] added _get_service_desk_url helping function to make sure all methods calling service desk do it from the same root api endpoint --- jira/client.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/jira/client.py b/jira/client.py index 60cc27282..6d4cbcdb7 100644 --- a/jira/client.py +++ b/jira/client.py @@ -1311,10 +1311,18 @@ def update_filter( raw_filter_json = json.loads(r.text) return Filter(self._options, self._session, raw=raw_filter_json) + def _get_service_desk_url(self) -> str: + """Returns the service desk root url. + + Returns: + str: service desk api url + """ + return f"{self.server_url}/rest/servicedeskapi" + # Organisations def create_org(self, org_name: str) -> Organization: - url = f"/rest/servicedeskapi/organization" + url = f"{self._get_service_desk_url()}/organization" payload = { "name": org_name } @@ -1323,14 +1331,14 @@ def create_org(self, org_name: str) -> Organization: return Organization(self._options, self._session, raw=raw_org_json) def remove_org(self,org_id:str) -> bool: - url = f"/rest/servicedeskapi/organization/{org_id}" + url = f"{self._get_service_desk_url()}/organization/{org_id}" r = self._session.delete(url) if r.status_code == 204: return True return False def org(self, org_id: str) -> Organization: - url = f"/rest/servicedeskapi/organization/{org_id}" + url = f"{self._get_service_desk_url()}/organization/{org_id}" r = self._session.get(url) raw_org_json: dict[str, Any] = json_loads(r) if r.status_code == 200: @@ -1338,7 +1346,7 @@ def org(self, org_id: str) -> Organization: return None def orgs(self, start=0, limit=50) -> ResultList[Organization]: - url = f"/rest/servicedeskapi/organization" + url = f"{self._get_service_desk_url()}/organization" return self._fetch_pages( Organization, "values", @@ -1349,7 +1357,7 @@ def orgs(self, start=0, limit=50) -> ResultList[Organization]: ) def org_users(self, org_id, start, limit) -> ResultList[User]: - url = f"/rest/servicedeskapi/organization/{org_id}/user" + url = f"{self._get_service_desk_url()}/organization/{org_id}/user" return self._fetch_pages( User, None, @@ -1360,7 +1368,7 @@ def org_users(self, org_id, start, limit) -> ResultList[User]: ) def add_users_to_org(self, org_id: str, users: list[str]) -> bool: - url=f"/rest/servicedeskapi/organization/{org_id}/user" + url=f"{self._get_service_desk_url()}/organization/{org_id}/user" payload={ "usernames": users } @@ -1370,7 +1378,7 @@ def add_users_to_org(self, org_id: str, users: list[str]) -> bool: return False def remove_users_from_org(self, org_id: str, users: list[str]) -> bool: - url=f"/rest/servicedeskapi/organization/{org_id}/user" + url=f"{self._get_service_desk_url()}/organization/{org_id}/user" payload={ "usernames": users } @@ -1780,7 +1788,7 @@ def supports_service_desk(self): Returns: bool """ - url = self.server_url + "/rest/servicedeskapi/info" + url = f"{self._get_service_desk_url()}/info" headers = {"X-ExperimentalApi": "opt-in"} try: r = self._session.get(url, headers=headers) @@ -1798,7 +1806,7 @@ def create_customer(self, email: str, displayName: str) -> Customer: Returns: Customer """ - url = self.server_url + "/rest/servicedeskapi/customer" + url = f"{self._get_service_desk_url()}/customer" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post( url, @@ -1818,7 +1826,7 @@ def service_desks(self) -> list[ServiceDesk]: Returns: List[ServiceDesk] """ - url = self.server_url + "/rest/servicedeskapi/servicedesk" + url = f"{self._get_service_desk_url()}/servicedesk" headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) projects = [ @@ -1879,7 +1887,7 @@ def create_customer_request( elif isinstance(p, str): data["requestTypeId"] = self.request_type_by_name(service_desk, p).id - url = self.server_url + "/rest/servicedeskapi/request" + url = f"{self._get_service_desk_url()}/request" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post(url, headers=headers, data=json.dumps(data)) @@ -2876,8 +2884,7 @@ def request_types(self, service_desk: ServiceDesk) -> list[RequestType]: if hasattr(service_desk, "id"): service_desk = service_desk.id url = ( - self.server_url - + f"/rest/servicedeskapi/servicedesk/{service_desk}/requesttype" + f"{self._get_service_desk_url()}/servicedesk/{service_desk}/requesttype" ) headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) From 09a08f7d81f1e02f4ed49471d61a0bb892962c2d Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sun, 7 Jan 2024 08:24:40 -0500 Subject: [PATCH 24/38] lint --- jira/client.py | 56 +++++++++++++++-------------------------------- jira/resources.py | 9 ++++++-- tests/tests.py | 17 ++++++-------- 3 files changed, 32 insertions(+), 50 deletions(-) mode change 100644 => 100755 jira/client.py diff --git a/jira/client.py b/jira/client.py old mode 100644 new mode 100755 index 6d4cbcdb7..7edf55b15 --- a/jira/client.py +++ b/jira/client.py @@ -71,6 +71,7 @@ IssueType, IssueTypeScheme, NotificationScheme, + Organization, PermissionScheme, Priority, PriorityScheme, @@ -85,7 +86,6 @@ Sprint, Status, StatusCategory, - Organization, Team, User, Version, @@ -1323,14 +1323,12 @@ def _get_service_desk_url(self) -> str: def create_org(self, org_name: str) -> Organization: url = f"{self._get_service_desk_url()}/organization" - payload = { - "name": org_name - } + payload = {"name": org_name} r = self._session.post(url, data=json.dumps(payload)) raw_org_json: dict[str, Any] = json_loads(r) return Organization(self._options, self._session, raw=raw_org_json) - - def remove_org(self,org_id:str) -> bool: + + def remove_org(self, org_id: str) -> bool: url = f"{self._get_service_desk_url()}/organization/{org_id}" r = self._session.delete(url) if r.status_code == 204: @@ -1344,45 +1342,29 @@ def org(self, org_id: str) -> Organization: if r.status_code == 200: return Organization(self._options, self._session, raw=raw_org_json) return None - + def orgs(self, start=0, limit=50) -> ResultList[Organization]: url = f"{self._get_service_desk_url()}/organization" return self._fetch_pages( - Organization, - "values", - url, - start, - limit, - base=self.server_url - ) - + Organization, "values", url, start, limit, base=self.server_url + ) + def org_users(self, org_id, start, limit) -> ResultList[User]: url = f"{self._get_service_desk_url()}/organization/{org_id}/user" - return self._fetch_pages( - User, - None, - url, - start, - limit, - base=self.server_url - ) + return self._fetch_pages(User, None, url, start, limit, base=self.server_url) def add_users_to_org(self, org_id: str, users: list[str]) -> bool: - url=f"{self._get_service_desk_url()}/organization/{org_id}/user" - payload={ - "usernames": users - } - r= self._session.post(url, data=json.dumps(payload)) + url = f"{self._get_service_desk_url()}/organization/{org_id}/user" + payload = {"usernames": users} + r = self._session.post(url, data=json.dumps(payload)) if r.status_code == 204: return True return False - + def remove_users_from_org(self, org_id: str, users: list[str]) -> bool: - url=f"{self._get_service_desk_url()}/organization/{org_id}/user" - payload={ - "usernames": users - } - r= self._session.delete(url, data=json.dumps(payload)) + url = f"{self._get_service_desk_url()}/organization/{org_id}/user" + payload = {"usernames": users} + r = self._session.delete(url, data=json.dumps(payload)) if r.status_code == 204: return True return False @@ -1434,7 +1416,7 @@ def update_team( pass def team_members(self, team_id: str, org_id: str) -> list[str]: - """Return list of account Ids in the team. + """Return list of account Ids in the team. Args: team_id (str): Id of the team. @@ -2883,9 +2865,7 @@ def request_types(self, service_desk: ServiceDesk) -> list[RequestType]: """ if hasattr(service_desk, "id"): service_desk = service_desk.id - url = ( - f"{self._get_service_desk_url()}/servicedesk/{service_desk}/requesttype" - ) + url = f"{self._get_service_desk_url()}/servicedesk/{service_desk}/requesttype" headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) request_types = [ diff --git a/jira/resources.py b/jira/resources.py index 90fff8726..55ce6bd41 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1235,6 +1235,7 @@ def __init__( self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + class Organization(Resource): """A JIRA Organization.""" @@ -1244,14 +1245,18 @@ def __init__( session: ResilientSession, raw: dict[str, Any] = None, ): - Resource.__init__( - self, "organization/{0}", options, session, "{server}/rest/servicedeskapi/{path}" + self, + "organization/{0}", + options, + session, + "{server}/rest/servicedeskapi/{path}", ) if raw: self._parse_raw(raw) self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + class Team(Resource): """A Jira team.""" diff --git a/tests/tests.py b/tests/tests.py index 1f070f94e..97552e763 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -630,12 +630,9 @@ def test_create_org(self): self.jira.remove_org(self.test_org_name) except JIRAError: pass - + result_org = self.jira.create_org(self.test_org_name) - self.assertEqual( - self.test_org_name, - result_org.__getattr__("name") - ) + self.assertEqual(self.test_org_name, result_org.__getattr__("name")) def test_remove_org(self): if self._should_skip_for_pycontribs_instance(): @@ -654,25 +651,25 @@ def test_fetching_orgs(self): sleep(1) # avoid 400 except JIRAError: pass - all_orgs=self.jira.orgs() + all_orgs = self.jira.orgs() assert len(all_orgs) != 0 def test_fetching_org(self): self.jira.remove_org(self.test_org_name) org = self.jira.create_org(self.test_org_name) - org_id= org.id + org_id = org.id response_org = self.jira.org(org_id) assert response_org.id == org_id def test_adding_and_fetching_users_to_org(self): try: self.jira.add_user( - self.test_username, self.test_email, password=self.test_password - ) + self.test_username, self.test_email, password=self.test_password + ) org = self.jira.create_org(self.test_org_name) except JIRAError: pass - assert True, self.jira.add_users_to_org(org.id,[self.test_username]) + assert True, self.jira.add_users_to_org(org.id, [self.test_username]) users = self.jira.org_users(org.id) is_test_user_in_org = False for u in users: From b3237f5e9d1987051a5ca42da965de492508061a Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sun, 7 Jan 2024 12:38:42 -0500 Subject: [PATCH 25/38] added docstring to all member methods --- jira/client.py | 117 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 7 deletions(-) diff --git a/jira/client.py b/jira/client.py index 7edf55b15..cbae5a118 100755 --- a/jira/client.py +++ b/jira/client.py @@ -1379,6 +1379,18 @@ def create_team( 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, @@ -1392,6 +1404,16 @@ def create_team( 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}" if site_id is not None: url += f"?siteId={site_id}" @@ -1404,7 +1426,20 @@ def remove_team( org_id: str, team_id: str, ): - pass + """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) + if r.status_code == 204: + return True + return False def update_team( self, @@ -1413,14 +1448,47 @@ def update_team( description: str, displayName: str, ) -> Team: - pass + """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 team_members(self, team_id: str, org_id: str) -> list[str]: - """Return list of account Ids in the team. + """Return the list of account Ids corresponding to the team members. Args: team_id (str): Id of the team. org_id (str): Id of the org. + + Returns: + list[str] """ url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" payload = {"first": 50} @@ -1446,16 +1514,51 @@ def add_team_members( org_id: str, team_id: str, members: list[str], - ): - pass + ) -> (list[str], list[str]): + """Adds a list of members (accountIds) to the team members. + + 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: + (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], - ): - pass + ) -> 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)) + if r.status_code == 204: + return True + return False # Groups From df5fbf705e8488d877c1cb4ee73e1c366f55f10e Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sun, 7 Jan 2024 12:44:34 -0500 Subject: [PATCH 26/38] removed all code paths about orgs both in client.py and tests.py --- jira/client.py | 58 -------------------------------------------------- tests/tests.py | 55 +---------------------------------------------- 2 files changed, 1 insertion(+), 112 deletions(-) diff --git a/jira/client.py b/jira/client.py index cbae5a118..2cf9213fd 100755 --- a/jira/client.py +++ b/jira/client.py @@ -1311,64 +1311,6 @@ def update_filter( raw_filter_json = json.loads(r.text) return Filter(self._options, self._session, raw=raw_filter_json) - def _get_service_desk_url(self) -> str: - """Returns the service desk root url. - - Returns: - str: service desk api url - """ - return f"{self.server_url}/rest/servicedeskapi" - - # Organisations - - def create_org(self, org_name: str) -> Organization: - url = f"{self._get_service_desk_url()}/organization" - payload = {"name": org_name} - r = self._session.post(url, data=json.dumps(payload)) - raw_org_json: dict[str, Any] = json_loads(r) - return Organization(self._options, self._session, raw=raw_org_json) - - def remove_org(self, org_id: str) -> bool: - url = f"{self._get_service_desk_url()}/organization/{org_id}" - r = self._session.delete(url) - if r.status_code == 204: - return True - return False - - def org(self, org_id: str) -> Organization: - url = f"{self._get_service_desk_url()}/organization/{org_id}" - r = self._session.get(url) - raw_org_json: dict[str, Any] = json_loads(r) - if r.status_code == 200: - return Organization(self._options, self._session, raw=raw_org_json) - return None - - def orgs(self, start=0, limit=50) -> ResultList[Organization]: - url = f"{self._get_service_desk_url()}/organization" - return self._fetch_pages( - Organization, "values", url, start, limit, base=self.server_url - ) - - def org_users(self, org_id, start, limit) -> ResultList[User]: - url = f"{self._get_service_desk_url()}/organization/{org_id}/user" - return self._fetch_pages(User, None, url, start, limit, base=self.server_url) - - def add_users_to_org(self, org_id: str, users: list[str]) -> bool: - url = f"{self._get_service_desk_url()}/organization/{org_id}/user" - payload = {"usernames": users} - r = self._session.post(url, data=json.dumps(payload)) - if r.status_code == 204: - return True - return False - - def remove_users_from_org(self, org_id: str, users: list[str]) -> bool: - url = f"{self._get_service_desk_url()}/organization/{org_id}/user" - payload = {"usernames": users} - r = self._session.delete(url, data=json.dumps(payload)) - if r.status_code == 204: - return True - return False - # Teams def create_team( diff --git a/tests/tests.py b/tests/tests.py index 97552e763..2cd4572c7 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -556,8 +556,8 @@ def setUp(self): self.test_password = rndpassword() self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" self.test_team_name = f"testTeamFor_{self.test_manager.project_a}" - self.test_org_name = "testOrgFor_{self.test_manager.project_a}" self.test_team_type = "OPEN" + self.org_id= os.environ["CI_JIRA_ORG_ID"] self.test_team_description = "test Description" def _skip_pycontribs_instance(self): @@ -623,59 +623,6 @@ def test_add_and_remove_user(self): result = self.jira.delete_user(self.test_username) assert result, True - def test_create_org(self): - if self._should_skip_for_pycontribs_instance(): - self._skip_pycontribs_instance() - try: - self.jira.remove_org(self.test_org_name) - except JIRAError: - pass - - result_org = self.jira.create_org(self.test_org_name) - self.assertEqual(self.test_org_name, result_org.__getattr__("name")) - - def test_remove_org(self): - if self._should_skip_for_pycontribs_instance(): - self._skip_pycontribs_instance() - try: - self.jira.create_org(self.test_org_name) - sleep(1) # avoid 400 - except JIRAError: - pass - result = self.jira.remove_org(self.test_org_name) - assert result, True - - def test_fetching_orgs(self): - try: - self.jira.create_org(self.test_org_name) - sleep(1) # avoid 400 - except JIRAError: - pass - all_orgs = self.jira.orgs() - assert len(all_orgs) != 0 - - def test_fetching_org(self): - self.jira.remove_org(self.test_org_name) - org = self.jira.create_org(self.test_org_name) - org_id = org.id - response_org = self.jira.org(org_id) - assert response_org.id == org_id - - def test_adding_and_fetching_users_to_org(self): - try: - self.jira.add_user( - self.test_username, self.test_email, password=self.test_password - ) - org = self.jira.create_org(self.test_org_name) - except JIRAError: - pass - assert True, self.jira.add_users_to_org(org.id, [self.test_username]) - users = self.jira.org_users(org.id) - is_test_user_in_org = False - for u in users: - if u.username == self.test_username: - is_test_user_in_org = True - assert is_test_user_in_org, True def test_create_team(self): if self._should_skip_for_pycontribs_instance(): From cf3c2762d1f910e51b107da99e034eca933b0c96 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sun, 7 Jan 2024 12:53:40 -0500 Subject: [PATCH 27/38] removed last code path for orgs --- jira/client.py | 10 +++++----- jira/resources.py | 21 --------------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/jira/client.py b/jira/client.py index 2cf9213fd..9574084c0 100755 --- a/jira/client.py +++ b/jira/client.py @@ -1815,7 +1815,7 @@ def supports_service_desk(self): Returns: bool """ - url = f"{self._get_service_desk_url()}/info" + url = self.server_url + "/rest/servicedeskapi/info" headers = {"X-ExperimentalApi": "opt-in"} try: r = self._session.get(url, headers=headers) @@ -1833,7 +1833,7 @@ def create_customer(self, email: str, displayName: str) -> Customer: Returns: Customer """ - url = f"{self._get_service_desk_url()}/customer" + url = self.server_url + "/rest/servicedeskapi/customer" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post( url, @@ -1853,7 +1853,7 @@ def service_desks(self) -> list[ServiceDesk]: Returns: List[ServiceDesk] """ - url = f"{self._get_service_desk_url()}/servicedesk" + url = self.server_url + "/rest/servicedeskapi/servicedesk" headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) projects = [ @@ -1914,7 +1914,7 @@ def create_customer_request( elif isinstance(p, str): data["requestTypeId"] = self.request_type_by_name(service_desk, p).id - url = f"{self._get_service_desk_url()}/request" + url = self.server_url + "/rest/servicedeskapi/request" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post(url, headers=headers, data=json.dumps(data)) @@ -2910,7 +2910,7 @@ def request_types(self, service_desk: ServiceDesk) -> list[RequestType]: """ if hasattr(service_desk, "id"): service_desk = service_desk.id - url = f"{self._get_service_desk_url()}/servicedesk/{service_desk}/requesttype" + url = self.server_url + "/rest/servicedeskapi/servicedesk/{service_desk}/requesttype" headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) request_types = [ diff --git a/jira/resources.py b/jira/resources.py index 55ce6bd41..5208345a0 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1236,27 +1236,6 @@ def __init__( self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) -class Organization(Resource): - """A JIRA Organization.""" - - def __init__( - self, - options: dict[str, str], - session: ResilientSession, - raw: dict[str, Any] = None, - ): - Resource.__init__( - self, - "organization/{0}", - options, - session, - "{server}/rest/servicedeskapi/{path}", - ) - if raw: - self._parse_raw(raw) - self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) - - class Team(Resource): """A Jira team.""" From 1c3e9651b180c28aa72838902eeacf33a6a6cbd5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jan 2024 17:54:06 +0000 Subject: [PATCH 28/38] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- jira/client.py | 39 ++++++++++++++++----------------------- tests/tests.py | 3 +-- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/jira/client.py b/jira/client.py index 9574084c0..fa6813d45 100755 --- a/jira/client.py +++ b/jira/client.py @@ -71,7 +71,6 @@ IssueType, IssueTypeScheme, NotificationScheme, - Organization, PermissionScheme, Priority, PriorityScheme, @@ -1401,27 +1400,20 @@ def update_team( """ url = f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" - headers = { - "Accept": "application/json", - "Content-Type": "application/json" - } + headers = {"Accept": "application/json", "Content-Type": "application/json"} - payload = {} + payload = {} if description != "": - payload["description"]=description + payload["description"] = description if displayName != "": payload["displayName"] = displayName response = self._session.request( - "PATCH", - url, - data=json.dumps(payload), - headers=headers - ) + "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 team_members(self, team_id: str, org_id: str) -> list[str]: """Return the list of account Ids corresponding to the team members. @@ -1469,12 +1461,10 @@ def add_team_members( """ 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 - } + 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"] + response_json = r.json() + return response_json["members"], response_json["errors"] def remove_team_members( self, @@ -1492,11 +1482,11 @@ def remove_team_members( Returns: bool """ - url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members/remove" + 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 - } + payload = {"members": payload_members_list} r = self._session.post(url, data=json.dumps(payload)) if r.status_code == 204: return True @@ -2910,7 +2900,10 @@ def request_types(self, service_desk: ServiceDesk) -> list[RequestType]: """ if hasattr(service_desk, "id"): service_desk = service_desk.id - url = self.server_url + "/rest/servicedeskapi/servicedesk/{service_desk}/requesttype" + url = ( + self.server_url + + "/rest/servicedeskapi/servicedesk/{service_desk}/requesttype" + ) headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) request_types = [ diff --git a/tests/tests.py b/tests/tests.py index 2cd4572c7..6b0299fcb 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -557,7 +557,7 @@ def setUp(self): self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" 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"] + self.org_id = os.environ["CI_JIRA_ORG_ID"] self.test_team_description = "test Description" def _skip_pycontribs_instance(self): @@ -623,7 +623,6 @@ def test_add_and_remove_user(self): result = self.jira.delete_user(self.test_username) assert result, True - def test_create_team(self): if self._should_skip_for_pycontribs_instance(): self._skip_pycontribs_instance() From 7c57de8e69a358c1fd88e6fb41872d578e9a19ea Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sun, 7 Jan 2024 12:55:05 -0500 Subject: [PATCH 29/38] removed last code path for orgs --- jira/client.py | 1 - jira/resources.py | 1 - 2 files changed, 2 deletions(-) diff --git a/jira/client.py b/jira/client.py index 9574084c0..348c83fd0 100755 --- a/jira/client.py +++ b/jira/client.py @@ -71,7 +71,6 @@ IssueType, IssueTypeScheme, NotificationScheme, - Organization, PermissionScheme, Priority, PriorityScheme, diff --git a/jira/resources.py b/jira/resources.py index 5208345a0..8702fafd1 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -57,7 +57,6 @@ class AnyLike: "Resolution", "SecurityLevel", "Status", - "Organization", "Team", "User", "Group", From f879605de94153ebaf39b0b3bc859f74da59fb6a Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sun, 7 Jan 2024 12:58:35 -0500 Subject: [PATCH 30/38] mypy typing --- jira/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jira/client.py b/jira/client.py index fa6813d45..ada62fcec 100755 --- a/jira/client.py +++ b/jira/client.py @@ -1448,7 +1448,7 @@ def add_team_members( org_id: str, team_id: str, members: list[str], - ) -> (list[str], list[str]): + ) -> tuple[list[str], list[str]]: """Adds a list of members (accountIds) to the team members. Args: From 0fe0f3ca9c8f0773d93c5ee92279d0173f84a63a Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:37:20 -0500 Subject: [PATCH 31/38] using siteId in params instead of passing it in url --- jira/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jira/client.py b/jira/client.py index ada62fcec..4fde45019 100755 --- a/jira/client.py +++ b/jira/client.py @@ -1356,9 +1356,10 @@ def get_team(self, org_id: str, team_id: str, site_id: str = None) -> Team: Team """ url = f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" + params = {} if site_id is not None: - url += f"?siteId={site_id}" - r = self._session.get(url) + 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) From d4880e409861b8ac34331f3bf0f186f4090ccab8 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:24:56 -0500 Subject: [PATCH 32/38] minor refactoring to client.py teams api --- jira/client.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/jira/client.py b/jira/client.py index 4fde45019..d9637f728 100755 --- a/jira/client.py +++ b/jira/client.py @@ -1379,9 +1379,7 @@ def remove_team( """ url = f"gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}" r = self._session.delete(url) - if r.status_code == 204: - return True - return False + return r.ok def update_team( self, @@ -1415,12 +1413,16 @@ def update_team( raw_team_json: dict[str, Any] = json_loads(response) return Team(self._options, self._session, raw=raw_team_json) - def team_members(self, team_id: str, org_id: str) -> list[str]: + def team_members( + self, + org_id: str, + team_id: str, + ) -> list[str]: """Return the list of account Ids corresponding to the team members. Args: - team_id (str): Id of the team. org_id (str): Id of the org. + team_id (str): Id of the team. Returns: list[str] @@ -1453,8 +1455,8 @@ def add_team_members( """Adds a list of members (accountIds) to the team members. Args: - team_id (str): Id of the team. org_id (str): Id of the org. + team_id (str): Id of the team. members (list[str]): Account Ids of the new members. Returns: @@ -1489,9 +1491,7 @@ def remove_team_members( payload_members_list = [{"accountId": accountId} for accountId in members] payload = {"members": payload_members_list} r = self._session.post(url, data=json.dumps(payload)) - if r.status_code == 204: - return True - return False + return r.ok # Groups From 39a1ced4e806480b9890c563010b3b502bcbeec5 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:25:26 -0500 Subject: [PATCH 33/38] moved teams API tests into tests/resources/test_teams.py and improved tests --- tests/resources/test_teams.py | 80 +++++++++++++++++++++++++++++++++++ tests/tests.py | 25 ----------- 2 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 tests/resources/test_teams.py diff --git a/tests/resources/test_teams.py b/tests/resources/test_teams.py new file mode 100644 index 000000000..234289dc3 --- /dev/null +++ b/tests/resources/test_teams.py @@ -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"] + 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) diff --git a/tests/tests.py b/tests/tests.py index 6b0299fcb..fc101844a 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -555,10 +555,6 @@ def setUp(self): self.test_email = f"{self.test_username}@example.com" self.test_password = rndpassword() self.test_groupname = f"testGroupFor_{self.test_manager.project_a}" - 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"] - self.test_team_description = "test Description" def _skip_pycontribs_instance(self): pytest.skip( @@ -623,27 +619,6 @@ def test_add_and_remove_user(self): result = self.jira.delete_user(self.test_username) assert result, True - def test_create_team(self): - if self._should_skip_for_pycontribs_instance(): - self._skip_pycontribs_instance() - try: - self.jira.remove_team(self.test_team_name) - except JIRAError: - pass - - sleep(2) # avoid 500 errors - result_team = self.jira.create_team( - self.org_id, - self.test_team_description, - self.test_team_name, - self.test_team_type, - ) - self.assertEqual( - self.test_team_name, - result_team.__getattr__("displayName"), - "Did not find expected group after trying to add" " it. Test Fails.", - ) - def test_add_group(self): if self._should_skip_for_pycontribs_instance(): self._skip_pycontribs_instance() From 33ecea33963ae7dcf374f07c1e0731c86e72856e Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:38:14 -0500 Subject: [PATCH 34/38] created function to retrieve paginated results --- jira/client.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/jira/client.py b/jira/client.py index d9637f728..dbfb7782b 100755 --- a/jira/client.py +++ b/jira/client.py @@ -1413,6 +1413,20 @@ def update_team( 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, @@ -1429,18 +1443,7 @@ def team_members( """ url = f"/gateway/api/public/teams/v1/org/{org_id}/teams/{team_id}/members" payload = {"first": 50} - r = self._session.get(url, data=json.dumps(payload)).json() - has_next_page = r["pageInfo"]["hasNextPage"] - end_index = r["pageInfo"]["endCursor"] - - while has_next_page: - payload["after"] = end_index - r2 = self._session.get(url, data=json.dumps(payload)).json() - for user in r2["results"]: - r["results"].append(user) - end_index = r2["pageInfo"]["endCursor"] - has_next_page = r2["pageInfo"]["hasNextPage"] - + r = self._fetch_paginated(url, payload) result = [] for accounts in r["results"]: result.append(accounts.get("accountId")) From 37b33d1eaba174e5884bef60e5a4dcf9addd60ee Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:46:11 -0500 Subject: [PATCH 35/38] moved TEAM_API_BASE_URL outside of init as in AgileResource --- jira/resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jira/resources.py b/jira/resources.py index 8702fafd1..5f41043f5 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -1238,16 +1238,16 @@ def __init__( 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, ): - TEAM_API_BASE_URL = "{server}/gateway/api/public/teams/v1/" # rest/{rest_path}/{rest_api_version}/{path}" - Resource.__init__( - self, "org/{0}/teams/{1}", options, session, base_url=TEAM_API_BASE_URL + self, "org/{0}/teams/{1}", options, session, base_url=self.TEAM_API_BASE_URL ) if raw: self._parse_raw(raw) From 30a994006e29953d3603252510ccbde164e352bd Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 20 Jan 2024 08:31:53 -0500 Subject: [PATCH 36/38] Added organisation API, Organisation resource and refactored client.py to use _get_service_desk_url --- jira/client.py | 65 +++++++++++++++++++++++++++++++++++++++++------ jira/resources.py | 22 ++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/jira/client.py b/jira/client.py index dbfb7782b..02368413c 100755 --- a/jira/client.py +++ b/jira/client.py @@ -71,6 +71,7 @@ IssueType, IssueTypeScheme, NotificationScheme, + Organization, PermissionScheme, Priority, PriorityScheme, @@ -1310,6 +1311,57 @@ def update_filter( raw_filter_json = json.loads(r.text) return Filter(self._options, self._session, raw=raw_filter_json) + # Organisations + def _get_service_desk_url(self) -> str: + """Returns the service desk root url. + + Returns: + str: service desk api url + """ + return f"{self.server_url}/rest/servicedeskapi" + + def create_org(self, org_name: str) -> Organization: + url = f"{self._get_service_desk_url()}/organization" + payload = {"name": org_name} + r = self._session.post(url, data=json.dumps(payload)) + raw_org_json: dict[str, Any] = json_loads(r) + return Organization(self._options, self._session, raw=raw_org_json) + + def remove_org(self, org_id: str) -> bool: + url = f"{self._get_service_desk_url()}/organization/{org_id}" + r = self._session.delete(url) + return r.ok + + def org(self, org_id: str) -> Organization: + url = f"{self._get_service_desk_url()}/organization/{org_id}" + r = self._session.get(url) + raw_org_json: dict[str, Any] = json_loads(r) + if r.status_code == 200: + return Organization(self._options, self._session, raw=raw_org_json) + return None + + def orgs(self, start=0, limit=50) -> ResultList[Organization]: + url = f"{self._get_service_desk_url()}/organization" + return self._fetch_pages( + Organization, "values", url, start, limit, base=self.server_url + ) + + def org_users(self, org_id, start, limit) -> ResultList[User]: + url = f"{self._get_service_desk_url()}/organization/{org_id}/user" + return self._fetch_pages(User, None, url, start, limit, base=self.server_url) + + def add_users_to_org(self, org_id: str, users: list[str]) -> bool: + url = f"{self._get_service_desk_url()}/organization/{org_id}/user" + payload = {"usernames": users} + r = self._session.post(url, data=json.dumps(payload)) + return r.ok + + def remove_users_from_org(self, org_id: str, users: list[str]) -> bool: + url = f"{self._get_service_desk_url()}/organization/{org_id}/user" + payload = {"usernames": users} + r = self._session.delete(url, data=json.dumps(payload)) + return r.ok + # Teams def create_team( @@ -1809,7 +1861,7 @@ def supports_service_desk(self): Returns: bool """ - url = self.server_url + "/rest/servicedeskapi/info" + url = f"{self._get_service_desk_url()}/info" headers = {"X-ExperimentalApi": "opt-in"} try: r = self._session.get(url, headers=headers) @@ -1827,7 +1879,7 @@ def create_customer(self, email: str, displayName: str) -> Customer: Returns: Customer """ - url = self.server_url + "/rest/servicedeskapi/customer" + url = f"{self._get_service_desk_url()}/customer" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post( url, @@ -1847,7 +1899,7 @@ def service_desks(self) -> list[ServiceDesk]: Returns: List[ServiceDesk] """ - url = self.server_url + "/rest/servicedeskapi/servicedesk" + url = f"{self._get_service_desk_url()}/servicedesk" headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) projects = [ @@ -1908,7 +1960,7 @@ def create_customer_request( elif isinstance(p, str): data["requestTypeId"] = self.request_type_by_name(service_desk, p).id - url = self.server_url + "/rest/servicedeskapi/request" + url = f"{self._get_service_desk_url()}/request" headers = {"X-ExperimentalApi": "opt-in"} r = self._session.post(url, headers=headers, data=json.dumps(data)) @@ -2904,10 +2956,7 @@ def request_types(self, service_desk: ServiceDesk) -> list[RequestType]: """ if hasattr(service_desk, "id"): service_desk = service_desk.id - url = ( - self.server_url - + "/rest/servicedeskapi/servicedesk/{service_desk}/requesttype" - ) + url = f"{self._get_service_desk_url()}/servicedesk/{service_desk}/requesttype" headers = {"X-ExperimentalApi": "opt-in"} r_json = json_loads(self._session.get(url, headers=headers)) request_types = [ diff --git a/jira/resources.py b/jira/resources.py index 5f41043f5..6dfee22fc 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -57,6 +57,7 @@ class AnyLike: "Resolution", "SecurityLevel", "Status", + "Organization", "Team", "User", "Group", @@ -1235,6 +1236,27 @@ def __init__( self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) +class Organization(Resource): + """A JIRA Organization.""" + + def __init__( + self, + options: dict[str, str], + session: ResilientSession, + raw: dict[str, Any] = None, + ): + Resource.__init__( + self, + "organization/{0}", + options, + session, + "{server}/rest/servicedeskapi/{path}", + ) + if raw: + self._parse_raw(raw) + self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw) + + class Team(Resource): """A Jira team.""" From 6817ee7771c63a4ef4aab8f2c5da396fa7eac0cb Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:19:33 -0500 Subject: [PATCH 37/38] added test for organisation API --- jira/client.py | 2 +- tests/resources/test_organisation.py | 70 ++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/resources/test_organisation.py diff --git a/jira/client.py b/jira/client.py index 02368413c..421c90ff8 100755 --- a/jira/client.py +++ b/jira/client.py @@ -1346,7 +1346,7 @@ def orgs(self, start=0, limit=50) -> ResultList[Organization]: Organization, "values", url, start, limit, base=self.server_url ) - def org_users(self, org_id, start, limit) -> ResultList[User]: + def org_users(self, org_id, start=0, limit=50) -> ResultList[User]: url = f"{self._get_service_desk_url()}/organization/{org_id}/user" return self._fetch_pages(User, None, url, start, limit, base=self.server_url) diff --git a/tests/resources/test_organisation.py b/tests/resources/test_organisation.py new file mode 100644 index 000000000..fe31855f8 --- /dev/null +++ b/tests/resources/test_organisation.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +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_org_name = "testOrg" + + @contextmanager + def make_org(self): + try: + new_org = self.jira.create_org(self.test_org_name) + yield new_org + finally: + new_org.delete() + + def test_org_creation(self): + with self.make_org() as test_org: + self.assertEqual(self.test_org_name, test_org["name"]) + + def test_org_deletion(self): + with self.make_org() as test_org: + ok = self.jira.remove_org(test_org.id) + self.assertTrue(ok) + + def test_fetch_one_org(self): + with self.make_org() as test_org: + found_org = self.jira.org(test_org.id) + self.assertEqual(test_org["name"], found_org["name"]) + + def test_fetch_multiple_orgs(self): + expected_org_was_found = False + with self.make_org() as test_org: + found_orgs = self.jira.orgs() + self.assertGreater(len(found_orgs), 0) + for org in found_orgs: + if org["name"] == test_org["name"]: + expected_org_was_found = True + self.assertTrue(expected_org_was_found) + + def test_add_users_to_org(self): + users_to_add = [self.user_admin.id] + with self.make_org() as test_org: + ok = self.jira.add_users_to_org(test_org.id, users_to_add) + self.assertTrue(ok) + + def test_get_users_in_org(self): + users_to_add = [self.user_admin.id] + with self.make_org() as test_org: + ok = self.jira.add_users_to_org(test_org.id, users_to_add) + self.assertTrue(ok) + found_users = self.jira.org_users(test_org.id) + self.assertIn(self.user_admin.id, found_users) + + def test_remove_user_from_org(self): + users_to_add = [self.user_admin.id] + with self.make_org() as test_org: + ok = self.jira.add_users_to_org(test_org.id, users_to_add) + self.assertTrue(ok) + found_users = self.jira.org_users(test_org.id) + self.assertIn(self.user_admin.id, found_users) + removal_ok = self.jira.remove_users_from_org(test_org.id, users_to_add) + self.assertTrue(removal_ok) + found_users_after_removal = self.jira.org_users(test_org.id) + self.assertNotIn(self.user_admin.id, found_users_after_removal) From d1f7b7d131a47f1f705e36040ae2ba09a2d5bf37 Mon Sep 17 00:00:00 2001 From: Maxim-Durand <72691393+Maxim-Durand@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:40:44 -0500 Subject: [PATCH 38/38] updated teams API tests to use the Org API to create a dedicated testing Org --- tests/resources/test_teams.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/resources/test_teams.py b/tests/resources/test_teams.py index 234289dc3..8bacbbdef 100644 --- a/tests/resources/test_teams.py +++ b/tests/resources/test_teams.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os from contextlib import contextmanager from tests.conftest import JiraTestCase, allow_on_cloud @@ -8,15 +7,21 @@ @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"] - self.test_team_description = "test Description" + @classmethod + def setUpClass(cls): + JiraTestCase.setUp(cls) + cls.test_team_name = f"testTeamFor_{cls.test_manager.project_a}" + cls.test_team_type = "OPEN" + cls.org = cls.jira.create_org("TestOrgUsedByTeamsAPI") + cls.org_id = cls.org.id + cls.test_team_description = "test Description" + + @classmethod + def tearDownClass(cls): + cls.org.delete() @contextmanager - def make_team(self, **kwargs): + def make_team(self): try: new_team = self.jira.create_team( self.org_id, @@ -24,9 +29,6 @@ def make_team(self, **kwargs): self.test_team_name, self.test_team_type, ) - - if len(kwargs): - raise ValueError("Incorrect kwarg used !") yield new_team finally: new_team.delete()