last-updates

This commit is contained in:
2025-07-29 21:01:46 -04:00
parent 18a82711cb
commit 40c801f053
9 changed files with 351 additions and 1020 deletions

View File

@@ -84,6 +84,7 @@ new_page = client.pages.create(PageCreate(
### **For Maintainers**
- **[Architecture](docs/wikijs_sdk_architecture.md)**: Technical design and patterns
- **[Development Plan](docs/wikijs_sdk_release_plan.md)**: Complete roadmap and milestones
- **[PyPI Publishing](docs/PIP_INSTRUCTIONS.md)**: Complete guide to publishing on PyPI
- **[AI Coordination](CLAUDE.md)**: AI-assisted development workflow
---

232
docs/PIP_INSTRUCTIONS.md Normal file
View File

@@ -0,0 +1,232 @@
# PyPI Publishing Instructions
This document provides step-by-step instructions for publishing the Wiki.js Python SDK to PyPI.
## Prerequisites
- Package has been built and tested locally
- All tests pass with >85% coverage
- Documentation is complete and up-to-date
- GitHub repository is properly configured
## Pre-Publishing Checklist
### 1. Update Repository URLs
Replace `yourusername` with your actual GitHub username in the following files:
**setup.py** (lines 44, 46-48):
```python
url="https://github.com/YOUR_USERNAME/wikijs-python-sdk",
project_urls={
"Bug Reports": "https://github.com/YOUR_USERNAME/wikijs-python-sdk/issues",
"Source": "https://github.com/YOUR_USERNAME/wikijs-python-sdk",
"Documentation": "https://github.com/YOUR_USERNAME/wikijs-python-sdk/docs",
}
```
**pyproject.toml** (lines 65-68):
```toml
[project.urls]
Homepage = "https://github.com/YOUR_USERNAME/wikijs-python-sdk"
"Bug Reports" = "https://github.com/YOUR_USERNAME/wikijs-python-sdk/issues"
Source = "https://github.com/YOUR_USERNAME/wikijs-python-sdk"
Documentation = "https://github.com/YOUR_USERNAME/wikijs-python-sdk/docs"
```
**README.md** (lines 6-7):
```markdown
[![CI Status](https://github.com/YOUR_USERNAME/wikijs-python-sdk/workflows/Test%20Suite/badge.svg)](https://github.com/YOUR_USERNAME/wikijs-python-sdk/actions)
[![Coverage](https://codecov.io/gh/YOUR_USERNAME/wikijs-python-sdk/branch/main/graph/badge.svg)](https://codecov.io/gh/YOUR_USERNAME/wikijs-python-sdk)
```
### 2. Verify Package Name Availability
Check if `wikijs-python-sdk` is available on PyPI:
- Visit https://pypi.org/project/wikijs-python-sdk/
- If it returns a 404, the name is available
- If needed, update the package name in `setup.py` and `pyproject.toml`
### 3. Version Management
Ensure the version in `wikijs/version.py` reflects the release:
```python
__version__ = "0.1.0" # Update as needed
```
## PyPI Account Setup
### 1. Create Accounts
Register for both test and production PyPI:
- **Test PyPI**: https://test.pypi.org/account/register/
- **Production PyPI**: https://pypi.org/account/register/
### 2. Generate API Tokens
For each account, create API tokens:
1. Go to Account Settings
2. Navigate to "API tokens"
3. Click "Add API token"
4. Choose scope: "Entire account" (for first upload)
5. Save the token securely
## Publishing Process
### 1. Install Publishing Tools
```bash
pip install twine build
```
### 2. Build the Package
```bash
# Clean previous builds
rm -rf dist/ build/ *.egg-info/
# Build the package
python -m build
```
### 3. Validate the Package
```bash
# Check package for common issues
twine check dist/*
```
Expected output:
```
Checking dist/wikijs_python_sdk-0.1.0-py3-none-any.whl: PASSED
Checking dist/wikijs_python_sdk-0.1.0.tar.gz: PASSED
```
### 4. Test Upload (Recommended)
Always test on Test PyPI first:
```bash
# Upload to Test PyPI
twine upload --repository testpypi dist/*
```
When prompted:
- **Username**: `__token__`
- **Password**: Your Test PyPI API token
### 5. Test Installation
```bash
# Test installation from Test PyPI
pip install --index-url https://test.pypi.org/simple/ wikijs-python-sdk
# Test basic functionality
python -c "from wikijs import WikiJSClient; print('Import successful')"
```
### 6. Production Upload
Once testing is successful:
```bash
# Upload to production PyPI
twine upload dist/*
```
When prompted:
- **Username**: `__token__`
- **Password**: Your Production PyPI API token
### 7. Verify Production Installation
```bash
# Install from PyPI
pip install wikijs-python-sdk
# Verify installation
python -c "from wikijs import WikiJSClient; print('Production install successful')"
```
## Post-Publishing Tasks
### 1. Update Documentation
- Update README.md installation instructions
- Remove "Coming soon" notes
- Add PyPI badge if desired
### 2. Create GitHub Release
1. Go to your GitHub repository
2. Click "Releases" → "Create a new release"
3. Tag version: `v0.1.0`
4. Release title: `v0.1.0 - MVP Release`
5. Copy changelog content as description
6. Attach built files from `dist/`
### 3. Announce Release
- Update project status in README.md
- Consider posting to relevant communities
- Update project documentation
## Troubleshooting
### Common Issues
**"Package already exists"**
- The package name is taken
- Update package name in configuration files
- Or contact PyPI if you believe you own the name
**"Invalid authentication credentials"**
- Verify you're using `__token__` as username
- Check that the API token is correct and has proper scope
- Ensure the token hasn't expired
**"File already exists"**
- You're trying to upload the same version twice
- Increment the version number in `wikijs/version.py`
- Rebuild the package
**Package validation errors**
- Run `twine check dist/*` for detailed error messages
- Common issues: missing README, invalid metadata
- Fix issues and rebuild
### Getting Help
- **PyPI Help**: https://pypi.org/help/
- **Packaging Guide**: https://packaging.python.org/
- **Twine Documentation**: https://twine.readthedocs.io/
## Automated Publishing (Future)
Consider setting up GitHub Actions for automated publishing:
```yaml
# .github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install build twine
- name: Build package
run: python -m build
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*
```
## Security Notes
- Never commit API tokens to version control
- Use repository secrets for automated publishing
- Regularly rotate API tokens
- Use scoped tokens when possible
- Monitor package downloads for suspicious activity
---
**Next Steps**: Once published, users can install with `pip install wikijs-python-sdk`

View File

@@ -1,568 +0,0 @@
#!/usr/bin/env python3
"""
Wiki.js Python SDK Experimentation Script
This interactive script lets you experiment with the SDK features in a safe,
mocked environment. Perfect for learning how the SDK works without needing
a real Wiki.js instance.
Usage:
python experiment.py
The script will guide you through different SDK features and let you
try them out interactively.
"""
import json
import sys
from datetime import datetime
from unittest.mock import Mock, patch
# Import SDK components
from wikijs import WikiJSClient
from wikijs.models import PageCreate, PageUpdate, Page
from wikijs.auth import APIKeyAuth, JWTAuth, NoAuth
from wikijs.exceptions import APIError, ValidationError, NotFoundError
class Colors:
"""ANSI color codes for pretty output."""
HEADER = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
END = '\033[0m'
def print_header(text):
"""Print a colored header."""
print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.END}")
print(f"{Colors.HEADER}{Colors.BOLD}{text.center(60)}{Colors.END}")
print(f"{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.END}\n")
def print_success(text):
"""Print success message."""
print(f"{Colors.GREEN}{text}{Colors.END}")
def print_info(text):
"""Print info message."""
print(f"{Colors.CYAN} {text}{Colors.END}")
def print_warning(text):
"""Print warning message."""
print(f"{Colors.YELLOW}⚠️ {text}{Colors.END}")
def print_error(text):
"""Print error message."""
print(f"{Colors.RED}{text}{Colors.END}")
def print_code(code):
"""Print code snippet."""
print(f"{Colors.BLUE}{code}{Colors.END}")
def wait_for_enter(prompt="Press Enter to continue..."):
"""Wait for user input."""
input(f"\n{Colors.YELLOW}{prompt}{Colors.END}")
def setup_mock_session():
"""Set up a mock session for API calls."""
mock_session = Mock()
# Sample pages data
sample_pages = [
{
"id": 1,
"title": "Welcome to Wiki.js",
"path": "home",
"content": "# Welcome!\n\nThis is your wiki home page.",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T12:00:00Z",
"is_published": True,
"tags": ["welcome", "home"]
},
{
"id": 2,
"title": "Getting Started Guide",
"path": "getting-started",
"content": "# Getting Started\n\nLearn how to use this wiki effectively.",
"created_at": "2023-01-02T00:00:00Z",
"updated_at": "2023-01-02T10:00:00Z",
"is_published": True,
"tags": ["guide", "tutorial"]
},
{
"id": 3,
"title": "API Documentation",
"path": "api-docs",
"content": "# API Documentation\n\nComplete API reference.",
"created_at": "2023-01-03T00:00:00Z",
"updated_at": "2023-01-03T14:00:00Z",
"is_published": False,
"tags": ["api", "documentation"]
}
]
def mock_request(method, url, **kwargs):
"""Mock HTTP request handler."""
response = Mock()
response.ok = True
response.status_code = 200
# Simulate different API endpoints
if "pages" in url and method.upper() == "GET":
if url.endswith("/pages"):
# List pages
response.json.return_value = {"data": {"pages": sample_pages}}
else:
# Get specific page
page_id = int(url.split("/")[-1]) if url.split("/")[-1].isdigit() else 1
page = next((p for p in sample_pages if p["id"] == page_id), sample_pages[0])
response.json.return_value = page
elif "pages" in url and method.upper() == "POST":
# Create page
new_page = {
"id": len(sample_pages) + 1,
"title": kwargs.get("json", {}).get("title", "New Page"),
"path": kwargs.get("json", {}).get("path", "new-page"),
"content": kwargs.get("json", {}).get("content", ""),
"created_at": datetime.now().isoformat() + "Z",
"updated_at": datetime.now().isoformat() + "Z",
"is_published": kwargs.get("json", {}).get("is_published", True),
"tags": kwargs.get("json", {}).get("tags", [])
}
sample_pages.append(new_page)
response.json.return_value = new_page
response.status_code = 201
elif "pages" in url and method.upper() == "PUT":
# Update page
page_id = int(url.split("/")[-1]) if url.split("/")[-1].isdigit() else 1
page = next((p for p in sample_pages if p["id"] == page_id), sample_pages[0])
# Update fields from request
update_data = kwargs.get("json", {})
for key, value in update_data.items():
if key in page:
page[key] = value
page["updated_at"] = datetime.now().isoformat() + "Z"
response.json.return_value = page
elif "pages" in url and method.upper() == "DELETE":
# Delete page
page_id = int(url.split("/")[-1]) if url.split("/")[-1].isdigit() else 1
sample_pages[:] = [p for p in sample_pages if p["id"] != page_id]
response.json.return_value = {"success": True}
response.status_code = 204
else:
# Default response
response.json.return_value = {"message": "Success"}
return response
mock_session.request.side_effect = mock_request
return mock_session
def experiment_client_setup():
"""Experiment with client setup."""
print_header("🔧 CLIENT SETUP EXPERIMENT")
print_info("Let's create different types of Wiki.js clients!")
print("\n1. Creating a client with API key authentication:")
print_code("client = WikiJSClient('https://wiki.example.com', auth='your-api-key')")
try:
client = WikiJSClient('https://wiki.example.com', auth='demo-api-key-12345')
print_success(f"Client created! Base URL: {client.base_url}")
print_info(f"Auth type: {type(client._auth_handler).__name__}")
except Exception as e:
print_error(f"Error creating client: {e}")
wait_for_enter()
print("\n2. Creating a client with JWT authentication:")
print_code("jwt_token = 'eyJ0eXAiOiJKV1Q...'")
print_code("jwt_auth = JWTAuth(jwt_token)")
print_code("client = WikiJSClient('https://wiki.example.com', auth=jwt_auth)")
try:
jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
jwt_auth = JWTAuth(jwt_token)
jwt_client = WikiJSClient('https://wiki.example.com', auth=jwt_auth)
print_success("JWT client created successfully!")
print_info(f"Token preview: {jwt_auth.token_preview}")
except Exception as e:
print_error(f"Error creating JWT client: {e}")
wait_for_enter()
print("\n3. URL normalization demo:")
test_urls = [
"wiki.example.com",
"https://wiki.example.com/",
"http://localhost:3000///",
"wiki.company.internal:8080"
]
for url in test_urls:
try:
client = WikiJSClient(url, auth='test-key')
print_success(f"'{url}''{client.base_url}'")
except Exception as e:
print_error(f"'{url}' → Error: {e}")
return client
def experiment_data_models():
"""Experiment with data models."""
print_header("📋 DATA MODELS EXPERIMENT")
print_info("Let's create and manipulate Wiki.js data models!")
print("\n1. Creating a new page:")
print_code("""
page_data = PageCreate(
title="My Awesome Page",
path="awesome-page",
content="# Welcome\\n\\nThis is **awesome** content!",
tags=["awesome", "demo"],
is_published=True
)""")
try:
page_data = PageCreate(
title="My Awesome Page",
path="awesome-page",
content="# Welcome\n\nThis is **awesome** content!",
tags=["awesome", "demo"],
is_published=True
)
print_success("PageCreate model created!")
print_info(f"Title: {page_data.title}")
print_info(f"Path: {page_data.path}")
print_info(f"Tags: {page_data.tags}")
except Exception as e:
print_error(f"Error creating page model: {e}")
wait_for_enter()
print("\n2. Model serialization:")
print_code("page_dict = page_data.to_dict()")
print_code("page_json = page_data.to_json()")
try:
page_dict = page_data.to_dict()
page_json = page_data.to_json()
print_success("Serialization successful!")
print_info("Dictionary format:")
print(json.dumps(page_dict, indent=2))
print_info("\nJSON format:")
print(page_json)
except Exception as e:
print_error(f"Serialization error: {e}")
wait_for_enter()
print("\n3. Creating update data:")
print_code("""
update_data = PageUpdate(
title="Updated Awesome Page",
content="# Updated Content\\n\\nThis content has been updated!",
tags=["awesome", "demo", "updated"]
)""")
try:
update_data = PageUpdate(
title="Updated Awesome Page",
content="# Updated Content\n\nThis content has been updated!",
tags=["awesome", "demo", "updated"]
)
print_success("PageUpdate model created!")
print_info(f"New title: {update_data.title}")
print_info(f"New tags: {update_data.tags}")
except Exception as e:
print_error(f"Error creating update model: {e}")
return page_data, update_data
@patch('wikijs.client.requests.Session')
def experiment_api_operations(mock_session_class, client, page_data, update_data):
"""Experiment with API operations."""
print_header("🌐 API OPERATIONS EXPERIMENT")
# Set up mock session
mock_session = setup_mock_session()
mock_session_class.return_value = mock_session
print_info("Let's try different API operations with mocked responses!")
print("\n1. Listing all pages:")
print_code("pages = client.pages.list()")
try:
pages = client.pages.list()
print_success(f"Found {len(pages)} pages!")
for i, page in enumerate(pages[:3], 1):
print_info(f"{i}. {page.title} ({page.path}) - {len(page.tags)} tags")
except Exception as e:
print_error(f"Error listing pages: {e}")
wait_for_enter()
print("\n2. Getting a specific page:")
print_code("page = client.pages.get(1)")
try:
page = client.pages.get(1)
print_success("Page retrieved!")
print_info(f"Title: {page.title}")
print_info(f"Path: {page.path}")
print_info(f"Published: {page.is_published}")
print_info(f"Content preview: {page.content[:50]}...")
except Exception as e:
print_error(f"Error getting page: {e}")
wait_for_enter()
print("\n3. Creating a new page:")
print_code("new_page = client.pages.create(page_data)")
try:
new_page = client.pages.create(page_data)
print_success("Page created!")
print_info(f"New page ID: {new_page.id}")
print_info(f"Title: {new_page.title}")
print_info(f"Created at: {new_page.created_at}")
except Exception as e:
print_error(f"Error creating page: {e}")
wait_for_enter()
print("\n4. Updating a page:")
print_code("updated_page = client.pages.update(1, update_data)")
try:
updated_page = client.pages.update(1, update_data)
print_success("Page updated!")
print_info(f"Updated title: {updated_page.title}")
print_info(f"Updated at: {updated_page.updated_at}")
except Exception as e:
print_error(f"Error updating page: {e}")
wait_for_enter()
print("\n5. Searching pages:")
print_code("search_results = client.pages.search('guide')")
try:
search_results = client.pages.search('guide')
print_success(f"Found {len(search_results)} matching pages!")
for result in search_results:
print_info(f"{result.title} - {result.path}")
except Exception as e:
print_error(f"Error searching pages: {e}")
def experiment_error_handling():
"""Experiment with error handling."""
print_header("⚠️ ERROR HANDLING EXPERIMENT")
print_info("Let's see how the SDK handles different types of errors!")
print("\n1. Validation errors:")
print_code("""
try:
invalid_page = PageCreate(title="", path="", content="")
except ValidationError as e:
print(f"Validation error: {e}")
""")
try:
invalid_page = PageCreate(title="", path="", content="")
print_warning("Expected validation error, but none occurred!")
except ValidationError as e:
print_success(f"Caught validation error: {e}")
except Exception as e:
print_error(f"Unexpected error: {e}")
wait_for_enter()
print("\n2. Authentication errors:")
print_code("""
try:
bad_auth = APIKeyAuth("")
except ValidationError as e:
print(f"Auth error: {e}")
""")
try:
bad_auth = APIKeyAuth("")
print_warning("Expected authentication error, but none occurred!")
except ValidationError as e:
print_success(f"Caught auth error: {e}")
except Exception as e:
print_error(f"Unexpected error: {e}")
wait_for_enter()
print("\n3. URL validation errors:")
print_code("""
try:
from wikijs.utils.helpers import normalize_url
normalize_url("")
except ValidationError as e:
print(f"URL error: {e}")
""")
try:
from wikijs.utils.helpers import normalize_url
normalize_url("")
print_warning("Expected URL validation error!")
except ValidationError as e:
print_success(f"Caught URL error: {e}")
except Exception as e:
print_error(f"Unexpected error: {e}")
def experiment_utilities():
"""Experiment with utility functions."""
print_header("🛠️ UTILITIES EXPERIMENT")
print_info("Let's try out the SDK's utility functions!")
from wikijs.utils.helpers import (
normalize_url, sanitize_path, chunk_list,
safe_get, build_api_url
)
print("\n1. URL normalization:")
test_urls = [
"wiki.example.com",
"https://wiki.example.com/",
"localhost:3000",
"wiki.company.internal:8080/"
]
for url in test_urls:
try:
normalized = normalize_url(url)
print_success(f"'{url}''{normalized}'")
except Exception as e:
print_error(f"'{url}' → Error: {e}")
wait_for_enter()
print("\n2. Path sanitization:")
test_paths = [
"hello world",
"/my/wiki/page/",
"special-chars!@#$",
" multiple spaces "
]
for path in test_paths:
try:
sanitized = sanitize_path(path)
print_success(f"'{path}''{sanitized}'")
except Exception as e:
print_error(f"'{path}' → Error: {e}")
wait_for_enter()
print("\n3. List chunking:")
test_list = list(range(1, 13)) # [1, 2, 3, ..., 12]
chunk_sizes = [3, 4, 5]
for size in chunk_sizes:
chunks = chunk_list(test_list, size)
print_success(f"Chunks of {size}: {chunks}")
wait_for_enter()
print("\n4. Safe dictionary access:")
test_data = {
"user": {
"profile": {
"name": "John Doe",
"email": "john@example.com"
}
},
"settings": {
"theme": "dark",
"notifications": True
}
}
test_keys = [
"user.profile.name",
"user.profile.email",
"settings.theme",
"user.missing.key",
"nonexistent"
]
for key in test_keys:
value = safe_get(test_data, key, "NOT_FOUND")
print_success(f"'{key}'{value}")
def main():
"""Main experimentation function."""
print_header("🧪 WIKI.JS SDK EXPERIMENTATION LAB")
print(f"{Colors.CYAN}Welcome to the Wiki.js Python SDK Experiment Lab!{Colors.END}")
print(f"{Colors.CYAN}Here you can safely try out all the SDK features with mocked data.{Colors.END}")
wait_for_enter("Ready to start experimenting?")
# Experiment with different features
client = experiment_client_setup()
page_data, update_data = experiment_data_models()
experiment_api_operations(client, page_data, update_data)
experiment_error_handling()
experiment_utilities()
# Final summary
print_header("🎉 EXPERIMENT COMPLETE")
print_success("Congratulations! You've experimented with:")
print_info("✨ Client setup and authentication")
print_info("✨ Data models and serialization")
print_info("✨ API operations (mocked)")
print_info("✨ Error handling")
print_info("✨ Utility functions")
print(f"\n{Colors.YELLOW}💡 Next steps:{Colors.END}")
print(f"{Colors.CYAN}1. Check out the examples/ directory for real-world usage{Colors.END}")
print(f"{Colors.CYAN}2. Read the docs/user_guide.md for detailed documentation{Colors.END}")
print(f"{Colors.CYAN}3. Try connecting to a real Wiki.js instance{Colors.END}")
print(f"\n{Colors.GREEN}Happy coding with Wiki.js SDK! 🚀{Colors.END}")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print(f"\n\n{Colors.YELLOW}Experiment interrupted. Goodbye! 👋{Colors.END}")
sys.exit(0)
except Exception as e:
print_error(f"Unexpected error: {e}")
sys.exit(1)

View File

@@ -5,14 +5,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "wikijs-python-sdk"
description = "A professional Python SDK for Wiki.js API integration"
authors = [{name = "Wiki.js SDK Contributors"}]
license = {text = "MIT"}
authors = [{name = "Wiki.js SDK Contributors", email = "contact@wikijs-sdk.dev"}]
license = "MIT"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
@@ -31,6 +30,7 @@ dependencies = [
"typing-extensions>=4.0.0",
]
dynamic = ["version"]
keywords = ["wiki", "wikijs", "api", "sdk", "client", "http", "rest"]
[project.optional-dependencies]
dev = [

View File

@@ -37,7 +37,7 @@ setup(
name="wikijs-python-sdk",
version=get_version(),
author="Wiki.js SDK Contributors",
author_email="",
author_email="contact@wikijs-sdk.dev",
description="A professional Python SDK for Wiki.js API integration",
long_description=get_long_description(),
long_description_content_type="text/markdown",

View File

@@ -1,341 +0,0 @@
#!/usr/bin/env python3
"""
Test Runner for Wiki.js Python SDK
This file provides a simple way to test the SDK functionality without needing
a real Wiki.js instance. It uses mocked responses to simulate API interactions.
Usage:
python test_runner.py
Or import and run specific tests:
from test_runner import run_all_tests, test_client_creation
run_all_tests()
"""
import json
from unittest.mock import Mock, patch
from datetime import datetime
# Import SDK components
from wikijs import WikiJSClient
from wikijs.models import PageCreate, PageUpdate
from wikijs.auth import APIKeyAuth, JWTAuth
from wikijs.exceptions import APIError, ValidationError
def test_client_creation():
"""Test basic client creation and configuration."""
print("🔧 Testing client creation...")
# Test with API key
client = WikiJSClient("https://wiki.example.com", auth="test-api-key")
assert client.base_url == "https://wiki.example.com"
assert isinstance(client._auth_handler, APIKeyAuth)
print(" ✅ API key authentication works")
# Test with JWT
jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
jwt_auth = JWTAuth(jwt_token)
client_jwt = WikiJSClient("https://wiki.example.com", auth=jwt_auth)
assert isinstance(client_jwt._auth_handler, JWTAuth)
print(" ✅ JWT authentication works")
# Test URL normalization
client_normalized = WikiJSClient("wiki.example.com/", auth="test-key")
assert client_normalized.base_url == "https://wiki.example.com"
print(" ✅ URL normalization works")
print("✅ Client creation tests passed!\n")
return True
def test_models():
"""Test data model functionality."""
print("📋 Testing data models...")
# Test PageCreate model
page_data = PageCreate(
title="Test Page",
path="test-page",
content="# Hello World\n\nThis is a test page.",
tags=["test", "example"],
)
assert page_data.title == "Test Page"
assert page_data.path == "test-page"
assert "test" in page_data.tags
print(" ✅ PageCreate model works")
# Test model serialization
page_dict = page_data.to_dict()
assert page_dict["title"] == "Test Page"
assert isinstance(page_dict, dict)
print(" ✅ Model serialization works")
# Test JSON serialization
page_json = page_data.to_json()
parsed = json.loads(page_json)
assert parsed["title"] == "Test Page"
print(" ✅ JSON serialization works")
# Test PageUpdate model
update_data = PageUpdate(title="Updated Title", content="Updated content")
assert update_data.title == "Updated Title"
print(" ✅ PageUpdate model works")
print("✅ Data model tests passed!\n")
return True
@patch("wikijs.client.requests.Session")
def test_mocked_api_calls(mock_session_class):
"""Test API calls with mocked responses."""
print("🌐 Testing mocked API calls...")
# Setup mock session
mock_session = Mock()
mock_session_class.return_value = mock_session
# Mock successful response for list pages
mock_response = Mock()
mock_response.ok = True
mock_response.status_code = 200
mock_response.json.return_value = {
"data": {
"pages": [
{
"id": 1,
"title": "Home Page",
"path": "home",
"content": "Welcome to the wiki!",
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T12:00:00Z",
"is_published": True,
"tags": ["welcome"],
},
{
"id": 2,
"title": "Getting Started",
"path": "getting-started",
"content": "How to use this wiki.",
"created_at": "2023-01-02T00:00:00Z",
"updated_at": "2023-01-02T10:00:00Z",
"is_published": True,
"tags": ["guide"],
},
]
}
}
mock_session.request.return_value = mock_response
# Test client with mocked session
client = WikiJSClient("https://wiki.example.com", auth="test-key")
# Test list pages (this would normally make an HTTP request)
try:
pages = client.pages.list()
print(" ✅ Pages list method called successfully")
except Exception as e:
print(f" ⚠️ List pages method exists but may need actual implementation: {e}")
# Test individual page operations
try:
# Mock response for creating a page
mock_response.json.return_value = {
"id": 3,
"title": "New Page",
"path": "new-page",
"content": "This is new content",
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"is_published": True,
"tags": [],
}
page_data = PageCreate(
title="New Page", path="new-page", content="This is new content"
)
new_page = client.pages.create(page_data)
print(" ✅ Page creation method called successfully")
except Exception as e:
print(
f" ⚠️ Page creation method exists but may need implementation details: {e}"
)
print("✅ Mocked API call tests completed!\n")
return True
def test_authentication():
"""Test different authentication methods."""
print("🔐 Testing authentication methods...")
# Test API Key Authentication
api_auth = APIKeyAuth("test-api-key-12345")
headers = api_auth.get_headers()
assert "Authorization" in headers
assert "Bearer test-api-key-12345" in headers["Authorization"]
assert api_auth.is_valid() == True
print(" ✅ API Key authentication works")
# Test JWT Authentication
jwt_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
jwt_auth = JWTAuth(jwt_token)
jwt_headers = jwt_auth.get_headers()
assert "Authorization" in jwt_headers
assert jwt_token in jwt_headers["Authorization"]
print(" ✅ JWT authentication works")
# Test authentication validation
try:
api_auth.validate_credentials()
print(" ✅ Authentication validation works")
except Exception as e:
print(f" ⚠️ Authentication validation: {e}")
print("✅ Authentication tests passed!\n")
return True
def test_exceptions():
"""Test exception handling."""
print("⚠️ Testing exception handling...")
# Test validation errors
try:
PageCreate(title="", path="invalid path", content="test")
print(" ❌ Should have raised validation error")
except ValidationError:
print(" ✅ Validation error handling works")
except Exception as e:
print(f" ⚠️ Got different exception: {e}")
# Test API error creation
try:
from wikijs.exceptions import create_api_error
error = create_api_error(404, "Not found", None)
assert "Not found" in str(error)
print(" ✅ API error creation works")
except Exception as e:
print(f" ⚠️ API error creation: {e}")
print("✅ Exception handling tests completed!\n")
return True
def test_utilities():
"""Test utility functions."""
print("🛠️ Testing utility functions...")
from wikijs.utils.helpers import normalize_url, sanitize_path, chunk_list
# Test URL normalization
normalized = normalize_url("wiki.example.com/")
assert normalized == "https://wiki.example.com"
print(" ✅ URL normalization works")
# Test path sanitization
try:
sanitized = sanitize_path("hello world/test")
assert "hello-world" in sanitized
print(" ✅ Path sanitization works")
except Exception as e:
print(f" ⚠️ Path sanitization: {e}")
# Test list chunking
chunks = chunk_list([1, 2, 3, 4, 5], 2)
assert len(chunks) == 3
assert chunks[0] == [1, 2]
print(" ✅ List chunking works")
print("✅ Utility function tests passed!\n")
return True
def run_all_tests():
"""Run all test functions."""
print("🚀 Running Wiki.js Python SDK Tests")
print("=" * 50)
tests = [
test_client_creation,
test_models,
test_authentication,
test_mocked_api_calls,
test_exceptions,
test_utilities,
]
passed = 0
total = len(tests)
for test_func in tests:
try:
if test_func():
passed += 1
except Exception as e:
print(f"{test_func.__name__} failed: {e}\n")
print("=" * 50)
print(f"📊 Test Results: {passed}/{total} tests passed")
if passed == total:
print("🎉 All tests passed! The SDK is working correctly.")
else:
print(f"⚠️ {total - passed} tests had issues. Check output above for details.")
return passed == total
def demo_usage():
"""Demonstrate basic SDK usage."""
print("\n" + "=" * 50)
print("📖 SDK USAGE DEMO")
print("=" * 50)
print("1. Creating a client:")
print(" client = WikiJSClient('https://wiki.example.com', auth='your-api-key')")
print("\n2. Creating page data:")
print(" page_data = PageCreate(")
print(" title='My Page',")
print(" path='my-page',")
print(" content='# Hello\\n\\nThis is my page content!'")
print(" )")
print("\n3. Working with the client:")
print(" # List pages")
print(" pages = client.pages.list()")
print(" ")
print(" # Create a page")
print(" new_page = client.pages.create(page_data)")
print(" ")
print(" # Get a specific page")
print(" page = client.pages.get(123)")
print(" ")
print(" # Update a page")
print(" update_data = PageUpdate(title='Updated Title')")
print(" updated_page = client.pages.update(123, update_data)")
print("\n4. Error handling:")
print(" try:")
print(" page = client.pages.get(999)")
print(" except NotFoundError:")
print(" print('Page not found!')")
print(" except APIError as e:")
print(" print(f'API error: {e}')")
if __name__ == "__main__":
# Run all tests
success = run_all_tests()
# Show usage demo
demo_usage()
# Exit with appropriate code
exit(0 if success else 1)

View File

@@ -82,68 +82,32 @@ class PagesEndpoint(BaseEndpoint):
if order_direction not in ["ASC", "DESC"]:
raise ValidationError("order_direction must be ASC or DESC")
# Build GraphQL query
# Build GraphQL query using actual Wiki.js schema
query = """
query($limit: Int, $offset: Int, $search: String, $tags: [String], $locale: String, $authorId: Int, $orderBy: String, $orderDirection: String) {
pages(
limit: $limit,
offset: $offset,
search: $search,
tags: $tags,
locale: $locale,
authorId: $authorId,
orderBy: $orderBy,
orderDirection: $orderDirection
) {
id
title
path
content
description
isPublished
isPrivate
tags
locale
authorId
authorName
authorEmail
editor
createdAt
updatedAt
query {
pages {
list {
id
title
path
isPublished
createdAt
updatedAt
}
}
}
"""
# Build variables
variables = {
"orderBy": order_by,
"orderDirection": order_direction
}
if limit is not None:
variables["limit"] = limit
if offset is not None:
variables["offset"] = offset
if search:
variables["search"] = search
if tags:
variables["tags"] = tags
if locale:
variables["locale"] = locale
if author_id is not None:
variables["authorId"] = author_id
# Make request
# Make request (no variables needed for simple list query)
response = self._post("/graphql", json_data={
"query": query,
"variables": variables
"query": query
})
# Parse response
if "errors" in response:
raise APIError(f"GraphQL errors: {response['errors']}")
pages_data = response.get("data", {}).get("pages", [])
pages_data = response.get("data", {}).get("pages", {}).get("list", [])
# Convert to Page objects
pages = []
@@ -174,25 +138,29 @@ class PagesEndpoint(BaseEndpoint):
if not isinstance(page_id, int) or page_id < 1:
raise ValidationError("page_id must be a positive integer")
# Build GraphQL query
# Build GraphQL query using actual Wiki.js schema
query = """
query($id: Int!) {
page(id: $id) {
id
title
path
content
description
isPublished
isPrivate
tags
locale
authorId
authorName
authorEmail
editor
createdAt
updatedAt
pages {
single(id: $id) {
id
title
path
content
description
isPublished
isPrivate
tags {
tag
}
locale
authorId
authorName
authorEmail
editor
createdAt
updatedAt
}
}
}
"""
@@ -207,7 +175,7 @@ class PagesEndpoint(BaseEndpoint):
if "errors" in response:
raise APIError(f"GraphQL errors: {response['errors']}")
page_data = response.get("data", {}).get("page")
page_data = response.get("data", {}).get("pages", {}).get("single")
if not page_data:
raise APIError(f"Page with ID {page_id} not found")
@@ -304,35 +272,37 @@ class PagesEndpoint(BaseEndpoint):
elif not isinstance(page_data, PageCreate):
raise ValidationError("page_data must be PageCreate object or dict")
# Build GraphQL mutation
# Build GraphQL mutation using actual Wiki.js schema
mutation = """
mutation($title: String!, $path: String!, $content: String!, $description: String, $isPublished: Boolean, $isPrivate: Boolean, $tags: [String], $locale: String, $editor: String) {
createPage(
title: $title,
path: $path,
content: $content,
description: $description,
isPublished: $isPublished,
isPrivate: $isPrivate,
tags: $tags,
locale: $locale,
editor: $editor
) {
id
title
path
content
description
isPublished
isPrivate
tags
locale
authorId
authorName
authorEmail
editor
createdAt
updatedAt
mutation($content: String!, $description: String!, $editor: String!, $isPublished: Boolean!, $isPrivate: Boolean!, $locale: String!, $path: String!, $tags: [String]!, $title: String!) {
pages {
create(content: $content, description: $description, editor: $editor, isPublished: $isPublished, isPrivate: $isPrivate, locale: $locale, path: $path, tags: $tags, title: $title) {
responseResult {
succeeded
errorCode
slug
message
}
page {
id
title
path
content
description
isPublished
isPrivate
tags {
tag
}
locale
authorId
authorName
authorEmail
editor
createdAt
updatedAt
}
}
}
}
"""
@@ -342,6 +312,7 @@ class PagesEndpoint(BaseEndpoint):
"title": page_data.title,
"path": page_data.path,
"content": page_data.content,
"description": page_data.description or f"Created via SDK: {page_data.title}",
"isPublished": page_data.is_published,
"isPrivate": page_data.is_private,
"tags": page_data.tags,
@@ -349,9 +320,6 @@ class PagesEndpoint(BaseEndpoint):
"editor": page_data.editor
}
if page_data.description is not None:
variables["description"] = page_data.description
# Make request
response = self._post("/graphql", json_data={
"query": mutation,
@@ -362,9 +330,16 @@ class PagesEndpoint(BaseEndpoint):
if "errors" in response:
raise APIError(f"Failed to create page: {response['errors']}")
created_page_data = response.get("data", {}).get("createPage")
create_result = response.get("data", {}).get("pages", {}).get("create", {})
response_result = create_result.get("responseResult", {})
if not response_result.get("succeeded"):
error_msg = response_result.get("message", "Unknown error")
raise APIError(f"Page creation failed: {error_msg}")
created_page_data = create_result.get("page")
if not created_page_data:
raise APIError("Page creation failed - no data returned")
raise APIError("Page creation failed - no page data returned")
# Convert to Page object
try:
@@ -613,7 +588,6 @@ class PagesEndpoint(BaseEndpoint):
"description": "description",
"isPublished": "is_published",
"isPrivate": "is_private",
"tags": "tags",
"locale": "locale",
"authorId": "author_id",
"authorName": "author_name",
@@ -627,8 +601,20 @@ class PagesEndpoint(BaseEndpoint):
if api_field in page_data:
normalized[model_field] = page_data[api_field]
# Ensure required fields have defaults
if "tags" not in normalized:
# Handle tags - convert from Wiki.js format
if "tags" in page_data:
if isinstance(page_data["tags"], list):
# Handle both formats: ["tag1", "tag2"] or [{"tag": "tag1"}, {"tag": "tag2"}]
tags = []
for tag in page_data["tags"]:
if isinstance(tag, dict) and "tag" in tag:
tags.append(tag["tag"])
elif isinstance(tag, str):
tags.append(tag)
normalized["tags"] = tags
else:
normalized["tags"] = []
else:
normalized["tags"] = []
return normalized

View File

@@ -19,7 +19,7 @@ class Page(TimestampedModel):
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")
content: Optional[str] = Field(None, description="Page content")
# Optional fields that may be present
description: Optional[str] = Field(None, description="Page description")

21
working_playground.py Normal file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from wikijs import WikiJSClient
client = WikiJSClient(
base_url="https://wikijs.hotserv.cloud",
auth="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGkiOjEsImdycCI6MSwiaWF0IjoxNTM4ODM1NTQ1LCJleHAiOjE3ODUzOTMxNDUsImF1ZCI6InVybjp3aWtpLmpzIiwiaXNzIjoidXJuOndpa2kuanMifQ.d1fCZMqS-4gR5TfcMU4CLc_mD-uyYxlUxPbxbqqdIazruKKmBLACkVEumf-RFgEatsuCQjQiU0A6E_IfwFBgqFy1g5W_Ly9st7_5k6JOHfn4shGnCrRv3FBLHOtiRUexURcXNvHxh00oEJ8IPuhmTDSpc1g5ssVeNR9oHwz8V-CIvtmP_S5NIalTVEeOXmSSfyHXK4_sMx8zbBb8tCHNt1tbhZ8Z5N--pqvWZFC_ddYZ8-kMkQo-ni1rP48WLpEngWCij6mAPKhdqLjykmIkZF_hwnfvunG7iIZpFVoUJ3uIc09GkIVa5VdpcBHD4w1rnpouWZP8FuR9aHlAL7sB3Q"
)
print("✅ Client created")
print("✅ Connection:", client.test_connection())
pages = client.pages.list()
print(f"✅ Found {len(pages)} pages")
for i, page in enumerate(pages[:5], 1):
print(f" {i}. {page.title} (ID: {page.id})")
client.close()
print("✅ SDK working!")