We struggled a lot with this problem a while ago until we made a custom context manager to solve it `manually. If this helps someone else then it was worth it. (I miss Snippet Wednesday)
"""Context manager for handling VIKTOR environment variables.
Made with love (and some frustration it was necessary) by:
Enrique García
"""
import os
from typing import Any, Self
import requests
COMPANY_NAME = "YOUR-COMPANY-NAME"
VIKTOR_DEV_TOKEN = "vktr..........."
def get_viktor_headers() -> dict[str, str]:
"""Get the headers for the VIKTOR API.
Returns a dictionary with Authorization, Accept, and Content-Type headers
required for all VIKTOR API calls.
Returns
-------
dict[str, str]
Headers with Bearer token authorization
Raises
------
ValueError
If the VIKTOR_DEV_TOKEN is not set.
Examples
--------
>>> headers = get_viktor_headers()
>>> import requests
>>> response = requests.get("https://mycompany.viktor.ai/api/apps/", headers=headers)
"""
# Check if token exists
if not VIKTOR_DEV_TOKEN:
raise ValueError("Viktor dev token is not set")
# Set up headers with authentication
return {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {VIKTOR_DEV_TOKEN}",
}
def get_all_viktor_app_ids() -> list[int]:
"""Get all VIKTOR app IDs from the server.
Fetches the list of all application IDs available in your VIKTOR workspace.
Returns
-------
list[int]
A list of app IDs.
Raises
------
requests.HTTPError
If the API request fails.
ValueError
If VIKTOR_DEV_TOKEN is not set.
Examples
--------
>>> app_ids = get_all_viktor_app_ids()
>>> print(app_ids)
[237, 238, 240]
>>> # Use in a loop to process all apps
>>> for app_id in get_all_viktor_app_ids():
... name = get_viktor_app_name_by_id(app_id)
... print(f"App {app_id}: {name}")
"""
# Define the API endpoint
url = f"https://{COMPANY_NAME}.viktor.ai/api/apps/"
# Make the GET request
response = requests.get(url, headers=get_viktor_headers(), data={})
# Check if the request was successful
response.raise_for_status()
# Return the workspace IDs from the response JSON
return [workspace["id"] for workspace in response.json()['results']]
def post_variable_to_viktor_app(
app_id: int,
key: str,
value: str,
is_secret: bool = True,
) -> None:
"""Post (create or update) an environment variable to a VIKTOR app.
Parameters
----------
app_id : int
The ID of the app to post the variable to
key : str
The name of the variable (must be a valid Python identifier)
value : str
The value of the variable
is_secret : bool, optional
Whether to mark this as a secret (hidden in UI), by default True
Raises
------
requests.HTTPError
If the API request fails.
ValueError
If VIKTOR_DEV_TOKEN is not set.
Examples
--------
>>> # Add a single variable
>>> post_variable_to_viktor_app(
... app_id=237,
... key="DATABASE_URL",
... value="postgresql://user:pass@host/db",
... is_secret=True
... )
>>> # Add a public (non-secret) variable
>>> post_variable_to_viktor_app(
... app_id=237,
... key="LOG_LEVEL",
... value="INFO",
... is_secret=False
... )
"""
# Define the API endpoint
url = f"https://{COMPANY_NAME}.viktor.ai/api/apps/{app_id}/variables/"
# prepare the keys to be posted
payload = {
"key": key,
"value": value,
"is_secret": is_secret,
}
# Make the POST request
response = requests.post(url, headers=get_viktor_headers(), json=payload)
# Check if the request was successful
response.raise_for_status()
def delete_variable_from_viktor_app(
app_id: int,
key_id: int,
) -> None:
"""Delete an environment variable from a VIKTOR app.
Parameters
----------
app_id : int
The ID of the app to delete the variable from
key_id : int
The ID of the variable (not the name — use the id from the API response)
Raises
------
requests.HTTPError
If the API request fails.
ValueError
If VIKTOR_DEV_TOKEN is not set.
Examples
--------
>>> # Delete variable with ID 123 from app 237
>>> delete_variable_from_viktor_app(app_id=237, key_id=123)
See Also
--------
ViktorEnvironmentVariables : Use the context manager for easier delete operations
"""
# Define the API endpoint
url = f"https://{COMPANY_NAME}.viktor.ai/api/apps/{app_id}/variables/{key_id}/"
# Make the DELETE request
response = requests.delete(url, headers=get_viktor_headers(), json={})
# Check if the request was successful
response.raise_for_status()
def get_viktor_app_name_by_id(app_id: int) -> str:
"""Get the VIKTOR app name by its ID.
Parameters
----------
app_id : int
The ID of the app to get the name for
Returns
-------
str
The friendly name of the app
Raises
------
requests.HTTPError
If the API request fails.
ValueError
If VIKTOR_DEV_TOKEN is not set.
KeyError
If the app ID doesn't exist.
Examples
--------
>>> name = get_viktor_app_name_by_id(237)
>>> print(name)
'My Analysis App'
>>> # Combine with get_all_viktor_app_ids() to list all apps
>>> for app_id in get_all_viktor_app_ids():
... name = get_viktor_app_name_by_id(app_id)
... print(f"{app_id}: {name}")
"""
# Define the API endpoint
url = f"https://{COMPANY_NAME}.viktor.ai/api/apps/{app_id}/"
# Make the GET request
response = requests.get(url, headers=get_viktor_headers())
# Check if the request was successful
response.raise_for_status()
# Return the app name from the response JSON
return response.json()["name"]
def get_viktor_app_id_by_name(app_name: str) -> int:
"""Get the VIKTOR app ID by its friendly name.
Parameters
----------
app_name : str
The friendly name of the app (case-sensitive exact match)
Returns
-------
int
The ID of the app
Raises
------
requests.HTTPError
If the API request fails.
ValueError
If VIKTOR_DEV_TOKEN is not set, or app name not found.
Examples
--------
>>> app_id = get_viktor_app_id_by_name('My Analysis App')
>>> print(app_id)
237
>>> # Use with the context manager for easy variable management
>>> with ViktorEnvironmentVariables(app=237) as env:
... env.add_variable('API_KEY', 'secret123')
"""
# Define the API endpoint
url = f"https://{COMPANY_NAME}.viktor.ai/api/apps/"
# Make the GET request
response = requests.get(url, headers=get_viktor_headers())
# Check if the request was successful
response.raise_for_status()
# Find and return the app ID by name from the response JSON
for app in response.json():
if app["name"] == app_name:
return app["id"]
raise ValueError(f"App with name '{app_name}' not found.")
class ViktorEnvironmentVariables:
"""Context manager for safely managing VIKTOR app environment variables.
Fetches current variables on enter, applies changes on exit (add, modify, delete).
All changes are sent to the API only when exiting the context.
Examples
--------
**Add new variables:**
>>> with ViktorEnvironmentVariables(app=237) as env:
... env.add_variable('DATABASE_URL', 'postgres://localhost/mydb')
... env.add_variable('LOG_LEVEL', 'DEBUG', is_secret=False)
**Modify an existing variable:**
>>> with ViktorEnvironmentVariables(app='My Analysis App') as env:
... env.change_variable('API_KEY', 'new_secret_value')
**Delete a variable:**
>>> with ViktorEnvironmentVariables(app=237) as env:
... env.delete_variable('OLD_VARIABLE')
**Clear all variables:**
>>> with ViktorEnvironmentVariables(app=237) as env:
... env.clear_variables()
**View current variables (read-only):**
>>> with ViktorEnvironmentVariables(app=237) as env:
... for key, value_dict in env.variables.items():
... print(f"{key} = {value_dict['value']}")
"""
def __init__(self, app: int | str) -> None:
"""Initialize the context manager with an app ID or name.
Parameters
----------
app : int | str
The app ID (int) or the app name (str, case-sensitive exact match)
Raises
------
ValueError
If app is a string and doesn't match any existing app name
"""
self.app_id = (
app if isinstance(app, int) else get_viktor_app_id_by_name(app_name=app)
)
self.app_name = (
app
if isinstance(app, str)
else get_viktor_app_name_by_id(app_id=self.app_id)
)
self.variables = {}
self.url = f"https://{COMPANY_NAME}.viktor.ai/api/apps/{self.app_id}/variables/"
def __enter__(self) -> Self:
# delete id's from api call and set variables as a dictionary
for env_key in self._variables_from_api():
env_key["modified"] = False # all variables are not modified at the start
self.variables[env_key["key"]] = env_key
return self
def _variables_from_api(self) -> list[dict[str, Any]]:
"""Fetch the current variables from the VIKTOR API.
Returns
-------
list[dict[str, Any]]
List of variable dicts with keys: id, key, value, is_secret
Raises
------
requests.HTTPError
If the API request fails
"""
# Make the GET request
response = requests.get(self.url, headers=get_viktor_headers())
# Check if the request was successful
response.raise_for_status()
# Return the response as JSON
return response.json()
def delete_variable(self, key: str) -> None:
"""Mark a variable for deletion (removed on exit).
Parameters
----------
key : str
The name of the variable to delete
Raises
------
KeyError
If the variable doesn't exist
Examples
--------
>>> with ViktorEnvironmentVariables(app=237) as env:
... env.delete_variable('OLD_API_KEY')
... env.delete_variable('DEPRECATED_SETTING')
"""
try:
self.variables.pop(key)
except KeyError:
raise KeyError(
f"Variable '{key}' not found in the environment of app '{self.app_name}'."
)
def add_variable(self, key: str, value: str, is_secret: bool = True) -> None:
"""Add or update a variable in the context (sent on exit).
Parameters
----------
key : str
Variable name (must be a valid Python identifier: letters, digits, '_')
value : str
The value to set
is_secret : bool, optional
If True, value is hidden in VIKTOR UI (default True)
Raises
------
ValueError
If key is not a valid identifier
Examples
--------
>>> with ViktorEnvironmentVariables(app=237) as env:
... env.add_variable('DATABASE_URL', 'postgresql://localhost/mydb')
... env.add_variable('DEBUG_MODE', 'true', is_secret=False)
"""
# check if the key is valid
if not key.isidentifier():
raise ValueError(
f"Invalid key '{key}'. Variable name can only contain letters, digits and the character '_'."
)
key_to_set = {
"key": key,
"value": value,
"is_secret": is_secret,
"modified": True,
}
# if the key already exists in the variables, then set the id to the current key
if key in self.variables:
key_to_set["id"] = self.variables[key]["id"]
self.variables[key] = key_to_set
def change_variable(self, key: str, value: str, is_secret: bool = True) -> None:
"""Modify an existing variable's value and/or secret status (sent on exit).
Parameters
----------
key : str
The name of the variable to modify (must exist)
value : str
The new value
is_secret : bool, optional
If True, value is hidden in VIKTOR UI (default True)
Raises
------
KeyError
If the variable doesn't exist
Examples
--------
>>> with ViktorEnvironmentVariables(app=237) as env:
... env.change_variable('API_KEY', 'new_secret_123')
... env.change_variable('LOG_LEVEL', 'DEBUG', is_secret=False)
"""
if key in self.variables:
self.variables[key]["value"] = value
self.variables[key]["is_secret"] = is_secret
self.variables[key]["modified"] = True
else:
raise KeyError(
f"Variable '{key}' not found in the environment of app '{self.app_name}'."
)
def clear_variables(self) -> None:
"""Remove all environment variables from the app (sent immediately).
This clears all variables synchronously by posting an empty list to the API.
Examples
--------
>>> with ViktorEnvironmentVariables(app=237) as env:
... env.clear_variables()
... # All variables removed from app 237
"""
self.variables = {}
request = requests.post(url=self.url, headers=get_viktor_headers(), json=[])
request.raise_for_status()
return
def __exit__(self, exc_type, exc_value, traceback) -> None:
"""Exit the context manager and synchronize all changes with the VIKTOR API.
Deletes variables that are no longer in the context, updates modified ones,
and adds new ones. All API calls are made during this exit phase.
Parameters
----------
exc_type : type | None
Exception type if an exception occurred in the context
exc_value : Exception | None
Exception instance if an exception occurred
traceback : TracebackType | None
Traceback object if an exception occurred
"""
# if all variables has been cleared then delete them from the API
if not self.variables:
self.clear_variables()
# delete all variables that are present in the API but not in this context manager
for env_key in self._variables_from_api():
if env_key["key"] not in self.variables:
delete_variable_from_viktor_app(
app_id=self.app_id,
key_id=env_key["id"],
)
# Post the variables to the VIKTOR API
for key, value in self.variables.items():
if value["modified"]:
if value.get("id"):
# delete the variable from the API to make sure it is not duplicated
delete_variable_from_viktor_app(
app_id=self.app_id,
key_id=value["id"],
)
# Post the variable to the VIKTOR API
post_variable_to_viktor_app(
app_id=self.app_id,
key=key,
value=value["value"],
is_secret=value["is_secret"],
)
# Clear everything
self.variables.clear()
if __name__ == "__main__":
with ViktorEnvironmentVariables(app=237) as env:
env.add_variable(
key='API_KEY',
value='secret123',
is_secret=True,
)