From 40c801f0532456ab406ba5d24f6920fcc5ec0f33 Mon Sep 17 00:00:00 2001 From: l3ocho Date: Tue, 29 Jul 2025 21:01:46 -0400 Subject: [PATCH] last-updates --- README.md | 1 + docs/PIP_INSTRUCTIONS.md | 232 ++++++++++++++++ experiment.py | 568 -------------------------------------- pyproject.toml | 6 +- setup.py | 2 +- test_runner.py | 341 ----------------------- wikijs/endpoints/pages.py | 198 ++++++------- wikijs/models/page.py | 2 +- working_playground.py | 21 ++ 9 files changed, 351 insertions(+), 1020 deletions(-) create mode 100644 docs/PIP_INSTRUCTIONS.md delete mode 100644 experiment.py delete mode 100644 test_runner.py create mode 100644 working_playground.py diff --git a/README.md b/README.md index 2a0e6d5..1234164 100644 --- a/README.md +++ b/README.md @@ -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 --- diff --git a/docs/PIP_INSTRUCTIONS.md b/docs/PIP_INSTRUCTIONS.md new file mode 100644 index 0000000..e32dbdd --- /dev/null +++ b/docs/PIP_INSTRUCTIONS.md @@ -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` \ No newline at end of file diff --git a/experiment.py b/experiment.py deleted file mode 100644 index 009fb1b..0000000 --- a/experiment.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c0a66aa..fa1cba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/setup.py b/setup.py index 4fb9f8e..fa96b87 100644 --- a/setup.py +++ b/setup.py @@ -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", diff --git a/test_runner.py b/test_runner.py deleted file mode 100644 index 2b07bc4..0000000 --- a/test_runner.py +++ /dev/null @@ -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) diff --git a/wikijs/endpoints/pages.py b/wikijs/endpoints/pages.py index c85996a..882fb82 100644 --- a/wikijs/endpoints/pages.py +++ b/wikijs/endpoints/pages.py @@ -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: @@ -607,16 +582,15 @@ class PagesEndpoint(BaseEndpoint): # Map API field names to model field names field_mapping = { "id": "id", - "title": "title", + "title": "title", "path": "path", "content": "content", "description": "description", "isPublished": "is_published", "isPrivate": "is_private", - "tags": "tags", "locale": "locale", "authorId": "author_id", - "authorName": "author_name", + "authorName": "author_name", "authorEmail": "author_email", "editor": "editor", "createdAt": "created_at", @@ -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 \ No newline at end of file diff --git a/wikijs/models/page.py b/wikijs/models/page.py index 7b7e2c8..023cf91 100644 --- a/wikijs/models/page.py +++ b/wikijs/models/page.py @@ -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") diff --git a/working_playground.py b/working_playground.py new file mode 100644 index 0000000..0c43ad5 --- /dev/null +++ b/working_playground.py @@ -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!")