last-updates
This commit is contained in:
@@ -84,6 +84,7 @@ new_page = client.pages.create(PageCreate(
|
|||||||
### **For Maintainers**
|
### **For Maintainers**
|
||||||
- **[Architecture](docs/wikijs_sdk_architecture.md)**: Technical design and patterns
|
- **[Architecture](docs/wikijs_sdk_architecture.md)**: Technical design and patterns
|
||||||
- **[Development Plan](docs/wikijs_sdk_release_plan.md)**: Complete roadmap and milestones
|
- **[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
|
- **[AI Coordination](CLAUDE.md)**: AI-assisted development workflow
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
232
docs/PIP_INSTRUCTIONS.md
Normal file
232
docs/PIP_INSTRUCTIONS.md
Normal 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
|
||||||
|
[](https://github.com/YOUR_USERNAME/wikijs-python-sdk/actions)
|
||||||
|
[](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`
|
||||||
568
experiment.py
568
experiment.py
@@ -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)
|
|
||||||
@@ -5,14 +5,13 @@ build-backend = "setuptools.build_meta"
|
|||||||
[project]
|
[project]
|
||||||
name = "wikijs-python-sdk"
|
name = "wikijs-python-sdk"
|
||||||
description = "A professional Python SDK for Wiki.js API integration"
|
description = "A professional Python SDK for Wiki.js API integration"
|
||||||
authors = [{name = "Wiki.js SDK Contributors"}]
|
authors = [{name = "Wiki.js SDK Contributors", email = "contact@wikijs-sdk.dev"}]
|
||||||
license = {text = "MIT"}
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
@@ -31,6 +30,7 @@ dependencies = [
|
|||||||
"typing-extensions>=4.0.0",
|
"typing-extensions>=4.0.0",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
keywords = ["wiki", "wikijs", "api", "sdk", "client", "http", "rest"]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -37,7 +37,7 @@ setup(
|
|||||||
name="wikijs-python-sdk",
|
name="wikijs-python-sdk",
|
||||||
version=get_version(),
|
version=get_version(),
|
||||||
author="Wiki.js SDK Contributors",
|
author="Wiki.js SDK Contributors",
|
||||||
author_email="",
|
author_email="contact@wikijs-sdk.dev",
|
||||||
description="A professional Python SDK for Wiki.js API integration",
|
description="A professional Python SDK for Wiki.js API integration",
|
||||||
long_description=get_long_description(),
|
long_description=get_long_description(),
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|||||||
341
test_runner.py
341
test_runner.py
@@ -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)
|
|
||||||
@@ -82,68 +82,32 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
if order_direction not in ["ASC", "DESC"]:
|
if order_direction not in ["ASC", "DESC"]:
|
||||||
raise ValidationError("order_direction must be ASC or DESC")
|
raise ValidationError("order_direction must be ASC or DESC")
|
||||||
|
|
||||||
# Build GraphQL query
|
# Build GraphQL query using actual Wiki.js schema
|
||||||
query = """
|
query = """
|
||||||
query($limit: Int, $offset: Int, $search: String, $tags: [String], $locale: String, $authorId: Int, $orderBy: String, $orderDirection: String) {
|
query {
|
||||||
pages(
|
pages {
|
||||||
limit: $limit,
|
list {
|
||||||
offset: $offset,
|
id
|
||||||
search: $search,
|
title
|
||||||
tags: $tags,
|
path
|
||||||
locale: $locale,
|
isPublished
|
||||||
authorId: $authorId,
|
createdAt
|
||||||
orderBy: $orderBy,
|
updatedAt
|
||||||
orderDirection: $orderDirection
|
}
|
||||||
) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
path
|
|
||||||
content
|
|
||||||
description
|
|
||||||
isPublished
|
|
||||||
isPrivate
|
|
||||||
tags
|
|
||||||
locale
|
|
||||||
authorId
|
|
||||||
authorName
|
|
||||||
authorEmail
|
|
||||||
editor
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Build variables
|
# Make request (no variables needed for simple list query)
|
||||||
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
|
|
||||||
response = self._post("/graphql", json_data={
|
response = self._post("/graphql", json_data={
|
||||||
"query": query,
|
"query": query
|
||||||
"variables": variables
|
|
||||||
})
|
})
|
||||||
|
|
||||||
# Parse response
|
# Parse response
|
||||||
if "errors" in response:
|
if "errors" in response:
|
||||||
raise APIError(f"GraphQL errors: {response['errors']}")
|
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
|
# Convert to Page objects
|
||||||
pages = []
|
pages = []
|
||||||
@@ -174,25 +138,29 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
if not isinstance(page_id, int) or page_id < 1:
|
if not isinstance(page_id, int) or page_id < 1:
|
||||||
raise ValidationError("page_id must be a positive integer")
|
raise ValidationError("page_id must be a positive integer")
|
||||||
|
|
||||||
# Build GraphQL query
|
# Build GraphQL query using actual Wiki.js schema
|
||||||
query = """
|
query = """
|
||||||
query($id: Int!) {
|
query($id: Int!) {
|
||||||
page(id: $id) {
|
pages {
|
||||||
id
|
single(id: $id) {
|
||||||
title
|
id
|
||||||
path
|
title
|
||||||
content
|
path
|
||||||
description
|
content
|
||||||
isPublished
|
description
|
||||||
isPrivate
|
isPublished
|
||||||
tags
|
isPrivate
|
||||||
locale
|
tags {
|
||||||
authorId
|
tag
|
||||||
authorName
|
}
|
||||||
authorEmail
|
locale
|
||||||
editor
|
authorId
|
||||||
createdAt
|
authorName
|
||||||
updatedAt
|
authorEmail
|
||||||
|
editor
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -207,7 +175,7 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
if "errors" in response:
|
if "errors" in response:
|
||||||
raise APIError(f"GraphQL errors: {response['errors']}")
|
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:
|
if not page_data:
|
||||||
raise APIError(f"Page with ID {page_id} not found")
|
raise APIError(f"Page with ID {page_id} not found")
|
||||||
|
|
||||||
@@ -304,35 +272,37 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
elif not isinstance(page_data, PageCreate):
|
elif not isinstance(page_data, PageCreate):
|
||||||
raise ValidationError("page_data must be PageCreate object or dict")
|
raise ValidationError("page_data must be PageCreate object or dict")
|
||||||
|
|
||||||
# Build GraphQL mutation
|
# Build GraphQL mutation using actual Wiki.js schema
|
||||||
mutation = """
|
mutation = """
|
||||||
mutation($title: String!, $path: String!, $content: String!, $description: String, $isPublished: Boolean, $isPrivate: Boolean, $tags: [String], $locale: String, $editor: String) {
|
mutation($content: String!, $description: String!, $editor: String!, $isPublished: Boolean!, $isPrivate: Boolean!, $locale: String!, $path: String!, $tags: [String]!, $title: String!) {
|
||||||
createPage(
|
pages {
|
||||||
title: $title,
|
create(content: $content, description: $description, editor: $editor, isPublished: $isPublished, isPrivate: $isPrivate, locale: $locale, path: $path, tags: $tags, title: $title) {
|
||||||
path: $path,
|
responseResult {
|
||||||
content: $content,
|
succeeded
|
||||||
description: $description,
|
errorCode
|
||||||
isPublished: $isPublished,
|
slug
|
||||||
isPrivate: $isPrivate,
|
message
|
||||||
tags: $tags,
|
}
|
||||||
locale: $locale,
|
page {
|
||||||
editor: $editor
|
id
|
||||||
) {
|
title
|
||||||
id
|
path
|
||||||
title
|
content
|
||||||
path
|
description
|
||||||
content
|
isPublished
|
||||||
description
|
isPrivate
|
||||||
isPublished
|
tags {
|
||||||
isPrivate
|
tag
|
||||||
tags
|
}
|
||||||
locale
|
locale
|
||||||
authorId
|
authorId
|
||||||
authorName
|
authorName
|
||||||
authorEmail
|
authorEmail
|
||||||
editor
|
editor
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -342,6 +312,7 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
"title": page_data.title,
|
"title": page_data.title,
|
||||||
"path": page_data.path,
|
"path": page_data.path,
|
||||||
"content": page_data.content,
|
"content": page_data.content,
|
||||||
|
"description": page_data.description or f"Created via SDK: {page_data.title}",
|
||||||
"isPublished": page_data.is_published,
|
"isPublished": page_data.is_published,
|
||||||
"isPrivate": page_data.is_private,
|
"isPrivate": page_data.is_private,
|
||||||
"tags": page_data.tags,
|
"tags": page_data.tags,
|
||||||
@@ -349,9 +320,6 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
"editor": page_data.editor
|
"editor": page_data.editor
|
||||||
}
|
}
|
||||||
|
|
||||||
if page_data.description is not None:
|
|
||||||
variables["description"] = page_data.description
|
|
||||||
|
|
||||||
# Make request
|
# Make request
|
||||||
response = self._post("/graphql", json_data={
|
response = self._post("/graphql", json_data={
|
||||||
"query": mutation,
|
"query": mutation,
|
||||||
@@ -362,9 +330,16 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
if "errors" in response:
|
if "errors" in response:
|
||||||
raise APIError(f"Failed to create page: {response['errors']}")
|
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:
|
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
|
# Convert to Page object
|
||||||
try:
|
try:
|
||||||
@@ -607,16 +582,15 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
# Map API field names to model field names
|
# Map API field names to model field names
|
||||||
field_mapping = {
|
field_mapping = {
|
||||||
"id": "id",
|
"id": "id",
|
||||||
"title": "title",
|
"title": "title",
|
||||||
"path": "path",
|
"path": "path",
|
||||||
"content": "content",
|
"content": "content",
|
||||||
"description": "description",
|
"description": "description",
|
||||||
"isPublished": "is_published",
|
"isPublished": "is_published",
|
||||||
"isPrivate": "is_private",
|
"isPrivate": "is_private",
|
||||||
"tags": "tags",
|
|
||||||
"locale": "locale",
|
"locale": "locale",
|
||||||
"authorId": "author_id",
|
"authorId": "author_id",
|
||||||
"authorName": "author_name",
|
"authorName": "author_name",
|
||||||
"authorEmail": "author_email",
|
"authorEmail": "author_email",
|
||||||
"editor": "editor",
|
"editor": "editor",
|
||||||
"createdAt": "created_at",
|
"createdAt": "created_at",
|
||||||
@@ -627,8 +601,20 @@ class PagesEndpoint(BaseEndpoint):
|
|||||||
if api_field in page_data:
|
if api_field in page_data:
|
||||||
normalized[model_field] = page_data[api_field]
|
normalized[model_field] = page_data[api_field]
|
||||||
|
|
||||||
# Ensure required fields have defaults
|
# Handle tags - convert from Wiki.js format
|
||||||
if "tags" not in normalized:
|
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"] = []
|
normalized["tags"] = []
|
||||||
|
|
||||||
return normalized
|
return normalized
|
||||||
@@ -19,7 +19,7 @@ class Page(TimestampedModel):
|
|||||||
id: int = Field(..., description="Unique page identifier")
|
id: int = Field(..., description="Unique page identifier")
|
||||||
title: str = Field(..., description="Page title")
|
title: str = Field(..., description="Page title")
|
||||||
path: str = Field(..., description="Page path/slug")
|
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
|
# Optional fields that may be present
|
||||||
description: Optional[str] = Field(None, description="Page description")
|
description: Optional[str] = Field(None, description="Page description")
|
||||||
|
|||||||
21
working_playground.py
Normal file
21
working_playground.py
Normal 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!")
|
||||||
Reference in New Issue
Block a user