Implementation: - Group data models (wikijs/models/group.py) - Group, GroupCreate, GroupUpdate models - GroupPermission, GroupPageRule, GroupUser models - GroupAssignUser, GroupUnassignUser models - Field validation and normalization - Sync GroupsEndpoint (wikijs/endpoints/groups.py) - list() - List all groups with users - get(group_id) - Get single group - create(group_data) - Create new group - update(group_id, group_data) - Update existing group - delete(group_id) - Delete group - assign_user(group_id, user_id) - Add user to group - unassign_user(group_id, user_id) - Remove user from group - Async AsyncGroupsEndpoint (wikijs/aio/endpoints/groups.py) - Complete async implementation - Identical interface to sync version - All CRUD operations + user management - Integration with clients - WikiJSClient.groups - AsyncWikiJSClient.groups GraphQL operations for all group management features. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
559 lines
18 KiB
Python
559 lines
18 KiB
Python
"""Async groups endpoint for Wiki.js API."""
|
|
|
|
from typing import Dict, List, Union
|
|
|
|
from ...exceptions import APIError, ValidationError
|
|
from ...models import Group, GroupCreate, GroupUpdate
|
|
from .base import AsyncBaseEndpoint
|
|
|
|
|
|
class AsyncGroupsEndpoint(AsyncBaseEndpoint):
|
|
"""Async endpoint for managing Wiki.js groups.
|
|
|
|
Provides async methods to:
|
|
- List all groups
|
|
- Get a specific group by ID
|
|
- Create new groups
|
|
- Update existing groups
|
|
- Delete groups
|
|
- Assign users to groups
|
|
- Remove users from groups
|
|
"""
|
|
|
|
async def list(self) -> List[Group]:
|
|
"""List all groups asynchronously.
|
|
|
|
Returns:
|
|
List of Group objects
|
|
|
|
Raises:
|
|
APIError: If the API request fails
|
|
|
|
Example:
|
|
>>> async with AsyncWikiJSClient(...) as client:
|
|
... groups = await client.groups.list()
|
|
... for group in groups:
|
|
... print(f"{group.name}: {len(group.users)} users")
|
|
"""
|
|
query = """
|
|
query {
|
|
groups {
|
|
list {
|
|
id
|
|
name
|
|
isSystem
|
|
redirectOnLogin
|
|
permissions
|
|
pageRules {
|
|
id
|
|
path
|
|
roles
|
|
match
|
|
deny
|
|
locales
|
|
}
|
|
users {
|
|
id
|
|
name
|
|
email
|
|
}
|
|
createdAt
|
|
updatedAt
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
response = await self._post("/graphql", json_data={"query": query})
|
|
|
|
# Check for GraphQL errors
|
|
if "errors" in response:
|
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
|
|
|
# Extract and normalize groups
|
|
groups_data = response.get("data", {}).get("groups", {}).get("list", [])
|
|
return [Group(**self._normalize_group_data(g)) for g in groups_data]
|
|
|
|
async def get(self, group_id: int) -> Group:
|
|
"""Get a specific group by ID asynchronously.
|
|
|
|
Args:
|
|
group_id: The group ID
|
|
|
|
Returns:
|
|
Group object with user list
|
|
|
|
Raises:
|
|
ValidationError: If group_id is invalid
|
|
APIError: If the group is not found or API request fails
|
|
|
|
Example:
|
|
>>> async with AsyncWikiJSClient(...) as client:
|
|
... group = await client.groups.get(1)
|
|
... print(f"{group.name}: {group.permissions}")
|
|
"""
|
|
# Validate group_id
|
|
if not isinstance(group_id, int) or group_id <= 0:
|
|
raise ValidationError("group_id must be a positive integer")
|
|
|
|
query = """
|
|
query ($id: Int!) {
|
|
groups {
|
|
single(id: $id) {
|
|
id
|
|
name
|
|
isSystem
|
|
redirectOnLogin
|
|
permissions
|
|
pageRules {
|
|
id
|
|
path
|
|
roles
|
|
match
|
|
deny
|
|
locales
|
|
}
|
|
users {
|
|
id
|
|
name
|
|
email
|
|
}
|
|
createdAt
|
|
updatedAt
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
response = await self._post(
|
|
"/graphql", json_data={"query": query, "variables": {"id": group_id}}
|
|
)
|
|
|
|
# Check for GraphQL errors
|
|
if "errors" in response:
|
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
|
|
|
# Extract group data
|
|
group_data = response.get("data", {}).get("groups", {}).get("single")
|
|
|
|
if not group_data:
|
|
raise APIError(f"Group with ID {group_id} not found")
|
|
|
|
return Group(**self._normalize_group_data(group_data))
|
|
|
|
async def create(self, group_data: Union[GroupCreate, Dict]) -> Group:
|
|
"""Create a new group asynchronously.
|
|
|
|
Args:
|
|
group_data: GroupCreate object or dict with group data
|
|
|
|
Returns:
|
|
Created Group object
|
|
|
|
Raises:
|
|
ValidationError: If group data is invalid
|
|
APIError: If the API request fails
|
|
|
|
Example:
|
|
>>> from wikijs.models import GroupCreate
|
|
>>> async with AsyncWikiJSClient(...) as client:
|
|
... group_data = GroupCreate(
|
|
... name="Editors",
|
|
... permissions=["read:pages", "write:pages"]
|
|
... )
|
|
... group = await client.groups.create(group_data)
|
|
"""
|
|
# Validate and convert to dict
|
|
if isinstance(group_data, dict):
|
|
try:
|
|
group_data = GroupCreate(**group_data)
|
|
except Exception as e:
|
|
raise ValidationError(f"Invalid group data: {e}")
|
|
elif not isinstance(group_data, GroupCreate):
|
|
raise ValidationError("group_data must be a GroupCreate object or dict")
|
|
|
|
# Build mutation
|
|
mutation = """
|
|
mutation ($name: String!, $redirectOnLogin: String, $permissions: [String]!, $pageRules: [PageRuleInput]!) {
|
|
groups {
|
|
create(
|
|
name: $name
|
|
redirectOnLogin: $redirectOnLogin
|
|
permissions: $permissions
|
|
pageRules: $pageRules
|
|
) {
|
|
responseResult {
|
|
succeeded
|
|
errorCode
|
|
slug
|
|
message
|
|
}
|
|
group {
|
|
id
|
|
name
|
|
isSystem
|
|
redirectOnLogin
|
|
permissions
|
|
pageRules {
|
|
id
|
|
path
|
|
roles
|
|
match
|
|
deny
|
|
locales
|
|
}
|
|
createdAt
|
|
updatedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
variables = {
|
|
"name": group_data.name,
|
|
"redirectOnLogin": group_data.redirect_on_login or "/",
|
|
"permissions": group_data.permissions,
|
|
"pageRules": group_data.page_rules,
|
|
}
|
|
|
|
response = await self._post(
|
|
"/graphql", json_data={"query": mutation, "variables": variables}
|
|
)
|
|
|
|
# Check for GraphQL errors
|
|
if "errors" in response:
|
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
|
|
|
# Check response result
|
|
result = response.get("data", {}).get("groups", {}).get("create", {})
|
|
response_result = result.get("responseResult", {})
|
|
|
|
if not response_result.get("succeeded"):
|
|
error_msg = response_result.get("message", "Unknown error")
|
|
raise APIError(f"Failed to create group: {error_msg}")
|
|
|
|
# Extract and return created group
|
|
group_data = result.get("group")
|
|
if not group_data:
|
|
raise APIError("Group created but no data returned")
|
|
|
|
return Group(**self._normalize_group_data(group_data))
|
|
|
|
async def update(
|
|
self, group_id: int, group_data: Union[GroupUpdate, Dict]
|
|
) -> Group:
|
|
"""Update an existing group asynchronously.
|
|
|
|
Args:
|
|
group_id: The group ID
|
|
group_data: GroupUpdate object or dict with fields to update
|
|
|
|
Returns:
|
|
Updated Group object
|
|
|
|
Raises:
|
|
ValidationError: If group_id or group_data is invalid
|
|
APIError: If the API request fails
|
|
|
|
Example:
|
|
>>> from wikijs.models import GroupUpdate
|
|
>>> async with AsyncWikiJSClient(...) as client:
|
|
... update_data = GroupUpdate(
|
|
... name="Senior Editors",
|
|
... permissions=["read:pages", "write:pages", "delete:pages"]
|
|
... )
|
|
... group = await client.groups.update(1, update_data)
|
|
"""
|
|
# Validate group_id
|
|
if not isinstance(group_id, int) or group_id <= 0:
|
|
raise ValidationError("group_id must be a positive integer")
|
|
|
|
# Validate and convert to dict
|
|
if isinstance(group_data, dict):
|
|
try:
|
|
group_data = GroupUpdate(**group_data)
|
|
except Exception as e:
|
|
raise ValidationError(f"Invalid group data: {e}")
|
|
elif not isinstance(group_data, GroupUpdate):
|
|
raise ValidationError("group_data must be a GroupUpdate object or dict")
|
|
|
|
# Build mutation with only non-None fields
|
|
mutation = """
|
|
mutation ($id: Int!, $name: String, $redirectOnLogin: String, $permissions: [String], $pageRules: [PageRuleInput]) {
|
|
groups {
|
|
update(
|
|
id: $id
|
|
name: $name
|
|
redirectOnLogin: $redirectOnLogin
|
|
permissions: $permissions
|
|
pageRules: $pageRules
|
|
) {
|
|
responseResult {
|
|
succeeded
|
|
errorCode
|
|
slug
|
|
message
|
|
}
|
|
group {
|
|
id
|
|
name
|
|
isSystem
|
|
redirectOnLogin
|
|
permissions
|
|
pageRules {
|
|
id
|
|
path
|
|
roles
|
|
match
|
|
deny
|
|
locales
|
|
}
|
|
createdAt
|
|
updatedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
variables = {"id": group_id}
|
|
|
|
# Add only non-None fields to variables
|
|
if group_data.name is not None:
|
|
variables["name"] = group_data.name
|
|
if group_data.redirect_on_login is not None:
|
|
variables["redirectOnLogin"] = group_data.redirect_on_login
|
|
if group_data.permissions is not None:
|
|
variables["permissions"] = group_data.permissions
|
|
if group_data.page_rules is not None:
|
|
variables["pageRules"] = group_data.page_rules
|
|
|
|
response = await self._post(
|
|
"/graphql", json_data={"query": mutation, "variables": variables}
|
|
)
|
|
|
|
# Check for GraphQL errors
|
|
if "errors" in response:
|
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
|
|
|
# Check response result
|
|
result = response.get("data", {}).get("groups", {}).get("update", {})
|
|
response_result = result.get("responseResult", {})
|
|
|
|
if not response_result.get("succeeded"):
|
|
error_msg = response_result.get("message", "Unknown error")
|
|
raise APIError(f"Failed to update group: {error_msg}")
|
|
|
|
# Extract and return updated group
|
|
group_data_response = result.get("group")
|
|
if not group_data_response:
|
|
raise APIError("Group updated but no data returned")
|
|
|
|
return Group(**self._normalize_group_data(group_data_response))
|
|
|
|
async def delete(self, group_id: int) -> bool:
|
|
"""Delete a group asynchronously.
|
|
|
|
Args:
|
|
group_id: The group ID
|
|
|
|
Returns:
|
|
True if deletion was successful
|
|
|
|
Raises:
|
|
ValidationError: If group_id is invalid
|
|
APIError: If the API request fails
|
|
|
|
Example:
|
|
>>> async with AsyncWikiJSClient(...) as client:
|
|
... success = await client.groups.delete(5)
|
|
... if success:
|
|
... print("Group deleted")
|
|
"""
|
|
# Validate group_id
|
|
if not isinstance(group_id, int) or group_id <= 0:
|
|
raise ValidationError("group_id must be a positive integer")
|
|
|
|
mutation = """
|
|
mutation ($id: Int!) {
|
|
groups {
|
|
delete(id: $id) {
|
|
responseResult {
|
|
succeeded
|
|
errorCode
|
|
slug
|
|
message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
response = await self._post(
|
|
"/graphql", json_data={"query": mutation, "variables": {"id": group_id}}
|
|
)
|
|
|
|
# Check for GraphQL errors
|
|
if "errors" in response:
|
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
|
|
|
# Check response result
|
|
result = response.get("data", {}).get("groups", {}).get("delete", {})
|
|
response_result = result.get("responseResult", {})
|
|
|
|
if not response_result.get("succeeded"):
|
|
error_msg = response_result.get("message", "Unknown error")
|
|
raise APIError(f"Failed to delete group: {error_msg}")
|
|
|
|
return True
|
|
|
|
async def assign_user(self, group_id: int, user_id: int) -> bool:
|
|
"""Assign a user to a group asynchronously.
|
|
|
|
Args:
|
|
group_id: The group ID
|
|
user_id: The user ID
|
|
|
|
Returns:
|
|
True if assignment was successful
|
|
|
|
Raises:
|
|
ValidationError: If group_id or user_id is invalid
|
|
APIError: If the API request fails
|
|
|
|
Example:
|
|
>>> async with AsyncWikiJSClient(...) as client:
|
|
... success = await client.groups.assign_user(group_id=1, user_id=5)
|
|
... if success:
|
|
... print("User assigned to group")
|
|
"""
|
|
# Validate IDs
|
|
if not isinstance(group_id, int) or group_id <= 0:
|
|
raise ValidationError("group_id must be a positive integer")
|
|
if not isinstance(user_id, int) or user_id <= 0:
|
|
raise ValidationError("user_id must be a positive integer")
|
|
|
|
mutation = """
|
|
mutation ($groupId: Int!, $userId: Int!) {
|
|
groups {
|
|
assignUser(groupId: $groupId, userId: $userId) {
|
|
responseResult {
|
|
succeeded
|
|
errorCode
|
|
slug
|
|
message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
response = await self._post(
|
|
"/graphql",
|
|
json_data={
|
|
"query": mutation,
|
|
"variables": {"groupId": group_id, "userId": user_id},
|
|
},
|
|
)
|
|
|
|
# Check for GraphQL errors
|
|
if "errors" in response:
|
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
|
|
|
# Check response result
|
|
result = response.get("data", {}).get("groups", {}).get("assignUser", {})
|
|
response_result = result.get("responseResult", {})
|
|
|
|
if not response_result.get("succeeded"):
|
|
error_msg = response_result.get("message", "Unknown error")
|
|
raise APIError(f"Failed to assign user to group: {error_msg}")
|
|
|
|
return True
|
|
|
|
async def unassign_user(self, group_id: int, user_id: int) -> bool:
|
|
"""Remove a user from a group asynchronously.
|
|
|
|
Args:
|
|
group_id: The group ID
|
|
user_id: The user ID
|
|
|
|
Returns:
|
|
True if removal was successful
|
|
|
|
Raises:
|
|
ValidationError: If group_id or user_id is invalid
|
|
APIError: If the API request fails
|
|
|
|
Example:
|
|
>>> async with AsyncWikiJSClient(...) as client:
|
|
... success = await client.groups.unassign_user(group_id=1, user_id=5)
|
|
... if success:
|
|
... print("User removed from group")
|
|
"""
|
|
# Validate IDs
|
|
if not isinstance(group_id, int) or group_id <= 0:
|
|
raise ValidationError("group_id must be a positive integer")
|
|
if not isinstance(user_id, int) or user_id <= 0:
|
|
raise ValidationError("user_id must be a positive integer")
|
|
|
|
mutation = """
|
|
mutation ($groupId: Int!, $userId: Int!) {
|
|
groups {
|
|
unassignUser(groupId: $groupId, userId: $userId) {
|
|
responseResult {
|
|
succeeded
|
|
errorCode
|
|
slug
|
|
message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
|
|
response = await self._post(
|
|
"/graphql",
|
|
json_data={
|
|
"query": mutation,
|
|
"variables": {"groupId": group_id, "userId": user_id},
|
|
},
|
|
)
|
|
|
|
# Check for GraphQL errors
|
|
if "errors" in response:
|
|
raise APIError(f"GraphQL errors: {response['errors']}")
|
|
|
|
# Check response result
|
|
result = response.get("data", {}).get("groups", {}).get("unassignUser", {})
|
|
response_result = result.get("responseResult", {})
|
|
|
|
if not response_result.get("succeeded"):
|
|
error_msg = response_result.get("message", "Unknown error")
|
|
raise APIError(f"Failed to remove user from group: {error_msg}")
|
|
|
|
return True
|
|
|
|
def _normalize_group_data(self, data: Dict) -> Dict:
|
|
"""Normalize group data from API response to Python naming convention.
|
|
|
|
Args:
|
|
data: Raw group data from API
|
|
|
|
Returns:
|
|
Normalized group data with snake_case field names
|
|
"""
|
|
normalized = {
|
|
"id": data.get("id"),
|
|
"name": data.get("name"),
|
|
"is_system": data.get("isSystem", False),
|
|
"redirect_on_login": data.get("redirectOnLogin"),
|
|
"permissions": data.get("permissions", []),
|
|
"page_rules": data.get("pageRules", []),
|
|
"users": data.get("users", []),
|
|
"created_at": data.get("createdAt"),
|
|
"updated_at": data.get("updatedAt"),
|
|
}
|
|
|
|
return normalized
|