Complete Phase 1 foundation: Tasks 1.1 and 1.2

 Task 1.1 - Project Foundation Setup:
- Repository structure with Python packaging (setup.py, pyproject.toml)
- Development dependencies and requirements
- Contributing guidelines and MIT license
- GitHub workflows for CI/CD (test.yml, release.yml)
- Issue and PR templates for community contributions
- Comprehensive project documentation

 Task 1.2 - Core Client Structure:
- wikijs package with proper module organization
- Core client class foundation in client.py
- Exception hierarchy for error handling
- Base model classes and page models
- Type checking support (py.typed)
- Utility modules and helper functions

📊 Progress: Phase 1 MVP Development now 40% complete
🎯 Next: Task 1.3 - Authentication System implementation

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-29 13:25:36 -04:00
parent 3554d7d69c
commit 11b6be87c8
31 changed files with 3805 additions and 115 deletions

11
wikijs/models/__init__.py Normal file
View File

@@ -0,0 +1,11 @@
"""Data models for wikijs-python-sdk."""
from .base import BaseModel
from .page import Page, PageCreate, PageUpdate
__all__ = [
"BaseModel",
"Page",
"PageCreate",
"PageUpdate",
]

90
wikijs/models/base.py Normal file
View File

@@ -0,0 +1,90 @@
"""Base model functionality for wikijs-python-sdk."""
from datetime import datetime
from typing import Any, Dict, Optional
from pydantic import BaseModel as PydanticBaseModel, ConfigDict
class BaseModel(PydanticBaseModel):
"""Base model with common functionality for all data models.
Provides:
- Automatic validation via Pydantic
- JSON serialization/deserialization
- Field aliases for API compatibility
- Consistent datetime handling
"""
model_config = ConfigDict(
# Allow population by field name or alias
populate_by_name=True,
# Validate assignment to attributes
validate_assignment=True,
# Use enum values instead of names
use_enum_values=True,
# Allow extra fields for forward compatibility
extra="ignore",
# Serialize datetime as ISO format
json_encoders={
datetime: lambda v: v.isoformat() if v else None
}
)
def to_dict(self, exclude_none: bool = True) -> Dict[str, Any]:
"""Convert model to dictionary.
Args:
exclude_none: Whether to exclude None values
Returns:
Dictionary representation of the model
"""
return self.model_dump(exclude_none=exclude_none, by_alias=True)
def to_json(self, exclude_none: bool = True) -> str:
"""Convert model to JSON string.
Args:
exclude_none: Whether to exclude None values
Returns:
JSON string representation of the model
"""
return self.model_dump_json(exclude_none=exclude_none, by_alias=True)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "BaseModel":
"""Create model instance from dictionary.
Args:
data: Dictionary data
Returns:
Model instance
"""
return cls(**data)
@classmethod
def from_json(cls, json_str: str) -> "BaseModel":
"""Create model instance from JSON string.
Args:
json_str: JSON string
Returns:
Model instance
"""
return cls.model_validate_json(json_str)
class TimestampedModel(BaseModel):
"""Base model with timestamp fields."""
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
@property
def is_new(self) -> bool:
"""Check if this is a new (unsaved) model."""
return self.created_at is None

188
wikijs/models/page.py Normal file
View File

