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:
@@ -2,10 +2,15 @@
|
||||
|
||||
from .base import BaseModel
|
||||
from .page import Page, PageCreate, PageUpdate
|
||||
from .user import User, UserCreate, UserGroup, UserUpdate
|
||||
|
||||
__all__ = [
|
||||
"BaseModel",
|
||||
"Page",
|
||||
"PageCreate",
|
||||
"PageUpdate",
|
||||
"User",
|
||||
"UserCreate",
|
||||
"UserUpdate",
|
||||
"UserGroup",
|
||||
]
|
||||
|
||||
192
wikijs/models/user.py
Normal file
192
wikijs/models/user.py
Normal 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
|
||||
Reference in New Issue
Block a user