Fix code formatting and linting issues

- Updated GitHub Actions workflow to use correct flake8 configuration
- Fixed line length issues by using 88 characters as configured
- Removed unused imports and trailing whitespace
- Fixed f-string placeholders and unused variables
- All linting checks now pass with project configuration

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-30 20:49:40 -04:00
parent 16bd151337
commit ade9aacf56
33 changed files with 1099 additions and 1096 deletions

View File

@@ -23,13 +23,13 @@ jobs:
pip install -e ".[dev]" pip install -e ".[dev]"
- name: Lint with flake8 - name: Lint with flake8
run: flake8 wikijs tests run: flake8 wikijs tests --max-line-length=88 --ignore=E203,E501,W503
- name: Format check with black - name: Format check with black
run: black --check wikijs tests run: black --check wikijs tests --line-length=88
- name: Import sort check - name: Import sort check
run: isort --check-only wikijs tests run: isort --check-only wikijs tests --line-length=88
- name: Type check with mypy - name: Type check with mypy
run: mypy wikijs run: mypy wikijs

View File

@@ -39,7 +39,7 @@ class TestAPIKeyAuth:
expected_headers = { expected_headers = {
"Authorization": f"Bearer {mock_api_key}", "Authorization": f"Bearer {mock_api_key}",
"Content-Type": "application/json" "Content-Type": "application/json",
} }
assert headers == expected_headers assert headers == expected_headers
@@ -75,7 +75,9 @@ class TestAPIKeyAuth:
# Test long key (>8 chars) - shows first 4 and last 4 # Test long key (>8 chars) - shows first 4 and last 4
auth = APIKeyAuth("this-is-a-very-long-api-key-for-testing") auth = APIKeyAuth("this-is-a-very-long-api-key-for-testing")
expected = "this" + "*" * (len("this-is-a-very-long-api-key-for-testing") - 8) + "ting" expected = (
"this" + "*" * (len("this-is-a-very-long-api-key-for-testing") - 8) + "ting"
)
assert auth.api_key == expected assert auth.api_key == expected
def test_repr_shows_masked_key(self, mock_api_key): def test_repr_shows_masked_key(self, mock_api_key):
@@ -102,11 +104,19 @@ class TestAPIKeyAuth:
("abcd", "****"), ("abcd", "****"),
("abcdefgh", "********"), ("abcdefgh", "********"),
("abcdefghi", "abcd*fghi"), # 9 chars: first 4 + 1 star + last 4 ("abcdefghi", "abcd*fghi"), # 9 chars: first 4 + 1 star + last 4
("abcdefghij", "abcd**ghij"), # 10 chars: first 4 + 2 stars + last 4 (
("very-long-api-key-here", "very**************here"), # 22 chars: first 4 + 14 stars + last 4 "abcdefghij",
"abcd**ghij",
), # 10 chars: first 4 + 2 stars + last 4
(
"very-long-api-key-here",
"very**************here",
), # 22 chars: first 4 + 14 stars + last 4
] ]
for key, expected_mask in test_cases: for key, expected_mask in test_cases:
auth = APIKeyAuth(key) auth = APIKeyAuth(key)
actual = auth.api_key actual = auth.api_key
assert actual == expected_mask, f"Failed for key '{key}': expected '{expected_mask}', got '{actual}'" assert (
actual == expected_mask
), f"Failed for key '{key}': expected '{expected_mask}', got '{actual}'"

View File

@@ -1,7 +1,6 @@
"""Tests for base authentication functionality.""" """Tests for base authentication functionality."""
import pytest import pytest
from unittest.mock import Mock
from wikijs.auth.base import AuthHandler, NoAuth from wikijs.auth.base import AuthHandler, NoAuth
from wikijs.exceptions import AuthenticationError from wikijs.exceptions import AuthenticationError
@@ -17,6 +16,7 @@ class TestAuthHandler:
def test_validate_credentials_calls_is_valid(self): def test_validate_credentials_calls_is_valid(self):
"""Test that validate_credentials calls is_valid.""" """Test that validate_credentials calls is_valid."""
# Create concrete implementation for testing # Create concrete implementation for testing
class TestAuth(AuthHandler): class TestAuth(AuthHandler):
def __init__(self, valid=True): def __init__(self, valid=True):
@@ -46,6 +46,7 @@ class TestAuthHandler:
def test_validate_credentials_raises_on_invalid_after_refresh(self): def test_validate_credentials_raises_on_invalid_after_refresh(self):
"""Test that validate_credentials raises if still invalid after refresh.""" """Test that validate_credentials raises if still invalid after refresh."""
class TestAuth(AuthHandler): class TestAuth(AuthHandler):
def get_headers(self): def get_headers(self):
return {"Authorization": "test"} return {"Authorization": "test"}
@@ -57,7 +58,9 @@ class TestAuthHandler:
pass # No-op refresh pass # No-op refresh
auth = TestAuth() auth = TestAuth()
with pytest.raises(AuthenticationError, match="Authentication credentials are invalid"): with pytest.raises(
AuthenticationError, match="Authentication credentials are invalid"
):
auth.validate_credentials() auth.validate_credentials()

View File

@@ -1,7 +1,7 @@
"""Tests for JWT authentication.""" """Tests for JWT authentication."""
import time import time
from datetime import datetime, timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@@ -56,7 +56,7 @@ class TestJWTAuth:
expected_headers = { expected_headers = {
"Authorization": f"Bearer {mock_jwt_token}", "Authorization": f"Bearer {mock_jwt_token}",
"Content-Type": "application/json" "Content-Type": "application/json",
} }
assert headers == expected_headers assert headers == expected_headers
@@ -69,7 +69,7 @@ class TestJWTAuth:
auth = JWTAuth(mock_jwt_token, refresh_token, expires_at) auth = JWTAuth(mock_jwt_token, refresh_token, expires_at)
# Mock the refresh method to avoid actual implementation # Mock the refresh method to avoid actual implementation
with patch.object(auth, 'refresh') as mock_refresh: with patch.object(auth, "refresh") as mock_refresh:
mock_refresh.side_effect = AuthenticationError("Refresh not implemented") mock_refresh.side_effect = AuthenticationError("Refresh not implemented")
with pytest.raises(AuthenticationError): with pytest.raises(AuthenticationError):
@@ -111,7 +111,10 @@ class TestJWTAuth:
def test_refresh_raises_error_without_refresh_token(self, jwt_auth): def test_refresh_raises_error_without_refresh_token(self, jwt_auth):
"""Test that refresh raises error when no refresh token available.""" """Test that refresh raises error when no refresh token available."""
with pytest.raises(AuthenticationError, match="JWT token expired and no refresh token available"): with pytest.raises(
AuthenticationError,
match="JWT token expired and no refresh token available",
):
jwt_auth.refresh() jwt_auth.refresh()
def test_refresh_raises_not_implemented_error(self, mock_jwt_token): def test_refresh_raises_not_implemented_error(self, mock_jwt_token):
@@ -119,7 +122,9 @@ class TestJWTAuth:
refresh_token = "refresh-token-123" refresh_token = "refresh-token-123"
auth = JWTAuth(mock_jwt_token, refresh_token) auth = JWTAuth(mock_jwt_token, refresh_token)
with pytest.raises(AuthenticationError, match="JWT token refresh not yet implemented"): with pytest.raises(
AuthenticationError, match="JWT token refresh not yet implemented"
):
auth.refresh() auth.refresh()
def test_is_expired_returns_false_no_expiry(self, jwt_auth): def test_is_expired_returns_false_no_expiry(self, jwt_auth):

View File

@@ -2,7 +2,6 @@
import pytest import pytest
import responses import responses
from unittest.mock import Mock
from wikijs.auth import APIKeyAuth, JWTAuth, NoAuth from wikijs.auth import APIKeyAuth, JWTAuth, NoAuth
@@ -60,21 +59,12 @@ def sample_page_data():
"content": "This is a test page content.", "content": "This is a test page content.",
"created_at": "2023-01-01T00:00:00Z", "created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T12:00:00Z", "updated_at": "2023-01-01T12:00:00Z",
"author": { "author": {"id": 1, "name": "Test User", "email": "test@example.com"},
"id": 1, "tags": ["test", "example"],
"name": "Test User",
"email": "test@example.com"
},
"tags": ["test", "example"]
} }
@pytest.fixture @pytest.fixture
def sample_error_response(): def sample_error_response():
"""Fixture providing sample error response.""" """Fixture providing sample error response."""
return { return {"error": {"message": "Not found", "code": "PAGE_NOT_FOUND"}}
"error": {
"message": "Not found",
"code": "PAGE_NOT_FOUND"
}
}

View File

