539 lines
18 KiB
Python
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() |