first commit

This commit is contained in:
2025-12-02 21:26:08 +01:00
commit 790f4a9f1d
23 changed files with 4085 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
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