Implement Users API with complete CRUD operations

Phase 2, Task 2.2.1: Users API Implementation (Sync)

This commit adds comprehensive user management capabilities to the
Wiki.js Python SDK with full CRUD operations.

User Data Models (wikijs/models/user.py):
-----------------------------------------

1. **User** - Complete user data model
   - Profile information (name, email, location, job title)
   - Authentication details (provider, verified, active status)
   - Group memberships with UserGroup nested model
   - Timestamps (created, updated, last login)
   - Email validation with pydantic EmailStr
   - Name validation (2-255 characters)

2. **UserCreate** - User creation model
   - Required: email, name, password
   - Optional: groups, profile fields, provider settings
   - Validation: email format, password strength (min 6 chars)
   - Welcome email configuration
   - Password change enforcement

3. **UserUpdate** - User update model
   - All fields optional for partial updates
   - Email, name, password updates
   - Profile field updates
   - Group membership changes
   - Status flags (active, verified)

4. **UserGroup** - Group membership model
   - Group ID and name
   - Used in User model for group associations

Users Endpoint (wikijs/endpoints/users.py):
-------------------------------------------

Complete CRUD Operations:

1. **list()** - List users with filtering
   - Pagination (limit, offset)
   - Search by name/email
   - Ordering (name, email, createdAt, lastLoginAt)
   - Client-side pagination fallback

2. **get(user_id)** - Get user by ID
   - Fetch single user with full details
   - Include group memberships
   - Comprehensive error handling

3. **create(user_data)** - Create new user
   - Accept UserCreate object or dict
   - Full validation before API call
   - Returns created User object
   - Handles creation failures gracefully

4. **update(user_id, user_data)** - Update existing user
   - Partial updates supported
   - Only sends changed fields
   - Returns updated User object
   - Validates all updates

5. **delete(user_id)** - Delete user
   - Permanent deletion
   - Returns boolean success
   - Clear error messages

6. **search(query)** - Search users
   - Search by name or email
   - Optional result limiting
   - Uses list() with search filter

Helper Methods:
   - _normalize_user_data() - API response normalization
   - Handles field name mapping (camelCase → snake_case)
   - Group data structure conversion

Integration:
------------
- Added UsersEndpoint to WikiJSClient
- Updated endpoints module exports
- Added user models to main package exports
- Installed email-validator dependency for EmailStr

GraphQL Queries:
----------------
- users.list - List/search users
- users.single - Get user by ID
- users.create - Create new user
- users.update - Update existing user
- users.delete - Delete user

All queries include proper error handling and response validation.

Code Quality:
-------------
 Compiles without errors
 Type hints on all methods
 Comprehensive docstrings
 Input validation
 Proper exception handling
 Follows existing code patterns

Next Steps:
-----------
- Implement AsyncUsersEndpoint (async version)
- Write comprehensive tests
- Add usage documentation
- Create examples

Phase 2, Task 2.2.1 Progress: ~50% Complete
Users API (sync):  COMPLETE
Users API (async):  IN PROGRESS

This establishes the foundation for complete user management
in the Wiki.js Python SDK.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2025-10-22 18:21:46 +00:00
parent 3b11b09cde
commit 930fe50e60
5 changed files with 772 additions and 3 deletions

192
wikijs/models/user.py Normal file
View File

