Source code for afnio.tellurio.client

import json
import logging
import os

import httpx
import keyring
from dotenv import load_dotenv

from afnio.logging_config import configure_logging
from afnio.tellurio.utils import get_config_path

# Configure logging
configure_logging()
logger = logging.getLogger(__name__)

# Load environment variables from .env
load_dotenv()


[docs] def save_username(username): """ Saves the username to a JSON configuration file. If the username is different from the one already stored, this function updates the 'username' field and clears all other preferences. Otherwise, it preserves all existing values. """ config_path = get_config_path() # Load existing config if present, otherwise start with empty dict if os.path.exists(config_path): with open(config_path, "r") as f: try: config = json.load(f) except json.JSONDecodeError: config = {} else: config = {} # If username is different, clear all preferences except username if config.get("username") != username: config = {"username": username} else: config["username"] = username with open(config_path, "w") as f: json.dump(config, f, indent=2)
[docs] def load_username(): """ Loads the username from a JSON configuration file. This function reads the JSON file at the specified path and retrieves the username stored in it. If the file does not exist, it returns None. """ config_path = get_config_path() if os.path.exists(config_path): with open(config_path, "r") as f: try: return json.load(f).get("username") except json.JSONDecodeError: logger.debug("Failed to decode JSON from the configuration file.") return None return None
[docs] class InvalidAPIKeyError(Exception): """Exception raised when the API key is invalid.""" pass
[docs] class TellurioClient: """ A client for interacting with the Tellurio backend. This client provides methods for authenticating with the backend, making HTTP requests (GET, POST, DELETE), and verifying API keys. It is designed to simplify communication with the Tellurio platform. """ def __init__(self, base_url: str = None, port: int = None): """ Initializes the TellurioClient instance. Args: base_url (str, optional): The base URL of the Tellurio backend. If not provided, it defaults to the value of the ``TELLURIO_BACKEND_HTTP_BASE_URL`` environment variable or "https://platform.tellurio.ai". port (int, optional): The port number for the backend. If not provided, it defaults to the value of the ``TELLURIO_BACKEND_HTTP_PORT`` environment variable or 443. """ self.base_url = base_url or os.getenv( "TELLURIO_BACKEND_HTTP_BASE_URL", "https://platform.tellurio.ai" ) self.port = port or os.getenv("TELLURIO_BACKEND_HTTP_PORT", 443) self.url = f"{self.base_url}:{self.port}" self.service_name = os.getenv( "KEYRING_SERVICE_NAME", "Tellurio" ) # Service name for keyring self.api_key = None
[docs] def login(self, api_key: str = None, relogin: bool = False): """ Logs in the user using an API key and verifies its validity. Credential resolution order: 1. If ``api_key`` is provided, it is used. 2. Otherwise, if the ``TELLURIO_API_KEY`` environment variable is set, it is used. 3. Otherwise, if not relogin, attempts to load a stored API key from the keyring. If authentication succeeds and the API key was provided directly (not via keyring), it is stored in the keyring for future use. Args: api_key (str, optional): The user's API key. If not provided, the method attempts to use the ``TELLURIO_API_KEY`` environment variable, then the keyring. relogin (bool): If True, forces a re-login and requires a new API key. Returns: dict: A dictionary containing the user's email and username. Raises: ValueError: If the API key is invalid or not provided during re-login. """ # Use the provided API key if passed, otherwise check env var, then keyring if api_key: self.api_key = api_key elif os.getenv("TELLURIO_API_KEY"): self.api_key = os.getenv("TELLURIO_API_KEY") logger.info("Using API key from TELLURIO_API_KEY environment variable.") elif not relogin: username = load_username() self.api_key = keyring.get_password(self.service_name, username) if self.api_key: logger.info("Using stored API key from local keyring.") else: logger.error("No API key found in local keyring.") raise ValueError("API key is required for the first login.") else: logger.error("No API key provided for re-login.") raise ValueError("API key is required for re-login.") # Verify the API key response_data = self._verify_api_key() if response_data: email = response_data.get("email", "unknown user") username = response_data.get("username", "unknown user") logger.debug(f"API key is valid for user '{username}'.") # Save the API key securely only if it was provided and is valid if api_key or os.getenv("TELLURIO_API_KEY"): if _is_keyring_usable(): keyring.set_password( self.service_name, response_data["username"], self.api_key ) logger.info( "API key provided and stored securely in local keyring." ) save_username(response_data["username"]) else: logger.info( "Keyring is not available; skipping secure storage of API key." ) return { "email": email, "username": username, } else: logger.warning("Invalid API key. Please provide a valid API key.") if relogin: raise InvalidAPIKeyError("Re-login failed due to invalid API key.") raise InvalidAPIKeyError("Login failed due to invalid API key.")
[docs] def get(self, endpoint: str) -> httpx.Response: """ Makes a GET request to the specified endpoint. Args: endpoint (str): The API endpoint (relative to the base URL). Returns: httpx.Response: The HTTP response object. """ url = f"{self.url}{endpoint}" headers = { "Authorization": f"Api-Key {self.api_key}", "Accept": "*/*", } try: with httpx.Client() as client: response = client.get(url, headers=headers) return response except httpx.RequestError as e: logger.error(f"Network error occurred while making GET request: {e}") raise ValueError("Network error occurred. Please check your connection.") except Exception as e: logger.error(f"An unexpected error occurred: {e}") raise ValueError("An unexpected error occurred. Please try again later.")
[docs] def post(self, endpoint: str, json: dict) -> httpx.Response: """ Makes a POST request to the specified endpoint. Args: endpoint (str): The API endpoint (relative to the base URL). json (dict): The JSON payload to send in the request. Returns: httpx.Response: The HTTP response object. """ url = f"{self.url}{endpoint}" headers = { "Authorization": f"Api-Key {self.api_key}", "Content-Type": "application/json", } try: with httpx.Client() as client: response = client.post(url, headers=headers, json=json) return response except httpx.RequestError as e: logger.error(f"Network error occurred while making POST request: {e}") raise ValueError("Network error occurred. Please check your connection.") except Exception as e: logger.error(f"An unexpected error occurred: {e}") raise ValueError("An unexpected error occurred. Please try again later.")
[docs] def patch(self, endpoint: str, json: dict) -> httpx.Response: """ Makes a PATCH request to the specified endpoint. Args: endpoint (str): The API endpoint (relative to the base URL). json (dict): The JSON payload to send in the request. Returns: httpx.Response: The HTTP response object. """ url = f"{self.url}{endpoint}" headers = { "Authorization": f"Api-Key {self.api_key}", "Content-Type": "application/json", } try: with httpx.Client() as client: response = client.patch(url, headers=headers, json=json) return response except httpx.RequestError as e: logger.error(f"Network error occurred while making PATCH request: {e}") raise ValueError("Network error occurred. Please check your connection.") except Exception as e: logger.error(f"An unexpected error occurred: {e}") raise ValueError("An unexpected error occurred. Please try again later.")
[docs] def delete(self, endpoint: str) -> httpx.Response: """ Makes a DELETE request to the specified endpoint. Args: endpoint (str): The API endpoint (relative to the base URL). Returns: httpx.Response: The HTTP response object. """ url = f"{self.url}{endpoint}" headers = { "Authorization": f"Api-Key {self.api_key}", "Accept": "*/*", } try: with httpx.Client() as client: response = client.delete(url, headers=headers) return response except httpx.RequestError as e: logger.error(f"Network error occurred while making DELETE request: {e}") raise ValueError("Network error occurred. Please check your connection.") except Exception as e: logger.error(f"An unexpected error occurred: {e}") raise ValueError("An unexpected error occurred. Please try again later.")
def _verify_api_key(self) -> dict: """ Verifies the validity of the API key by calling the /api/v0/verify-api-key/ endpoint. Returns: dict: A dictionary containing the user's email, username and a message indicating if the API key is valid, None otherwise. """ endpoint = "/api/v0/verify-api-key/" try: response = self.get(endpoint) if response.status_code == 200: try: data = response.json() logger.debug(f"API key verification successful: {data}") return data except ValueError: logger.error("Failed to parse JSON response from backend.") return None elif response.status_code == 401: logger.warning("API key is invalid or missing.") else: logger.error(f"Error: {response.status_code} - {response.text}") except ValueError as e: logger.error(f"Error during API key verification: {e}") raise return None
def _is_keyring_usable(): """ Checks if the keyring backend is usable (i.e., not a fail-safe or gauth backend). """ kr = keyring.get_keyring() # The fail-safe and gauth backends are not usable return kr.__class__.__module__ not in ( "keyring.backends.fail", "keyrings.gauth", )