Atoti Python

What is the Atoti Limits Python extension

The Atoti Limits Python extension is a Spring Boot auto-configuration that connects an Atoti Python session to a running Atoti Limits server. It allows Python users to define limits against cubes created in Jupyter notebooks or Python applications.

The extension includes two components:

  • Java extension: A JAR that configures the connection between the Atoti Python session and the Atoti Limits server.
  • UI extension: A TypeScript/React extension that adds Atoti Limits functionality to the Atoti UI.

Both components are packaged in a single JAR file.

tip

This extension replaces the old Python plugin, which is no longer supported for versions of the Atoti Python SDK later than 0.9.0. If you were using the old plugin, please migrate to this new extension to continue using Atoti Limits with Atoti Python.

Compatible versions

Atoti Limits version Atoti Python version
4.2.1 0.9.12

Prerequisites

  • Java: 21 or higher
  • Python: 3.10 or higher
  • UV package manager: Latest version (installation guide)
  • Atoti license: A valid Base64-encoded license set in the ATOTI_LICENSE environment variable
  • A running Atoti Limits server

How the extension works

  1. A standalone Atoti Limits server starts and waits for a connection request.
  2. An Atoti Python session starts with the extension JAR loaded.
  3. After the cube is built and security is configured, a manual REST call triggers the connection.
  4. The extension sends connection information to the Atoti Limits server.
  5. The Atoti Limits server connects to the Atoti Python session and is ready to manage limits.

How to set up the extension

Step 1: Download the extension

Download the extension, which is available in the released artifacts accessible from this link.

Step 2: Start a standalone Atoti Limits server

A running Atoti Limits server is required before the Python session can connect. The server must be configured to communicate with the Atoti Python session. See the getting started section for more details on building the Atoti Limits server.

Start the server with the following command:

cd limits-starter
java --add-opens=java.base/java.util=ALL-UNNAMED \
     --add-opens=java.base/java.util.concurrent=ALL-UNNAMED \
     -Dactiveviam.apps.inter-server-event-service.issuer.target-servers[0].name=atoti \
     -Dactiveviam.apps.inter-server-event-service.issuer.target-servers[0].url=http://localhost:7070 \
     -jar target/limits-starter-4.2.1-exec.jar

where:

  • activeviam.apps.inter-server-event-service.issuer.target-servers[0].name specifies the name of the Atoti Python server, which is atoti by default
  • activeviam.apps.inter-server-event-service.issuer.target-servers[0].url specifies the URL of the Atoti Python server, which by default in the following Atoti Python session is http://localhost:7070

info

The extension expects the Atoti Limits server to be available on http://localhost:3090 by default. If it is not running on this URL, the connection will fail.

Step 3: Extract the UI extension from the JAR

The UI extension is bundled inside the extension JAR. It must be extracted to a directory that the Atoti session can reference.

Include the following function in your Atoti Python code to extract the UI extension to a temporary directory:

import tempfile
import zipfile
from pathlib import Path

def extract_ui_extension(extension_jar: Path) -> str:
    temp_dir = Path(tempfile.mkdtemp(prefix="atoti-limits-ui-extension-"))

    with zipfile.ZipFile(extension_jar, 'r') as jar:
        for entry in jar.namelist():
            if entry.startswith('ui-extension/') and not entry.endswith('/'):
                target = temp_dir / entry[len('ui-extension/'):]
                target.parent.mkdir(parents=True, exist_ok=True)
                target.write_bytes(jar.read(entry))

    return temp_dir

Step 4: Configure and start the Atoti Python session

The Atoti Python session must be configured with the extension JAR, the UI extension path, and the server URLs.

By default, the extension expects the Atoti Python server to be available on http://localhost:7070, hence the use of prot 7070 in the following session. You can use the ATOTI_BASE_URL environment variable to control the Python server URL.

The following example creates a session with the extension loaded:

import os
from pathlib import Path
import atoti as tt

# Path to the downloaded extension JAR
server_extension_path = Path("path/to/atoti-limits-python-extension-4.2.1.jar")

# Extract the UI extension
ui_extension_path = extract_ui_extension(server_extension_path)

# Get URLs from environment variables with defaults
atoti_base_url = os.environ.get("ATOTI_BASE_URL", "http://localhost:7070")

# Build Java options to pass URLs to the extension
java_options = [
    f"-Dlimits.autoconfiguration.atoti-base-url={atoti_base_url}",
]

