Files
py-wikijs/wikijs/ratelimit.py
Claude cef6903cbc feat: implement production-ready features from improvement plan phase 2.5 & 2.6
Phase 2.5: Fix Foundation (CRITICAL)
- Fixed 4 failing tests by adding cache attribute to mock_client fixture
- Created comprehensive cache tests for Pages endpoint (test_pages_cache.py)
- Added missing dependencies: pydantic[email] and aiohttp to core requirements
- Updated requirements.txt with proper dependency versions
- Achieved 82.67% test coverage with 454 passing tests

Phase 2.6: Production Essentials
- Implemented structured logging (wikijs/logging.py)
  * JSON and text log formatters
  * Configurable log levels and output destinations
  * Integration with client operations

- Implemented metrics and telemetry (wikijs/metrics.py)
  * Request tracking with duration, status codes, errors
  * Latency percentiles (min, max, avg, p50, p95, p99)
  * Error rate calculation
  * Thread-safe metrics collection

- Implemented rate limiting (wikijs/ratelimit.py)
  * Token bucket algorithm for request throttling
  * Per-endpoint rate limiting support
  * Configurable timeout handling
  * Burst capacity management

- Created SECURITY.md policy
  * Vulnerability reporting procedures
  * Security best practices
  * Response timelines
  * Supported versions

Documentation
- Added comprehensive logging guide (docs/logging.md)
- Added metrics and telemetry guide (docs/metrics.md)
- Added rate limiting guide (docs/rate_limiting.md)
- Updated README.md with production features section
- Updated IMPROVEMENT_PLAN_2.md with completed checkboxes

Testing
- Created test suite for logging (tests/test_logging.py)
- Created test suite for metrics (tests/test_metrics.py)
- Created test suite for rate limiting (tests/test_ratelimit.py)
- All 454 tests passing
- Test coverage: 82.67%

Breaking Changes: None
Dependencies Added: pydantic[email], email-validator, dnspython

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 16:45:02 +00:00

111 lines
3.2 KiB
Python

"""Rate limiting for wikijs-python-sdk."""
import time
import threading
from typing import Optional, Dict
class RateLimiter:
"""Token bucket rate limiter."""
def __init__(
self,
requests_per_second: float = 10.0,
burst: Optional[int] = None
):
"""Initialize rate limiter.
Args:
requests_per_second: Maximum requests per second
burst: Maximum burst size (defaults to requests_per_second)
"""
self.rate = requests_per_second
self.burst = burst or int(requests_per_second)
self._tokens = float(self.burst)
self._last_update = time.time()
self._lock = threading.Lock()
def acquire(self, timeout: Optional[float] = None) -> bool:
"""Acquire permission to make a request.
Args:
timeout: Maximum time to wait in seconds (None = wait forever)
Returns:
True if acquired, False if timeout
"""
deadline = time.time() + timeout if timeout else None
while True:
with self._lock:
now = time.time()
# Refill tokens based on elapsed time
elapsed = now - self._last_update
self._tokens = min(
self.burst,
self._tokens + elapsed * self.rate
)
self._last_update = now
# Check if we have tokens
if self._tokens >= 1.0:
self._tokens -= 1.0
return True
# Calculate wait time
wait_time = (1.0 - self._tokens) / self.rate
# Check timeout
if deadline and time.time() + wait_time > deadline:
return False
# Sleep and retry
time.sleep(min(wait_time, 0.1))
def reset(self) -> None:
"""Reset rate limiter."""
with self._lock:
self._tokens = float(self.burst)
self._last_update = time.time()
class PerEndpointRateLimiter:
"""Rate limiter with per-endpoint limits."""
def __init__(self, default_rate: float = 10.0):
"""Initialize per-endpoint rate limiter.
Args:
default_rate: Default rate limit for endpoints
"""
self.default_rate = default_rate
self._limiters: Dict[str, RateLimiter] = {}
self._lock = threading.Lock()
def set_limit(self, endpoint: str, rate: float) -> None:
"""Set rate limit for specific endpoint.
Args:
endpoint: The endpoint path
rate: Requests per second for this endpoint
"""
with self._lock:
self._limiters[endpoint] = RateLimiter(rate)
def acquire(self, endpoint: str, timeout: Optional[float] = None) -> bool:
"""Acquire for specific endpoint.
Args:
endpoint: The endpoint path
timeout: Maximum time to wait
Returns:
True if acquired, False if timeout
"""
with self._lock:
if endpoint not in self._limiters:
self._limiters[endpoint] = RateLimiter(self.default_rate)
limiter = self._limiters[endpoint]
return limiter.acquire(timeout)