@@ -1,8 +1,9 @@
"""Tests for base endpoint class.""" """Tests for base endpoint class."""
import pytest
from unittest.mock import Mock from unittest.mock import Mock
import pytest
from wikijs.client import WikiJSClient from wikijs.client import WikiJSClient
from wikijs.endpoints.base import BaseEndpoint from wikijs.endpoints.base import BaseEndpoint
@@ -38,7 +39,7 @@ class TestBaseEndpoint:
"/test", "/test",
params={"param": "value"}, params={"param": "value"},
json_data={"data": "test"}, json_data={"data": "test"},
extra_param="extra" extra_param="extra",
) )
# Verify delegation to client # Verify delegation to client
@@ -47,7 +48,7 @@ class TestBaseEndpoint:
endpoint="/test", endpoint="/test",
params={"param": "value"}, params={"param": "value"},
json_data={"data": "test"}, json_data={"data": "test"},
extra_param="extra" extra_param="extra",
) )
# Verify response # Verify response
@@ -64,7 +65,7 @@ class TestBaseEndpoint:
method="GET", method="GET",
endpoint="/test", endpoint="/test",
params={"param": "value"}, params={"param": "value"},
json_data=None json_data=None,
) )
assert result == mock_response assert result == mock_response
@@ -74,16 +75,14 @@ class TestBaseEndpoint:
mock_client._request.return_value = mock_response mock_client._request.return_value = mock_response
result = base_endpoint._post( result = base_endpoint._post(
"/test", "/test", json_data={"data": "test"}, params={"param": "value"}
json_data={"data": "test"},
params={"param": "value"}
) )
mock_client._request.assert_called_once_with( mock_client._request.assert_called_once_with(
method="POST", method="POST",
endpoint="/test", endpoint="/test",
params={"param": "value"}, params={"param": "value"},
json_data={"data": "test"} json_data={"data": "test"},
) )
assert result == mock_response assert result == mock_response
@@ -93,16 +92,14 @@ class TestBaseEndpoint:
mock_client._request.return_value = mock_response mock_client._request.return_value = mock_response
result = base_endpoint._put( result = base_endpoint._put(
"/test", "/test", json_data={"data": "test"}, params={"param": "value"}
json_data={"data": "test"},
params={"param": "value"}
) )
mock_client._request.assert_called_once_with( mock_client._request.assert_called_once_with(
method="PUT", method="PUT",
endpoint="/test", endpoint="/test",
params={"param": "value"}, params={"param": "value"},
json_data={"data": "test"} json_data={"data": "test"},
) )
assert result == mock_response assert result == mock_response
@@ -117,7 +114,7 @@ class TestBaseEndpoint:
method="DELETE", method="DELETE",
endpoint="/test", endpoint="/test",
params={"param": "value"}, params={"param": "value"},
json_data=None json_data=None,
) )
assert result == mock_response assert result == mock_response

View File

