Implement core infrastructure: - BaseClientMixin with retry logic and validation - FastGPTClient base class with httpx - ChatClient with 11 chat operation methods - AppClient for analytics and logs - Custom exceptions (APIError, AuthenticationError, etc.) - Package configuration (pyproject.toml, setup.py) - Documentation (README.md, CLAUDE.md) - Basic usage examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
115 lines
4.0 KiB
Python
115 lines
4.0 KiB
Python
"""Base client mixin with retry logic and validation."""
|
|
|
|
import logging
|
|
import time
|
|
from typing import Any
|
|
|
|
|
|
class BaseClientMixin:
|
|
"""Mixin class providing retry logic, validation, and logging for FastGPT clients."""
|
|
|
|
def __init__(
|
|
self,
|
|
api_key: str,
|
|
base_url: str,
|
|
timeout: float = 60.0,
|
|
max_retries: int = 3,
|
|
retry_delay: float = 1.0,
|
|
enable_logging: bool = False,
|
|
):
|
|
"""Initialize base client.
|
|
|
|
Args:
|
|
api_key: FastGPT API key
|
|
base_url: Base URL for FastGPT API
|
|
timeout: Request timeout in seconds
|
|
max_retries: Maximum number of retry attempts
|
|
retry_delay: Delay between retries in seconds
|
|
enable_logging: Whether to enable request logging
|
|
"""
|
|
self.api_key = api_key
|
|
self.base_url = base_url
|
|
self.timeout = timeout
|
|
self.max_retries = max_retries
|
|
self.retry_delay = retry_delay
|
|
self.enable_logging = enable_logging
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
def _validate_params(self, **params) -> None:
|
|
"""Validate request parameters.
|
|
|
|
Args:
|
|
**params: Parameters to validate
|
|
|
|
Raises:
|
|
ValidationError: If any parameter is invalid
|
|
"""
|
|
for key, value in params.items():
|
|
if value is None:
|
|
continue
|
|
# Check for empty strings that should be non-empty
|
|
if isinstance(value, str) and key in ("query", "chatId", "appId", "dataId", "content"):
|
|
if not value.strip():
|
|
from .exceptions import ValidationError
|
|
raise ValidationError(f"{key} must be a non-empty string")
|
|
# Check for valid lists/dicts
|
|
elif isinstance(value, (list, dict)) and not value:
|
|
# Empty lists/dicts are usually valid, but log for debugging
|
|
if self.enable_logging and self.logger.isEnabledFor(logging.DEBUG):
|
|
self.logger.debug(f"Parameter {key} is empty")
|
|
|
|
def _retry_request(self, request_func, request_context: str):
|
|
"""Execute a request with retry logic.
|
|
|
|
Args:
|
|
request_func: Function that executes the HTTP request
|
|
request_context: Description of the request for logging
|
|
|
|
Returns:
|
|
Response from the request
|
|
|
|
Raises:
|
|
APIError: If all retries are exhausted
|
|
"""
|
|
last_exception = None
|
|
|
|
for attempt in range(self.max_retries):
|
|
try:
|
|
response = request_func()
|
|
|
|
# Success on non-5xx responses
|
|
if response.status_code < 500:
|
|
return response
|
|
|
|
# Server error - will retry
|
|
if self.enable_logging:
|
|
self.logger.warning(
|
|
f"{request_context} failed with status {response.status_code} "
|
|
f"(attempt {attempt + 1}/{self.max_retries})"
|
|
)
|
|
|
|
if attempt < self.max_retries - 1:
|
|
# Exponential backoff
|
|
sleep_time = self.retry_delay * (2 ** attempt)
|
|
time.sleep(sleep_time)
|
|
|
|
except Exception as e:
|
|
last_exception = e
|
|
if self.enable_logging:
|
|
self.logger.warning(
|
|
f"{request_context} raised exception: {e} "
|
|
f"(attempt {attempt + 1}/{self.max_retries})"
|
|
)
|
|
|
|
if attempt < self.max_retries - 1:
|
|
sleep_time = self.retry_delay * (2 ** attempt)
|
|
time.sleep(sleep_time)
|
|
|
|
# All retries exhausted
|
|
if last_exception:
|
|
from .exceptions import APIError
|
|
raise APIError(f"Request failed after {self.max_retries} attempts: {last_exception}")
|
|
|
|
from .exceptions import APIError
|
|
raise APIError(f"Request failed after {self.max_retries} attempts")
|