Skip to content

Commit

Permalink
chore: move team related calls to team client, remove automatically a…
Browse files Browse the repository at this point in the history
…dded maintainer after creation of a team
  • Loading branch information
netomi committed Jan 7, 2025
1 parent 9846636 commit f024659
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 133 deletions.
4 changes: 1 addition & 3 deletions otterdog/models/github_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,9 +717,7 @@ async def _load_repos_from_provider(
if repo_filter is not None:
repo_names = fnmatch.filter(repo_names, repo_filter)

teams = {
str(team["id"]): f"{github_id}/{team['slug']}" for team in await provider.rest_api.org.get_teams(github_id)
}
teams = {str(team["id"]): f"{github_id}/{team['slug']}" for team in await provider.get_org_teams(github_id)}

app_installations = {
str(installation["app_id"]): installation["app_slug"]
Expand Down
2 changes: 1 addition & 1 deletion otterdog/models/ruleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ def extract_actor_and_bypass_mode(encoded_data: str) -> tuple[str, str]:
elif actor.startswith("@"):
team, bypass_mode = extract_actor_and_bypass_mode(actor[1:])
actor_type = "Team"
actor_id = (await provider.rest_api.org.get_team_ids(team))[0]
actor_id = (await provider.rest_api.team.get_team_ids(team))[0]
else:
app, bypass_mode = extract_actor_and_bypass_mode(actor)
actor_type = "Integration"
Expand Down
14 changes: 7 additions & 7 deletions otterdog/providers/github/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# *******************************************************************************
# Copyright (c) 2023-2024 Eclipse Foundation and others.
# Copyright (c) 2023-2025 Eclipse Foundation and others.
# This program and the accompanying materials are made available
# under the terms of the Eclipse Public License 2.0
# which is available at http://www.eclipse.org/legal/epl-v20.html
Expand Down Expand Up @@ -161,19 +161,19 @@ async def delete_org_custom_role(self, org_id: str, role_id: int, role_name: str
await self.rest_api.org.delete_custom_role(org_id, role_id, role_name)

async def get_org_teams(self, org_id: str) -> list[dict[str, Any]]:
return await self.rest_api.org.get_teams(org_id)
return await self.rest_api.team.get_teams(org_id)

async def get_org_team_members(self, org_id: str, team_slug: str) -> list[dict[str, Any]]:
return await self.rest_api.org.get_team_members(org_id, team_slug)
return await self.rest_api.team.get_team_members(org_id, team_slug)

async def add_org_team(self, org_id: str, team_name: str, data: dict[str, str]) -> None:
return await self.rest_api.org.add_team(org_id, team_name, data)
await self.rest_api.team.add_team(org_id, team_name, data)

async def update_org_team(self, org_id: str, team_slug: str, data: dict[str, str]) -> None:
return await self.rest_api.org.update_team(org_id, team_slug, data)
await self.rest_api.team.update_team(org_id, team_slug, data)

async def delete_org_team(self, org_id: str, team_slug: str) -> None:
return await self.rest_api.org.delete_team(org_id, team_slug)
await self.rest_api.team.delete_team(org_id, team_slug)

async def get_org_custom_properties(self, org_id: str) -> list[dict[str, Any]]:
return await self.rest_api.org.get_custom_properties(org_id)
Expand Down Expand Up @@ -441,7 +441,7 @@ async def get_actor_ids_with_type(self, actor_names: list[str]) -> list[tuple[st
# - user-names are not allowed to contain a /
if "/" in actor:
try:
result.append(("Team", await self.rest_api.org.get_team_ids(actor[1:])))
result.append(("Team", await self.rest_api.team.get_team_ids(actor[1:])))
except RuntimeError:
_logger.warning(f"team '{actor[1:]}' does not exist, skipping")
else:
Expand Down
116 changes: 0 additions & 116 deletions otterdog/providers/github/rest/org_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
# *******************************************************************************

import json
import re
from typing import Any

from otterdog.providers.github.exception import GitHubException
Expand Down Expand Up @@ -473,121 +472,6 @@ async def get_public_key(self, org_id: str) -> tuple[str, str]:
except GitHubException as ex:
raise RuntimeError(f"failed retrieving org public key:\n{ex}") from ex

async def get_team_ids(self, combined_slug: str) -> tuple[int, str]:
_logger.debug("retrieving team ids for slug '%s'", combined_slug)
org_id, team_slug = re.split("/", combined_slug)

try:
response = await self.requester.request_json("GET", f"/orgs/{org_id}/teams/{team_slug}")
return response["id"], response["node_id"]
except GitHubException as ex:
raise RuntimeError(f"failed retrieving team node id:\n{ex}") from ex

async def get_teams(self, org_id: str) -> list[dict[str, Any]]:
_logger.debug("retrieving teams for org '%s'", org_id)

try:
return await self.requester.request_json("GET", f"/orgs/{org_id}/teams")
except GitHubException as ex:
raise RuntimeError(f"failed retrieving teams for org '{org_id}':\n{ex}") from ex

async def get_team_members(self, org_id: str, team_slug: str) -> list[dict[str, Any]]:
_logger.debug("retrieving team members for team '%s/%s'", org_id, team_slug)

try:
return await self.requester.request_paged_json("GET", f"/orgs/{org_id}/teams/{team_slug}/members")
except GitHubException as ex:
raise RuntimeError(f"failed retrieving team members for team '{org_id}/{team_slug}':\n{ex}") from ex

async def add_team(self, org_id: str, team_name: str, data: dict[str, str]) -> None:
_logger.debug("adding team '%s' for org '%s'", team_name, org_id)

status, body = await self.requester.request_raw("POST", f"/orgs/{org_id}/teams", json.dumps(data))

if status != 201:
raise RuntimeError(f"failed to add team '{team_name}': {body}")

if "members" in data:
team_data = json.loads(body)
team_slug = team_data["slug"]
members = data["members"]
for user in members:
await self.add_member_to_team(org_id, team_slug, user)

_logger.debug("added team '%s'", team_name)

async def update_team(self, org_id: str, team_slug: str, team: dict[str, Any]) -> None:
_logger.debug("updating team '%s' for org '%s'", team_slug, org_id)

try:
await self.requester.request_json("PATCH", f"/orgs/{org_id}/teams/{team_slug}", team)

if "members" in team:
await self.update_team_members(org_id, team_slug, team["members"])

_logger.debug("updated team '%s'", team_slug)
except GitHubException as ex:
raise RuntimeError(f"failed to update team '{team_slug}':\n{ex}") from ex

async def update_team_members(self, org_id: str, team_slug: str, members: list[str]) -> None:
_logger.debug("updating team members for team '%s' in org '%s'", team_slug, org_id)

current_members = {x["login"] for x in await self.get_team_members(org_id, team_slug)}

# first, add all users that are not members yet.
for member in members:
if member in current_members:
current_members.remove(member)
else:
await self.add_member_to_team(org_id, team_slug, member)

# second, remove the current members that are remaining.
for member in current_members:
await self.remove_member_from_team(org_id, team_slug, member)

async def add_member_to_team(self, org_id: str, team_slug: str, user: str) -> None:
_logger.debug("adding user with id '%s' to team '%s' in org '%s'", user, team_slug, org_id)

status, body = await self.requester.request_raw("PUT", f"/orgs/{org_id}/teams/{team_slug}/memberships/{user}")

if status == 200:
_logger.debug("added user '%s' to team '%s' for org '%s'", user, team_slug, org_id)
else:
raise RuntimeError(
f"failed adding user '{user}' to team '{team_slug}' in org '{org_id}'" f"\n{status}: {body}"
)

async def remove_member_from_team(self, org_id: str, team_slug: str, user: str) -> None:
_logger.debug("removing user '%s' from team '%s' in org '%s'", user, team_slug, org_id)

status, body = await self.requester.request_raw(
"DELETE", f"/orgs/{org_id}/teams/{team_slug}/memberships/{user}"
)
if status != 204:
raise RuntimeError(
f"failed removing user '{user}' from team '{team_slug}' in org '{org_id}'" f"\n{status}: {body}"
)

_logger.debug("removed user '%s' from team '%s' in org '%s'", user, team_slug, org_id)

async def delete_team(self, org_id: str, team_slug: str) -> None:
_logger.debug("deleting team '%s' for org '%s'", team_slug, org_id)

status, body = await self.requester.request_raw("DELETE", f"/orgs/{org_id}/teams/{team_slug}")

if status != 204:
raise RuntimeError(f"failed to delete team '{team_slug}': {body}")

_logger.debug("removed team '%s'", team_slug)

async def get_membership(self, org_id: str, user_name: str) -> dict[str, Any]:
_logger.debug("retrieving membership for user '%s' in org '%s'", user_name, org_id)

try:
return await self.requester.request_json("GET", f"/orgs/{org_id}/memberships/{user_name}")
except GitHubException as ex:
raise RuntimeError(f"failed retrieving membership for user '{user_name}' in org '{org_id}':\n{ex}") from ex

async def get_app_installations(self, org_id: str) -> list[dict[str, Any]]:
_logger.debug("retrieving app installations for org '%s'", org_id)

Expand Down
133 changes: 129 additions & 4 deletions otterdog/providers/github/rest/team_client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# *******************************************************************************
# Copyright (c) 2024 Eclipse Foundation and others.
# Copyright (c) 2024-2025 Eclipse Foundation and others.
# This program and the accompanying materials are made available
# under the terms of the Eclipse Public License 2.0
# which is available at http://www.eclipse.org/legal/epl-v20.html
# SPDX-License-Identifier: EPL-2.0
# *******************************************************************************

import json
import re
from typing import Any

from otterdog.logging import get_logger
Expand All @@ -19,15 +21,130 @@ class TeamClient(RestClient):
def __init__(self, rest_api: RestApi):
super().__init__(rest_api)

async def get_team_slugs(self, org_id: str) -> list[dict[str, Any]]:
async def get_team_ids(self, combined_slug: str) -> tuple[int, str]:
_logger.debug("retrieving team ids for slug '%s'", combined_slug)
org_id, team_slug = re.split("/", combined_slug)

try:
response = await self.requester.request_json("GET", f"/orgs/{org_id}/teams/{team_slug}")
return response["id"], response["node_id"]
except GitHubException as ex:
raise RuntimeError(f"failed retrieving team node id:\n{ex}") from ex

async def get_teams(self, org_id: str) -> list[dict[str, Any]]:
_logger.debug("retrieving teams for org '%s'", org_id)

try:
response = await self.requester.request_json("GET", f"/orgs/{org_id}/teams")
return [team["slug"] for team in response]
return await self.requester.request_json("GET", f"/orgs/{org_id}/teams")
except GitHubException as ex:
raise RuntimeError(f"failed retrieving teams for org '{org_id}':\n{ex}") from ex

async def get_team_slugs(self, org_id: str) -> list[dict[str, Any]]:
_logger.debug("retrieving team slugs for org '%s'", org_id)

try:
teams = await self.get_teams(org_id)
return [team["slug"] for team in teams]
except GitHubException as ex:
raise RuntimeError(f"failed retrieving teams:\n{ex}") from ex

async def add_team(self, org_id: str, team_name: str, data: dict[str, str]) -> str:
_logger.debug("adding team '%s' for org '%s'", team_name, org_id)

status, body = await self.requester.request_raw("POST", f"/orgs/{org_id}/teams", json.dumps(data))

if status != 201:
raise RuntimeError(f"failed to add team '{team_name}': {body}")

team_data = json.loads(body)
team_slug = team_data["slug"]

# GitHub automatically adds the creator of the team to the list of maintainers
# Remove any member of the team right after creation again
current_members = await self.get_team_members(org_id, team_slug)
for current_member in current_members:
await self.remove_member_from_team(org_id, team_slug, current_member["login"])

if "members" in data:
members = data["members"]
for user in members:
await self.add_member_to_team(org_id, team_slug, user)

_logger.debug("added team '%s'", team_name)
return team_slug

async def update_team(self, org_id: str, team_slug: str, team: dict[str, Any]) -> None:
_logger.debug("updating team '%s' for org '%s'", team_slug, org_id)

try:
await self.requester.request_json("PATCH", f"/orgs/{org_id}/teams/{team_slug}", team)

if "members" in team:
await self.update_team_members(org_id, team_slug, team["members"])

_logger.debug("updated team '%s'", team_slug)
except GitHubException as ex:
raise RuntimeError(f"failed to update team '{team_slug}':\n{ex}") from ex

async def get_team_members(self, org_id: str, team_slug: str) -> list[dict[str, Any]]:
_logger.debug("retrieving team members for team '%s/%s'", org_id, team_slug)

try:
return await self.requester.request_paged_json("GET", f"/orgs/{org_id}/teams/{team_slug}/members")
except GitHubException as ex:
raise RuntimeError(f"failed retrieving team members for team '{org_id}/{team_slug}':\n{ex}") from ex

async def update_team_members(self, org_id: str, team_slug: str, members: list[str]) -> None:
_logger.debug("updating team members for team '%s' in org '%s'", team_slug, org_id)

current_members = {x["login"] for x in await self.get_team_members(org_id, team_slug)}

# first, add all users that are not members yet.
for member in members:
if member in current_members:
current_members.remove(member)
else:
await self.add_member_to_team(org_id, team_slug, member)

# second, remove the current members that are remaining.
for member in current_members:
await self.remove_member_from_team(org_id, team_slug, member)

async def add_member_to_team(self, org_id: str, team_slug: str, user: str) -> None:
_logger.debug("adding user with id '%s' to team '%s' in org '%s'", user, team_slug, org_id)

status, body = await self.requester.request_raw("PUT", f"/orgs/{org_id}/teams/{team_slug}/memberships/{user}")

if status == 200:
_logger.debug("added user '%s' to team '%s' for org '%s'", user, team_slug, org_id)
else:
raise RuntimeError(
f"failed adding user '{user}' to team '{team_slug}' in org '{org_id}'" f"\n{status}: {body}"
)

async def remove_member_from_team(self, org_id: str, team_slug: str, user: str) -> None:
_logger.debug("removing user '%s' from team '%s' in org '%s'", user, team_slug, org_id)

status, body = await self.requester.request_raw(
"DELETE", f"/orgs/{org_id}/teams/{team_slug}/memberships/{user}"
)
if status != 204:
raise RuntimeError(
f"failed removing user '{user}' from team '{team_slug}' in org '{org_id}'" f"\n{status}: {body}"
)

_logger.debug("removed user '%s' from team '%s' in org '%s'", user, team_slug, org_id)

async def delete_team(self, org_id: str, team_slug: str) -> None:
_logger.debug("deleting team '%s' for org '%s'", team_slug, org_id)

status, body = await self.requester.request_raw("DELETE", f"/orgs/{org_id}/teams/{team_slug}")

if status != 204:
raise RuntimeError(f"failed to delete team '{team_slug}': {body}")

_logger.debug("removed team '%s'", team_slug)

async def is_user_member_of_team(self, org_id: str, team_slug: str, user: str) -> bool:
_logger.debug("retrieving membership of user '%s' for team '%s' in org '%s'", user, team_slug, org_id)

Expand All @@ -41,3 +158,11 @@ async def is_user_member_of_team(self, org_id: str, team_slug: str, user: str) -
raise RuntimeError(
f"failed retrieving team membership for user '{user}' in org '{org_id}'" f"\n{status}: {body}"
)

async def get_membership(self, org_id: str, user_name: str) -> dict[str, Any]:
_logger.debug("retrieving membership for user '%s' in org '%s'", user_name, org_id)

try:
return await self.requester.request_json("GET", f"/orgs/{org_id}/memberships/{user_name}")
except GitHubException as ex:
raise RuntimeError(f"failed retrieving membership for user '{user_name}' in org '{org_id}':\n{ex}") from ex
4 changes: 2 additions & 2 deletions otterdog/webapp/tasks/retrieve_team_membership.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# *******************************************************************************
# Copyright (c) 2024 Eclipse Foundation and others.
# Copyright (c) 2024-2025 Eclipse Foundation and others.
# This program and the accompanying materials are made available
# under the terms of the Eclipse Public License 2.0
# which is available at http://www.eclipse.org/legal/epl-v20.html
Expand Down Expand Up @@ -73,7 +73,7 @@ async def _execute(self) -> None:
user = self._pull_request.user.login

try:
membership = await rest_api.org.get_membership(self.org_id, user)
membership = await rest_api.team.get_membership(self.org_id, user)
state = membership["state"]
role = membership["role"]

Expand Down

0 comments on commit f024659

Please sign in to comment.