@@ -1,8 +1,9 @@
"""Tests for Pages API endpoint.""" """Tests for Pages API endpoint."""
import pytest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest
from wikijs.client import WikiJSClient from wikijs.client import WikiJSClient
from wikijs.endpoints.pages import PagesEndpoint from wikijs.endpoints.pages import PagesEndpoint
from wikijs.exceptions import APIError, ValidationError from wikijs.exceptions import APIError, ValidationError
@@ -41,7 +42,7 @@ class TestPagesEndpoint:
"authorEmail": "test@example.com", "authorEmail": "test@example.com",
"editor": "markdown", "editor": "markdown",
"createdAt": "2023-01-01T00:00:00Z", "createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-02T00:00:00Z" "updatedAt": "2023-01-02T00:00:00Z",
} }
@pytest.fixture @pytest.fixture
@@ -52,7 +53,7 @@ class TestPagesEndpoint:
path="new-page", path="new-page",
content="# New Page\n\nContent here.", content="# New Page\n\nContent here.",
description="A new page", description="A new page",
tags=["new", "test"] tags=["new", "test"],
) )
@pytest.fixture @pytest.fixture
@@ -61,7 +62,7 @@ class TestPagesEndpoint:
return PageUpdate( return PageUpdate(
title="Updated Page", title="Updated Page",
content="# Updated Page\n\nUpdated content.", content="# Updated Page\n\nUpdated content.",
tags=["updated", "test"] tags=["updated", "test"],
) )
def test_init(self, mock_client): def test_init(self, mock_client):
@@ -72,11 +73,7 @@ class TestPagesEndpoint:
def test_list_basic(self, pages_endpoint, sample_page_data): def test_list_basic(self, pages_endpoint, sample_page_data):
"""Test basic page listing.""" """Test basic page listing."""
# Mock the GraphQL response # Mock the GraphQL response
mock_response = { mock_response = {"data": {"pages": [sample_page_data]}}
"data": {
"pages": [sample_page_data]
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call list method # Call list method
@@ -96,11 +93,7 @@ class TestPagesEndpoint:
def test_list_with_parameters(self, pages_endpoint, sample_page_data): def test_list_with_parameters(self, pages_endpoint, sample_page_data):
"""Test page listing with filter parameters.""" """Test page listing with filter parameters."""
mock_response = { mock_response = {"data": {"pages": [sample_page_data]}}
"data": {
"pages": [sample_page_data]
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call with parameters # Call with parameters
@@ -112,7 +105,7 @@ class TestPagesEndpoint:
locale="en", locale="en",
author_id=1, author_id=1,
order_by="created_at", order_by="created_at",
order_direction="DESC" order_direction="DESC",
) )
# Verify request # Verify request
@@ -148,15 +141,15 @@ class TestPagesEndpoint:
pages_endpoint.list(order_by="invalid") pages_endpoint.list(order_by="invalid")
# Test invalid order_direction # Test invalid order_direction
with pytest.raises(ValidationError, match="order_direction must be ASC or DESC"): with pytest.raises(
ValidationError, match="order_direction must be ASC or DESC"
):
pages_endpoint.list(order_direction="INVALID") pages_endpoint.list(order_direction="INVALID")
def test_list_api_error(self, pages_endpoint): def test_list_api_error(self, pages_endpoint):
"""Test list method handling API errors.""" """Test list method handling API errors."""
# Mock GraphQL error response # Mock GraphQL error response
mock_response = { mock_response = {"errors": [{"message": "GraphQL error"}]}
"errors": [{"message": "GraphQL error"}]
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
with pytest.raises(APIError, match="GraphQL errors"): with pytest.raises(APIError, match="GraphQL errors"):
@@ -164,11 +157,7 @@ class TestPagesEndpoint:
def test_get_success(self, pages_endpoint, sample_page_data): def test_get_success(self, pages_endpoint, sample_page_data):
"""Test getting a page by ID.""" """Test getting a page by ID."""
mock_response = { mock_response = {"data": {"page": sample_page_data}}
"data": {
"page": sample_page_data
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call method # Call method
@@ -197,11 +186,7 @@ class TestPagesEndpoint:
def test_get_not_found(self, pages_endpoint): def test_get_not_found(self, pages_endpoint):
"""Test get method when page not found.""" """Test get method when page not found."""
mock_response = { mock_response = {"data": {"page": None}}
"data": {
"page": None
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
with pytest.raises(APIError, match="Page with ID 123 not found"): with pytest.raises(APIError, match="Page with ID 123 not found"):
@@ -209,11 +194,7 @@ class TestPagesEndpoint:
def test_get_by_path_success(self, pages_endpoint, sample_page_data): def test_get_by_path_success(self, pages_endpoint, sample_page_data):
"""Test getting a page by path.""" """Test getting a page by path."""
mock_response = { mock_response = {"data": {"pageByPath": sample_page_data}}
"data": {
"pageByPath": sample_page_data
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call method # Call method
@@ -240,11 +221,7 @@ class TestPagesEndpoint:
def test_create_success(self, pages_endpoint, sample_page_create, sample_page_data): def test_create_success(self, pages_endpoint, sample_page_create, sample_page_data):
"""Test creating a new page.""" """Test creating a new page."""
mock_response = { mock_response = {"data": {"createPage": sample_page_data}}
"data": {
"createPage": sample_page_data
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call method # Call method
@@ -269,11 +246,7 @@ class TestPagesEndpoint:
def test_create_with_dict(self, pages_endpoint, sample_page_data): def test_create_with_dict(self, pages_endpoint, sample_page_data):
"""Test creating a page with dict data.""" """Test creating a page with dict data."""
mock_response = { mock_response = {"data": {"createPage": sample_page_data}}
"data": {
"createPage": sample_page_data
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
page_dict = { page_dict = {
@@ -291,7 +264,10 @@ class TestPagesEndpoint:
def test_create_validation_error(self, pages_endpoint): def test_create_validation_error(self, pages_endpoint):
"""Test create method validation errors.""" """Test create method validation errors."""
# Test invalid data type # Test invalid data type
with pytest.raises(ValidationError, match="page_data must be PageCreate object or dict"): with pytest.raises(
ValidationError,
match="page_data must be PageCreate object or dict",
):
pages_endpoint.create("invalid") pages_endpoint.create("invalid")
# Test invalid dict data # Test invalid dict data
@@ -300,9 +276,7 @@ class TestPagesEndpoint:
def test_create_api_error(self, pages_endpoint, sample_page_create): def test_create_api_error(self, pages_endpoint, sample_page_create):
"""Test create method API errors.""" """Test create method API errors."""
mock_response = { mock_response = {"errors": [{"message": "Creation failed"}]}
"errors": [{"message": "Creation failed"}]
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
with pytest.raises(APIError, match="Failed to create page"): with pytest.raises(APIError, match="Failed to create page"):
@@ -310,11 +284,7 @@ class TestPagesEndpoint:
def test_update_success(self, pages_endpoint, sample_page_update, sample_page_data): def test_update_success(self, pages_endpoint, sample_page_update, sample_page_data):
"""Test updating a page.""" """Test updating a page."""
mock_response = { mock_response = {"data": {"updatePage": sample_page_data}}
"data": {
"updatePage": sample_page_data
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call method # Call method
@@ -341,7 +311,10 @@ class TestPagesEndpoint:
pages_endpoint.update(0, sample_page_update) pages_endpoint.update(0, sample_page_update)
# Test invalid page_data type # Test invalid page_data type
with pytest.raises(ValidationError, match="page_data must be PageUpdate object or dict"): with pytest.raises(
ValidationError,
match="page_data must be PageUpdate object or dict",
):
pages_endpoint.update(123, "invalid") pages_endpoint.update(123, "invalid")
def test_delete_success(self, pages_endpoint): def test_delete_success(self, pages_endpoint):
@@ -350,7 +323,7 @@ class TestPagesEndpoint:
"data": { "data": {
"deletePage": { "deletePage": {
"success": True, "success": True,
"message": "Page deleted successfully" "message": "Page deleted successfully",
} }
} }
} }
@@ -375,12 +348,7 @@ class TestPagesEndpoint:
def test_delete_failure(self, pages_endpoint): def test_delete_failure(self, pages_endpoint):
"""Test delete method when deletion fails.""" """Test delete method when deletion fails."""
mock_response = { mock_response = {
"data": { "data": {"deletePage": {"success": False, "message": "Deletion failed"}}
"deletePage": {
"success": False,
"message": "Deletion failed"
}
}
} }
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
@@ -389,11 +357,7 @@ class TestPagesEndpoint:
def test_search_success(self, pages_endpoint, sample_page_data): def test_search_success(self, pages_endpoint, sample_page_data):
"""Test searching pages.""" """Test searching pages."""
mock_response = { mock_response = {"data": {"pages": [sample_page_data]}}
"data": {
"pages": [sample_page_data]
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call method # Call method
@@ -421,11 +385,7 @@ class TestPagesEndpoint:
def test_get_by_tags_match_all(self, pages_endpoint, sample_page_data): def test_get_by_tags_match_all(self, pages_endpoint, sample_page_data):
"""Test getting pages by tags (match all).""" """Test getting pages by tags (match all)."""
mock_response = { mock_response = {"data": {"pages": [sample_page_data]}}
"data": {
"pages": [sample_page_data]
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call method # Call method
@@ -457,7 +417,7 @@ class TestPagesEndpoint:
"title": "Test", "title": "Test",
"isPublished": True, "isPublished": True,
"authorId": 1, "authorId": 1,
"createdAt": "2023-01-01T00:00:00Z" "createdAt": "2023-01-01T00:00:00Z",
} }
normalized = pages_endpoint._normalize_page_data(api_data) normalized = pages_endpoint._normalize_page_data(api_data)
@@ -472,10 +432,7 @@ class TestPagesEndpoint:
def test_normalize_page_data_missing_fields(self, pages_endpoint): def test_normalize_page_data_missing_fields(self, pages_endpoint):
"""Test page data normalization with missing fields.""" """Test page data normalization with missing fields."""
api_data = { api_data = {"id": 123, "title": "Test"}
"id": 123,
"title": "Test"
}
normalized = pages_endpoint._normalize_page_data(api_data) normalized = pages_endpoint._normalize_page_data(api_data)
@@ -485,17 +442,15 @@ class TestPagesEndpoint:
assert "is_published" not in normalized assert "is_published" not in normalized
assert "tags" in normalized # Should have default value assert "tags" in normalized # Should have default value
@patch('wikijs.endpoints.pages.Page') @patch("wikijs.endpoints.pages.Page")
def test_list_page_parsing_error(self, mock_page_class, pages_endpoint, sample_page_data): def test_list_page_parsing_error(
self, mock_page_class, pages_endpoint, sample_page_data
):
"""Test handling of page parsing errors in list method.""" """Test handling of page parsing errors in list method."""
# Mock Page constructor to raise an exception # Mock Page constructor to raise an exception
mock_page_class.side_effect = ValueError("Parsing error") mock_page_class.side_effect = ValueError("Parsing error")
mock_response = { mock_response = {"data": {"pages": [sample_page_data]}}
"data": {
"pages": [sample_page_data]
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
with pytest.raises(APIError, match="Failed to parse page data"): with pytest.raises(APIError, match="Failed to parse page data"):
@@ -503,11 +458,7 @@ class TestPagesEndpoint:
def test_graphql_query_structure(self, pages_endpoint, sample_page_data): def test_graphql_query_structure(self, pages_endpoint, sample_page_data):
"""Test that GraphQL queries have correct structure.""" """Test that GraphQL queries have correct structure."""
mock_response = { mock_response = {"data": {"pages": [sample_page_data]}}
"data": {
"pages": [sample_page_data]
}
}
pages_endpoint._post = Mock(return_value=mock_response) pages_endpoint._post = Mock(return_value=mock_response)
# Call list method # Call list method

View File

@@ -3,8 +3,6 @@
import json import json
from datetime import datetime from datetime import datetime
import pytest
from wikijs.models.base import BaseModel, TimestampedModel from wikijs.models.base import BaseModel, TimestampedModel
@@ -51,7 +49,11 @@ class TestBaseModel:
# Test including None values # Test including None values
result_include = model.to_dict(exclude_none=False) result_include = model.to_dict(exclude_none=False)
expected_include = {"name": "test", "value": 100, "optional_field": None} expected_include = {
"name": "test",
"value": 100,
"optional_field": None,
}
assert result_include == expected_include assert result_include == expected_include
def test_to_json_basic(self): def test_to_json_basic(self):
@@ -77,7 +79,11 @@ class TestBaseModel:
# Test including None values # Test including None values
result_include = model.to_json(exclude_none=False) result_include = model.to_json(exclude_none=False)
parsed_include = json.loads(result_include) parsed_include = json.loads(result_include)
expected_include = {"name": "test", "value": 100, "optional_field": None} expected_include = {
"name": "test",
"value": 100,
"optional_field": None,
}
assert parsed_include == expected_include assert parsed_include == expected_include
def test_from_dict(self): def test_from_dict(self):
@@ -113,9 +119,7 @@ class TestTimestampedModel:
"""Test timestamped model with timestamps.""" """Test timestamped model with timestamps."""
now = datetime.now() now = datetime.now()
model = TestTimestampedModelForTesting( model = TestTimestampedModelForTesting(
title="Test Title", title="Test Title", created_at=now, updated_at=now
created_at=now,
updated_at=now
) )
assert model.title == "Test Title" assert model.title == "Test Title"
assert model.created_at == now assert model.created_at == now
@@ -129,19 +133,14 @@ class TestTimestampedModel:
def test_is_new_property_false(self): def test_is_new_property_false(self):
"""Test is_new property returns False for existing models.""" """Test is_new property returns False for existing models."""
now = datetime.now() now = datetime.now()
model = TestTimestampedModelForTesting( model = TestTimestampedModelForTesting(title="Test Title", created_at=now)
title="Test Title",
created_at=now
)
assert model.is_new is False assert model.is_new is False
def test_datetime_serialization(self): def test_datetime_serialization(self):
"""Test datetime serialization in JSON.""" """Test datetime serialization in JSON."""
now = datetime(2023, 1, 1, 12, 0, 0) now = datetime(2023, 1, 1, 12, 0, 0)
model = TestTimestampedModelForTesting( model = TestTimestampedModelForTesting(
title="Test Title", title="Test Title", created_at=now, updated_at=now
created_at=now,
updated_at=now
) )
json_str = model.to_json() json_str = model.to_json()

View File

@@ -1,7 +1,6 @@
"""Tests for Page models.""" """Tests for Page models."""
import pytest import pytest
from datetime import datetime
from wikijs.models.page import Page, PageCreate, PageUpdate from wikijs.models.page import Page, PageCreate, PageUpdate
@@ -27,7 +26,7 @@ class TestPageModel:
"author_email": "test@example.com", "author_email": "test@example.com",
"editor": "markdown", "editor": "markdown",
"created_at": "2023-01-01T00:00:00Z", "created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-02T00:00:00Z" "updated_at": "2023-01-02T00:00:00Z",
} }
def test_page_creation_valid(self, valid_page_data): def test_page_creation_valid(self, valid_page_data):
@@ -37,7 +36,10 @@ class TestPageModel:
assert page.id == 123 assert page.id == 123
assert page.title == "Test Page" assert page.title == "Test Page"
assert page.path == "test-page" assert page.path == "test-page"
assert page.content == "# Test Page\n\nThis is test content with **bold** and *italic* text." assert (
page.content
== "# Test Page\n\nThis is test content with **bold** and *italic* text."
)
assert page.is_published is True assert page.is_published is True
assert page.tags == ["test", "example"] assert page.tags == ["test", "example"]
@@ -49,7 +51,7 @@ class TestPageModel:
path="minimal", path="minimal",
content="Content", content="Content",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
assert page.id == 1 assert page.id == 1
@@ -64,7 +66,7 @@ class TestPageModel:
"path/with/slashes", "path/with/slashes",
"path_with_underscores", "path_with_underscores",
"path123", "path123",
"category/subcategory/page-name" "category/subcategory/page-name",
] ]
for path in valid_paths: for path in valid_paths:
@@ -74,7 +76,7 @@ class TestPageModel:
path=path, path=path,
content="Content", content="Content",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
assert page.path == path assert page.path == path
@@ -95,7 +97,7 @@ class TestPageModel:
path=path, path=path,
content="Content", content="Content",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
def test_page_path_normalization(self): def test_page_path_normalization(self):
@@ -107,7 +109,7 @@ class TestPageModel:
path="/path/to/page/", path="/path/to/page/",
content="Content", content="Content",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
assert page.path == "path/to/page" assert page.path == "path/to/page"
@@ -119,7 +121,7 @@ class TestPageModel:
path="test", path="test",
content="Content", content="Content",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
assert page.title == "Valid Title with Spaces" assert page.title == "Valid Title with Spaces"
@@ -139,7 +141,7 @@ class TestPageModel:
path="test", path="test",
content="Content", content="Content",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
def test_page_word_count(self, valid_page_data): def test_page_word_count(self, valid_page_data):
@@ -157,7 +159,7 @@ class TestPageModel:
path="test", path="test",
content="", content="",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
assert page.word_count == 0 assert page.word_count == 0
@@ -176,7 +178,7 @@ class TestPageModel:
path="test", path="test",
content=long_content, content=long_content,
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
# 500 words / 200 words per minute = 2.5, rounded down to 2 # 500 words / 200 words per minute = 2.5, rounded down to 2
assert page.reading_time == 2 assert page.reading_time == 2
@@ -210,11 +212,16 @@ Final content."""
path="test", path="test",
content=content, content=content,
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
headings = page.extract_headings() headings = page.extract_headings()
expected = ["Main Title", "Secondary Heading", "Third Level", "Another Secondary"] expected = [
"Main Title",
"Secondary Heading",
"Third Level",
"Another Secondary",
]
assert headings == expected assert headings == expected
def test_page_extract_headings_empty_content(self): def test_page_extract_headings_empty_content(self):
@@ -225,7 +232,7 @@ Final content."""
path="test", path="test",
content="", content="",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
assert page.extract_headings() == [] assert page.extract_headings() == []
@@ -246,7 +253,7 @@ Final content."""
path="test", path="test",
content="Content", content="Content",
created_at="2023-01-01T00:00:00Z", created_at="2023-01-01T00:00:00Z",
updated_at="2023-01-01T00:00:00Z" updated_at="2023-01-01T00:00:00Z",
) )
assert page.has_tag("any") is False assert page.has_tag("any") is False
@@ -259,7 +266,7 @@ class TestPageCreateModel:
page_create = PageCreate( page_create = PageCreate(
title="New Page", title="New Page",
path="new-page", path="new-page",
content="# New Page\n\nContent here." content="# New Page\n\nContent here.",
) )
assert page_create.title == "New Page" assert page_create.title == "New Page"
@@ -279,7 +286,7 @@ class TestPageCreateModel:
is_private=True, is_private=True,
tags=["new", "test"], tags=["new", "test"],
locale="fr", locale="fr",
editor="html" editor="html",
) )
assert page_create.description == "A new page" assert page_create.description == "A new page"
@@ -329,10 +336,7 @@ class TestPageUpdateModel:
def test_page_update_partial(self): def test_page_update_partial(self):
"""Test partial PageUpdate.""" """Test partial PageUpdate."""
page_update = PageUpdate( page_update = PageUpdate(title="Updated Title", content="Updated content")
title="Updated Title",
content="Updated content"
)
assert page_update.title == "Updated Title" assert page_update.title == "Updated Title"
assert page_update.content == "Updated content" assert page_update.content == "Updated content"
@@ -346,7 +350,7 @@ class TestPageUpdateModel:
description="Updated description", description="Updated description",
is_published=False, is_published=False,
is_private=True, is_private=True,
tags=["updated", "test"] tags=["updated", "test"],
) )
assert page_update.title == "Updated Title" assert page_update.title == "Updated Title"

View File

@@ -1,9 +1,10 @@
"""Tests for WikiJS client.""" """Tests for WikiJS client."""
import json import json
import pytest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest
from wikijs.auth import APIKeyAuth from wikijs.auth import APIKeyAuth
from wikijs.client import WikiJSClient from wikijs.client import WikiJSClient
from wikijs.exceptions import ( from wikijs.exceptions import (
@@ -20,7 +21,7 @@ class TestWikiJSClientInit:
def test_init_with_api_key_string(self): def test_init_with_api_key_string(self):
"""Test initialization with API key string.""" """Test initialization with API key string."""
with patch('wikijs.client.requests.Session'): with patch("wikijs.client.requests.Session"):
client = WikiJSClient("https://wiki.example.com", auth="test-key") client = WikiJSClient("https://wiki.example.com", auth="test-key")
assert client.base_url == "https://wiki.example.com" assert client.base_url == "https://wiki.example.com"
@@ -33,7 +34,7 @@ class TestWikiJSClientInit:
"""Test initialization with auth handler.""" """Test initialization with auth handler."""
auth_handler = APIKeyAuth("test-key") auth_handler = APIKeyAuth("test-key")
with patch('wikijs.client.requests.Session'): with patch("wikijs.client.requests.Session"):
client = WikiJSClient("https://wiki.example.com", auth=auth_handler) client = WikiJSClient("https://wiki.example.com", auth=auth_handler)
assert client._auth_handler is auth_handler assert client._auth_handler is auth_handler
@@ -45,13 +46,13 @@ class TestWikiJSClientInit:
def test_init_with_custom_settings(self): def test_init_with_custom_settings(self):
"""Test initialization with custom settings.""" """Test initialization with custom settings."""
with patch('wikijs.client.requests.Session'): with patch("wikijs.client.requests.Session"):
client = WikiJSClient( client = WikiJSClient(
"https://wiki.example.com", "https://wiki.example.com",
auth="test-key", auth="test-key",
timeout=60, timeout=60,
verify_ssl=False, verify_ssl=False,
user_agent="Custom Agent" user_agent="Custom Agent",
) )
assert client.timeout == 60 assert client.timeout == 60
@@ -60,10 +61,10 @@ class TestWikiJSClientInit:
def test_has_pages_endpoint(self): def test_has_pages_endpoint(self):
"""Test that client has pages endpoint.""" """Test that client has pages endpoint."""
with patch('wikijs.client.requests.Session'): with patch("wikijs.client.requests.Session"):
client = WikiJSClient("https://wiki.example.com", auth="test-key") client = WikiJSClient("https://wiki.example.com", auth="test-key")
assert hasattr(client, 'pages') assert hasattr(client, "pages")
assert client.pages._client is client assert client.pages._client is client
@@ -80,7 +81,7 @@ class TestWikiJSClientTestConnection:
"""Mock API key.""" """Mock API key."""
return "test-api-key-12345" return "test-api-key-12345"
@patch('wikijs.client.requests.Session.get') @patch("wikijs.client.requests.Session.get")
def test_test_connection_success(self, mock_get, mock_wiki_base_url, mock_api_key): def test_test_connection_success(self, mock_get, mock_wiki_base_url, mock_api_key):
"""Test successful connection test.""" """Test successful connection test."""
mock_response = Mock() mock_response = Mock()
@@ -92,10 +93,11 @@ class TestWikiJSClientTestConnection:
assert result is True assert result is True
@patch('wikijs.client.requests.Session.get') @patch("wikijs.client.requests.Session.get")
def test_test_connection_timeout(self, mock_get, mock_wiki_base_url, mock_api_key): def test_test_connection_timeout(self, mock_get, mock_wiki_base_url, mock_api_key):
"""Test connection test timeout.""" """Test connection test timeout."""
import requests import requests
mock_get.side_effect = requests.exceptions.Timeout("Request timed out") mock_get.side_effect = requests.exceptions.Timeout("Request timed out")
client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key) client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key)
@@ -103,10 +105,11 @@ class TestWikiJSClientTestConnection:
with pytest.raises(TimeoutError, match="Connection test timed out"): with pytest.raises(TimeoutError, match="Connection test timed out"):
client.test_connection() client.test_connection()
@patch('wikijs.client.requests.Session.get') @patch("wikijs.client.requests.Session.get")
def test_test_connection_error(self, mock_get, mock_wiki_base_url, mock_api_key): def test_test_connection_error(self, mock_get, mock_wiki_base_url, mock_api_key):
"""Test connection test with connection error.""" """Test connection test with connection error."""
import requests import requests
mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed") mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed")
client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key) client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key)
@@ -116,7 +119,7 @@ class TestWikiJSClientTestConnection:
def test_test_connection_no_base_url(self): def test_test_connection_no_base_url(self):
"""Test connection test with no base URL.""" """Test connection test with no base URL."""
with patch('wikijs.client.requests.Session'): with patch("wikijs.client.requests.Session"):
client = WikiJSClient("https://wiki.example.com", auth="test-key") client = WikiJSClient("https://wiki.example.com", auth="test-key")
client.base_url = "" # Simulate empty base URL after creation client.base_url = "" # Simulate empty base URL after creation
@@ -125,11 +128,13 @@ class TestWikiJSClientTestConnection:
def test_test_connection_no_auth(self): def test_test_connection_no_auth(self):
"""Test connection test with no auth.""" """Test connection test with no auth."""
with patch('wikijs.client.requests.Session'): with patch("wikijs.client.requests.Session"):
client = WikiJSClient("https://wiki.example.com", auth="test-key") client = WikiJSClient("https://wiki.example.com", auth="test-key")
client._auth_handler = None # Simulate no auth client._auth_handler = None # Simulate no auth
with pytest.raises(ConfigurationError, match="Authentication not configured"): with pytest.raises(
ConfigurationError, match="Authentication not configured"
):
client.test_connection() client.test_connection()
@@ -146,7 +151,7 @@ class TestWikiJSClientRequests:
"""Mock API key.""" """Mock API key."""
return "test-api-key-12345" return "test-api-key-12345"
@patch('wikijs.client.requests.Session.request') @patch("wikijs.client.requests.Session.request")
def test_request_success(self, mock_request, mock_wiki_base_url, mock_api_key): def test_request_success(self, mock_request, mock_wiki_base_url, mock_api_key):
"""Test successful API request.""" """Test successful API request."""
mock_response = Mock() mock_response = Mock()
@@ -160,8 +165,10 @@ class TestWikiJSClientRequests:
assert result == {"data": "test"} assert result == {"data": "test"}
mock_request.assert_called_once() mock_request.assert_called_once()
@patch('wikijs.client.requests.Session.request') @patch("wikijs.client.requests.Session.request")
def test_request_with_json_data(self, mock_request, mock_wiki_base_url, mock_api_key): def test_request_with_json_data(
self, mock_request, mock_wiki_base_url, mock_api_key
):
"""Test API request with JSON data.""" """Test API request with JSON data."""
mock_response = Mock() mock_response = Mock()
mock_response.ok = True mock_response.ok = True
@@ -174,8 +181,10 @@ class TestWikiJSClientRequests:
assert result == {"success": True} assert result == {"success": True}
mock_request.assert_called_once() mock_request.assert_called_once()
@patch('wikijs.client.requests.Session.request') @patch("wikijs.client.requests.Session.request")
def test_request_authentication_error(self, mock_request, mock_wiki_base_url, mock_api_key): def test_request_authentication_error(
self, mock_request, mock_wiki_base_url, mock_api_key
):
"""Test request with authentication error.""" """Test request with authentication error."""
mock_response = Mock() mock_response = Mock()
mock_response.ok = False mock_response.ok = False
@@ -187,7 +196,7 @@ class TestWikiJSClientRequests:
with pytest.raises(AuthenticationError, match="Authentication failed"): with pytest.raises(AuthenticationError, match="Authentication failed"):
client._request("GET", "/test") client._request("GET", "/test")
@patch('wikijs.client.requests.Session.request') @patch("wikijs.client.requests.Session.request")
def test_request_api_error(self, mock_request, mock_wiki_base_url, mock_api_key): def test_request_api_error(self, mock_request, mock_wiki_base_url, mock_api_key):
"""Test request with API error.""" """Test request with API error."""
mock_response = Mock() mock_response = Mock()
@@ -201,8 +210,10 @@ class TestWikiJSClientRequests:
with pytest.raises(APIError): with pytest.raises(APIError):
client._request("GET", "/test") client._request("GET", "/test")
@patch('wikijs.client.requests.Session.request') @patch("wikijs.client.requests.Session.request")
def test_request_invalid_json_response(self, mock_request, mock_wiki_base_url, mock_api_key): def test_request_invalid_json_response(
self, mock_request, mock_wiki_base_url, mock_api_key
):
"""Test request with invalid JSON response.""" """Test request with invalid JSON response."""
mock_response = Mock() mock_response = Mock()
mock_response.ok = True mock_response.ok = True
@@ -214,10 +225,11 @@ class TestWikiJSClientRequests:
with pytest.raises(APIError, match="Invalid JSON response"): with pytest.raises(APIError, match="Invalid JSON response"):
client._request("GET", "/test") client._request("GET", "/test")
@patch('wikijs.client.requests.Session.request') @patch("wikijs.client.requests.Session.request")
def test_request_timeout(self, mock_request, mock_wiki_base_url, mock_api_key): def test_request_timeout(self, mock_request, mock_wiki_base_url, mock_api_key):
"""Test request timeout handling.""" """Test request timeout handling."""
import requests import requests
mock_request.side_effect = requests.exceptions.Timeout("Request timed out") mock_request.side_effect = requests.exceptions.Timeout("Request timed out")
client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key) client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key)
@@ -225,21 +237,29 @@ class TestWikiJSClientRequests:
with pytest.raises(TimeoutError, match="Request timed out"): with pytest.raises(TimeoutError, match="Request timed out"):
client._request("GET", "/test") client._request("GET", "/test")
@patch('wikijs.client.requests.Session.request') @patch("wikijs.client.requests.Session.request")
def test_request_connection_error(self, mock_request, mock_wiki_base_url, mock_api_key): def test_request_connection_error(
self, mock_request, mock_wiki_base_url, mock_api_key
):
"""Test request connection error handling.""" """Test request connection error handling."""
import requests import requests
mock_request.side_effect = requests.exceptions.ConnectionError("Connection failed")
mock_request.side_effect = requests.exceptions.ConnectionError(
"Connection failed"
)
client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key) client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key)
with pytest.raises(ConnectionError, match="Failed to connect"): with pytest.raises(ConnectionError, match="Failed to connect"):
client._request("GET", "/test") client._request("GET", "/test")
@patch('wikijs.client.requests.Session.request') @patch("wikijs.client.requests.Session.request")
def test_request_general_exception(self, mock_request, mock_wiki_base_url, mock_api_key): def test_request_general_exception(
self, mock_request, mock_wiki_base_url, mock_api_key
):
"""Test request general exception handling.""" """Test request general exception handling."""
import requests import requests
mock_request.side_effect = requests.exceptions.RequestException("General error") mock_request.side_effect = requests.exceptions.RequestException("General error")
client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key) client = WikiJSClient(mock_wiki_base_url, auth=mock_api_key)
@@ -251,7 +271,7 @@ class TestWikiJSClientRequests:
class TestWikiJSClientWithDifferentAuth: class TestWikiJSClientWithDifferentAuth:
"""Test WikiJSClient with different auth types.""" """Test WikiJSClient with different auth types."""
@patch('wikijs.client.requests.Session') @patch("wikijs.client.requests.Session")
def test_auth_validation_during_session_creation(self, mock_session_class): def test_auth_validation_during_session_creation(self, mock_session_class):
"""Test that auth validation happens during session creation.""" """Test that auth validation happens during session creation."""
mock_session = Mock() mock_session = Mock()
@@ -262,7 +282,9 @@ class TestWikiJSClientWithDifferentAuth:
from wikijs.exceptions import AuthenticationError from wikijs.exceptions import AuthenticationError
mock_auth = Mock(spec=AuthHandler) mock_auth = Mock(spec=AuthHandler)
mock_auth.validate_credentials.side_effect = AuthenticationError("Invalid credentials") mock_auth.validate_credentials.side_effect = AuthenticationError(
"Invalid credentials"
)
mock_auth.get_headers.return_value = {} mock_auth.get_headers.return_value = {}
with pytest.raises(AuthenticationError, match="Invalid credentials"): with pytest.raises(AuthenticationError, match="Invalid credentials"):
@@ -272,7 +294,7 @@ class TestWikiJSClientWithDifferentAuth:
class TestWikiJSClientContextManager: class TestWikiJSClientContextManager:
"""Test WikiJSClient context manager functionality.""" """Test WikiJSClient context manager functionality."""
@patch('wikijs.client.requests.Session') @patch("wikijs.client.requests.Session")
def test_context_manager(self, mock_session_class): def test_context_manager(self, mock_session_class):
"""Test client as context manager.""" """Test client as context manager."""
mock_session = Mock() mock_session = Mock()
@@ -284,7 +306,7 @@ class TestWikiJSClientContextManager:
# Verify session was closed # Verify session was closed
mock_session.close.assert_called_once() mock_session.close.assert_called_once()
@patch('wikijs.client.requests.Session') @patch("wikijs.client.requests.Session")
def test_close_method(self, mock_session_class): def test_close_method(self, mock_session_class):
"""Test explicit close method.""" """Test explicit close method."""
mock_session = Mock() mock_session = Mock()
@@ -295,7 +317,7 @@ class TestWikiJSClientContextManager:
mock_session.close.assert_called_once() mock_session.close.assert_called_once()
@patch('wikijs.client.requests.Session') @patch("wikijs.client.requests.Session")
def test_repr(self, mock_session_class): def test_repr(self, mock_session_class):
"""Test string representation.""" """Test string representation."""
mock_session = Mock() mock_session = Mock()
@@ -307,7 +329,7 @@ class TestWikiJSClientContextManager:
assert "WikiJSClient" in repr_str assert "WikiJSClient" in repr_str
assert "https://wiki.example.com" in repr_str assert "https://wiki.example.com" in repr_str
@patch('wikijs.client.requests.Session') @patch("wikijs.client.requests.Session")
def test_connection_test_generic_exception(self, mock_session_class): def test_connection_test_generic_exception(self, mock_session_class):
"""Test connection test with generic exception.""" """Test connection test with generic exception."""
mock_session = Mock() mock_session = Mock()
@@ -319,5 +341,8 @@ class TestWikiJSClientContextManager:
client = WikiJSClient("https://wiki.example.com", auth="test-key") client = WikiJSClient("https://wiki.example.com", auth="test-key")
from wikijs.exceptions import ConnectionError from wikijs.exceptions import ConnectionError
with pytest.raises(ConnectionError, match="Connection test failed: Unexpected error"):
with pytest.raises(
ConnectionError, match="Connection test failed: Unexpected error"
):
client.test_connection() client.test_connection()

View File

@@ -1,21 +1,20 @@
"""Tests for exception classes.""" """Tests for exception classes."""
import pytest
from unittest.mock import Mock from unittest.mock import Mock
from wikijs.exceptions import ( from wikijs.exceptions import (
WikiJSException,
APIError, APIError,
ClientError,
ServerError,
AuthenticationError, AuthenticationError,
ClientError,
ConfigurationError, ConfigurationError,
ValidationError, ConnectionError,
NotFoundError, NotFoundError,
PermissionError, PermissionError,
RateLimitError, RateLimitError,
ConnectionError, ServerError,
TimeoutError, TimeoutError,
ValidationError,
WikiJSException,
create_api_error, create_api_error,
) )

View File

@@ -1,11 +1,10 @@
"""Integration tests for the full WikiJS client with Pages API.""" """Integration tests for the full WikiJS client with Pages API."""
import pytest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from wikijs import WikiJSClient from wikijs import WikiJSClient
from wikijs.endpoints.pages import PagesEndpoint from wikijs.endpoints.pages import PagesEndpoint
from wikijs.models.page import Page, PageCreate from wikijs.models.page import Page
class TestWikiJSClientIntegration: class TestWikiJSClientIntegration:
@@ -13,14 +12,14 @@ class TestWikiJSClientIntegration:
def test_client_has_pages_endpoint(self): def test_client_has_pages_endpoint(self):
"""Test that client has pages endpoint initialized.""" """Test that client has pages endpoint initialized."""
with patch('wikijs.client.requests.Session'): with patch("wikijs.client.requests.Session"):
client = WikiJSClient("https://test.wiki", auth="test-key") client = WikiJSClient("https://test.wiki", auth="test-key")
assert hasattr(client, 'pages') assert hasattr(client, "pages")
assert isinstance(client.pages, PagesEndpoint) assert isinstance(client.pages, PagesEndpoint)
assert client.pages._client is client assert client.pages._client is client
@patch('wikijs.client.requests.Session') @patch("wikijs.client.requests.Session")
def test_client_pages_integration(self, mock_session_class): def test_client_pages_integration(self, mock_session_class):
"""Test that pages endpoint works through client.""" """Test that pages endpoint works through client."""
# Mock the session and response # Mock the session and response
@@ -31,7 +30,8 @@ class TestWikiJSClientIntegration:
mock_response.ok = True mock_response.ok = True
mock_response.json.return_value = { mock_response.json.return_value = {
"data": { "data": {
"pages": [{ "pages": [
{
"id": 1, "id": 1,
"title": "Test Page", "title": "Test Page",
"path": "test", "path": "test",
@@ -41,8 +41,9 @@ class TestWikiJSClientIntegration:
"tags": [], "tags": [],
"locale": "en", "locale": "en",
"createdAt": "2023-01-01T00:00:00Z", "createdAt": "2023-01-01T00:00:00Z",
"updatedAt": "2023-01-01T00:00:00Z" "updatedAt": "2023-01-01T00:00:00Z",
}] }
]
} }
} }
mock_session.request.return_value = mock_response mock_session.request.return_value = mock_response

View File

@@ -1,18 +1,19 @@
"""Tests for utility helper functions.""" """Tests for utility helper functions."""
import pytest
from unittest.mock import Mock from unittest.mock import Mock
import pytest
from wikijs.exceptions import ValidationError from wikijs.exceptions import ValidationError
from wikijs.utils.helpers import ( from wikijs.utils.helpers import (
normalize_url,
validate_url,
sanitize_path,
build_api_url, build_api_url,
parse_wiki_response,
extract_error_message,
chunk_list, chunk_list,
extract_error_message,
normalize_url,
parse_wiki_response,
safe_get, safe_get,
sanitize_path,
validate_url,
) )
@@ -29,11 +30,16 @@ class TestNormalizeUrl:
def test_normalize_url_remove_multiple_trailing_slashes(self): def test_normalize_url_remove_multiple_trailing_slashes(self):
"""Test multiple trailing slash removal.""" """Test multiple trailing slash removal."""
assert normalize_url("https://wiki.example.com///") == "https://wiki.example.com" assert (
normalize_url("https://wiki.example.com///") == "https://wiki.example.com"
)
def test_normalize_url_with_path(self): def test_normalize_url_with_path(self):
"""Test URL with path normalization.""" """Test URL with path normalization."""
assert normalize_url("https://wiki.example.com/wiki/") == "https://wiki.example.com/wiki" assert (
normalize_url("https://wiki.example.com/wiki/")
== "https://wiki.example.com/wiki"
)
def test_normalize_url_empty(self): def test_normalize_url_empty(self):
"""Test empty URL raises error.""" """Test empty URL raises error."""
@@ -63,7 +69,10 @@ class TestNormalizeUrl:
def test_normalize_url_with_port(self): def test_normalize_url_with_port(self):
"""Test URL with port.""" """Test URL with port."""
assert normalize_url("https://wiki.example.com:8080") == "https://wiki.example.com:8080" assert (
normalize_url("https://wiki.example.com:8080")
== "https://wiki.example.com:8080"
)
class TestValidateUrl: class TestValidateUrl:
@@ -334,10 +343,9 @@ class TestUtilityEdgeCases:
def test_validate_url_malformed_url(self): def test_validate_url_malformed_url(self):
"""Test validate_url with malformed URL that causes exception.""" """Test validate_url with malformed URL that causes exception."""
# Test with a string that could cause urlparse to raise an exception # Test with a string that could cause urlparse to raise an exception
import sys
from unittest.mock import patch from unittest.mock import patch
with patch('wikijs.utils.helpers.urlparse') as mock_urlparse: with patch("wikijs.utils.helpers.urlparse") as mock_urlparse:
mock_urlparse.side_effect = Exception("Parse error") mock_urlparse.side_effect = Exception("Parse error")
assert validate_url("http://example.com") is False assert validate_url("http://example.com") is False
@@ -362,6 +370,7 @@ class TestUtilityEdgeCases:
response = {"error": {"message": "Not found", "code": "404"}} response = {"error": {"message": "Not found", "code": "404"}}
from wikijs.exceptions import APIError from wikijs.exceptions import APIError
with pytest.raises(APIError, match="API Error: Not found"): with pytest.raises(APIError, match="API Error: Not found"):
parse_wiki_response(response) parse_wiki_response(response)
@@ -370,14 +379,21 @@ class TestUtilityEdgeCases:
response = {"error": "Simple error message"} response = {"error": "Simple error message"}
from wikijs.exceptions import APIError from wikijs.exceptions import APIError
with pytest.raises(APIError, match="API Error: Simple error message"): with pytest.raises(APIError, match="API Error: Simple error message"):
parse_wiki_response(response) parse_wiki_response(response)
def test_parse_wiki_response_with_errors_array(self): def test_parse_wiki_response_with_errors_array(self):
"""Test parse_wiki_response with errors array.""" """Test parse_wiki_response with errors array."""
response = {"errors": [{"message": "GraphQL error"}, {"message": "Another error"}]} response = {
"errors": [
{"message": "GraphQL error"},
{"message": "Another error"},
]
}
from wikijs.exceptions import APIError from wikijs.exceptions import APIError
with pytest.raises(APIError, match="GraphQL Error: GraphQL error"): with pytest.raises(APIError, match="GraphQL Error: GraphQL error"):
parse_wiki_response(response) parse_wiki_response(response)
@@ -386,6 +402,7 @@ class TestUtilityEdgeCases:
response = {"errors": "String error"} response = {"errors": "String error"}
from wikijs.exceptions import APIError from wikijs.exceptions import APIError
with pytest.raises(APIError, match="GraphQL Error: String error"): with pytest.raises(APIError, match="GraphQL Error: String error"):
parse_wiki_response(response) parse_wiki_response(response)

View File

@@ -18,21 +18,21 @@ Features:
- Context manager support for resource cleanup - Context manager support for resource cleanup
""" """
from .auth import AuthHandler, NoAuth, APIKeyAuth, JWTAuth from .auth import APIKeyAuth, AuthHandler, JWTAuth, NoAuth
from .client import WikiJSClient from .client import WikiJSClient
from .exceptions import ( from .exceptions import (
WikiJSException,
APIError, APIError,
AuthenticationError, AuthenticationError,
ConfigurationError,
ValidationError,
ClientError, ClientError,
ServerError, ConfigurationError,
ConnectionError,
NotFoundError, NotFoundError,
PermissionError, PermissionError,
RateLimitError, RateLimitError,
ConnectionError, ServerError,
TimeoutError, TimeoutError,
ValidationError,
WikiJSException,
) )
from .models import BaseModel, Page, PageCreate, PageUpdate from .models import BaseModel, Page, PageCreate, PageUpdate
from .version import __version__, __version_info__ from .version import __version__, __version_info__
@@ -41,19 +41,16 @@ from .version import __version__, __version_info__
__all__ = [ __all__ = [
# Main client # Main client
"WikiJSClient", "WikiJSClient",
# Authentication # Authentication
"AuthHandler", "AuthHandler",
"NoAuth", "NoAuth",
"APIKeyAuth", "APIKeyAuth",
"JWTAuth", "JWTAuth",
# Data models # Data models
"BaseModel", "BaseModel",
"Page", "Page",
"PageCreate", "PageCreate",
"PageUpdate", "PageUpdate",
# Exceptions # Exceptions
"WikiJSException", "WikiJSException",
"APIError", "APIError",
@@ -67,7 +64,6 @@ __all__ = [
"RateLimitError", "RateLimitError",
"ConnectionError", "ConnectionError",
"TimeoutError", "TimeoutError",
# Version info # Version info
"__version__", "__version__",
"__version_info__", "__version_info__",
@@ -81,4 +77,10 @@ __description__ = "Professional Python SDK for Wiki.js API integration"
__url__ = "https://github.com/yourusername/wikijs-python-sdk" __url__ = "https://github.com/yourusername/wikijs-python-sdk"
# For type checking # For type checking
__all__ += ["__author__", "__email__", "__license__", "__description__", "__url__"] __all__ += [
"__author__",
"__email__",
"__license__",
"__description__",
"__url__",
]

View File

@@ -9,8 +9,8 @@ Supported authentication methods:
- No authentication for testing (NoAuth) - No authentication for testing (NoAuth)
""" """
from .base import AuthHandler, NoAuth
from .api_key import APIKeyAuth from .api_key import APIKeyAuth
from .base import AuthHandler, NoAuth
from .jwt import JWTAuth from .jwt import JWTAuth
__all__ = [ __all__ = [

View File

@@ -4,7 +4,7 @@ This module implements API key authentication for Wiki.js instances.
API keys are typically used for server-to-server authentication. API keys are typically used for server-to-server authentication.
""" """
from typing import Dict, Optional from typing import Dict
from .base import AuthHandler from .base import AuthHandler
@@ -45,7 +45,7 @@ class APIKeyAuth(AuthHandler):
""" """
return { return {
"Authorization": f"Bearer {self._api_key}", "Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json" "Content-Type": "application/json",
} }
def is_valid(self) -> bool: def is_valid(self) -> bool:
@@ -66,7 +66,6 @@ class APIKeyAuth(AuthHandler):
If the API key becomes invalid, a new one must be provided. If the API key becomes invalid, a new one must be provided.
""" """
# API keys don't refresh - they're static until manually replaced # API keys don't refresh - they're static until manually replaced
pass
@property @property
def api_key(self) -> str: def api_key(self) -> str:
@@ -78,7 +77,9 @@ class APIKeyAuth(AuthHandler):
if len(self._api_key) <= 8: if len(self._api_key) <= 8:
return "*" * len(self._api_key) return "*" * len(self._api_key)
return f"{self._api_key[:4]}{'*' * (len(self._api_key) - 8)}{self._api_key[-4:]}" return (
f"{self._api_key[:4]}{'*' * (len(self._api_key) - 8)}{self._api_key[-4:]}"
)
def __repr__(self) -> str: def __repr__(self) -> str:
"""String representation of the auth handler. """String representation of the auth handler.

View File

@@ -5,7 +5,7 @@ providing a consistent interface for different authentication methods.
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Optional from typing import Dict
class AuthHandler(ABC): class AuthHandler(ABC):
@@ -25,7 +25,6 @@ class AuthHandler(ABC):
Raises: Raises:
AuthenticationError: If authentication is invalid or expired. AuthenticationError: If authentication is invalid or expired.
""" """
pass
@abstractmethod @abstractmethod
def is_valid(self) -> bool: def is_valid(self) -> bool:
@@ -34,7 +33,6 @@ class AuthHandler(ABC):
Returns: Returns:
bool: True if authentication is valid, False otherwise. bool: True if authentication is valid, False otherwise.
""" """
pass
@abstractmethod @abstractmethod
def refresh(self) -> None: def refresh(self) -> None:
@@ -46,7 +44,6 @@ class AuthHandler(ABC):
Raises: Raises:
AuthenticationError: If refresh fails. AuthenticationError: If refresh fails.
""" """
pass
def validate_credentials(self) -> None: def validate_credentials(self) -> None:
"""Validate credentials and refresh if necessary. """Validate credentials and refresh if necessary.
@@ -62,6 +59,7 @@ class AuthHandler(ABC):
if not self.is_valid(): if not self.is_valid():
from ..exceptions import AuthenticationError from ..exceptions import AuthenticationError
raise AuthenticationError("Authentication credentials are invalid") raise AuthenticationError("Authentication credentials are invalid")
@@ -94,4 +92,3 @@ class NoAuth(AuthHandler):
This method does nothing since there's no authentication to refresh. This method does nothing since there's no authentication to refresh.
""" """
pass

View File

@@ -5,7 +5,7 @@ JWT tokens are typically used for user-based authentication and have expiration
""" """
import time import time
from datetime import datetime, timedelta from datetime import timedelta
from typing import Dict, Optional from typing import Dict, Optional
from .base import AuthHandler from .base import AuthHandler
@@ -31,7 +31,7 @@ class JWTAuth(AuthHandler):
self, self,
token: str, token: str,
refresh_token: Optional[str] = None, refresh_token: Optional[str] = None,
expires_at: Optional[float] = None expires_at: Optional[float] = None,
) -> None: ) -> None:
"""Initialize JWT authentication. """Initialize JWT authentication.
@@ -68,7 +68,7 @@ class JWTAuth(AuthHandler):
return { return {
"Authorization": f"Bearer {self._token}", "Authorization": f"Bearer {self._token}",
"Content-Type": "application/json" "Content-Type": "application/json",
} }
def is_valid(self) -> bool: def is_valid(self) -> bool:

View File

@@ -7,7 +7,7 @@ import requests
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from .auth import AuthHandler, APIKeyAuth from .auth import APIKeyAuth, AuthHandler
from .endpoints import PagesEndpoint from .endpoints import PagesEndpoint
from .exceptions import ( from .exceptions import (
APIError, APIError,
@@ -17,7 +17,12 @@ from .exceptions import (
TimeoutError, TimeoutError,
create_api_error, create_api_error,
) )
from .utils import normalize_url, build_api_url, parse_wiki_response, extract_error_message from .utils import (
build_api_url,
extract_error_message,
normalize_url,
parse_wiki_response,
)
class WikiJSClient: class WikiJSClient:
@@ -73,7 +78,7 @@ class WikiJSClient:
# Request configuration # Request configuration
self.timeout = timeout self.timeout = timeout
self.verify_ssl = verify_ssl self.verify_ssl = verify_ssl
self.user_agent = user_agent or f"wikijs-python-sdk/0.1.0" self.user_agent = user_agent or "wikijs-python-sdk/0.1.0"
# Initialize HTTP session # Initialize HTTP session
self._session = self._create_session() self._session = self._create_session()
@@ -97,7 +102,14 @@ class WikiJSClient:
total=3, total=3,
backoff_factor=1, backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504], status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"], allowed_methods=[
"HEAD",
"GET",
"OPTIONS",
"POST",
"PUT",
"DELETE",
],
) )
adapter = HTTPAdapter(max_retries=retry_strategy) adapter = HTTPAdapter(max_retries=retry_strategy)
@@ -105,11 +117,13 @@ class WikiJSClient:
session.mount("https://", adapter) session.mount("https://", adapter)
# Set default headers # Set default headers
session.headers.update({ session.headers.update(
{
"User-Agent": self.user_agent, "User-Agent": self.user_agent,
"Accept": "application/json", "Accept": "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
}) }
)
# Set authentication headers # Set authentication headers
if self._auth_handler: if self._auth_handler:
@@ -126,7 +140,7 @@ class WikiJSClient:
endpoint: str, endpoint: str,
params: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None,
json_data: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None,
**kwargs **kwargs,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make HTTP request to Wiki.js API. """Make HTTP request to Wiki.js API.
@@ -154,7 +168,7 @@ class WikiJSClient:
"timeout": self.timeout, "timeout": self.timeout,
"verify": self.verify_ssl, "verify": self.verify_ssl,
"params": params, "params": params,
**kwargs **kwargs,
} }
# Add JSON data if provided # Add JSON data if provided
@@ -197,11 +211,7 @@ class WikiJSClient:
# Handle other HTTP errors # Handle other HTTP errors
if not response.ok: if not response.ok:
error_message = extract_error_message(response) error_message = extract_error_message(response)
raise create_api_error( raise create_api_error(response.status_code, error_message, response)
response.status_code,
error_message,
response
)
# Parse JSON response # Parse JSON response
try: try:
@@ -232,15 +242,15 @@ class WikiJSClient:
try: try:
# Try to hit a basic endpoint (will implement with actual endpoints) # Try to hit a basic endpoint (will implement with actual endpoints)
# For now, just test basic connectivity # For now, just test basic connectivity
response = self._session.get( self._session.get(
self.base_url, self.base_url, timeout=self.timeout, verify=self.verify_ssl
timeout=self.timeout,
verify=self.verify_ssl
) )
return True return True
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
raise TimeoutError(f"Connection test timed out after {self.timeout} seconds") raise TimeoutError(
f"Connection test timed out after {self.timeout} seconds"
)
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
raise ConnectionError(f"Cannot connect to {self.base_url}: {str(e)}") raise ConnectionError(f"Cannot connect to {self.base_url}: {str(e)}")

View File

@@ -1,6 +1,6 @@
"""Base endpoint class for wikijs-python-sdk.""" """Base endpoint class for wikijs-python-sdk."""
from typing import Any, Dict, List, Optional, TYPE_CHECKING from typing import TYPE_CHECKING, Any, Dict, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from ..client import WikiJSClient from ..client import WikiJSClient
@@ -30,7 +30,7 @@ class BaseEndpoint:
endpoint: str, endpoint: str,
params: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None,
json_data: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None,
**kwargs **kwargs,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make HTTP request through the client. """Make HTTP request through the client.
@@ -49,14 +49,11 @@ class BaseEndpoint:
endpoint=endpoint, endpoint=endpoint,
params=params, params=params,
json_data=json_data, json_data=json_data,
**kwargs **kwargs,
) )
def _get( def _get(
self, self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs
endpoint: str,
params: Optional[Dict[str, Any]] = None,
**kwargs
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make GET request. """Make GET request.
@@ -75,7 +72,7 @@ class BaseEndpoint:
endpoint: str, endpoint: str,
json_data: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None,
**kwargs **kwargs,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make POST request. """Make POST request.
@@ -88,14 +85,16 @@ class BaseEndpoint:
Returns: Returns:
Parsed response data Parsed response data
""" """
return self._request("POST", endpoint, params=params, json_data=json_data, **kwargs) return self._request(
"POST", endpoint, params=params, json_data=json_data, **kwargs
)
def _put( def _put(
self, self,
endpoint: str, endpoint: str,
json_data: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None,
**kwargs **kwargs,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make PUT request. """Make PUT request.
@@ -108,13 +107,12 @@ class BaseEndpoint:
Returns: Returns:
Parsed response data Parsed response data
""" """
return self._request("PUT", endpoint, params=params, json_data=json_data, **kwargs) return self._request(
"PUT", endpoint, params=params, json_data=json_data, **kwargs
)
def _delete( def _delete(
self, self, endpoint: str, params: Optional[Dict[str, Any]] = None, **kwargs
endpoint: str,
params: Optional[Dict[str, Any]] = None,
**kwargs
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make DELETE request. """Make DELETE request.

View File

@@ -48,7 +48,7 @@ class PagesEndpoint(BaseEndpoint):
locale: Optional[str] = None, locale: Optional[str] = None,
author_id: Optional[int] = None, author_id: Optional[int] = None,
order_by: str = "title", order_by: str = "title",
order_direction: str = "ASC" order_direction: str = "ASC",
) -> List[Page]: ) -> List[Page]:
"""List pages with optional filtering. """List pages with optional filtering.
@@ -77,7 +77,9 @@ class PagesEndpoint(BaseEndpoint):
raise ValidationError("offset must be non-negative") raise ValidationError("offset must be non-negative")
if order_by not in ["title", "created_at", "updated_at", "path"]: if order_by not in ["title", "created_at", "updated_at", "path"]:
raise ValidationError("order_by must be one of: title, created_at, updated_at, path") raise ValidationError(
"order_by must be one of: title, created_at, updated_at, path"
)
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")
@@ -99,9 +101,7 @@ class PagesEndpoint(BaseEndpoint):
""" """
# Make request (no variables needed for simple list query) # Make request (no variables needed for simple list query)
response = self._post("/graphql", json_data={ response = self._post("/graphql", json_data={"query": query})
"query": query
})
# Parse response # Parse response
if "errors" in response: if "errors" in response:
@@ -166,10 +166,10 @@ class PagesEndpoint(BaseEndpoint):
""" """
# Make request # Make request
response = self._post("/graphql", json_data={ response = self._post(
"query": query, "/graphql",
"variables": {"id": page_id} json_data={"query": query, "variables": {"id": page_id}},
}) )
# Parse response # Parse response
if "errors" in response: if "errors" in response:
@@ -230,10 +230,13 @@ class PagesEndpoint(BaseEndpoint):
""" """
# Make request # Make request
response = self._post("/graphql", json_data={ response = self._post(
"/graphql",
json_data={
"query": query, "query": query,
"variables": {"path": path, "locale": locale} "variables": {"path": path, "locale": locale},
}) },
)
# Parse response # Parse response
if "errors" in response: if "errors" in response:
@@ -312,19 +315,19 @@ 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}", "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,
"locale": page_data.locale, "locale": page_data.locale,
"editor": page_data.editor "editor": page_data.editor,
} }
# Make request # Make request
response = self._post("/graphql", json_data={ response = self._post(
"query": mutation, "/graphql", json_data={"query": mutation, "variables": variables}
"variables": variables )
})
# Parse response # Parse response
if "errors" in response: if "errors" in response:
@@ -349,9 +352,7 @@ class PagesEndpoint(BaseEndpoint):
raise APIError(f"Failed to parse created page data: {str(e)}") from e raise APIError(f"Failed to parse created page data: {str(e)}") from e
def update( def update(
self, self, page_id: int, page_data: Union[PageUpdate, Dict[str, Any]]
page_id: int,
page_data: Union[PageUpdate, Dict[str, Any]]
) -> Page: ) -> Page:
"""Update an existing page. """Update an existing page.
@@ -426,10 +427,9 @@ class PagesEndpoint(BaseEndpoint):
variables["tags"] = page_data.tags variables["tags"] = page_data.tags
# Make request # Make request
response = self._post("/graphql", json_data={ response = self._post(
"query": mutation, "/graphql", json_data={"query": mutation, "variables": variables}
"variables": variables )
})
# Parse response # Parse response
if "errors" in response: if "errors" in response:
@@ -473,10 +473,10 @@ class PagesEndpoint(BaseEndpoint):
""" """
# Make request # Make request
response = self._post("/graphql", json_data={ response = self._post(
"query": mutation, "/graphql",
"variables": {"id": page_id} json_data={"query": mutation, "variables": {"id": page_id}},
}) )
# Parse response # Parse response
if "errors" in response: if "errors" in response:
@@ -495,7 +495,7 @@ class PagesEndpoint(BaseEndpoint):
self, self,
query: str, query: str,
limit: Optional[int] = None, limit: Optional[int] = None,
locale: Optional[str] = None locale: Optional[str] = None,
) -> List[Page]: ) -> List[Page]:
"""Search for pages by content and title. """Search for pages by content and title.
@@ -518,17 +518,13 @@ class PagesEndpoint(BaseEndpoint):
raise ValidationError("limit must be greater than 0") raise ValidationError("limit must be greater than 0")
# Use the list method with search parameter # Use the list method with search parameter
return self.list( return self.list(search=query, limit=limit, locale=locale)
search=query,
limit=limit,
locale=locale
)
def get_by_tags( def get_by_tags(
self, self,
tags: List[str], tags: List[str],
match_all: bool = True, match_all: bool = True,
limit: Optional[int] = None limit: Optional[int] = None,
) -> List[Page]: ) -> List[Page]:
"""Get pages by tags. """Get pages by tags.
@@ -557,7 +553,9 @@ class PagesEndpoint(BaseEndpoint):
# For match_all=False, we need a more complex query # For match_all=False, we need a more complex query
# This would require a custom GraphQL query or multiple requests # This would require a custom GraphQL query or multiple requests
# For now, implement a simple approach # For now, implement a simple approach
all_pages = self.list(limit=limit * 2 if limit else None) # Get more pages to filter all_pages = self.list(
limit=limit * 2 if limit else None
) # Get more pages to filter
matching_pages = [] matching_pages = []
for page in all_pages: for page in all_pages:
@@ -594,7 +592,7 @@ class PagesEndpoint(BaseEndpoint):
"authorEmail": "author_email", "authorEmail": "author_email",
"editor": "editor", "editor": "editor",
"createdAt": "created_at", "createdAt": "created_at",
"updatedAt": "updated_at" "updatedAt": "updated_at",
} }
for api_field, model_field in field_mapping.items(): for api_field, model_field in field_mapping.items():

View File

@@ -14,12 +14,10 @@ class WikiJSException(Exception):
class ConfigurationError(WikiJSException): class ConfigurationError(WikiJSException):
"""Raised when there's an issue with SDK configuration.""" """Raised when there's an issue with SDK configuration."""
pass
class AuthenticationError(WikiJSException): class AuthenticationError(WikiJSException):
"""Raised when authentication fails.""" """Raised when authentication fails."""
pass
class ValidationError(WikiJSException): class ValidationError(WikiJSException):
@@ -39,7 +37,7 @@ class APIError(WikiJSException):
message: str, message: str,
status_code: Optional[int] = None, status_code: Optional[int] = None,
response: Optional[Any] = None, response: Optional[Any] = None,
details: Optional[Dict[str, Any]] = None details: Optional[Dict[str, Any]] = None,
): ):
super().__init__(message, details) super().__init__(message, details)
self.status_code = status_code self.status_code = status_code
@@ -48,22 +46,18 @@ class APIError(WikiJSException):
class ClientError(APIError): class ClientError(APIError):
"""Raised for 4xx HTTP status codes (client errors).""" """Raised for 4xx HTTP status codes (client errors)."""
pass
class ServerError(APIError): class ServerError(APIError):
"""Raised for 5xx HTTP status codes (server errors).""" """Raised for 5xx HTTP status codes (server errors)."""
pass
class NotFoundError(ClientError): class NotFoundError(ClientError):
"""Raised when a requested resource is not found (404).""" """Raised when a requested resource is not found (404)."""
pass
class PermissionError(ClientError): class PermissionError(ClientError):
"""Raised when access is forbidden (403).""" """Raised when access is forbidden (403)."""
pass
class RateLimitError(ClientError): class RateLimitError(ClientError):
@@ -71,19 +65,17 @@ class RateLimitError(ClientError):
def __init__(self, message: str, retry_after: Optional[int] = None, **kwargs): def __init__(self, message: str, retry_after: Optional[int] = None, **kwargs):
# Remove status_code from kwargs if present to avoid duplicate argument # Remove status_code from kwargs if present to avoid duplicate argument
kwargs.pop('status_code', None) kwargs.pop("status_code", None)
super().__init__(message, status_code=429, **kwargs) super().__init__(message, status_code=429, **kwargs)
self.retry_after = retry_after self.retry_after = retry_after
class ConnectionError(WikiJSException): class ConnectionError(WikiJSException):
"""Raised when there's a connection issue.""" """Raised when there's a connection issue."""
pass
class TimeoutError(WikiJSException): class TimeoutError(WikiJSException):
"""Raised when a request times out.""" """Raised when a request times out."""
pass
def create_api_error(status_code: int, message: str, response: Any = None) -> APIError: def create_api_error(status_code: int, message: str, response: Any = None) -> APIError:

View File

@@ -3,7 +3,8 @@
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from pydantic import BaseModel as PydanticBaseModel, ConfigDict from pydantic import BaseModel as PydanticBaseModel
from pydantic import ConfigDict
class BaseModel(PydanticBaseModel): class BaseModel(PydanticBaseModel):
@@ -26,9 +27,7 @@ class BaseModel(PydanticBaseModel):
# Allow extra fields for forward compatibility # Allow extra fields for forward compatibility
extra="ignore", extra="ignore",
# Serialize datetime as ISO format # Serialize datetime as ISO format
json_encoders={ json_encoders={datetime: lambda v: v.isoformat() if v else None},
datetime: lambda v: v.isoformat() if v else None
}
) )
def to_dict(self, exclude_none: bool = True) -> Dict[str, Any]: def to_dict(self, exclude_none: bool = True) -> Dict[str, Any]:

View File

@@ -1,7 +1,6 @@
"""Page-related data models for wikijs-python-sdk.""" """Page-related data models for wikijs-python-sdk."""
import re import re
from datetime import datetime
from typing import List, Optional from typing import List, Optional
from pydantic import Field, validator from pydantic import Field, validator

View File

@@ -1,14 +1,14 @@
"""Utility functions for wikijs-python-sdk.""" """Utility functions for wikijs-python-sdk."""
from .helpers import ( from .helpers import (
build_api_url,
chunk_list,
extract_error_message,
normalize_url, normalize_url,
parse_wiki_response,
safe_get,
sanitize_path, sanitize_path,
validate_url, validate_url,
build_api_url,
parse_wiki_response,
extract_error_message,
chunk_list,
safe_get,
) )
__all__ = [ __all__ = [

View File

@@ -1,7 +1,7 @@
"""Helper utilities for wikijs-python-sdk.""" """Helper utilities for wikijs-python-sdk."""
import re import re
from typing import Any, Dict, Optional from typing import Any, Dict
from urllib.parse import urljoin, urlparse from urllib.parse import urljoin, urlparse
from ..exceptions import APIError, ValidationError from ..exceptions import APIError, ValidationError
@@ -135,7 +135,11 @@ def parse_wiki_response(response_data: Dict[str, Any]) -> Dict[str, Any]:
errors = response_data["errors"] errors = response_data["errors"]
if errors: if errors:
first_error = errors[0] if isinstance(errors, list) else errors first_error = errors[0] if isinstance(errors, list) else errors
message = first_error.get("message", "GraphQL error") if isinstance(first_error, dict) else str(first_error) message = (
first_error.get("message", "GraphQL error")
if isinstance(first_error, dict)
else str(first_error)
)
raise APIError(f"GraphQL Error: {message}", details={"errors": errors}) raise APIError(f"GraphQL Error: {message}", details={"errors": errors})
return response_data return response_data
@@ -162,7 +166,9 @@ def extract_error_message(response: Any) -> str:
pass pass
if hasattr(response, "text"): if hasattr(response, "text"):
return response.text[:200] + "..." if len(response.text) > 200 else response.text return (
response.text[:200] + "..." if len(response.text) > 200 else response.text
)
return str(response) return str(response)