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() | 
