Files
HCFS/hcfs-python/hcfs/sdk/client.py
2025-07-30 09:34:16 +10:00

539 lines
18 KiB
Python

"""
HCFS Synchronous Client
High-level synchronous client for HCFS API operations.
"""
import json
import time
from typing import List, Optional, Dict, Any, Iterator
from datetime import datetime
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from .models import (
Context, SearchResult, ContextFilter, PaginationOptions,
SearchOptions, ClientConfig, AnalyticsData, BatchResult
)
from .exceptions import (
HCFSError, HCFSConnectionError, HCFSAuthenticationError,
HCFSNotFoundError, HCFSValidationError, handle_api_error
)
from .utils import MemoryCache, validate_path, normalize_path
from .decorators import cached_context, retry_on_failure, rate_limited
class HCFSClient:
"""
Synchronous HCFS API client with caching and retry capabilities.
This client provides a high-level interface for interacting with the HCFS API,
including context management, search operations, and batch processing.
Example:
>>> from hcfs.sdk import HCFSClient, Context
>>>
>>> # Initialize client
>>> client = HCFSClient(
... base_url="https://api.hcfs.example.com",
... api_key="your-api-key"
... )
>>>
>>> # Create a context
>>> context = Context(
... path="/docs/readme",
... content="This is a README file",
... summary="Project documentation"
... )
>>> created = client.create_context(context)
>>>
>>> # Search contexts
>>> results = client.search_contexts("README documentation")
>>> for result in results:
... print(f"Found: {result.context.path} (score: {result.score})")
"""
def __init__(self, config: Optional[ClientConfig] = None, **kwargs):
"""
Initialize HCFS client.
Args:
config: Client configuration object
**kwargs: Configuration overrides (base_url, api_key, etc.)
"""
# Merge configuration
if config:
self.config = config
else:
self.config = ClientConfig(**kwargs)
# Initialize session with retry strategy
self.session = requests.Session()
# Configure retries
retry_strategy = Retry(
total=self.config.retry.max_attempts if self.config.retry.enabled else 0,
status_forcelist=[429, 500, 502, 503, 504],
backoff_factor=self.config.retry.base_delay,
raise_on_status=False
)
adapter = HTTPAdapter(
max_retries=retry_strategy,
pool_connections=self.config.max_connections,
pool_maxsize=self.config.max_keepalive_connections
)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
# Set headers
self.session.headers.update({
"User-Agent": self.config.user_agent,
"Content-Type": "application/json"
})
if self.config.api_key:
self.session.headers["X-API-Key"] = self.config.api_key
elif self.config.jwt_token:
self.session.headers["Authorization"] = f"Bearer {self.config.jwt_token}"
# Initialize cache
self._cache = MemoryCache(
max_size=self.config.cache.max_size,
strategy=self.config.cache.strategy,
ttl_seconds=self.config.cache.ttl_seconds
) if self.config.cache.enabled else None
# Analytics
self.analytics = AnalyticsData()
def health_check(self) -> Dict[str, Any]:
"""
Check API health status.
Returns:
Health status information
Raises:
HCFSConnectionError: If health check fails
"""
try:
response = self.session.get(
f"{self.config.base_url}/health",
timeout=self.config.timeout
)
if response.status_code == 200:
self._update_analytics("health_check", success=True)
return response.json()
else:
self._update_analytics("health_check", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
self._update_analytics("health_check", success=False, error=str(e))
raise HCFSConnectionError(f"Health check failed: {str(e)}")
@cached_context()
@retry_on_failure()
def create_context(self, context: Context) -> Context:
"""
Create a new context.
Args:
context: Context object to create
Returns:
Created context with assigned ID
Raises:
HCFSValidationError: If context data is invalid
HCFSError: If creation fails
"""
if not validate_path(context.path):
raise HCFSValidationError(f"Invalid context path: {context.path}")
context.path = normalize_path(context.path)
try:
response = self.session.post(
f"{self.config.base_url}/api/v1/contexts",
json=context.to_create_dict(),
timeout=self.config.timeout
)
if response.status_code == 200:
data = response.json()["data"]
created_context = Context(**data)
self._update_analytics("create_context", success=True)
return created_context
else:
self._update_analytics("create_context", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
self._update_analytics("create_context", success=False, error=str(e))
raise HCFSConnectionError(f"Failed to create context: {str(e)}")
@cached_context()
def get_context(self, context_id: int) -> Context:
"""
Retrieve a context by ID.
Args:
context_id: Context identifier
Returns:
Context object
Raises:
HCFSNotFoundError: If context doesn't exist
"""
try:
response = self.session.get(
f"{self.config.base_url}/api/v1/contexts/{context_id}",
timeout=self.config.timeout
)
if response.status_code == 200:
data = response.json()["data"]
context = Context(**data)
self._update_analytics("get_context", success=True)
return context
else:
self._update_analytics("get_context", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
self._update_analytics("get_context", success=False, error=str(e))
raise HCFSConnectionError(f"Failed to get context: {str(e)}")
def list_contexts(self,
filter_opts: Optional[ContextFilter] = None,
pagination: Optional[PaginationOptions] = None) -> List[Context]:
"""
List contexts with filtering and pagination.
Args:
filter_opts: Context filtering options
pagination: Pagination configuration
Returns:
List of contexts
"""
params = {}
if filter_opts:
params.update(filter_opts.to_query_params())
if pagination:
params.update(pagination.to_query_params())
try:
response = self.session.get(
f"{self.config.base_url}/api/v1/contexts",
params=params,
timeout=self.config.timeout
)
if response.status_code == 200:
data = response.json()["data"]
contexts = [Context(**ctx_data) for ctx_data in data]
self._update_analytics("list_contexts", success=True)
return contexts
else:
self._update_analytics("list_contexts", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
self._update_analytics("list_contexts", success=False, error=str(e))
raise HCFSConnectionError(f"Failed to list contexts: {str(e)}")
def update_context(self, context_id: int, updates: Dict[str, Any]) -> Context:
"""
Update an existing context.
Args:
context_id: Context identifier
updates: Fields to update
Returns:
Updated context
Raises:
HCFSNotFoundError: If context doesn't exist
HCFSValidationError: If update data is invalid
"""
try:
response = self.session.put(
f"{self.config.base_url}/api/v1/contexts/{context_id}",
json=updates,
timeout=self.config.timeout
)
if response.status_code == 200:
data = response.json()["data"]
updated_context = Context(**data)
self._update_analytics("update_context", success=True)
# Invalidate cache
if self._cache:
cache_key = f"get_context:{context_id}"
self._cache.remove(cache_key)
return updated_context
else:
self._update_analytics("update_context", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
self._update_analytics("update_context", success=False, error=str(e))
raise HCFSConnectionError(f"Failed to update context: {str(e)}")
def delete_context(self, context_id: int) -> bool:
"""
Delete a context.
Args:
context_id: Context identifier
Returns:
True if deletion was successful
Raises:
HCFSNotFoundError: If context doesn't exist
"""
try:
response = self.session.delete(
f"{self.config.base_url}/api/v1/contexts/{context_id}",
timeout=self.config.timeout
)
if response.status_code == 200:
self._update_analytics("delete_context", success=True)
# Invalidate cache
if self._cache:
cache_key = f"get_context:{context_id}"
self._cache.remove(cache_key)
return True
else:
self._update_analytics("delete_context", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
self._update_analytics("delete_context", success=False, error=str(e))
raise HCFSConnectionError(f"Failed to delete context: {str(e)}")
@rate_limited(requests_per_second=10.0)
def search_contexts(self,
query: str,
options: Optional[SearchOptions] = None) -> List[SearchResult]:
"""
Search contexts using various search methods.
Args:
query: Search query string
options: Search configuration options
Returns:
List of search results ordered by relevance
"""
search_opts = options or SearchOptions()
request_data = {
"query": query,
**search_opts.to_request_dict()
}
try:
response = self.session.post(
f"{self.config.base_url}/api/v1/search",
json=request_data,
timeout=self.config.timeout
)
if response.status_code == 200:
data = response.json()["data"]
results = []
for result_data in data:
context = Context(**result_data["context"])
search_result = SearchResult(
context=context,
score=result_data["score"],
explanation=result_data.get("explanation"),
highlights=result_data.get("highlights", [])
)
results.append(search_result)
self._update_analytics("search_contexts", success=True)
return sorted(results, key=lambda x: x.score, reverse=True)
else:
self._update_analytics("search_contexts", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
self._update_analytics("search_contexts", success=False, error=str(e))
raise HCFSConnectionError(f"Search failed: {str(e)}")
def batch_create_contexts(self, contexts: List[Context]) -> BatchResult:
"""
Create multiple contexts in a single batch operation.
Args:
contexts: List of contexts to create
Returns:
Batch operation results
"""
request_data = {
"contexts": [ctx.to_create_dict() for ctx in contexts]
}
start_time = time.time()
try:
response = self.session.post(
f"{self.config.base_url}/api/v1/contexts/batch",
json=request_data,
timeout=self.config.timeout * 3 # Extended timeout for batch ops
)
execution_time = time.time() - start_time
if response.status_code == 200:
data = response.json()["data"]
result = BatchResult(
success_count=data["success_count"],
error_count=data["error_count"],
total_items=data["total_items"],
successful_items=data.get("created_ids", []),
failed_items=data.get("errors", []),
execution_time=execution_time
)
self._update_analytics("batch_create", success=True)
return result
else:
self._update_analytics("batch_create", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
execution_time = time.time() - start_time
self._update_analytics("batch_create", success=False, error=str(e))
return BatchResult(
success_count=0,
error_count=len(contexts),
total_items=len(contexts),
successful_items=[],
failed_items=[{"error": str(e)}],
execution_time=execution_time
)
def get_statistics(self) -> Dict[str, Any]:
"""
Get comprehensive system statistics.
Returns:
System statistics and metrics
"""
try:
response = self.session.get(
f"{self.config.base_url}/api/v1/stats",
timeout=self.config.timeout
)
if response.status_code == 200:
self._update_analytics("get_statistics", success=True)
return response.json()
else:
self._update_analytics("get_statistics", success=False)
handle_api_error(response)
except requests.exceptions.RequestException as e:
self._update_analytics("get_statistics", success=False, error=str(e))
raise HCFSConnectionError(f"Failed to get statistics: {str(e)}")
def iterate_contexts(self,
filter_opts: Optional[ContextFilter] = None,
page_size: int = 100) -> Iterator[Context]:
"""
Iterate through all contexts with automatic pagination.
Args:
filter_opts: Context filtering options
page_size: Number of contexts per page
Yields:
Context objects
"""
page = 1
while True:
pagination = PaginationOptions(page=page, page_size=page_size)
contexts = self.list_contexts(filter_opts, pagination)
if not contexts:
break
for context in contexts:
yield context
# If we got fewer contexts than requested, we've reached the end
if len(contexts) < page_size:
break
page += 1
def clear_cache(self) -> None:
"""Clear all cached data."""
if self._cache:
self._cache.clear()
def get_cache_stats(self) -> Dict[str, Any]:
"""Get cache statistics."""
if self._cache:
stats = self._cache.stats()
self.analytics.cache_stats = stats
return stats
return {}
def get_analytics(self) -> AnalyticsData:
"""
Get client analytics and usage statistics.
Returns:
Analytics data including operation counts and performance metrics
"""
# Update cache stats
if self._cache:
self.analytics.cache_stats = self._cache.stats()
return self.analytics
def _update_analytics(self, operation: str, success: bool, error: Optional[str] = None):
"""Update internal analytics tracking."""
self.analytics.operation_count[operation] = self.analytics.operation_count.get(operation, 0) + 1
if not success:
error_key = error or "unknown_error"
self.analytics.error_stats[error_key] = self.analytics.error_stats.get(error_key, 0) + 1
def close(self):
"""Close the client and cleanup resources."""
self.session.close()
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.close()