# Configure and start the session
session_config = tt.SessionConfig(
    port=7070, # This should be the same port as in the `activeviam.apps.inter-server-event-service.issuer.target-servers[0].url` property
    extra_jars=[server_extension_path],
    app_extensions={
        "@activeviam/atoti-limits-python-ui-extension": ui_extension_path
    },
    security=tt.SecurityConfig(),
    java_options=java_options,
)

session = tt.Session.start(session_config)

Step 5: Include a slicing date hierarchy in the cube

Atoti Limits requires a slicing date hierarchy to evaluate limits at specific points in time.

After creating the cube, ensure you have a slicing date hierarchy, for example with:

cube = session.create_cube(sales_table)

cube.hierarchies["Date"].slicing = True

Step 6: Configure user roles

User roles in the Python session must match the roles expected by the Atoti Limits server. This ensures proper authorization and workflow management.

The following example configures users and roles that match the default Atoti Limits server configuration:

def configure_security(session: tt.Session) -> None:
    users = {
        "admin": {
            "password": "admin",
            "roles": {
                "ROLE_ADMIN",
                "ROLE_CS_ROOT",
                "ROLE_USERS",
                "ROLE_MANAGERS",
                "ROLE_USER",
                "ROLE_ACTIVITI_USER",
                "ROLE_ACTIVITI_ADMIN",
                "ROLE_LIMITS",
            },
        },
        "user1": {
            "password": "user1",
            "roles": {
                "ROLE_USER",
                "ROLE_USERS",
                "ROLE_ACTIVITI_USER",
                "ROLE_CS_ROOT",
                "ROLE_LIMITS",
            },
        },
        "user2": {
            "password": "user2",
            "roles": {
                "ROLE_USER",
                "ROLE_USERS",
                "ROLE_ACTIVITI_USER",
                "ROLE_CS_ROOT",
                "ROLE_LIMITS",
            },
        },
        "manager1": {
            "password": "manager1",
            "roles": {
                "ROLE_USER",
                "ROLE_MANAGERS",
                "ROLE_ACTIVITI_USER",
                "ROLE_CS_ROOT",
                "ROLE_APPROVE_REJECT_LIMIT",
                "ROLE_EVALUATE_STRUCTURE",
                "ROLE_PROCESS_INCIDENT",
            },
        },
        "manager2": {
            "password": "manager2",
            "roles": {
                "ROLE_USER",
                "ROLE_MANAGERS",
                "ROLE_ACTIVITI_USER",
                "ROLE_CS_ROOT",
                "ROLE_APPROVE_REJECT_LIMIT",
                "ROLE_EVALUATE_STRUCTURE",
                "ROLE_PROCESS_INCIDENT",
            },
        },
    }

    for username, config in users.items():
        session.security.individual_roles[username] = config["roles"]
        session.security.basic_authentication.credentials[username] = config[
            "password"
        ]


configure_security(session)

The key roles are:

  • ROLE_LIMITS: Provides access to Atoti Limits functionality
  • ROLE_APPROVE_REJECT_LIMIT: Allows approving or rejecting limit changes
  • ROLE_EVALUATE_STRUCTURE: Allows triggering limit evaluations
  • ROLE_PROCESS_INCIDENT: Allows processing limit breaches

For a full list of roles, see Roles.

Step 7: Trigger the connection

After both servers are running and the cube is built, the connection between them must be triggered manually with a REST call.

The following example triggers and polls the connection:

import requests
import time

# Trigger the connection
response = requests.post(
    f"{session.url}/limits/autoconfig/connect",
    headers={"Authorization": "Basic YWRtaW46YWRtaW4="},  # admin:admin
    json={},
)

if response.ok:
    print("Connection initiated successfully")

    # Poll the connection status
    max_wait_time = 90
    poll_interval = 5
    start_time = time.time()

    while time.time() - start_time < max_wait_time:
        status_response = requests.get(
            f"{session.url}/limits/autoconfig/connected",
            headers={"Authorization": "Basic YWRtaW46YWRtaW4="},
        )

        if status_response.ok and status_response.json() is True:
            print("Servers connected successfully")
            break

        time.sleep(poll_interval)
else:
    print(f"Connection failed: {response.status_code}")

The two connection endpoints are:

  • POST /limits/autoconfig/connect: Initiates the connection between the two servers
  • GET /limits/autoconfig/connected: Returns true or false to indicate the connection status

Navigate to your Atoti Python session URL and you can now define limits in your Atoti Python session using the Atoti Limits functionality!