import logging from typing import Any, Dict, Optional import requests from fastapi import HTTPException from config import Config class KleerAPIError(Exception): """Custom exception for Kleer API errors.""" pass class KleerAPI: """Client for interacting with the Kleer API.""" def __init__(self, api_url: Optional[str] = None, company_id: Optional[str] = None): """ Initialize Kleer API client. Args: api_url: Base URL for the Kleer API company_id: Company ID for API requests Raises: ValueError: If API key is not configured """ self.api_url = api_url or Config.API_URL self.company_id = company_id or Config.get_company_id() self.api_key = Config.get_api_key() if not self.api_key: raise ValueError("KLEER_API_KEY environment variable is not set") def _get_headers(self) -> Dict[str, str]: """Get headers for API requests.""" return { 'X-token': self.api_key, 'Content-Type': 'application/json' } def api_request( self, endpoint: str, method: str = 'GET', data: Optional[Any] = None ) -> Optional[Any]: """ Make a request to the Kleer API. Args: endpoint: API endpoint path method: HTTP method (GET, POST, PUT) data: Request payload for POST/PUT requests Returns: JSON response from the API Raises: HTTPException: If the request fails """ url = f"{self.api_url}/{endpoint}" headers = self._get_headers() response = None try: if method == 'GET': response = requests.get(url, headers=headers) elif method == 'POST': response = requests.post(url, headers=headers, json=data) elif method == 'PUT': response = requests.put(url, headers=headers, json=data) else: raise ValueError(f"Unsupported HTTP method: {method}") response.raise_for_status() return response.json() except requests.HTTPError as e: logging.error(f"HTTP error during API request: {e}") raise HTTPException( status_code=response.status_code if response else 502, detail=f"API request failed: {str(e)}" ) except requests.RequestException as e: logging.error(f"API request failed: {e}") raise HTTPException(status_code=502, detail=f"API request failed: {str(e)}") def get_events(self, user_id: int, start_date: str, end_date: str) -> Dict[str, Any]: """ Retrieve events for a user within a date range. Args: user_id: User ID start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format Returns: Event data from API """ endpoint = ( f"company/{self.company_id}/event" f"?userId={user_id}&startDate={start_date}&endDate={end_date}" ) return self.api_request(endpoint, method='GET') or {} def approve_events(self, user_id: int, start_date: str, end_date: str) -> Dict[str, Any]: """ Approve events for a user within a date range. Args: user_id: User ID start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format Returns: Approval response from API """ endpoint = f"company/{self.company_id}/event/approve" payload = { "user-id": {"id": user_id}, "start-date": start_date, "end-date": end_date } return self.api_request(endpoint, method='POST', data=payload) or {} def put_event(self, event: Dict[str, Any]) -> Optional[Any]: """ Submit a time event to the Kleer API. Args: event: Time event data Returns: API response Raises: HTTPException: If the request fails """ url = f"company/{self.company_id}/event" try: response = self.api_request(url, method='PUT', data=event) logging.info(f"Successfully reported event for date: {event.get('date')}") return response except HTTPException as e: logging.error( f"Failed to put event for date {event.get('date')}. " f"Status: {e.status_code}, Detail: {e.detail}" ) raise except Exception as e: logging.error( f"Unexpected error while putting event for date {event.get('date')}: {e}" ) raise HTTPException( status_code=500, detail=f"An unexpected error occurred: {str(e)}" ) def get_projects(self, user_id: int) -> Dict[str, Dict[str, Any]]: """ Retrieve active projects for a user. Args: user_id: User ID to filter projects Returns: Dictionary mapping project names to project info """ url = f"company/{self.company_id}/client-project?filter=active" data = self.api_request(url, method='GET') if not data: logging.warning("No data received from projects API.") return {} projects_data = data.get('client-project-readables') if isinstance(data, dict) else data if not isinstance(projects_data, list): logging.error("Unexpected projects data format.") return {} projects = {} for project in projects_data: if not isinstance(project, dict): logging.warning(f"Unexpected project data format: {project}") continue project_id = project.get('id') project_name = project.get('name') activity_id = None users = project.get('users', []) for user in users: if user.get('user', {}).get('id') != user_id: continue activities = user.get('activities', []) for activity in activities: activity_id = activity.get('activity', {}) if activity_id: break if activity_id: break if not project_id or not project_name: logging.info(f"Skipping project with missing ID or name: {project}") continue if activity_id: projects[project_name] = {"id": project_id, "activity": activity_id} else: logging.info( f"Project '{project_name}' (ID: {project_id}) has no associated activity." ) logging.info(f"Found {len(projects)} valid projects.") return projects def get_user_info(self) -> Optional[Dict[str, Any]]: """ Retrieve user information from the Kleer API. Returns: User information dictionary or None if not found """ url = f"company/{self.company_id}/user" data = self.api_request(url, method='GET') if data and isinstance(data, dict): logging.info("User info retrieved from Kleer API.") return data logging.warning("No user information found or data format is incorrect.") return None