feat: add team membership, invitations, and member profiles

- Add add_team_member and remove_team_member via /team-memberships
- Add send_team_invitation, list_team_invitations, revoke_team_invitation
- Add list_team_member_profiles and get_team_member_profile
- Add user_id field to TeamMemberProfile for profile filtering
- Both sync and async facades have identical method surfaces
This commit is contained in:
Ejiro Asiuwhu
2026-03-25 01:24:57 +01:00
parent c48b9ca7fc
commit cd64ecb703
2 changed files with 149 additions and 0 deletions
@@ -335,6 +335,78 @@ class StackServerApp:
return None
return self.get_team(data["team_id"])
# -- team membership -----------------------------------------------------
def add_team_member(self, team_id: str, user_id: str) -> None:
"""Add a user to a team."""
self._client.request(
"POST", f"/team-memberships/{team_id}/{user_id}", body={}
)
def remove_team_member(self, team_id: str, user_id: str) -> None:
"""Remove a user from a team."""
self._client.request(
"DELETE", f"/team-memberships/{team_id}/{user_id}"
)
# -- team invitations ----------------------------------------------------
def send_team_invitation(
self,
team_id: str,
email: str,
*,
callback_url: Optional[str] = None,
) -> None:
"""Send an invitation email to join a team."""
body = _build_params(
email=email, team_id=team_id, callback_url=callback_url
)
self._client.request("POST", "/team-invitations/send-code", body=body)
def list_team_invitations(self, team_id: str) -> list[TeamInvitation]:
"""List pending invitations for a team."""
data = self._client.request(
"GET", f"/teams/{team_id}/invitations"
)
return [
TeamInvitation.model_validate(item)
for item in (data or {}).get("items", [])
]
def revoke_team_invitation(
self, team_id: str, invitation_id: str
) -> None:
"""Revoke (delete) a team invitation."""
self._client.request(
"DELETE", f"/teams/{team_id}/invitations/{invitation_id}"
)
# -- team member profiles ------------------------------------------------
def list_team_member_profiles(
self, team_id: str
) -> list[TeamMemberProfile]:
"""List member profiles for a team."""
data = self._client.request(
"GET", "/team-member-profiles", params={"team_id": team_id}
)
return [
TeamMemberProfile.model_validate(item)
for item in (data or {}).get("items", [])
]
def get_team_member_profile(
self, team_id: str, user_id: str
) -> TeamMemberProfile | None:
"""Get a specific team member's profile.
Fetches all member profiles for the team and filters by *user_id*.
Returns ``None`` if no profile is found.
"""
profiles = self.list_team_member_profiles(team_id)
return next((p for p in profiles if p.user_id == user_id), None)
# ---------------------------------------------------------------------------
# AsyncStackServerApp (async)
@@ -642,3 +714,79 @@ class AsyncStackServerApp:
if data is None or "team_id" not in data:
return None
return await self.get_team(data["team_id"])
# -- team membership -----------------------------------------------------
async def add_team_member(self, team_id: str, user_id: str) -> None:
"""Add a user to a team."""
await self._client.request(
"POST", f"/team-memberships/{team_id}/{user_id}", body={}
)
async def remove_team_member(self, team_id: str, user_id: str) -> None:
"""Remove a user from a team."""
await self._client.request(
"DELETE", f"/team-memberships/{team_id}/{user_id}"
)
# -- team invitations ----------------------------------------------------
async def send_team_invitation(
self,
team_id: str,
email: str,
*,
callback_url: Optional[str] = None,
) -> None:
"""Send an invitation email to join a team."""
body = _build_params(
email=email, team_id=team_id, callback_url=callback_url
)
await self._client.request(
"POST", "/team-invitations/send-code", body=body
)
async def list_team_invitations(
self, team_id: str
) -> list[TeamInvitation]:
"""List pending invitations for a team."""
data = await self._client.request(
"GET", f"/teams/{team_id}/invitations"
)
return [
TeamInvitation.model_validate(item)
for item in (data or {}).get("items", [])
]
async def revoke_team_invitation(
self, team_id: str, invitation_id: str
) -> None:
"""Revoke (delete) a team invitation."""
await self._client.request(
"DELETE", f"/teams/{team_id}/invitations/{invitation_id}"
)
# -- team member profiles ------------------------------------------------
async def list_team_member_profiles(
self, team_id: str
) -> list[TeamMemberProfile]:
"""List member profiles for a team."""
data = await self._client.request(
"GET", "/team-member-profiles", params={"team_id": team_id}
)
return [
TeamMemberProfile.model_validate(item)
for item in (data or {}).get("items", [])
]
async def get_team_member_profile(
self, team_id: str, user_id: str
) -> TeamMemberProfile | None:
"""Get a specific team member's profile.
Fetches all member profiles for the team and filters by *user_id*.
Returns ``None`` if no profile is found.
"""
profiles = await self.list_team_member_profiles(team_id)
return next((p for p in profiles if p.user_id == user_id), None)
@@ -37,6 +37,7 @@ class ServerTeam(Team):
class TeamMemberProfile(StackAuthModel):
"""A user's profile within a specific team."""
user_id: str = Field(alias="userId")
display_name: str | None = Field(None, alias="displayName")
profile_image_url: str | None = Field(None, alias="profileImageUrl")