@@ -0,0 +1,188 @@
"""Page-related data models for wikijs-python-sdk."""
import re
from datetime import datetime
from typing import List, Optional
from pydantic import Field, validator
from .base import BaseModel, TimestampedModel
class Page(TimestampedModel):
"""Represents a Wiki.js page.
This model contains all the data for a wiki page including
content, metadata, and computed properties.
"""
id: int = Field(..., description="Unique page identifier")
title: str = Field(..., description="Page title")
path: str = Field(..., description="Page path/slug")
content: str = Field(..., description="Page content")
# Optional fields that may be present
description: Optional[str] = Field(None, description="Page description")
is_published: bool = Field(True, description="Whether page is published")
is_private: bool = Field(False, description="Whether page is private")
# Metadata
tags: List[str] = Field(default_factory=list, description="Page tags")
locale: str = Field("en", description="Page locale")
# Author information
author_id: Optional[int] = Field(None, alias="authorId")
author_name: Optional[str] = Field(None, alias="authorName")
author_email: Optional[str] = Field(None, alias="authorEmail")
# Editor information
editor: Optional[str] = Field(None, description="Editor used")
@validator("path")
def validate_path(cls, v):
"""Validate page path format."""
if not v:
raise ValueError("Path cannot be empty")
# Remove leading/trailing slashes and normalize
v = v.strip("/")
# Check for valid characters (letters, numbers, hyphens, underscores, slashes)
if not re.match(r"^[a-zA-Z0-9\-_/]+$", v):
raise ValueError("Path contains invalid characters")
return v
@validator("title")
def validate_title(cls, v):
"""Validate page title."""
if not v or not v.strip():
raise ValueError("Title cannot be empty")
# Limit title length
if len(v) > 255:
raise ValueError("Title cannot exceed 255 characters")
return v.strip()
@property
def word_count(self) -> int:
"""Calculate word count from content."""
if not self.content:
return 0
# Simple word count - split on whitespace
words = self.content.split()
return len(words)
@property
def reading_time(self) -> int:
"""Estimate reading time in minutes (assuming 200 words per minute)."""
return max(1, self.word_count // 200)
@property
def url_path(self) -> str:
"""Get the full URL path for this page."""
return f"/{self.path}"
def extract_headings(self) -> List[str]:
"""Extract markdown headings from content.
Returns:
List of heading text (without # markers)
"""
if not self.content:
return []
headings = []
for line in self.content.split("\n"):
line = line.strip()
if line.startswith("#"):
# Remove # markers and whitespace
heading = re.sub(r"^#+\s*", "", line).strip()
if heading:
headings.append(heading)
return headings
def has_tag(self, tag: str) -> bool:
"""Check if page has a specific tag.
Args:
tag: Tag to check for
Returns:
True if page has the tag
"""
return tag.lower() in [t.lower() for t in self.tags]
class PageCreate(BaseModel):
"""Data model for creating a new page."""
title: str = Field(..., description="Page title")
path: str = Field(..., description="Page path/slug")
content: str = Field(..., description="Page content")
# Optional fields
description: Optional[str] = Field(None, description="Page description")
is_published: bool = Field(True, description="Whether to publish immediately")
is_private: bool = Field(False, description="Whether page should be private")
tags: List[str] = Field(default_factory=list, description="Page tags")
locale: str = Field("en", description="Page locale")
editor: str = Field("markdown", description="Editor to use")
@validator("path")
def validate_path(cls, v):
"""Validate page path format."""
if not v:
raise ValueError("Path cannot be empty")
# Remove leading/trailing slashes and normalize
v = v.strip("/")
# Check for valid characters
if not re.match(r"^[a-zA-Z0-9\-_/]+$", v):
raise ValueError("Path contains invalid characters")
return v
@validator("title")
def validate_title(cls, v):
"""Validate page title."""
if not v or not v.strip():
raise ValueError("Title cannot be empty")
if len(v) > 255:
raise ValueError("Title cannot exceed 255 characters")
return v.strip()
class PageUpdate(BaseModel):
"""Data model for updating an existing page."""
# All fields optional for partial updates
title: Optional[str] = Field(None, description="Page title")
content: Optional[str] = Field(None, description="Page content")
description: Optional[str] = Field(None, description="Page description")
is_published: Optional[bool] = Field(None, description="Publication status")
is_private: Optional[bool] = Field(None, description="Privacy status")
tags: Optional[List[str]] = Field(None, description="Page tags")
@validator("title")
def validate_title(cls, v):
"""Validate page title if provided."""
if v is not None:
if not v.strip():
raise ValueError("Title cannot be empty")
if len(v) > 255:
raise ValueError("Title cannot exceed 255 characters")
return v.strip()
return v