Skip to content

afnio.tellurio.client

afnio.tellurio.client.InvalidAPIKeyError

Bases: Exception

Exception raised when the API key is invalid.

Source code in afnio/tellurio/client.py
72
73
74
75
class InvalidAPIKeyError(Exception):
    """Exception raised when the API key is invalid."""

    pass

afnio.tellurio.client.TellurioClient

A client for interacting with the Tellurio Studio 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 Studio platform.

Source code in afnio/tellurio/client.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
class TellurioClient:
    """A client for interacting with the
    [Tellurio Studio](https://platform.tellurio.ai/) 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 Studio](https://platform.tellurio.ai/) platform.
    """

    def __init__(self, base_url: str = None, port: int = None):
        """Initializes the `TellurioClient` instance.

        Args:
            base_url: 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: 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

    def login(self, api_key: str = None, relogin: bool = False) -> dict:
        """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: The user's API key. If not provided, the method
                attempts to use the `TELLURIO_API_KEY` environment variable, then the
                keyring.
            relogin: If True, forces a re-login and requires a new API key.

        Returns:
            A dictionary containing the user's email and username.

        Raises:
            afnio.tellurio.client.InvalidAPIKeyError: If the API key is invalid.
            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.")

    def get(self, endpoint: str) -> httpx.Response:
        """Makes a GET request to the specified endpoint.

        Args:
            endpoint: The API endpoint (relative to the base URL).

        Returns:
            The HTTP response object.

        Raises:
            ValueError: If a network error occurs or an unexpected error happens.
        """
        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.")

    def post(self, endpoint: str, json: dict) -> httpx.Response:
        """Makes a POST request to the specified endpoint.

        Args:
            endpoint: The API endpoint (relative to the base URL).
            json: The JSON payload to send in the request.

        Returns:
            The HTTP response object.

        Raises:
            ValueError: If a network error occurs or an unexpected error happens.
        """
        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.")

    def patch(self, endpoint: str, json: dict) -> httpx.Response:
        """Makes a PATCH request to the specified endpoint.

        Args:
            endpoint: The API endpoint (relative to the base URL).
            json: The JSON payload to send in the request.

        Returns:
            The HTTP response object.

        Raises:
            ValueError: If a network error occurs or an unexpected error happens.
        """
        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.")

    def delete(self, endpoint: str) -> httpx.Response:
        """Makes a DELETE request to the specified endpoint.

        Args:
            endpoint: The API endpoint (relative to the base URL).

        Returns:
            The HTTP response object.

        Raises:
            ValueError: If a network error occurs or an unexpected error happens.
        """
        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:
            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

__init__(base_url=None, port=None)

Initializes the TellurioClient instance.

Parameters:

Name Type Description Default
base_url str

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".

None
port int

The port number for the backend. If not provided, it defaults to the value of the TELLURIO_BACKEND_HTTP_PORT environment variable or 443.

None
Source code in afnio/tellurio/client.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def __init__(self, base_url: str = None, port: int = None):
    """Initializes the `TellurioClient` instance.

    Args:
        base_url: 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: 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

login(api_key=None, relogin=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.

Parameters:

Name Type Description Default
api_key str

The user's API key. If not provided, the method attempts to use the TELLURIO_API_KEY environment variable, then the keyring.

None
relogin bool

If True, forces a re-login and requires a new API key.

False

Returns:

Type Description
dict

A dictionary containing the user's email and username.

Raises:

Type Description
InvalidAPIKeyError

If the API key is invalid.

ValueError

If the API key is invalid or not provided during re-login.

Source code in afnio/tellurio/client.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def login(self, api_key: str = None, relogin: bool = False) -> dict:
    """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: The user's API key. If not provided, the method
            attempts to use the `TELLURIO_API_KEY` environment variable, then the
            keyring.
        relogin: If True, forces a re-login and requires a new API key.

    Returns:
        A dictionary containing the user's email and username.

    Raises:
        afnio.tellurio.client.InvalidAPIKeyError: If the API key is invalid.
        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.")

get(endpoint)

Makes a GET request to the specified endpoint.

Parameters:

Name Type Description Default
endpoint str

The API endpoint (relative to the base URL).

required

Returns:

Type Description
Response

The HTTP response object.

Raises:

Type Description
ValueError

If a network error occurs or an unexpected error happens.

Source code in afnio/tellurio/client.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
def get(self, endpoint: str) -> httpx.Response:
    """Makes a GET request to the specified endpoint.

    Args:
        endpoint: The API endpoint (relative to the base URL).

    Returns:
        The HTTP response object.

    Raises:
        ValueError: If a network error occurs or an unexpected error happens.
    """
    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.")

post(endpoint, json)

Makes a POST request to the specified endpoint.

Parameters:

Name Type Description Default
endpoint str

The API endpoint (relative to the base URL).

required
json dict

The JSON payload to send in the request.

required

Returns:

Type Description
Response

The HTTP response object.

Raises:

Type Description
ValueError

If a network error occurs or an unexpected error happens.

Source code in afnio/tellurio/client.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def post(self, endpoint: str, json: dict) -> httpx.Response:
    """Makes a POST request to the specified endpoint.

    Args:
        endpoint: The API endpoint (relative to the base URL).
        json: The JSON payload to send in the request.

    Returns:
        The HTTP response object.

    Raises:
        ValueError: If a network error occurs or an unexpected error happens.
    """
    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.")

patch(endpoint, json)

Makes a PATCH request to the specified endpoint.

Parameters:

Name Type Description Default
endpoint str

The API endpoint (relative to the base URL).

required
json dict

The JSON payload to send in the request.

required

Returns:

Type Description
Response

The HTTP response object.

Raises:

Type Description
ValueError

If a network error occurs or an unexpected error happens.

Source code in afnio/tellurio/client.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def patch(self, endpoint: str, json: dict) -> httpx.Response:
    """Makes a PATCH request to the specified endpoint.

    Args:
        endpoint: The API endpoint (relative to the base URL).
        json: The JSON payload to send in the request.

    Returns:
        The HTTP response object.

    Raises:
        ValueError: If a network error occurs or an unexpected error happens.
    """
    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.")

delete(endpoint)

Makes a DELETE request to the specified endpoint.

Parameters:

Name Type Description Default
endpoint str

The API endpoint (relative to the base URL).

required

Returns:

Type Description
Response

The HTTP response object.

Raises:

Type Description
ValueError

If a network error occurs or an unexpected error happens.

Source code in afnio/tellurio/client.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
def delete(self, endpoint: str) -> httpx.Response:
    """Makes a DELETE request to the specified endpoint.

    Args:
        endpoint: The API endpoint (relative to the base URL).

    Returns:
        The HTTP response object.

    Raises:
        ValueError: If a network error occurs or an unexpected error happens.
    """
    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.")

afnio.tellurio.client.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.

Parameters:

Name Type Description Default
username str

The username to save.

required
Source code in afnio/tellurio/client.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def save_username(username: str) -> None:
    """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.

    Args:
        username: The username to save.
    """
    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)

afnio.tellurio.client.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.

Returns:

Type Description
str | None

The username if found, otherwise None.

Source code in afnio/tellurio/client.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def load_username() -> Optional[str]:
    """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`.

    Returns:
        The username if found, otherwise `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