@@ -0,0 +1,192 @@
"""User-related data models for wikijs-python-sdk."""
import re
from typing import List, Optional
from pydantic import EmailStr, Field, field_validator
from .base import BaseModel, TimestampedModel
class UserGroup(BaseModel):
"""Represents a user's group membership.
This model contains information about a user's membership
in a specific group.
"""
id: int = Field(..., description="Group ID")
name: str = Field(..., description="Group name")
class User(TimestampedModel):
"""Represents a Wiki.js user.
This model contains all user data including profile information,
authentication details, and group memberships.
"""
id: int = Field(..., description="Unique user identifier")
name: str = Field(..., description="User's full name")
email: EmailStr = Field(..., description="User's email address")
# Authentication and status
provider_key: Optional[str] = Field(None, alias="providerKey", description="Auth provider key")
is_system: bool = Field(False, alias="isSystem", description="Whether user is system user")
is_active: bool = Field(True, alias="isActive", description="Whether user is active")
is_verified: bool = Field(False, alias="isVerified", description="Whether email is verified")
# Profile information
location: Optional[str] = Field(None, description="User's location")
job_title: Optional[str] = Field(None, alias="jobTitle", description="User's job title")
timezone: Optional[str] = Field(None, description="User's timezone")
# Permissions and groups
groups: List[UserGroup] = Field(default_factory=list, description="User's groups")
# Timestamps handled by TimestampedModel
last_login_at: Optional[str] = Field(None, alias="lastLoginAt", description="Last login timestamp")
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""Validate user name."""
if not v or not v.strip():
raise ValueError("Name cannot be empty")
# Check length
if len(v) < 2:
raise ValueError("Name must be at least 2 characters long")
if len(v) > 255:
raise ValueError("Name cannot exceed 255 characters")
return v.strip()
class Config:
"""Pydantic model configuration."""
populate_by_name = True
str_strip_whitespace = True
class UserCreate(BaseModel):
"""Model for creating a new user.
This model contains all required and optional fields
for creating a new Wiki.js user.
"""
email: EmailStr = Field(..., description="User's email address")
name: str = Field(..., description="User's full name")
password_raw: str = Field(..., alias="passwordRaw", description="User's password")
# Optional fields
provider_key: str = Field("local", alias="providerKey", description="Auth provider key")
groups: List[int] = Field(default_factory=list, description="Group IDs to assign")
must_change_password: bool = Field(False, alias="mustChangePassword", description="Force password change")
send_welcome_email: bool = Field(True, alias="sendWelcomeEmail", description="Send welcome email")
# Profile information
location: Optional[str] = Field(None, description="User's location")
job_title: Optional[str] = Field(None, alias="jobTitle", description="User's job title")
timezone: Optional[str] = Field(None, description="User's timezone")
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""Validate user name."""
if not v or not v.strip():
raise ValueError("Name cannot be empty")
if len(v) < 2:
raise ValueError("Name must be at least 2 characters long")
if len(v) > 255:
raise ValueError("Name cannot exceed 255 characters")
return v.strip()
@field_validator("password_raw")
@classmethod
def validate_password(cls, v: str) -> str:
"""Validate password strength."""
if not v:
raise ValueError("Password cannot be empty")
if len(v) < 6:
raise ValueError("Password must be at least 6 characters long")
if len(v) > 255:
raise ValueError("Password cannot exceed 255 characters")
return v
class Config:
"""Pydantic model configuration."""
populate_by_name = True
str_strip_whitespace = True
class UserUpdate(BaseModel):
"""Model for updating an existing user.
This model contains optional fields that can be updated
for an existing Wiki.js user. All fields are optional.
"""
name: Optional[str] = Field(None, description="User's full name")
email: Optional[EmailStr] = Field(None, description="User's email address")
password_raw: Optional[str] = Field(None, alias="passwordRaw", description="New password")
# Profile information
location: Optional[str] = Field(None, description="User's location")
job_title: Optional[str] = Field(None, alias="jobTitle", description="User's job title")
timezone: Optional[str] = Field(None, description="User's timezone")
# Group assignments
groups: Optional[List[int]] = Field(None, description="Group IDs to assign")
# Status flags
is_active: Optional[bool] = Field(None, alias="isActive", description="Whether user is active")
is_verified: Optional[bool] = Field(None, alias="isVerified", description="Whether email is verified")
@field_validator("name")
@classmethod
def validate_name(cls, v: Optional[str]) -> Optional[str]:
"""Validate user name if provided."""
if v is None:
return v
if not v.strip():
raise ValueError("Name cannot be empty")
if len(v) < 2:
raise ValueError("Name must be at least 2 characters long")
if len(v) > 255:
raise ValueError("Name cannot exceed 255 characters")
return v.strip()
@field_validator("password_raw")
@classmethod
def validate_password(cls, v: Optional[str]) -> Optional[str]:
"""Validate password strength if provided."""
if v is None:
return v
if len(v) < 6:
raise ValueError("Password must be at least 6 characters long")
if len(v) > 255:
raise ValueError("Password cannot exceed 255 characters")
return v
class Config:
"""Pydantic model configuration."""
populate_by_name = True
str_strip_whitespace = True