HABSlib: Python Library for EEG Analysis and Biomarker Evaluation

HABSlib is a versatile Python library designed to facilitate interaction with the HABS BrainOS API for comprehensive EEG data analysis and biomarker evaluation. Developed to support neuroscientific research and clinical applications, HABSlib simplifies the process of fetching, processing, and analyzing EEG data through a user-friendly interface.

Key Features:

  • API Integration: Connects with the BrainOS HABS API, allowing users to access EEG data and related services effortlessly.
  • Data Management: Provides a robust interface for managing EEG datasets, including storage on the HABS servers.
  • Biomarker Evaluation: Enables the analysis of EEG biomarkers, essential for diagnosing and monitoring neurological conditions.
  • Customizable Pipelines: Users can create custom analysis pipelines tailored to specific research needs, ensuring flexibility and adaptability in various use cases.

Sessions

The communications between the user and HABS BrainOS is based on a RESTful API (see doc) and structured into sessions.

A Session with the HABS BrainOS iinitiates with an handshake during which encryption keys are exchanged for the security of any following communication between the user and the server.

Simple sessions

There are two general types of session: real-time and off-line.

In setting a either a real-time or off-line session, the user provides the session metadata, such as their user_id, session_date, session type (all required), and additional notes depending on the nature of recordings.

Then, in a real-time session, the user specifies the type of EEG DEVICE ('board') used, the duration of the EEG recording, and the frequency of server update.

In an off-line session, the user specifies a session id (referring to data already exisiting, either acquired live at some point in time, or from an uploaded file).
The HABSlib can read EDF files (EDF file type only, for now, but it's growing) and sends it to the server.

In these simple types of session, after the real-time or offline uploading, the data can be selected via the session_id for further processing.

Piped sessions

There is another type of session, called piped session. This type of session is meant to help you organize the flow of analysis and make it reproducible.
Usually, an analysis implies several steps over the raw data. BrainOS allows you to perform a growing number of predefined and parametrizable functions over the data, to filter, remove artifacts, and extract features. And you can do it without taking the output of one function and passing it to another. You can pipe (|) the output of one function into the next.

This session type also is available as real-time and off-line. In the real-time version the EEG data is processed as per pipe by the server as soon as it is received, and the results are sent back to the user as soon as they are processed.

   1######################################################
   2# INTRO
   3
   4r"""
   5# HABSlib: Python Library for EEG Analysis and Biomarker Evaluation
   6
   7HABSlib is a versatile Python library designed to facilitate interaction with the HABS BrainOS API for comprehensive EEG data analysis and biomarker evaluation. 
   8Developed to support neuroscientific research and clinical applications, HABSlib simplifies the process of fetching, processing, and analyzing EEG data through a user-friendly interface.
   9
  10Key Features:
  11- **API Integration**: Connects with the BrainOS HABS API, allowing users to access EEG data and related services effortlessly.
  12- **Data Management**: Provides a robust interface for managing EEG datasets, including storage on the HABS servers.
  13- **Biomarker Evaluation**: Enables the analysis of EEG biomarkers, essential for diagnosing and monitoring neurological conditions.
  14- **Customizable Pipelines**: Users can create custom analysis pipelines tailored to specific research needs, ensuring flexibility and adaptability in various use cases.
  15
  16## Sessions
  17
  18The communications between the user and HABS BrainOS is based on a RESTful API (see doc) and structured into sessions.    
  19
  20A *Session* with the HABS BrainOS iinitiates with an handshake during which encryption keys are exchanged for the security of any following communication between the user and the server.
  21
  22### Simple sessions
  23
  24There are two general types of session: *real-time* and *off-line*.
  25
  26In setting a either a real-time or off-line session, the user provides the session metadata, such as their user_id, session_date, session type (all required), and additional notes depending on the nature of recordings.
  27
  28Then, in a real-time session, the user specifies the type of EEG DEVICE ('board') used, the duration of the EEG recording, and the frequency of server update. 
  29
  30In an off-line session, the user specifies a session id (referring to data already exisiting, either acquired live at some point in time, or from an uploaded file).    
  31The HABSlib can read EDF files (EDF file type only, for now, but it's growing) and sends it to the server.
  32
  33In these simple types of session, after the real-time or offline uploading, the data can be selected via the session_id for further processing.
  34
  35### Piped sessions
  36
  37There is another type of session, called *piped* session. This type of session is meant to help you organize the flow of analysis and make it reproducible.     
  38Usually, an analysis implies several steps over the raw data. BrainOS allows you to perform a growing number of predefined and parametrizable functions over the data, to filter, remove artifacts, and extract features. 
  39And you can do it without taking the output of one function and passing it to another. You can pipe (|) the output of one function into the next.
  40
  41This session type also is available as *real-time* and *off-line*. In the *real-time* version the EEG data is processed as per pipe by the server as soon as it is received, and the results are sent back to the user as soon as they are processed.    
  42"""
  43
  44import sys
  45import os
  46import base64
  47import requests
  48import json
  49import jsonschema
  50from jsonschema import validate
  51from jsonschema import exceptions
  52# from bson import json_util
  53
  54import numpy as np
  55
  56import time
  57from datetime import datetime
  58import uuid
  59import asyncio
  60import webbrowser
  61
  62from . import BASE_URL, VERSION
  63from . import BoardManager
  64
  65from cryptography.hazmat.primitives import hashes, serialization
  66from cryptography.hazmat.backends import default_backend
  67from . import store_public_key, load_public_key, generate_aes_key, encrypt_aes_key_with_rsa, encrypt_message, decrypt_message
  68
  69from pyedflib import highlevel
  70
  71from importlib.metadata import version
  72
  73
  74######################################################
  75# validate the metadata against a specified schema
  76def validate_metadata(metadata, schema_name, schemafile='metadata.json'):
  77    """
  78    Validate metadata against a given JSON schema.
  79
  80    Args:    
  81        **metadata** (*dict*): The metadata to be validated.    
  82        **schema_name** (*str*): The name of the schema to validate against. HABSlib currently supports the validation of Session metadata and User data.    
  83        **schemafile** (*str*, optional): The path to the JSON file containing the schemas. Defaults to 'metadata.json'.    
  84
  85    Returns:    
  86        *bool*: True if validation is successful, False otherwise.
  87
  88    Raises:    
  89        **FileNotFoundError**: If the schema file does not exist.     
  90        **json.JSONDecodeError**: If there is an error decoding the JSON schema file.     
  91        **exceptions.ValidationError**: If the metadata does not conform to the schema.     
  92        **Exception**: For any other errors that occur during validation.     
  93
  94    Example:
  95    ```
  96    metadata = {"name": "example", "type": "data"}
  97    schema_name = "example_schema"
  98    is_valid = validate_metadata(metadata, schema_name)
  99    if is_valid:
 100        print("Metadata is valid.")
 101    else:
 102        print("Metadata is invalid.")
 103    ```
 104    """
 105    print(metadata)
 106    try:
 107        with open(os.path.join(os.path.dirname(__file__), schemafile), 'r') as file:
 108            content = file.read()
 109            schemas = json.loads(content)
 110        schema = schemas[schema_name]
 111        validate(instance=metadata, schema=schema) #, format_checker=FormatChecker())
 112        print("Metadata validation successful!")
 113        return True
 114
 115    except json.JSONDecodeError as e:
 116        print("Failed to decode JSON:", e)
 117        return False
 118
 119    except exceptions.ValidationError as e:
 120        print("Validation error:", e)
 121        return False
 122
 123    except FileNotFoundError:
 124        print(f"No such file: {schemafile}")
 125        return False
 126
 127    except Exception as e:
 128        print("A general error occurred:", e)
 129        return False
 130
 131
 132def convert_datetime_in_dict(data):
 133    """
 134    Recursively converts all datetime objects in a dictionary to strings in ISO format.
 135
 136    Args:     
 137        **data** (*dict*): The dictionary containing the data.
 138
 139    Returns:     
 140        *dict*: The dictionary with datetime objects converted to strings.
 141    """
 142    for key, value in data.items():
 143        if isinstance(value, datetime):
 144            data[key] = value.isoformat()
 145        elif isinstance(value, dict):
 146            data[key] = convert_datetime_in_dict(value)
 147    return data
 148
 149
 150def head():
 151    """
 152    Every library should have a nice ASCII art :)
 153    Propose yours, there is a prize for the best one!
 154    """
 155    print()
 156    print("       HUMAN        AUGMENTED        BRAIN         SYSTEMS     ")
 157    print("   ----------------------------------------------------------- ")
 158    print("   ▒▒▒▒     ▒▒▒▒     ░▒▒▒▒▒░     ▒▒▒▒▒▒▒▒▒▒▒▒░   ░▒▒▒▒▒▒▒▒▒░   ")
 159    print("   ▒▒▒▒     ▒▒▒▒    ░▒▒▒▒▒▒▒░             ░▒▒▒▒ ░▒▒▒░     ░▒░  ")
 160    print("   ▒▒▒▒▒▒▒▒▒▒▒▒▒   ░▒▒▒▒ ▒▒▒▒░   ▒▒▒▒▒▒▒▒▒▒▒▒▒   ░▒▒▒▒▒▒▒▒▒░   ")
 161    print("   ▒▒▒▒     ▒▒▒▒  ░▒▒▒▒   ▒▒▒▒░  ▒▒▒▒     ░▒▒▒▒ ░▒░     ░▒▒▒░  ")
 162    print("   ▒▒▒▒     ▒▒▒▒ ░▒▒▒▒     ▒▒▒▒░ ▒▒▒▒▒▒▒▒▒▒▒▒░   ░▒▒▒▒▒▒▒▒▒░   ")
 163    print("   ----------------------------------------------------------- ")
 164    print("   version:", version("HABSlib"))
 165    print()
 166
 167
 168######################################################
 169def handshake(base_url, user_id):  
 170    """
 171    Perform a handshake with the server to exchange encryption keys for the current session.
 172
 173    This function performs the following steps:
 174    0. Performs login to the HABS server.
 175    1. Sends a GET request to the server to initiate an RSA handshake.
 176    2. Retrieves the server's public RSA key from the response.
 177    3. Generates a local AES key and stores it in the environment.
 178    4. Encrypts the AES key with the server's RSA key.
 179    5. Sends the encrypted AES key to the server to complete the AES handshake.
 180
 181    Args:      
 182        **base_url** (*str*): The base URL of the server's API.
 183        **user_id** (*str*): The user id (obtained through free registration with HABS)
 184
 185    Returns:     
 186        *bool*: True if the handshake is successful, None otherwise.
 187
 188    Raises:      
 189        **requests.RequestException**: If a request to the server fails.
 190
 191    Example:
 192    ```
 193    success = handshake("https://example.com")
 194    if success:
 195        print("Handshake completed successfully.")
 196    else:
 197        print("Handshake failed.")
 198    ```
 199    """
 200    head()
 201    global BASE_URL
 202    BASE_URL = base_url
 203    url = f"{BASE_URL}/api/{VERSION}/handshake_rsa"
 204    # response = requests.get(url)
 205    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
 206
 207    if response.status_code == 200:
 208        print("Handshake (RSA) successful.")
 209        api_public_key_pem = response.json().get('api_public_key')
 210        api_public_key = serialization.load_pem_public_key(
 211            api_public_key_pem.encode(),
 212            backend=default_backend()
 213        )
 214        os.environ['API_PUBLIC_KEY'] = api_public_key_pem
 215
 216        # Then we generate and store the AES key
 217        aes_key = generate_aes_key()
 218        # print("aes_key", aes_key)
 219        os.environ['AES_KEY'] = base64.b64encode( aes_key ).decode('utf-8')
 220
 221        encrypted_aes_key = encrypt_aes_key_with_rsa(aes_key, api_public_key)
 222        encrypted_aes_key_b64 = base64.b64encode(encrypted_aes_key).decode('utf-8')
 223        # print("encrypted_aes_key_b64",encrypted_aes_key_b64)
 224        aes_key_payload = {
 225            "encrypted_aes_key": encrypted_aes_key_b64
 226        }
 227        response = requests.post(f"{BASE_URL}/api/{VERSION}/handshake_aes", json=aes_key_payload, headers={'X-User-ID':user_id})
 228
 229        if response.status_code == 200:
 230            print("Handshake (AES) successful.")
 231            return True
 232        else:
 233            print("Handshake (AES) failed:", response.text)
 234            return None
 235    else:
 236        print("Handshake (RSA) failed:", response.text)
 237        return None
 238
 239
 240
 241######################################################
 242def set_user(user_id, first_name=None, last_name=None, role=None, group=None, email=None, age=None, weight=None, gender=None):
 243    """
 244    Creates a user by sending user data to the server.
 245
 246    This function performs the following steps:
 247    1. Constructs the user data dictionary.
 248    2. Validates the user data against the "userSchema".
 249    3. Encrypts the user data using the stored AES key.
 250    4. Sends the encrypted user data to the server.
 251    5. Handles the server's response.
 252
 253    Args:     
 254    **user_id** (*str*): The user id (obtained through free registration with HABS)
 255    **first_name** (*str*, optional): The user's first name.      
 256    **last_name** (*str*, optional): The user's last name.     
 257    **role** (*str*, required): The user's role (Admin, Developer, ... established at registration).     
 258    **group** (*str*, optional): The user's group (laboratory name, company name, ...).     
 259    **email** (*str*, required): The user's email address.     
 260    **age** (*int*, optional): The user's age.     
 261    **weight** (*float*, optional): The user's weight.     
 262    **gender** (*str*, optional): The user's gender.      
 263
 264    Returns:       
 265        *str*: The user ID if the user is successfully created/retrieved, None otherwise.
 266
 267    Example:
 268    ```
 269    user_id = set_user(first_name="John", last_name="Doe", email="john.doe@example.com", age=30, weight=70.5, gender="X")
 270    if user_id:
 271        print(f"User created/retrieved with ID: {user_id}")
 272    else:
 273        print("User creation failed.")
 274
 275    **NOTE**: In order to use this function, your role should be `Admin`
 276    ```
 277    """
 278    url = f"{BASE_URL}/api/{VERSION}/users"
 279    user_data = {
 280        "first_name": first_name, 
 281        "last_name": last_name, 
 282        "role": role, 
 283        "group": group, 
 284        "email": email, 
 285        "age": age, 
 286        "weight": weight, 
 287        "gender": gender
 288    }
 289    if validate_metadata(user_data, "userSchema"):
 290        _user = {
 291            "user_data": user_data
 292        }
 293        _user = json.dumps(_user).encode('utf-8')
 294        aes_key_b64 = os.environ.get('AES_KEY')
 295        aes_key_bytes = base64.b64decode(aes_key_b64)
 296        response = requests.post(
 297            url,
 298            data=encrypt_message(_user, aes_key_bytes),
 299            headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
 300        )
 301
 302        if response.status_code == 201 or response.status_code == 208:
 303            print("User successfully created/retrieved.")
 304            user_id = response.json().get('user_id')
 305            return user_id
 306        else:
 307            print("User creation failed:", response.text)
 308            return None
 309    else:
 310        print("User creation failed.")
 311
 312
 313######################################################
 314def search_user_by_mail(user_id, email):
 315    """
 316    Search for a user by email.
 317
 318    This function sends a GET request to the server to search for a user by the provided email address.
 319
 320    Args:     
 321        **user_id** (*str*): The user id (obtained through free registration with HABS)
 322        **email** (*str*): The email address of the user to search for.
 323
 324    Returns:     
 325        *str*: The user ID if the user is found, None otherwise.
 326
 327    Example:
 328    ```
 329    user_id = search_user_by_mail("john.doe@example.com")
 330    if user_id:
 331        print(f"User found with ID: {user_id}")
 332    else:
 333        print("User not found.")
 334    ```
 335    """
 336    url = f"{BASE_URL}/api/{VERSION}/users?email={email}"
 337
 338    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
 339
 340    if response.status_code == 200:
 341        user_id = response.json().get('user_id')
 342        print("User found:", user_id)
 343        return user_id
 344    else:
 345        print("User not found.", response.text)
 346        return None
 347
 348
 349######################################################
 350def get_user_by_id(user_id):
 351    """
 352    Retrieve user data by user ID.
 353
 354    This function sends a GET request to the server to retrieve user data for the specified user ID.     
 355    The response data is decrypted using AES before returning the user data.
 356
 357    Args:     
 358        **user_id** (*str*): The unique identifier of the user to retrieve.
 359
 360    Returns:     
 361        *dict*: The user data if the user is found, None otherwise.
 362
 363    Example:
 364    ```
 365    user_data = get_user_by_id("1234567890")
 366    if user_data:
 367        print(f"User data: {user_data}")
 368    else:
 369        print("User not found.")
 370    ```
 371    """
 372    url = f"{BASE_URL}/api/{VERSION}/users/{user_id}"
 373
 374    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
 375
 376    if response.status_code == 200:
 377        print("User found.")
 378        encrypted_data = response.content 
 379        aes_key_b64 = os.environ.get('AES_KEY')
 380        aes_key_bytes = base64.b64decode(aes_key_b64)
 381        decrypted_json_string = decrypt_message(encrypted_data, aes_key_bytes)
 382        user_data = json.loads(decrypted_json_string)['user_data']
 383        return user_data
 384    else:
 385        print("User not found:", response.text)
 386        return None
 387
 388
 389######################################################
 390def set_session(metadata, user_id):
 391    """
 392    Create a new simple session.
 393
 394    This function sends a POST request to the server to create a new simple session using the provided metadata.
 395    The metadata is encrypted using AES before being sent to the server.
 396
 397    Args:     
 398        **metadata** (*dict*): A dictionary containing the session metadata. The only required metadata for the simple session are the user_id and a date.
 399        **user_id** (*str*): The user id (obtained through free registration with HABS)
 400
 401    Returns:      
 402        *str*: The unique identifier of the created session if successful, None otherwise.
 403
 404    Example:
 405    ```
 406    session_metadata = {
 407        "user_id": "1076203852085",
 408        "session_date": "2024-05-30T12:00:00Z"
 409    }
 410    session_id = set_session(session_metadata)
 411    if session_id:
 412        print(f"Session created with ID: {session_id}")
 413    else:
 414        print("Failed to create session.")
 415    ```
 416    """
 417    url = f"{BASE_URL}/api/{VERSION}/sessions"
 418    _session = metadata
 419    _session = json.dumps(_session).encode('utf-8')
 420    aes_key_b64 = os.environ.get('AES_KEY')
 421    aes_key_bytes = base64.b64decode(aes_key_b64)
 422    response = requests.post(
 423        url,
 424        data=encrypt_message(_session, aes_key_bytes),
 425        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
 426    )
 427
 428    if response.status_code == 200:
 429        print("Session successfully created.")
 430        # Extract the unique identifier for the uploaded data
 431        session_id = response.json().get('session_id')
 432
 433        # print("session_id: ",session_id)
 434        return session_id
 435    else:
 436        print("Session failed:", response.text)
 437        return None
 438
 439
 440######################################################
 441def get_data_by_id(data_id, user_id):
 442    """
 443    Retrieve raw data by its unique identifier from the server.
 444
 445    This function sends a GET request to fetch raw data associated with a specific identifier. It
 446    assumes that the data, if retrieved successfully, does not require decryption and is directly accessible.
 447
 448    Args:      
 449        **data_id** (*str*): The unique identifier for the data to be retrieved.
 450        **user_id** (*str*): The user id (obtained through free registration with HABS)
 451
 452    Returns:       
 453        **dict**: The raw data if retrieval is successful, None otherwise.
 454
 455    Example:
 456    ```
 457    data_id = "1234"
 458    raw_data = get_data_by_id(data_id)
 459    ... use the data
 460    ```
 461    """
 462    url = f"{BASE_URL}/api/{VERSION}/rawdata/{data_id}"
 463    
 464    # response = requests.get(url)
 465    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
 466
 467    if response.status_code == 200:
 468        print("Retrieved data successfully.")
 469        # decrypt
 470        return response.json().get('rawData')
 471    else:
 472        print("Failed to retrieve data:", response.text)
 473
 474
 475
 476######################################################
 477def find_sessions_by_user(user_id):
 478    """
 479    Retrieve session IDs associated with a given user.
 480
 481    This function sends a GET request to the API to retrieve all session IDs for a specified user.
 482    It expects the user ID to be passed as an argument and uses the user ID for authentication.
 483
 484    Args:
 485        user_id (str): The user ID (obtained through free registration with HABS).
 486
 487    Returns:
 488        list: A list of session IDs if the request is successful.
 489
 490    Raises:
 491        Exception: If the request fails or if there is an error in the response.
 492
 493    Example:
 494        >>> sessions = find_sessions_by_user("12345")
 495        >>> print(sessions)
 496        ["session1", "session2", "session3"]
 497
 498    Notes:
 499        Ensure that the environment variable `AES_KEY` is set to the base64 encoded AES key.
 500    """
 501    url = f"{BASE_URL}/api/{VERSION}/sessions/{user_id}"
 502
 503    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
 504
 505    if response.status_code == 200:
 506        print("User found.")
 507        encrypted_data = response.content 
 508        aes_key_b64 = os.environ.get('AES_KEY')
 509        aes_key_bytes = base64.b64decode(aes_key_b64)
 510        decrypted_json_string = decrypt_message(encrypted_data, aes_key_bytes)
 511        session_ids = json.loads(decrypted_json_string)['session_ids']
 512        return session_ids
 513    else:
 514        print("Failed to retrieve data:", response.text)
 515
 516
 517
 518######################################################
 519def get_data_by_session(session_id, user_id):
 520    """
 521    Retrieve raw data associated with a specific session identifier from the server.
 522
 523    This function sends a GET request to fetch all raw data linked to the given session ID. The data
 524    is returned in its raw form assuming it does not require decryption for usage.
 525
 526    Args:      
 527        **session_id** (*str*): The session identifier whose associated data is to be retrieved.
 528        **user_id** (*str*): The user id (obtained through free registration with HABS)
 529
 530    Returns:       
 531        *dict*: The raw data linked to the session if retrieval is successful, None otherwise.
 532
 533    Example:
 534    ```
 535    session_id = "abcd1234"
 536    session_data = get_data_by_session(session_id)
 537    if session_data:
 538        print("Data retrieved:", session_data)
 539    else:
 540        print("Failed to retrieve data.")
 541    ```
 542    """
 543    url = f"{BASE_URL}/api/{VERSION}/sessions/{session_id}/rawdata"
 544    
 545    # response = requests.get(url)
 546    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
 547    
 548    if response.status_code == 200:
 549        print("Retrieved data successfully.")
 550        # decrypt
 551        return response.json().get('data')
 552    else:
 553        print("Failed to retrieve data:", response.text)
 554
 555
 556
 557######################################################
 558def get_data_ids_by_session(session_id, user_id):
 559    """
 560    Retrieve a list of data IDs associated with a specific session from the server.
 561
 562    This function sends a GET request to fetch the IDs of all data entries linked to a specified session ID.
 563    The IDs are returned as a list. The function assumes the data does not require decryption for usage.
 564
 565    Args:      
 566        **session_id** (*str*): The session identifier for which data IDs are to be retrieved.
 567        **user_id** (*str*): The user id (obtained through free registration with HABS)
 568
 569    Returns:       
 570        *list*: A list of data IDs if retrieval is successful, None otherwise.
 571
 572    Example:
 573    ```
 574    session_id = "abcd1234"
 575    data_ids = get_data_ids_by_session(session_id)
 576    if data_ids:
 577        print("Data IDs retrieved:", data_ids)
 578    else:
 579        print("Failed to retrieve data IDs.")
 580    ```
 581    """
 582    url = f"{BASE_URL}/api/{VERSION}/sessions/{session_id}/ids"
 583    
 584    # response = requests.get(url)
 585    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
 586    
 587    if response.status_code == 200:
 588        print("Retrieved ids successfully.")
 589        # decrypt
 590        return response.json().get('ids')
 591    else:
 592        print("Failed to retrieve ids:", response.text)
 593
 594
 595
 596######################################################
 597def upload_data(metadata, timestamps, user_id, data, ppg_red, ppg_ir):
 598    """
 599    Uploads EEG (and PPG) data to the server along with associated metadata.
 600
 601    This function compiles different types of physiological data along with metadata into a single dictionary,
 602    encrypts the data, and then uploads it via a POST request. Upon successful upload, the server returns a
 603    unique identifier for the data which can then be used for future queries or operations.
 604
 605    Args:     
 606        **metadata** (*dict*): Information about the data such as subject details and session parameters.     
 607        **timestamps** (*list*): List of timestamps correlating with each data point.     
 608        **user_id** (*str*): The user id (obtained through free registration with HABS)
 609        **data** (*list*): EEG data points.     
 610        **ppg_red** (*list*): Red photoplethysmogram data points.     
 611        **ppg_ir** (*list*): Infrared photoplethysmogram data points.      
 612
 613    Returns:     
 614        *tuple*: A tuple containing the data ID of the uploaded data if successful, and None otherwise.
 615
 616    Notes:
 617        Ensure that timestamps has the same length of data last dimension.
 618
 619    Example:
 620    ```
 621    metadata = {"session_id": "1234", "subject_id": "001"}
 622    timestamps = [1597709184, 1597709185]
 623    data = [0.1, 0.2]
 624    ppg_red = [123, 124]
 625    ppg_ir = [125, 126]
 626    data_id, error = upload_data(metadata, timestamps, data, ppg_red, ppg_ir)
 627    if data_id:
 628        print("Data uploaded successfully. Data ID:", data_id)
 629    else:
 630        print("Upload failed with error:", error)
 631    ```
 632    """
 633    url = f"{BASE_URL}/api/{VERSION}/rawdata"
 634    _data = {
 635        "metadata": metadata,
 636        "timestamps": timestamps,
 637        "data": data,
 638        "ppg_red": ppg_red,
 639        "ppg_ir": ppg_ir
 640    }
 641    _data = json.dumps(_data).encode('utf-8')
 642
 643    # response = requests.post(url, json=_data)
 644    aes_key_b64 = os.environ.get('AES_KEY')
 645    aes_key_bytes = base64.b64decode(aes_key_b64)
 646    response = requests.post(
 647        url,
 648        data=encrypt_message(_data, aes_key_bytes),
 649        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
 650    )
 651    # response = requests.get(url, headers={'X-User-ID':USERID}) # mongo _id for the user document. Communicated at user creation.
 652
 653    if response.status_code == 200:
 654        print('.', end='', flush=True)
 655        # Extract the unique identifier for the uploaded data
 656        data_id = response.json().get('data_id')
 657        return data_id, None
 658    else:
 659        print("Upload failed:", response.text)
 660        return None
 661
 662
 663
 664######################################################
 665def acquire_send_raw(user_id, date, board, serial_number, stream_duration, buffer_duration, session_type="", tags=[], callback=None, extra=None):
 666    """
 667    Asynchronously acquires raw data from a specific EEG board and sends it to the server.
 668
 669    This function connects to an EEG board, initiates a data acquisition session, and sends the collected data
 670    to the server in real-time or near real-time. It ensures that all the data handled during the session
 671    is associated with a unique session ID and metadata that includes user and session details. The function
 672    will validate the session metadata before proceeding with data acquisition and sending.
 673
 674    Args:      
 675    **user_id** (*str*): The unique identifier of the user for whom the data is being collected.      
 676    **date** (*str*): The date of the session, used for metadata purposes.     
 677    **board** (*int*): Identifier for the EEG board from which data will be acquired.      
 678    **stream_duration** (*int*): Duration in seconds for which data will be streamed from the board.     
 679    **buffer_duration** (*int*): Time in seconds for how often the data is buffered and sent.     
 680
 681    Returns:     
 682    *str* or *bool*: The session ID if the operation is successful; False otherwise.
 683
 684    Raises:
 685    **ConnectionError**: If the board connection fails.      
 686    **ValidationError**: If the metadata does not comply with the required schema.
 687
 688    Example:
 689    ```
 690    session = acquire_send_raw('user123', '2021-06-01', 'MUSE_S', 300, 10)
 691    if session:
 692        print(f"Session successfully started with ID: {session}")
 693    else:
 694        print("Failed to start session")
 695    ```
 696    """
 697    # set session for the data
 698    # We set a session id for the current interaction with the API (even if we fail to get the board, it will be important to store the failure)
 699    session_metadata = {
 700      "user_id": user_id, # add user to the session for reference
 701      "session_date": date,
 702      "session_type": session_type,
 703      "session_tags": tags
 704    }
 705    session_id = set_session(metadata={**session_metadata}, user_id=user_id)
 706    print("\nSession initialized. You can visualize it here:\n ", "https://habs.ai/live.html?session_id="+str(session_id), "\n")
 707
 708    if validate_metadata(session_metadata, "sessionSchema"):
 709        asyncio.run( 
 710            _acquire_send_raw(user_id, session_id, board, serial_number, stream_duration, buffer_duration, callback, extra) 
 711        )
 712        return session_id
 713    else:
 714        print("Session initialization failed.")
 715        return False
 716
 717# async appendage
 718async def _acquire_send_raw(user_id, session_id, board, serial_number, stream_duration, buffer_duration, callback=None, extra=None):
 719    # get board
 720    board_manager = BoardManager(enable_logger=False, board_id=board, serial_number=serial_number, extra=extra)
 721    board_manager.connect()
 722
 723    board_manager.metadata['session_id'] = session_id # add session to the data for reference
 724
 725    # stream_duration sec, buffer_duration sec
 726    await board_manager.data_acquisition_loop(
 727        stream_duration=stream_duration, 
 728        buffer_duration=buffer_duration, 
 729        service=upload_data,
 730        user_id=user_id,
 731        callback=callback
 732    )
 733
 734
 735
 736######################################################
 737def send_file(user_id, date, edf_file, ch_nrs=None, ch_names=None, session_type="", tags=[]):
 738    """
 739    Uploads EEG data from a file to the server along with associated metadata.
 740
 741    This function compiles EEG data from an [EDF file](https://www.edfplus.info/downloads/index.html) along with metadata into a single dictionary,
 742    encrypts the data, and then uploads it via a POST request. Upon successful upload, the server returns a
 743    unique identifier for the session which can then be used for future queries or operations.
 744
 745    Args:    
 746    **user_id** (*str*): The unique identifier of the user for whom the data is being collected.     
 747    **metadata** (*dict*): Information about the data such as subject details and session parameters.     
 748    **date** (*str*): The date of the session, used for metadata purposes.     
 749    **edf_file** (*str*): name of an EDF file.      
 750    **ch_nrs** (*list of int*, optional): The indices of the channels to read. The default is None.     
 751    **ch_names** (*list of str*, optional): The names of channels to read. The default is None.     
 752
 753    Returns:     
 754        *tuple*: A tuple containing the session ID of the uploaded data if successful, and None otherwise.
 755
 756    Example:
 757    ```
 758    session = send_file('user123', '2021-06-01', 'nameoffile.edf')
 759    if session:
 760        print(f"Session successfully started with ID: {session}")
 761    else:
 762        print("Failed to start session")
 763    ```
 764    """
 765
 766    try:
 767        signals, signal_headers, header = highlevel.read_edf(edf_file, ch_nrs, ch_names)
 768
 769        max_time = signals.shape[1] / signal_headers[0]['sample_frequency']
 770        timestamps = np.linspace(header['startdate'].timestamp(), max_time, signals.shape[1])
 771
 772        session_metadata = {
 773          "user_id": user_id, # add user to the session for reference
 774          "session_date": header['startdate'].strftime("%m/%d/%Y, %H:%M:%S"),
 775          "session_type": session_type,
 776          "session_tags": tags
 777        }
 778        if validate_metadata(session_metadata, "sessionSchema"):
 779            session_id = set_session(metadata={**session_metadata}, user_id=user_id)
 780            metadata = {'session_id':session_id, **session_metadata, **convert_datetime_in_dict(header), **convert_datetime_in_dict(signal_headers[0])}
 781
 782            chunks = ((signals.size * signals.itemsize)//300000)+1
 783            timestamps_chunks = np.array_split(timestamps, chunks)
 784            signals_chunks = np.array_split(signals, chunks, axis=1)
 785            json_data = json.dumps(signals_chunks[0].tolist())
 786            size_in_bytes = sys.getsizeof(json_data)
 787            print("%d total bytes will be sent into %d chunks of %d bytes" % (signals.size * signals.itemsize, chunks, size_in_bytes))
 788
 789            for timestamps_chunk,signals_chunk in zip(timestamps_chunks, signals_chunks):
 790                upload_data(metadata, timestamps_chunk.tolist(), user_id, signals_chunk.tolist(), [], [])
 791
 792            return session_id
 793        else:
 794            return False
 795    except Exception as e:
 796        print("A general error occurred:", e)
 797        return False    
 798
 799
 800
 801######################################################
 802######################################################
 803def set_pipe(metadata, pipeline, params, user_id):
 804    """
 805    Configures and initiates a data processing pipeline for a session on the server.
 806
 807    This function sends metadata and processing parameters to a specified pipeline endpoint
 808    to create a data processing session. It encrypts the session data before sending to ensure
 809    security. The function checks the server response to confirm the session creation.
 810
 811    Args:     
 812    **metadata** (*dict*): A dictionary containing metadata about the session, typically including
 813                     details such as user ID and session date.       
 814    **pipeline** (*str*): The identifier for the processing pipeline to be used.      
 815    **params** (*dict*): Parameters specific to the processing pipeline, detailing how data should
 816                   be processed.      
 817    **user_id** (*str*): The user id (obtained through free registration with HABS)
 818
 819    Returns:       
 820        *str* or *None*: The session ID if the session is successfully created, or None if the operation fails.
 821
 822    Raises:     
 823        **requests.exceptions.RequestException**: An error from the Requests library when an HTTP request fails.      
 824        **KeyError**: If necessary keys are missing in the environment variables.
 825
 826    Example:
 827    ```
 828    session_metadata = {"user_id": "123", "session_date": "2024-06-03"}
 829    processing_params = {"filter_type": "lowpass", "cutoff_freq": 30}
 830    session_id = set_pipe(session_metadata, 'eeg_smoothing', processing_params)
 831    if session_id:
 832        print(f"Pipeline session created with ID: {session_id}")
 833    else:
 834        print("Failed to create pipeline session")
 835    ```
 836    """
 837    url = f"{BASE_URL}/api/{VERSION}/sessions/pipe/{pipeline}"
 838    _session = {
 839        "metadata": metadata,
 840        "processing_params": params,
 841    }
 842    _session = json.dumps(_session).encode('utf-8')
 843    aes_key_b64 = os.environ.get('AES_KEY')
 844    aes_key_bytes = base64.b64decode(aes_key_b64)
 845    response = requests.post(
 846        url,
 847        data=encrypt_message(_session, aes_key_bytes),
 848        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
 849    )
 850    if response.status_code == 200:
 851        print("Session successfully created.")
 852        # Extract the unique identifier for the uploaded data
 853        session_id = response.json().get('session_id')
 854        # print(session_id)
 855        return session_id
 856    else:
 857        print("Session failed:", response.text)
 858        return None
 859
 860
 861
 862######################################################
 863def upload_pipedata(metadata, timestamps, user_id, data, ppg_red, ppg_ir):
 864    """
 865    Uploads processed data to a specific session on the server.
 866
 867    This function is responsible for uploading various data streams associated with a session, including
 868    timestamps and physiological measurements such as PPG (Photoplethysmogram). The data is encrypted before
 869    sending to ensure confidentiality and integrity.
 870
 871    Args:        
 872    **metadata** (*dict*): Contains session-related metadata including the session ID.      
 873    **timestamps** (*list*): A list of timestamps corresponding to each data point.      
 874    **user_id** (*str*): The user id (obtained through free registration with HABS)
 875    **data** (*list*): The main data collected, e.g., EEG readings.      
 876    **ppg_red** (*list*): Red channel data from a PPG sensor.    
 877    **ppg_ir** (*list*): Infrared channel data from a PPG sensor.     
 878
 879    Returns:     
 880    *tuple*: A tuple containing the data ID if the upload is successful and the processed data, or None if the upload fails.
 881
 882    Raises:     
 883    **requests.exceptions.RequestException: An error from the Requests library when an HTTP request fails.
 884    **KeyError**: If necessary keys are missing in the environment variables.
 885
 886    Example:
 887    ```
 888    session_metadata = {"session_id": "12345"}
 889    timestamps = [1597709165, 1597709166, ...]
 890    data = [0.1, 0.2, ...]
 891    ppg_red = [12, 15, ...]
 892    ppg_ir = [20, 22, ...]
 893    data_id, processed_data = upload_pipedata(session_metadata, timestamps, data, ppg_red, ppg_ir)
 894    if data_id:
 895        print(f"Data uploaded successfully with ID: {data_id}")
 896    else:
 897        print("Failed to upload data")
 898    ```
 899    """
 900    url = f"{BASE_URL}/api/{VERSION}/pipedata/{metadata['session_id']}" # the metadata contain session_id to consistently pass it with each upload
 901
 902    _data = {
 903        "metadata": metadata,
 904        "timestamps": timestamps,
 905        "data": data,
 906        "ppg_red": ppg_red,
 907        "ppg_ir": ppg_ir
 908    }
 909    _data = json.dumps(_data).encode('utf-8')
 910    aes_key_b64 = os.environ.get('AES_KEY')
 911    aes_key_bytes = base64.b64decode(aes_key_b64)
 912    response = requests.post(
 913        url,
 914        data=encrypt_message(_data, aes_key_bytes),
 915        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
 916    )
 917
 918    if response.status_code == 200:
 919        print('.', end='', flush=True)
 920        # Extract the unique identifier for the uploaded data
 921        data_id = response.json().get('data_id')
 922        # Retrieve the processed data
 923        data = response.json().get('pipeData')
 924        return data_id, data
 925    else:
 926        print("Upload failed:", response.text)
 927        return None
 928
 929
 930######################################################
 931def acquire_send_pipe(pipeline, params, user_id, date, board, serial_number, stream_duration, buffer_duration, session_type="", tags=[], callback=None, extra=None):
 932    """
 933    Acquires data from a board, processes it according to a specified pipeline, and sends it to a server.
 934    This function handles setting up a session for data acquisition and processing, connects to a board, 
 935    and manages the data flow from acquisition through processing to uploading. It uses an asynchronous loop
 936    to handle the operations efficiently, suitable for real-time data processing scenarios.
 937
 938    Args:
 939    **pipeline** (*str*): Name of the processing pipeline to use.     
 940    **params** (*dict*): Parameters for the pipeline processing.      
 941    **user_id** (*str*): The user ID to which the session will be associated.      
 942    **date** (*str*): Date of the session for tracking purposes.      
 943    **board** (*int*): Identifier for the hardware board to use for data acquisition.      
 944    **stream_duration** (*int*): Duration in seconds to stream data from the board.     
 945    **buffer_duration** (*int*): Duration in seconds to buffer data before processing.      
 946    **callback** (*function*): Optional callback function to execute after data is sent.
 947
 948    Returns:    
 949        *str* or *bool*: The session ID if successful, False otherwise.
 950
 951    """
 952    # set session for the data
 953    # We set a session id for the current interaction with the API (even if we fail to get the board, it will be important to store the failure)
 954    session_metadata = {
 955      "user_id": user_id, # add user to the session for reference
 956      "session_date": date,
 957      "session_type": session_type,
 958      "session_tags": tags
 959    }
 960    if validate_metadata(session_metadata, "sessionSchema"):
 961        session_id = set_pipe(metadata={**session_metadata}, pipeline=pipeline, params=params, user_id=user_id)
 962        print("\nSession initialized. You can visualize it here:\n ", "https://habs.ai/bos/live.html?session_id="+str(session_id), "\n")
 963
 964        asyncio.run( 
 965            _acquire_send_pipe(pipeline, params, user_id, session_id, board, serial_number, stream_duration, buffer_duration, callback, extra) 
 966        )
 967        return session_id #, self.processed_data
 968    else:
 969        print("Session initialization failed.")
 970        return False
 971
 972# async appendage
 973async def _acquire_send_pipe(pipeline, params, user_id, session_id, board, serial_number, stream_duration, buffer_duration, callback=None, extra=None):
 974    # get board
 975    board_manager = BoardManager(enable_logger=False, board_id=board, serial_number=serial_number, extra=extra)
 976    board_manager.connect()
 977
 978    board_manager.metadata['session_id'] = session_id # add session to the data for reference
 979
 980    # stream_duration sec, buffer_duration sec
 981    await board_manager.data_acquisition_loop(
 982        stream_duration=stream_duration, 
 983        buffer_duration=buffer_duration, 
 984        service=upload_pipedata,
 985        user_id=user_id,
 986        callback=callback
 987    )
 988
 989
 990
 991
 992######################################################
 993def get_user_database(user_id):
 994    """
 995    Retrieve all user data by user ID.
 996
 997    This function sends a GET request to the server to dump all data stored for the specified user ID.     
 998    The response data is decrypted using AES before returning the user data.
 999
1000    Args:     
1001        **user_id** (*str*): The unique identifier of the user to retrieve.
1002
1003    Returns:     
1004        *None*: A zip file contaning all data as JSON files, None otherwise.
1005
1006    Example:
1007    ```
1008    user_data = get_user_database("1234567890")
1009    if user_data:
1010        print(f"User data: {user_data}")
1011    else:
1012        print("User not found.")
1013    ```
1014    """
1015    url = f"{BASE_URL}/api/{VERSION}/database/dump/{user_id}"
1016
1017    response = requests.get(url, headers={'X-User-ID': user_id}, stream=True)
1018
1019    if response.status_code == 200:
1020        # Open a local file with write-binary mode
1021        strtime = datetime.today().strftime("%Y%m%d_%H%M%S")
1022
1023        with open(f"brainos_{strtime}_dump.zip", 'wb') as file:
1024            # Write the response content to the file in chunks
1025            for chunk in response.iter_content(chunk_size=8192):
1026                file.write(chunk)
1027        print("Database dump saved successfully.")
1028        return True
1029    else:
1030        print("User not found:", response.text)
1031        return None
1032
1033
1034
1035
1036######################################################
1037def create_tagged_interval(user_id, session_id, eeg_data_id, start_time, end_time, tags, channel_ids=None):
1038    """
1039    Creates a tagged interval by sending the interval data to the server.
1040
1041    This function performs the following steps:
1042    1. Constructs the interval data dictionary.
1043    2. Validates the interval data against the "tagSchema".
1044    3. Sends the interval data to the server.
1045
1046    Args:
1047    **session_id** (*str*): The session id.
1048    **eeg_data_id** (*str*): The EEG data id.
1049    **start_time** (*str*): The start time of the interval in ISO 8601 format.
1050    **end_time** (*str*): The end time of the interval in ISO 8601 format.
1051    **tags** (*list*): List of tags, each tag is a dictionary containing a "tag" and "properties".
1052    **channel_ids** (*list*, optional): List of channel ids the tag applies to. If None, applies to all channels.
1053
1054    Returns:
1055        *str*: The interval ID if the interval is successfully created, None otherwise.
1056
1057    Example:
1058    ```
1059    interval_id = create_tagged_interval(
1060        session_id="session_123",
1061        eeg_data_id="eeg_data_456",
1062        start_time="2023-01-01T00:00:00Z",
1063        end_time="2023-01-01T00:05:00Z",
1064        tags=[{"tag": "seizure", "properties": {"severity": "high"}}]
1065    )
1066    if interval_id:
1067        print(f"Tagged interval created with ID: {interval_id}")
1068    else:
1069        print("Tagged interval creation failed.")
1070    ```
1071    """
1072    url = f"{BASE_URL}/api/{VERSION}/session/{session_id}/tag"
1073    interval_data = {
1074        "user_id": user_id,
1075        "session_id": session_id,
1076        "eeg_data_id": eeg_data_id,
1077        "start_time": start_time,
1078        "end_time": end_time,
1079        "tags": tags,
1080        "channel_ids": channel_ids if channel_ids else []
1081    }
1082    
1083    if validate_metadata(interval_data, "tagSchema"):
1084        response = requests.post(
1085            url,
1086            json=interval_data,
1087            headers={'Content-Type': 'application/json'}
1088        )
1089
1090        if response.status_code == 201:
1091            print("Tagged interval successfully created.")
1092            interval_id = response.json().get('interval_id')
1093            return interval_id
1094        else:
1095            print("Tagged interval creation failed:", response.text)
1096            return None
1097    else:
1098        print("Tagged interval creation failed due to validation error.")
1099
1100
1101
1102
1103
1104######################################################
1105def process_session_pipe(pipeline, params, user_id, date, existing_session_id, session_type="", tags=[]):
1106    """
1107    Process a session pipeline with specified parameters and metadata.
1108
1109    This function processes an existing session by applying a specified pipeline and parameters.
1110    It sends a POST request to the API with the session metadata and processing parameters,
1111    creating a new session based on the existing one.
1112
1113    Args:
1114        **pipeline** (*str*): The pipeline to be applied to the session.
1115        **params** (*dict*): The processing parameters for the pipeline.
1116        **user_id** (*str*): The user ID (obtained through free registration with HABS).
1117        **date** (*str*): The date of the session.
1118        **existing_session_id** (*str*): The ID of the existing session to be processed.
1119        **session_type** (*str*, optional): The type of the new session. Defaults to an empty string.
1120        **tags** (*list*, optional): A list of tags associated with the session. Defaults to an empty list.
1121
1122    Returns:
1123        tuple: A tuple containing the new session ID and the processed data if the request is successful.
1124        None: If the session creation fails.
1125        bool: False if the session metadata is invalid.
1126
1127    Example:
1128        >>> new_session_id, processed_data = process_session_pipe("my_pipeline", {"param1": "value1"}, "12345", "2023-07-03", "existing_session_001")
1129        >>> print(new_session_id, processed_data)
1130
1131    Notes:
1132        Ensure that the environment variable `AES_KEY` is set to the base64 encoded AES key.
1133
1134    Raises:
1135        Exception: If there is an error in the request or response.
1136
1137    """
1138    session_metadata = {
1139      "user_id": user_id, # add user to the session for reference
1140      "session_date": date, # .strftime("%m/%d/%Y, %H:%M:%S"),
1141      "existing_session_id": existing_session_id,
1142      "session_type": f"[On {existing_session_id}]: {session_type}", # type of the new session
1143      "session_tags": tags
1144    }
1145    if validate_metadata(session_metadata, "sessionSchema"):
1146        url = f"{BASE_URL}/api/{VERSION}/sessions/{existing_session_id}/pipe/{pipeline}"
1147        _session = {
1148            "metadata": session_metadata,
1149            "processing_params": params,
1150        }
1151        _session = json.dumps(_session).encode('utf-8')
1152        aes_key_b64 = os.environ.get('AES_KEY')
1153        aes_key_bytes = base64.b64decode(aes_key_b64)
1154        response = requests.post(
1155            url,
1156            data=encrypt_message(_session, aes_key_bytes),
1157            headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
1158        )
1159        if response.status_code == 200:
1160            print("Session successfully created.")
1161            session_id = response.json().get('session_id')
1162            pipeData = response.json().get('pipeData')
1163            # print(session_id)
1164            return session_id, pipeData
1165        else:
1166            print("Session failed:", response.text)
1167            return None
1168
1169        return session_id # processed_data
1170    else:
1171        print("Session failed.")
1172        return False
1173
1174
1175
1176
1177######################################################
1178def train(session_id, params, user_id):
1179    """
1180    Sends a request to the server to train a machine learning algorithm on the data from a specified session.
1181
1182    Args:     
1183    **session_id** (*str*): The unique identifier of the session containing the data to be used for training.       
1184    **params** (*dict*): The parameters for the training process.
1185    **user_id** (*str*): The user id (obtained through free registration with HABS)
1186
1187    Returns:      
1188        *str* or *None*: The task ID if the request is successful, None otherwise.
1189
1190    This function sends the training parameters and session ID to the server, which initiates the training process.
1191    The response includes a task ID that can be used for future interactions related to the training task.
1192
1193    Example:
1194    ```
1195    train("session_12345", {"param1": "value1", "param2": "value2"})
1196    ```
1197    """
1198    url = f"{BASE_URL}/api/{VERSION}/train/{session_id}"
1199    _params = {
1200        "params": params,
1201    }
1202    _params = json.dumps(_params).encode('utf-8')
1203    aes_key_b64 = os.environ.get('AES_KEY')
1204    aes_key_bytes = base64.b64decode(aes_key_b64)
1205    response = requests.post(
1206        url,
1207        data=encrypt_message(_params, aes_key_bytes),
1208        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
1209    )
1210    # response = requests.get(url, headers={'X-User-ID':USERID}) # mongo _id for the user document. Communicated at user creation.
1211
1212    if response.status_code == 200:
1213        task_id = response.json().get('task_id')
1214        print("Published. For future interactions, use task_id:",task_id)
1215        return task_id
1216    else:
1217        print("Publish failed:", response.text)
1218        return None
1219
1220
1221
1222
1223######################################################
1224def infer(data_id, params, user_id):
1225    """
1226    Sends a request to the server to perform machine learning inference based on a previously trained model, given the data ID.
1227
1228    Args:     
1229    **data_id** (*str*): The unique identifier of the data to be used for inference.      
1230    **params** (*dict*): The parameters for the inference process.
1231    **user_id** (*str*): The user id (obtained through free registration with HABS)
1232
1233    Returns:
1234    *str* or *None*: The task ID if the request is successful, None otherwise.
1235
1236    This function sends the inference parameters and data ID to the server, which initiates the inference process.
1237    The response includes a task ID that can be used for future interactions related to the inference task.
1238
1239    Example:
1240    ```
1241    infer("data_12345", {"param1": "value1", "param2": "value2"})
1242    ```
1243    """
1244    url = f"{BASE_URL}/api/{VERSION}/infer/{data_id}"
1245    _params = {
1246        "params": params,
1247    }
1248    _params = json.dumps(_params).encode('utf-8')
1249    # response = requests.post(url, json=_params)
1250    aes_key_b64 = os.environ.get('AES_KEY')
1251    aes_key_bytes = base64.b64decode(aes_key_b64)
1252    response = requests.post(
1253        url,
1254        data=encrypt_message(_params, aes_key_bytes),
1255        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
1256    )
1257    # response = requests.get(url, headers={}) # mongo _id for the user document. Communicated at user creation.
1258    
1259    if response.status_code == 200:
1260        task_id = response.json().get('task_id')
1261        print("Published. For future interactions, use task_id:",task_id)
1262        return task_id
1263    else:
1264        print("Publish failed:", response.text)
1265        return None
def validate_metadata(metadata, schema_name, schemafile='metadata.json'):
 77def validate_metadata(metadata, schema_name, schemafile='metadata.json'):
 78    """
 79    Validate metadata against a given JSON schema.
 80
 81    Args:    
 82        **metadata** (*dict*): The metadata to be validated.    
 83        **schema_name** (*str*): The name of the schema to validate against. HABSlib currently supports the validation of Session metadata and User data.    
 84        **schemafile** (*str*, optional): The path to the JSON file containing the schemas. Defaults to 'metadata.json'.    
 85
 86    Returns:    
 87        *bool*: True if validation is successful, False otherwise.
 88
 89    Raises:    
 90        **FileNotFoundError**: If the schema file does not exist.     
 91        **json.JSONDecodeError**: If there is an error decoding the JSON schema file.     
 92        **exceptions.ValidationError**: If the metadata does not conform to the schema.     
 93        **Exception**: For any other errors that occur during validation.     
 94
 95    Example:
 96    ```
 97    metadata = {"name": "example", "type": "data"}
 98    schema_name = "example_schema"
 99    is_valid = validate_metadata(metadata, schema_name)
100    if is_valid:
101        print("Metadata is valid.")
102    else:
103        print("Metadata is invalid.")
104    ```
105    """
106    print(metadata)
107    try:
108        with open(os.path.join(os.path.dirname(__file__), schemafile), 'r') as file:
109            content = file.read()
110            schemas = json.loads(content)
111        schema = schemas[schema_name]
112        validate(instance=metadata, schema=schema) #, format_checker=FormatChecker())
113        print("Metadata validation successful!")
114        return True
115
116    except json.JSONDecodeError as e:
117        print("Failed to decode JSON:", e)
118        return False
119
120    except exceptions.ValidationError as e:
121        print("Validation error:", e)
122        return False
123
124    except FileNotFoundError:
125        print(f"No such file: {schemafile}")
126        return False
127
128    except Exception as e:
129        print("A general error occurred:", e)
130        return False

Validate metadata against a given JSON schema.

Args:
metadata (dict): The metadata to be validated.
schema_name (str): The name of the schema to validate against. HABSlib currently supports the validation of Session metadata and User data.
schemafile (str, optional): The path to the JSON file containing the schemas. Defaults to 'metadata.json'.

Returns:
bool: True if validation is successful, False otherwise.

Raises:
FileNotFoundError: If the schema file does not exist.
json.JSONDecodeError: If there is an error decoding the JSON schema file.
exceptions.ValidationError: If the metadata does not conform to the schema.
Exception: For any other errors that occur during validation.

Example:

metadata = {"name": "example", "type": "data"}
schema_name = "example_schema"
is_valid = validate_metadata(metadata, schema_name)
if is_valid:
    print("Metadata is valid.")
else:
    print("Metadata is invalid.")
def convert_datetime_in_dict(data):
133def convert_datetime_in_dict(data):
134    """
135    Recursively converts all datetime objects in a dictionary to strings in ISO format.
136
137    Args:     
138        **data** (*dict*): The dictionary containing the data.
139
140    Returns:     
141        *dict*: The dictionary with datetime objects converted to strings.
142    """
143    for key, value in data.items():
144        if isinstance(value, datetime):
145            data[key] = value.isoformat()
146        elif isinstance(value, dict):
147            data[key] = convert_datetime_in_dict(value)
148    return data

Recursively converts all datetime objects in a dictionary to strings in ISO format.

Args:
data (dict): The dictionary containing the data.

Returns:
dict: The dictionary with datetime objects converted to strings.

def handshake(base_url, user_id):
170def handshake(base_url, user_id):  
171    """
172    Perform a handshake with the server to exchange encryption keys for the current session.
173
174    This function performs the following steps:
175    0. Performs login to the HABS server.
176    1. Sends a GET request to the server to initiate an RSA handshake.
177    2. Retrieves the server's public RSA key from the response.
178    3. Generates a local AES key and stores it in the environment.
179    4. Encrypts the AES key with the server's RSA key.
180    5. Sends the encrypted AES key to the server to complete the AES handshake.
181
182    Args:      
183        **base_url** (*str*): The base URL of the server's API.
184        **user_id** (*str*): The user id (obtained through free registration with HABS)
185
186    Returns:     
187        *bool*: True if the handshake is successful, None otherwise.
188
189    Raises:      
190        **requests.RequestException**: If a request to the server fails.
191
192    Example:
193    ```
194    success = handshake("https://example.com")
195    if success:
196        print("Handshake completed successfully.")
197    else:
198        print("Handshake failed.")
199    ```
200    """
201    head()
202    global BASE_URL
203    BASE_URL = base_url
204    url = f"{BASE_URL}/api/{VERSION}/handshake_rsa"
205    # response = requests.get(url)
206    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
207
208    if response.status_code == 200:
209        print("Handshake (RSA) successful.")
210        api_public_key_pem = response.json().get('api_public_key')
211        api_public_key = serialization.load_pem_public_key(
212            api_public_key_pem.encode(),
213            backend=default_backend()
214        )
215        os.environ['API_PUBLIC_KEY'] = api_public_key_pem
216
217        # Then we generate and store the AES key
218        aes_key = generate_aes_key()
219        # print("aes_key", aes_key)
220        os.environ['AES_KEY'] = base64.b64encode( aes_key ).decode('utf-8')
221
222        encrypted_aes_key = encrypt_aes_key_with_rsa(aes_key, api_public_key)
223        encrypted_aes_key_b64 = base64.b64encode(encrypted_aes_key).decode('utf-8')
224        # print("encrypted_aes_key_b64",encrypted_aes_key_b64)
225        aes_key_payload = {
226            "encrypted_aes_key": encrypted_aes_key_b64
227        }
228        response = requests.post(f"{BASE_URL}/api/{VERSION}/handshake_aes", json=aes_key_payload, headers={'X-User-ID':user_id})
229
230        if response.status_code == 200:
231            print("Handshake (AES) successful.")
232            return True
233        else:
234            print("Handshake (AES) failed:", response.text)
235            return None
236    else:
237        print("Handshake (RSA) failed:", response.text)
238        return None

Perform a handshake with the server to exchange encryption keys for the current session.

This function performs the following steps:

  1. Performs login to the HABS server.
  2. Sends a GET request to the server to initiate an RSA handshake.
  3. Retrieves the server's public RSA key from the response.
  4. Generates a local AES key and stores it in the environment.
  5. Encrypts the AES key with the server's RSA key.
  6. Sends the encrypted AES key to the server to complete the AES handshake.

Args:
base_url (str): The base URL of the server's API. user_id (str): The user id (obtained through free registration with HABS)

Returns:
bool: True if the handshake is successful, None otherwise.

Raises:
requests.RequestException: If a request to the server fails.

Example:

success = handshake("https://example.com")
if success:
    print("Handshake completed successfully.")
else:
    print("Handshake failed.")
def set_user( user_id, first_name=None, last_name=None, role=None, group=None, email=None, age=None, weight=None, gender=None):
243def set_user(user_id, first_name=None, last_name=None, role=None, group=None, email=None, age=None, weight=None, gender=None):
244    """
245    Creates a user by sending user data to the server.
246
247    This function performs the following steps:
248    1. Constructs the user data dictionary.
249    2. Validates the user data against the "userSchema".
250    3. Encrypts the user data using the stored AES key.
251    4. Sends the encrypted user data to the server.
252    5. Handles the server's response.
253
254    Args:     
255    **user_id** (*str*): The user id (obtained through free registration with HABS)
256    **first_name** (*str*, optional): The user's first name.      
257    **last_name** (*str*, optional): The user's last name.     
258    **role** (*str*, required): The user's role (Admin, Developer, ... established at registration).     
259    **group** (*str*, optional): The user's group (laboratory name, company name, ...).     
260    **email** (*str*, required): The user's email address.     
261    **age** (*int*, optional): The user's age.     
262    **weight** (*float*, optional): The user's weight.     
263    **gender** (*str*, optional): The user's gender.      
264
265    Returns:       
266        *str*: The user ID if the user is successfully created/retrieved, None otherwise.
267
268    Example:
269    ```
270    user_id = set_user(first_name="John", last_name="Doe", email="john.doe@example.com", age=30, weight=70.5, gender="X")
271    if user_id:
272        print(f"User created/retrieved with ID: {user_id}")
273    else:
274        print("User creation failed.")
275
276    **NOTE**: In order to use this function, your role should be `Admin`
277    ```
278    """
279    url = f"{BASE_URL}/api/{VERSION}/users"
280    user_data = {
281        "first_name": first_name, 
282        "last_name": last_name, 
283        "role": role, 
284        "group": group, 
285        "email": email, 
286        "age": age, 
287        "weight": weight, 
288        "gender": gender
289    }
290    if validate_metadata(user_data, "userSchema"):
291        _user = {
292            "user_data": user_data
293        }
294        _user = json.dumps(_user).encode('utf-8')
295        aes_key_b64 = os.environ.get('AES_KEY')
296        aes_key_bytes = base64.b64decode(aes_key_b64)
297        response = requests.post(
298            url,
299            data=encrypt_message(_user, aes_key_bytes),
300            headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
301        )
302
303        if response.status_code == 201 or response.status_code == 208:
304            print("User successfully created/retrieved.")
305            user_id = response.json().get('user_id')
306            return user_id
307        else:
308            print("User creation failed:", response.text)
309            return None
310    else:
311        print("User creation failed.")

Creates a user by sending user data to the server.

This function performs the following steps:

  1. Constructs the user data dictionary.
  2. Validates the user data against the "userSchema".
  3. Encrypts the user data using the stored AES key.
  4. Sends the encrypted user data to the server.
  5. Handles the server's response.

Args:
user_id (str): The user id (obtained through free registration with HABS) first_name (str, optional): The user's first name.
last_name (str, optional): The user's last name.
role (str, required): The user's role (Admin, Developer, ... established at registration).
group (str, optional): The user's group (laboratory name, company name, ...).
email (str, required): The user's email address.
age (int, optional): The user's age.
weight (float, optional): The user's weight.
gender (str, optional): The user's gender.

Returns:
str: The user ID if the user is successfully created/retrieved, None otherwise.

Example:

user_id = set_user(first_name="John", last_name="Doe", email="john.doe@example.com", age=30, weight=70.5, gender="X")
if user_id:
    print(f"User created/retrieved with ID: {user_id}")
else:
    print("User creation failed.")

**NOTE**: In order to use this function, your role should be `Admin`
def search_user_by_mail(user_id, email):
315def search_user_by_mail(user_id, email):
316    """
317    Search for a user by email.
318
319    This function sends a GET request to the server to search for a user by the provided email address.
320
321    Args:     
322        **user_id** (*str*): The user id (obtained through free registration with HABS)
323        **email** (*str*): The email address of the user to search for.
324
325    Returns:     
326        *str*: The user ID if the user is found, None otherwise.
327
328    Example:
329    ```
330    user_id = search_user_by_mail("john.doe@example.com")
331    if user_id:
332        print(f"User found with ID: {user_id}")
333    else:
334        print("User not found.")
335    ```
336    """
337    url = f"{BASE_URL}/api/{VERSION}/users?email={email}"
338
339    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
340
341    if response.status_code == 200:
342        user_id = response.json().get('user_id')
343        print("User found:", user_id)
344        return user_id
345    else:
346        print("User not found.", response.text)
347        return None

Search for a user by email.

This function sends a GET request to the server to search for a user by the provided email address.

Args:
user_id (str): The user id (obtained through free registration with HABS) email (str): The email address of the user to search for.

Returns:
str: The user ID if the user is found, None otherwise.

Example:

user_id = search_user_by_mail("john.doe@example.com")
if user_id:
    print(f"User found with ID: {user_id}")
else:
    print("User not found.")
def get_user_by_id(user_id):
351def get_user_by_id(user_id):
352    """
353    Retrieve user data by user ID.
354
355    This function sends a GET request to the server to retrieve user data for the specified user ID.     
356    The response data is decrypted using AES before returning the user data.
357
358    Args:     
359        **user_id** (*str*): The unique identifier of the user to retrieve.
360
361    Returns:     
362        *dict*: The user data if the user is found, None otherwise.
363
364    Example:
365    ```
366    user_data = get_user_by_id("1234567890")
367    if user_data:
368        print(f"User data: {user_data}")
369    else:
370        print("User not found.")
371    ```
372    """
373    url = f"{BASE_URL}/api/{VERSION}/users/{user_id}"
374
375    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
376
377    if response.status_code == 200:
378        print("User found.")
379        encrypted_data = response.content 
380        aes_key_b64 = os.environ.get('AES_KEY')
381        aes_key_bytes = base64.b64decode(aes_key_b64)
382        decrypted_json_string = decrypt_message(encrypted_data, aes_key_bytes)
383        user_data = json.loads(decrypted_json_string)['user_data']
384        return user_data
385    else:
386        print("User not found:", response.text)
387        return None

Retrieve user data by user ID.

This function sends a GET request to the server to retrieve user data for the specified user ID.
The response data is decrypted using AES before returning the user data.

Args:
user_id (str): The unique identifier of the user to retrieve.

Returns:
dict: The user data if the user is found, None otherwise.

Example:

user_data = get_user_by_id("1234567890")
if user_data:
    print(f"User data: {user_data}")
else:
    print("User not found.")
def set_session(metadata, user_id):
391def set_session(metadata, user_id):
392    """
393    Create a new simple session.
394
395    This function sends a POST request to the server to create a new simple session using the provided metadata.
396    The metadata is encrypted using AES before being sent to the server.
397
398    Args:     
399        **metadata** (*dict*): A dictionary containing the session metadata. The only required metadata for the simple session are the user_id and a date.
400        **user_id** (*str*): The user id (obtained through free registration with HABS)
401
402    Returns:      
403        *str*: The unique identifier of the created session if successful, None otherwise.
404
405    Example:
406    ```
407    session_metadata = {
408        "user_id": "1076203852085",
409        "session_date": "2024-05-30T12:00:00Z"
410    }
411    session_id = set_session(session_metadata)
412    if session_id:
413        print(f"Session created with ID: {session_id}")
414    else:
415        print("Failed to create session.")
416    ```
417    """
418    url = f"{BASE_URL}/api/{VERSION}/sessions"
419    _session = metadata
420    _session = json.dumps(_session).encode('utf-8')
421    aes_key_b64 = os.environ.get('AES_KEY')
422    aes_key_bytes = base64.b64decode(aes_key_b64)
423    response = requests.post(
424        url,
425        data=encrypt_message(_session, aes_key_bytes),
426        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
427    )
428
429    if response.status_code == 200:
430        print("Session successfully created.")
431        # Extract the unique identifier for the uploaded data
432        session_id = response.json().get('session_id')
433
434        # print("session_id: ",session_id)
435        return session_id
436    else:
437        print("Session failed:", response.text)
438        return None

Create a new simple session.

This function sends a POST request to the server to create a new simple session using the provided metadata. The metadata is encrypted using AES before being sent to the server.

Args:
metadata (dict): A dictionary containing the session metadata. The only required metadata for the simple session are the user_id and a date. user_id (str): The user id (obtained through free registration with HABS)

Returns:
str: The unique identifier of the created session if successful, None otherwise.

Example:

session_metadata = {
    "user_id": "1076203852085",
    "session_date": "2024-05-30T12:00:00Z"
}
session_id = set_session(session_metadata)
if session_id:
    print(f"Session created with ID: {session_id}")
else:
    print("Failed to create session.")
def get_data_by_id(data_id, user_id):
442def get_data_by_id(data_id, user_id):
443    """
444    Retrieve raw data by its unique identifier from the server.
445
446    This function sends a GET request to fetch raw data associated with a specific identifier. It
447    assumes that the data, if retrieved successfully, does not require decryption and is directly accessible.
448
449    Args:      
450        **data_id** (*str*): The unique identifier for the data to be retrieved.
451        **user_id** (*str*): The user id (obtained through free registration with HABS)
452
453    Returns:       
454        **dict**: The raw data if retrieval is successful, None otherwise.
455
456    Example:
457    ```
458    data_id = "1234"
459    raw_data = get_data_by_id(data_id)
460    ... use the data
461    ```
462    """
463    url = f"{BASE_URL}/api/{VERSION}/rawdata/{data_id}"
464    
465    # response = requests.get(url)
466    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
467
468    if response.status_code == 200:
469        print("Retrieved data successfully.")
470        # decrypt
471        return response.json().get('rawData')
472    else:
473        print("Failed to retrieve data:", response.text)

Retrieve raw data by its unique identifier from the server.

This function sends a GET request to fetch raw data associated with a specific identifier. It assumes that the data, if retrieved successfully, does not require decryption and is directly accessible.

Args:
data_id (str): The unique identifier for the data to be retrieved. user_id (str): The user id (obtained through free registration with HABS)

Returns:
dict: The raw data if retrieval is successful, None otherwise.

Example:

data_id = "1234"
raw_data = get_data_by_id(data_id)
... use the data
def find_sessions_by_user(user_id):
478def find_sessions_by_user(user_id):
479    """
480    Retrieve session IDs associated with a given user.
481
482    This function sends a GET request to the API to retrieve all session IDs for a specified user.
483    It expects the user ID to be passed as an argument and uses the user ID for authentication.
484
485    Args:
486        user_id (str): The user ID (obtained through free registration with HABS).
487
488    Returns:
489        list: A list of session IDs if the request is successful.
490
491    Raises:
492        Exception: If the request fails or if there is an error in the response.
493
494    Example:
495        >>> sessions = find_sessions_by_user("12345")
496        >>> print(sessions)
497        ["session1", "session2", "session3"]
498
499    Notes:
500        Ensure that the environment variable `AES_KEY` is set to the base64 encoded AES key.
501    """
502    url = f"{BASE_URL}/api/{VERSION}/sessions/{user_id}"
503
504    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
505
506    if response.status_code == 200:
507        print("User found.")
508        encrypted_data = response.content 
509        aes_key_b64 = os.environ.get('AES_KEY')
510        aes_key_bytes = base64.b64decode(aes_key_b64)
511        decrypted_json_string = decrypt_message(encrypted_data, aes_key_bytes)
512        session_ids = json.loads(decrypted_json_string)['session_ids']
513        return session_ids
514    else:
515        print("Failed to retrieve data:", response.text)

Retrieve session IDs associated with a given user.

This function sends a GET request to the API to retrieve all session IDs for a specified user. It expects the user ID to be passed as an argument and uses the user ID for authentication.

Args: user_id (str): The user ID (obtained through free registration with HABS).

Returns: list: A list of session IDs if the request is successful.

Raises: Exception: If the request fails or if there is an error in the response.

Example:

sessions = find_sessions_by_user("12345") print(sessions) ["session1", "session2", "session3"]

Notes: Ensure that the environment variable AES_KEY is set to the base64 encoded AES key.

def get_data_by_session(session_id, user_id):
520def get_data_by_session(session_id, user_id):
521    """
522    Retrieve raw data associated with a specific session identifier from the server.
523
524    This function sends a GET request to fetch all raw data linked to the given session ID. The data
525    is returned in its raw form assuming it does not require decryption for usage.
526
527    Args:      
528        **session_id** (*str*): The session identifier whose associated data is to be retrieved.
529        **user_id** (*str*): The user id (obtained through free registration with HABS)
530
531    Returns:       
532        *dict*: The raw data linked to the session if retrieval is successful, None otherwise.
533
534    Example:
535    ```
536    session_id = "abcd1234"
537    session_data = get_data_by_session(session_id)
538    if session_data:
539        print("Data retrieved:", session_data)
540    else:
541        print("Failed to retrieve data.")
542    ```
543    """
544    url = f"{BASE_URL}/api/{VERSION}/sessions/{session_id}/rawdata"
545    
546    # response = requests.get(url)
547    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
548    
549    if response.status_code == 200:
550        print("Retrieved data successfully.")
551        # decrypt
552        return response.json().get('data')
553    else:
554        print("Failed to retrieve data:", response.text)

Retrieve raw data associated with a specific session identifier from the server.

This function sends a GET request to fetch all raw data linked to the given session ID. The data is returned in its raw form assuming it does not require decryption for usage.

Args:
session_id (str): The session identifier whose associated data is to be retrieved. user_id (str): The user id (obtained through free registration with HABS)

Returns:
dict: The raw data linked to the session if retrieval is successful, None otherwise.

Example:

session_id = "abcd1234"
session_data = get_data_by_session(session_id)
if session_data:
    print("Data retrieved:", session_data)
else:
    print("Failed to retrieve data.")
def get_data_ids_by_session(session_id, user_id):
559def get_data_ids_by_session(session_id, user_id):
560    """
561    Retrieve a list of data IDs associated with a specific session from the server.
562
563    This function sends a GET request to fetch the IDs of all data entries linked to a specified session ID.
564    The IDs are returned as a list. The function assumes the data does not require decryption for usage.
565
566    Args:      
567        **session_id** (*str*): The session identifier for which data IDs are to be retrieved.
568        **user_id** (*str*): The user id (obtained through free registration with HABS)
569
570    Returns:       
571        *list*: A list of data IDs if retrieval is successful, None otherwise.
572
573    Example:
574    ```
575    session_id = "abcd1234"
576    data_ids = get_data_ids_by_session(session_id)
577    if data_ids:
578        print("Data IDs retrieved:", data_ids)
579    else:
580        print("Failed to retrieve data IDs.")
581    ```
582    """
583    url = f"{BASE_URL}/api/{VERSION}/sessions/{session_id}/ids"
584    
585    # response = requests.get(url)
586    response = requests.get(url, headers={'X-User-ID':user_id}) # mongo _id for the user document. Communicated at user creation.
587    
588    if response.status_code == 200:
589        print("Retrieved ids successfully.")
590        # decrypt
591        return response.json().get('ids')
592    else:
593        print("Failed to retrieve ids:", response.text)

Retrieve a list of data IDs associated with a specific session from the server.

This function sends a GET request to fetch the IDs of all data entries linked to a specified session ID. The IDs are returned as a list. The function assumes the data does not require decryption for usage.

Args:
session_id (str): The session identifier for which data IDs are to be retrieved. user_id (str): The user id (obtained through free registration with HABS)

Returns:
list: A list of data IDs if retrieval is successful, None otherwise.

Example:

session_id = "abcd1234"
data_ids = get_data_ids_by_session(session_id)
if data_ids:
    print("Data IDs retrieved:", data_ids)
else:
    print("Failed to retrieve data IDs.")
def upload_data(metadata, timestamps, user_id, data, ppg_red, ppg_ir):
598def upload_data(metadata, timestamps, user_id, data, ppg_red, ppg_ir):
599    """
600    Uploads EEG (and PPG) data to the server along with associated metadata.
601
602    This function compiles different types of physiological data along with metadata into a single dictionary,
603    encrypts the data, and then uploads it via a POST request. Upon successful upload, the server returns a
604    unique identifier for the data which can then be used for future queries or operations.
605
606    Args:     
607        **metadata** (*dict*): Information about the data such as subject details and session parameters.     
608        **timestamps** (*list*): List of timestamps correlating with each data point.     
609        **user_id** (*str*): The user id (obtained through free registration with HABS)
610        **data** (*list*): EEG data points.     
611        **ppg_red** (*list*): Red photoplethysmogram data points.     
612        **ppg_ir** (*list*): Infrared photoplethysmogram data points.      
613
614    Returns:     
615        *tuple*: A tuple containing the data ID of the uploaded data if successful, and None otherwise.
616
617    Notes:
618        Ensure that timestamps has the same length of data last dimension.
619
620    Example:
621    ```
622    metadata = {"session_id": "1234", "subject_id": "001"}
623    timestamps = [1597709184, 1597709185]
624    data = [0.1, 0.2]
625    ppg_red = [123, 124]
626    ppg_ir = [125, 126]
627    data_id, error = upload_data(metadata, timestamps, data, ppg_red, ppg_ir)
628    if data_id:
629        print("Data uploaded successfully. Data ID:", data_id)
630    else:
631        print("Upload failed with error:", error)
632    ```
633    """
634    url = f"{BASE_URL}/api/{VERSION}/rawdata"
635    _data = {
636        "metadata": metadata,
637        "timestamps": timestamps,
638        "data": data,
639        "ppg_red": ppg_red,
640        "ppg_ir": ppg_ir
641    }
642    _data = json.dumps(_data).encode('utf-8')
643
644    # response = requests.post(url, json=_data)
645    aes_key_b64 = os.environ.get('AES_KEY')
646    aes_key_bytes = base64.b64decode(aes_key_b64)
647    response = requests.post(
648        url,
649        data=encrypt_message(_data, aes_key_bytes),
650        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
651    )
652    # response = requests.get(url, headers={'X-User-ID':USERID}) # mongo _id for the user document. Communicated at user creation.
653
654    if response.status_code == 200:
655        print('.', end='', flush=True)
656        # Extract the unique identifier for the uploaded data
657        data_id = response.json().get('data_id')
658        return data_id, None
659    else:
660        print("Upload failed:", response.text)
661        return None

Uploads EEG (and PPG) data to the server along with associated metadata.

This function compiles different types of physiological data along with metadata into a single dictionary, encrypts the data, and then uploads it via a POST request. Upon successful upload, the server returns a unique identifier for the data which can then be used for future queries or operations.

Args:
metadata (dict): Information about the data such as subject details and session parameters.
timestamps (list): List of timestamps correlating with each data point.
user_id (str): The user id (obtained through free registration with HABS) data (list): EEG data points.
ppg_red (list): Red photoplethysmogram data points.
ppg_ir (list): Infrared photoplethysmogram data points.

Returns:
tuple: A tuple containing the data ID of the uploaded data if successful, and None otherwise.

Notes: Ensure that timestamps has the same length of data last dimension.

Example:

metadata = {"session_id": "1234", "subject_id": "001"}
timestamps = [1597709184, 1597709185]
data = [0.1, 0.2]
ppg_red = [123, 124]
ppg_ir = [125, 126]
data_id, error = upload_data(metadata, timestamps, data, ppg_red, ppg_ir)
if data_id:
    print("Data uploaded successfully. Data ID:", data_id)
else:
    print("Upload failed with error:", error)
def acquire_send_raw( user_id, date, board, serial_number, stream_duration, buffer_duration, session_type='', tags=[], callback=None, extra=None):
666def acquire_send_raw(user_id, date, board, serial_number, stream_duration, buffer_duration, session_type="", tags=[], callback=None, extra=None):
667    """
668    Asynchronously acquires raw data from a specific EEG board and sends it to the server.
669
670    This function connects to an EEG board, initiates a data acquisition session, and sends the collected data
671    to the server in real-time or near real-time. It ensures that all the data handled during the session
672    is associated with a unique session ID and metadata that includes user and session details. The function
673    will validate the session metadata before proceeding with data acquisition and sending.
674
675    Args:      
676    **user_id** (*str*): The unique identifier of the user for whom the data is being collected.      
677    **date** (*str*): The date of the session, used for metadata purposes.     
678    **board** (*int*): Identifier for the EEG board from which data will be acquired.      
679    **stream_duration** (*int*): Duration in seconds for which data will be streamed from the board.     
680    **buffer_duration** (*int*): Time in seconds for how often the data is buffered and sent.     
681
682    Returns:     
683    *str* or *bool*: The session ID if the operation is successful; False otherwise.
684
685    Raises:
686    **ConnectionError**: If the board connection fails.      
687    **ValidationError**: If the metadata does not comply with the required schema.
688
689    Example:
690    ```
691    session = acquire_send_raw('user123', '2021-06-01', 'MUSE_S', 300, 10)
692    if session:
693        print(f"Session successfully started with ID: {session}")
694    else:
695        print("Failed to start session")
696    ```
697    """
698    # set session for the data
699    # We set a session id for the current interaction with the API (even if we fail to get the board, it will be important to store the failure)
700    session_metadata = {
701      "user_id": user_id, # add user to the session for reference
702      "session_date": date,
703      "session_type": session_type,
704      "session_tags": tags
705    }
706    session_id = set_session(metadata={**session_metadata}, user_id=user_id)
707    print("\nSession initialized. You can visualize it here:\n ", "https://habs.ai/live.html?session_id="+str(session_id), "\n")
708
709    if validate_metadata(session_metadata, "sessionSchema"):
710        asyncio.run( 
711            _acquire_send_raw(user_id, session_id, board, serial_number, stream_duration, buffer_duration, callback, extra) 
712        )
713        return session_id
714    else:
715        print("Session initialization failed.")
716        return False

Asynchronously acquires raw data from a specific EEG board and sends it to the server.

This function connects to an EEG board, initiates a data acquisition session, and sends the collected data to the server in real-time or near real-time. It ensures that all the data handled during the session is associated with a unique session ID and metadata that includes user and session details. The function will validate the session metadata before proceeding with data acquisition and sending.

Args:
user_id (str): The unique identifier of the user for whom the data is being collected.
date (str): The date of the session, used for metadata purposes.
board (int): Identifier for the EEG board from which data will be acquired.
stream_duration (int): Duration in seconds for which data will be streamed from the board.
buffer_duration (int): Time in seconds for how often the data is buffered and sent.

Returns:
str or bool: The session ID if the operation is successful; False otherwise.

Raises: ConnectionError: If the board connection fails.
ValidationError: If the metadata does not comply with the required schema.

Example:

session = acquire_send_raw('user123', '2021-06-01', 'MUSE_S', 300, 10)
if session:
    print(f"Session successfully started with ID: {session}")
else:
    print("Failed to start session")
def send_file( user_id, date, edf_file, ch_nrs=None, ch_names=None, session_type='', tags=[]):
738def send_file(user_id, date, edf_file, ch_nrs=None, ch_names=None, session_type="", tags=[]):
739    """
740    Uploads EEG data from a file to the server along with associated metadata.
741
742    This function compiles EEG data from an [EDF file](https://www.edfplus.info/downloads/index.html) along with metadata into a single dictionary,
743    encrypts the data, and then uploads it via a POST request. Upon successful upload, the server returns a
744    unique identifier for the session which can then be used for future queries or operations.
745
746    Args:    
747    **user_id** (*str*): The unique identifier of the user for whom the data is being collected.     
748    **metadata** (*dict*): Information about the data such as subject details and session parameters.     
749    **date** (*str*): The date of the session, used for metadata purposes.     
750    **edf_file** (*str*): name of an EDF file.      
751    **ch_nrs** (*list of int*, optional): The indices of the channels to read. The default is None.     
752    **ch_names** (*list of str*, optional): The names of channels to read. The default is None.     
753
754    Returns:     
755        *tuple*: A tuple containing the session ID of the uploaded data if successful, and None otherwise.
756
757    Example:
758    ```
759    session = send_file('user123', '2021-06-01', 'nameoffile.edf')
760    if session:
761        print(f"Session successfully started with ID: {session}")
762    else:
763        print("Failed to start session")
764    ```
765    """
766
767    try:
768        signals, signal_headers, header = highlevel.read_edf(edf_file, ch_nrs, ch_names)
769
770        max_time = signals.shape[1] / signal_headers[0]['sample_frequency']
771        timestamps = np.linspace(header['startdate'].timestamp(), max_time, signals.shape[1])
772
773        session_metadata = {
774          "user_id": user_id, # add user to the session for reference
775          "session_date": header['startdate'].strftime("%m/%d/%Y, %H:%M:%S"),
776          "session_type": session_type,
777          "session_tags": tags
778        }
779        if validate_metadata(session_metadata, "sessionSchema"):
780            session_id = set_session(metadata={**session_metadata}, user_id=user_id)
781            metadata = {'session_id':session_id, **session_metadata, **convert_datetime_in_dict(header), **convert_datetime_in_dict(signal_headers[0])}
782
783            chunks = ((signals.size * signals.itemsize)//300000)+1
784            timestamps_chunks = np.array_split(timestamps, chunks)
785            signals_chunks = np.array_split(signals, chunks, axis=1)
786            json_data = json.dumps(signals_chunks[0].tolist())
787            size_in_bytes = sys.getsizeof(json_data)
788            print("%d total bytes will be sent into %d chunks of %d bytes" % (signals.size * signals.itemsize, chunks, size_in_bytes))
789
790            for timestamps_chunk,signals_chunk in zip(timestamps_chunks, signals_chunks):
791                upload_data(metadata, timestamps_chunk.tolist(), user_id, signals_chunk.tolist(), [], [])
792
793            return session_id
794        else:
795            return False
796    except Exception as e:
797        print("A general error occurred:", e)
798        return False    

Uploads EEG data from a file to the server along with associated metadata.

This function compiles EEG data from an EDF file along with metadata into a single dictionary, encrypts the data, and then uploads it via a POST request. Upon successful upload, the server returns a unique identifier for the session which can then be used for future queries or operations.

Args:
user_id (str): The unique identifier of the user for whom the data is being collected.
metadata (dict): Information about the data such as subject details and session parameters.
date (str): The date of the session, used for metadata purposes.
edf_file (str): name of an EDF file.
ch_nrs (list of int, optional): The indices of the channels to read. The default is None.
ch_names (list of str, optional): The names of channels to read. The default is None.

Returns:
tuple: A tuple containing the session ID of the uploaded data if successful, and None otherwise.

Example:

session = send_file('user123', '2021-06-01', 'nameoffile.edf')
if session:
    print(f"Session successfully started with ID: {session}")
else:
    print("Failed to start session")
def set_pipe(metadata, pipeline, params, user_id):
804def set_pipe(metadata, pipeline, params, user_id):
805    """
806    Configures and initiates a data processing pipeline for a session on the server.
807
808    This function sends metadata and processing parameters to a specified pipeline endpoint
809    to create a data processing session. It encrypts the session data before sending to ensure
810    security. The function checks the server response to confirm the session creation.
811
812    Args:     
813    **metadata** (*dict*): A dictionary containing metadata about the session, typically including
814                     details such as user ID and session date.       
815    **pipeline** (*str*): The identifier for the processing pipeline to be used.      
816    **params** (*dict*): Parameters specific to the processing pipeline, detailing how data should
817                   be processed.      
818    **user_id** (*str*): The user id (obtained through free registration with HABS)
819
820    Returns:       
821        *str* or *None*: The session ID if the session is successfully created, or None if the operation fails.
822
823    Raises:     
824        **requests.exceptions.RequestException**: An error from the Requests library when an HTTP request fails.      
825        **KeyError**: If necessary keys are missing in the environment variables.
826
827    Example:
828    ```
829    session_metadata = {"user_id": "123", "session_date": "2024-06-03"}
830    processing_params = {"filter_type": "lowpass", "cutoff_freq": 30}
831    session_id = set_pipe(session_metadata, 'eeg_smoothing', processing_params)
832    if session_id:
833        print(f"Pipeline session created with ID: {session_id}")
834    else:
835        print("Failed to create pipeline session")
836    ```
837    """
838    url = f"{BASE_URL}/api/{VERSION}/sessions/pipe/{pipeline}"
839    _session = {
840        "metadata": metadata,
841        "processing_params": params,
842    }
843    _session = json.dumps(_session).encode('utf-8')
844    aes_key_b64 = os.environ.get('AES_KEY')
845    aes_key_bytes = base64.b64decode(aes_key_b64)
846    response = requests.post(
847        url,
848        data=encrypt_message(_session, aes_key_bytes),
849        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
850    )
851    if response.status_code == 200:
852        print("Session successfully created.")
853        # Extract the unique identifier for the uploaded data
854        session_id = response.json().get('session_id')
855        # print(session_id)
856        return session_id
857    else:
858        print("Session failed:", response.text)
859        return None

Configures and initiates a data processing pipeline for a session on the server.

This function sends metadata and processing parameters to a specified pipeline endpoint to create a data processing session. It encrypts the session data before sending to ensure security. The function checks the server response to confirm the session creation.

Args:
metadata (dict): A dictionary containing metadata about the session, typically including details such as user ID and session date.
pipeline (str): The identifier for the processing pipeline to be used.
params (dict): Parameters specific to the processing pipeline, detailing how data should be processed.
user_id (str): The user id (obtained through free registration with HABS)

Returns:
str or None: The session ID if the session is successfully created, or None if the operation fails.

Raises:
requests.exceptions.RequestException: An error from the Requests library when an HTTP request fails.
KeyError: If necessary keys are missing in the environment variables.

Example:

session_metadata = {"user_id": "123", "session_date": "2024-06-03"}
processing_params = {"filter_type": "lowpass", "cutoff_freq": 30}
session_id = set_pipe(session_metadata, 'eeg_smoothing', processing_params)
if session_id:
    print(f"Pipeline session created with ID: {session_id}")
else:
    print("Failed to create pipeline session")
def upload_pipedata(metadata, timestamps, user_id, data, ppg_red, ppg_ir):
864def upload_pipedata(metadata, timestamps, user_id, data, ppg_red, ppg_ir):
865    """
866    Uploads processed data to a specific session on the server.
867
868    This function is responsible for uploading various data streams associated with a session, including
869    timestamps and physiological measurements such as PPG (Photoplethysmogram). The data is encrypted before
870    sending to ensure confidentiality and integrity.
871
872    Args:        
873    **metadata** (*dict*): Contains session-related metadata including the session ID.      
874    **timestamps** (*list*): A list of timestamps corresponding to each data point.      
875    **user_id** (*str*): The user id (obtained through free registration with HABS)
876    **data** (*list*): The main data collected, e.g., EEG readings.      
877    **ppg_red** (*list*): Red channel data from a PPG sensor.    
878    **ppg_ir** (*list*): Infrared channel data from a PPG sensor.     
879
880    Returns:     
881    *tuple*: A tuple containing the data ID if the upload is successful and the processed data, or None if the upload fails.
882
883    Raises:     
884    **requests.exceptions.RequestException: An error from the Requests library when an HTTP request fails.
885    **KeyError**: If necessary keys are missing in the environment variables.
886
887    Example:
888    ```
889    session_metadata = {"session_id": "12345"}
890    timestamps = [1597709165, 1597709166, ...]
891    data = [0.1, 0.2, ...]
892    ppg_red = [12, 15, ...]
893    ppg_ir = [20, 22, ...]
894    data_id, processed_data = upload_pipedata(session_metadata, timestamps, data, ppg_red, ppg_ir)
895    if data_id:
896        print(f"Data uploaded successfully with ID: {data_id}")
897    else:
898        print("Failed to upload data")
899    ```
900    """
901    url = f"{BASE_URL}/api/{VERSION}/pipedata/{metadata['session_id']}" # the metadata contain session_id to consistently pass it with each upload
902
903    _data = {
904        "metadata": metadata,
905        "timestamps": timestamps,
906        "data": data,
907        "ppg_red": ppg_red,
908        "ppg_ir": ppg_ir
909    }
910    _data = json.dumps(_data).encode('utf-8')
911    aes_key_b64 = os.environ.get('AES_KEY')
912    aes_key_bytes = base64.b64decode(aes_key_b64)
913    response = requests.post(
914        url,
915        data=encrypt_message(_data, aes_key_bytes),
916        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
917    )
918
919    if response.status_code == 200:
920        print('.', end='', flush=True)
921        # Extract the unique identifier for the uploaded data
922        data_id = response.json().get('data_id')
923        # Retrieve the processed data
924        data = response.json().get('pipeData')
925        return data_id, data
926    else:
927        print("Upload failed:", response.text)
928        return None

Uploads processed data to a specific session on the server.

This function is responsible for uploading various data streams associated with a session, including timestamps and physiological measurements such as PPG (Photoplethysmogram). The data is encrypted before sending to ensure confidentiality and integrity.

Args:
metadata (dict): Contains session-related metadata including the session ID.
timestamps (list): A list of timestamps corresponding to each data point.
user_id (str): The user id (obtained through free registration with HABS) data (list): The main data collected, e.g., EEG readings.
ppg_red (list): Red channel data from a PPG sensor.
ppg_ir (list): Infrared channel data from a PPG sensor.

Returns:
tuple: A tuple containing the data ID if the upload is successful and the processed data, or None if the upload fails.

Raises:
requests.exceptions.RequestException: An error from the Requests library when an HTTP request fails. **KeyError: If necessary keys are missing in the environment variables.

Example:

session_metadata = {"session_id": "12345"}
timestamps = [1597709165, 1597709166, ...]
data = [0.1, 0.2, ...]
ppg_red = [12, 15, ...]
ppg_ir = [20, 22, ...]
data_id, processed_data = upload_pipedata(session_metadata, timestamps, data, ppg_red, ppg_ir)
if data_id:
    print(f"Data uploaded successfully with ID: {data_id}")
else:
    print("Failed to upload data")
def acquire_send_pipe( pipeline, params, user_id, date, board, serial_number, stream_duration, buffer_duration, session_type='', tags=[], callback=None, extra=None):
932def acquire_send_pipe(pipeline, params, user_id, date, board, serial_number, stream_duration, buffer_duration, session_type="", tags=[], callback=None, extra=None):
933    """
934    Acquires data from a board, processes it according to a specified pipeline, and sends it to a server.
935    This function handles setting up a session for data acquisition and processing, connects to a board, 
936    and manages the data flow from acquisition through processing to uploading. It uses an asynchronous loop
937    to handle the operations efficiently, suitable for real-time data processing scenarios.
938
939    Args:
940    **pipeline** (*str*): Name of the processing pipeline to use.     
941    **params** (*dict*): Parameters for the pipeline processing.      
942    **user_id** (*str*): The user ID to which the session will be associated.      
943    **date** (*str*): Date of the session for tracking purposes.      
944    **board** (*int*): Identifier for the hardware board to use for data acquisition.      
945    **stream_duration** (*int*): Duration in seconds to stream data from the board.     
946    **buffer_duration** (*int*): Duration in seconds to buffer data before processing.      
947    **callback** (*function*): Optional callback function to execute after data is sent.
948
949    Returns:    
950        *str* or *bool*: The session ID if successful, False otherwise.
951
952    """
953    # set session for the data
954    # We set a session id for the current interaction with the API (even if we fail to get the board, it will be important to store the failure)
955    session_metadata = {
956      "user_id": user_id, # add user to the session for reference
957      "session_date": date,
958      "session_type": session_type,
959      "session_tags": tags
960    }
961    if validate_metadata(session_metadata, "sessionSchema"):
962        session_id = set_pipe(metadata={**session_metadata}, pipeline=pipeline, params=params, user_id=user_id)
963        print("\nSession initialized. You can visualize it here:\n ", "https://habs.ai/bos/live.html?session_id="+str(session_id), "\n")
964
965        asyncio.run( 
966            _acquire_send_pipe(pipeline, params, user_id, session_id, board, serial_number, stream_duration, buffer_duration, callback, extra) 
967        )
968        return session_id #, self.processed_data
969    else:
970        print("Session initialization failed.")
971        return False

Acquires data from a board, processes it according to a specified pipeline, and sends it to a server. This function handles setting up a session for data acquisition and processing, connects to a board, and manages the data flow from acquisition through processing to uploading. It uses an asynchronous loop to handle the operations efficiently, suitable for real-time data processing scenarios.

Args: pipeline (str): Name of the processing pipeline to use.
params (dict): Parameters for the pipeline processing.
user_id (str): The user ID to which the session will be associated.
date (str): Date of the session for tracking purposes.
board (int): Identifier for the hardware board to use for data acquisition.
stream_duration (int): Duration in seconds to stream data from the board.
buffer_duration (int): Duration in seconds to buffer data before processing.
callback (function): Optional callback function to execute after data is sent.

Returns:
str or bool: The session ID if successful, False otherwise.

def get_user_database(user_id):
 994def get_user_database(user_id):
 995    """
 996    Retrieve all user data by user ID.
 997
 998    This function sends a GET request to the server to dump all data stored for the specified user ID.     
 999    The response data is decrypted using AES before returning the user data.
1000
1001    Args:     
1002        **user_id** (*str*): The unique identifier of the user to retrieve.
1003
1004    Returns:     
1005        *None*: A zip file contaning all data as JSON files, None otherwise.
1006
1007    Example:
1008    ```
1009    user_data = get_user_database("1234567890")
1010    if user_data:
1011        print(f"User data: {user_data}")
1012    else:
1013        print("User not found.")
1014    ```
1015    """
1016    url = f"{BASE_URL}/api/{VERSION}/database/dump/{user_id}"
1017
1018    response = requests.get(url, headers={'X-User-ID': user_id}, stream=True)
1019
1020    if response.status_code == 200:
1021        # Open a local file with write-binary mode
1022        strtime = datetime.today().strftime("%Y%m%d_%H%M%S")
1023
1024        with open(f"brainos_{strtime}_dump.zip", 'wb') as file:
1025            # Write the response content to the file in chunks
1026            for chunk in response.iter_content(chunk_size=8192):
1027                file.write(chunk)
1028        print("Database dump saved successfully.")
1029        return True
1030    else:
1031        print("User not found:", response.text)
1032        return None

Retrieve all user data by user ID.

This function sends a GET request to the server to dump all data stored for the specified user ID.
The response data is decrypted using AES before returning the user data.

Args:
user_id (str): The unique identifier of the user to retrieve.

Returns:
None: A zip file contaning all data as JSON files, None otherwise.

Example:

user_data = get_user_database("1234567890")
if user_data:
    print(f"User data: {user_data}")
else:
    print("User not found.")
def create_tagged_interval( user_id, session_id, eeg_data_id, start_time, end_time, tags, channel_ids=None):
1038def create_tagged_interval(user_id, session_id, eeg_data_id, start_time, end_time, tags, channel_ids=None):
1039    """
1040    Creates a tagged interval by sending the interval data to the server.
1041
1042    This function performs the following steps:
1043    1. Constructs the interval data dictionary.
1044    2. Validates the interval data against the "tagSchema".
1045    3. Sends the interval data to the server.
1046
1047    Args:
1048    **session_id** (*str*): The session id.
1049    **eeg_data_id** (*str*): The EEG data id.
1050    **start_time** (*str*): The start time of the interval in ISO 8601 format.
1051    **end_time** (*str*): The end time of the interval in ISO 8601 format.
1052    **tags** (*list*): List of tags, each tag is a dictionary containing a "tag" and "properties".
1053    **channel_ids** (*list*, optional): List of channel ids the tag applies to. If None, applies to all channels.
1054
1055    Returns:
1056        *str*: The interval ID if the interval is successfully created, None otherwise.
1057
1058    Example:
1059    ```
1060    interval_id = create_tagged_interval(
1061        session_id="session_123",
1062        eeg_data_id="eeg_data_456",
1063        start_time="2023-01-01T00:00:00Z",
1064        end_time="2023-01-01T00:05:00Z",
1065        tags=[{"tag": "seizure", "properties": {"severity": "high"}}]
1066    )
1067    if interval_id:
1068        print(f"Tagged interval created with ID: {interval_id}")
1069    else:
1070        print("Tagged interval creation failed.")
1071    ```
1072    """
1073    url = f"{BASE_URL}/api/{VERSION}/session/{session_id}/tag"
1074    interval_data = {
1075        "user_id": user_id,
1076        "session_id": session_id,
1077        "eeg_data_id": eeg_data_id,
1078        "start_time": start_time,
1079        "end_time": end_time,
1080        "tags": tags,
1081        "channel_ids": channel_ids if channel_ids else []
1082    }
1083    
1084    if validate_metadata(interval_data, "tagSchema"):
1085        response = requests.post(
1086            url,
1087            json=interval_data,
1088            headers={'Content-Type': 'application/json'}
1089        )
1090
1091        if response.status_code == 201:
1092            print("Tagged interval successfully created.")
1093            interval_id = response.json().get('interval_id')
1094            return interval_id
1095        else:
1096            print("Tagged interval creation failed:", response.text)
1097            return None
1098    else:
1099        print("Tagged interval creation failed due to validation error.")

Creates a tagged interval by sending the interval data to the server.

This function performs the following steps:

  1. Constructs the interval data dictionary.
  2. Validates the interval data against the "tagSchema".
  3. Sends the interval data to the server.

Args: session_id (str): The session id. eeg_data_id (str): The EEG data id. start_time (str): The start time of the interval in ISO 8601 format. end_time (str): The end time of the interval in ISO 8601 format. tags (list): List of tags, each tag is a dictionary containing a "tag" and "properties". channel_ids (list, optional): List of channel ids the tag applies to. If None, applies to all channels.

Returns: str: The interval ID if the interval is successfully created, None otherwise.

Example:

interval_id = create_tagged_interval(
    session_id="session_123",
    eeg_data_id="eeg_data_456",
    start_time="2023-01-01T00:00:00Z",
    end_time="2023-01-01T00:05:00Z",
    tags=[{"tag": "seizure", "properties": {"severity": "high"}}]
)
if interval_id:
    print(f"Tagged interval created with ID: {interval_id}")
else:
    print("Tagged interval creation failed.")
def process_session_pipe( pipeline, params, user_id, date, existing_session_id, session_type='', tags=[]):
1106def process_session_pipe(pipeline, params, user_id, date, existing_session_id, session_type="", tags=[]):
1107    """
1108    Process a session pipeline with specified parameters and metadata.
1109
1110    This function processes an existing session by applying a specified pipeline and parameters.
1111    It sends a POST request to the API with the session metadata and processing parameters,
1112    creating a new session based on the existing one.
1113
1114    Args:
1115        **pipeline** (*str*): The pipeline to be applied to the session.
1116        **params** (*dict*): The processing parameters for the pipeline.
1117        **user_id** (*str*): The user ID (obtained through free registration with HABS).
1118        **date** (*str*): The date of the session.
1119        **existing_session_id** (*str*): The ID of the existing session to be processed.
1120        **session_type** (*str*, optional): The type of the new session. Defaults to an empty string.
1121        **tags** (*list*, optional): A list of tags associated with the session. Defaults to an empty list.
1122
1123    Returns:
1124        tuple: A tuple containing the new session ID and the processed data if the request is successful.
1125        None: If the session creation fails.
1126        bool: False if the session metadata is invalid.
1127
1128    Example:
1129        >>> new_session_id, processed_data = process_session_pipe("my_pipeline", {"param1": "value1"}, "12345", "2023-07-03", "existing_session_001")
1130        >>> print(new_session_id, processed_data)
1131
1132    Notes:
1133        Ensure that the environment variable `AES_KEY` is set to the base64 encoded AES key.
1134
1135    Raises:
1136        Exception: If there is an error in the request or response.
1137
1138    """
1139    session_metadata = {
1140      "user_id": user_id, # add user to the session for reference
1141      "session_date": date, # .strftime("%m/%d/%Y, %H:%M:%S"),
1142      "existing_session_id": existing_session_id,
1143      "session_type": f"[On {existing_session_id}]: {session_type}", # type of the new session
1144      "session_tags": tags
1145    }
1146    if validate_metadata(session_metadata, "sessionSchema"):
1147        url = f"{BASE_URL}/api/{VERSION}/sessions/{existing_session_id}/pipe/{pipeline}"
1148        _session = {
1149            "metadata": session_metadata,
1150            "processing_params": params,
1151        }
1152        _session = json.dumps(_session).encode('utf-8')
1153        aes_key_b64 = os.environ.get('AES_KEY')
1154        aes_key_bytes = base64.b64decode(aes_key_b64)
1155        response = requests.post(
1156            url,
1157            data=encrypt_message(_session, aes_key_bytes),
1158            headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
1159        )
1160        if response.status_code == 200:
1161            print("Session successfully created.")
1162            session_id = response.json().get('session_id')
1163            pipeData = response.json().get('pipeData')
1164            # print(session_id)
1165            return session_id, pipeData
1166        else:
1167            print("Session failed:", response.text)
1168            return None
1169
1170        return session_id # processed_data
1171    else:
1172        print("Session failed.")
1173        return False

Process a session pipeline with specified parameters and metadata.

This function processes an existing session by applying a specified pipeline and parameters. It sends a POST request to the API with the session metadata and processing parameters, creating a new session based on the existing one.

Args: pipeline (str): The pipeline to be applied to the session. params (dict): The processing parameters for the pipeline. user_id (str): The user ID (obtained through free registration with HABS). date (str): The date of the session. existing_session_id (str): The ID of the existing session to be processed. session_type (str, optional): The type of the new session. Defaults to an empty string. tags (list, optional): A list of tags associated with the session. Defaults to an empty list.

Returns: tuple: A tuple containing the new session ID and the processed data if the request is successful. None: If the session creation fails. bool: False if the session metadata is invalid.

Example:

new_session_id, processed_data = process_session_pipe("my_pipeline", {"param1": "value1"}, "12345", "2023-07-03", "existing_session_001") print(new_session_id, processed_data)

Notes: Ensure that the environment variable AES_KEY is set to the base64 encoded AES key.

Raises: Exception: If there is an error in the request or response.

def train(session_id, params, user_id):
1179def train(session_id, params, user_id):
1180    """
1181    Sends a request to the server to train a machine learning algorithm on the data from a specified session.
1182
1183    Args:     
1184    **session_id** (*str*): The unique identifier of the session containing the data to be used for training.       
1185    **params** (*dict*): The parameters for the training process.
1186    **user_id** (*str*): The user id (obtained through free registration with HABS)
1187
1188    Returns:      
1189        *str* or *None*: The task ID if the request is successful, None otherwise.
1190
1191    This function sends the training parameters and session ID to the server, which initiates the training process.
1192    The response includes a task ID that can be used for future interactions related to the training task.
1193
1194    Example:
1195    ```
1196    train("session_12345", {"param1": "value1", "param2": "value2"})
1197    ```
1198    """
1199    url = f"{BASE_URL}/api/{VERSION}/train/{session_id}"
1200    _params = {
1201        "params": params,
1202    }
1203    _params = json.dumps(_params).encode('utf-8')
1204    aes_key_b64 = os.environ.get('AES_KEY')
1205    aes_key_bytes = base64.b64decode(aes_key_b64)
1206    response = requests.post(
1207        url,
1208        data=encrypt_message(_params, aes_key_bytes),
1209        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
1210    )
1211    # response = requests.get(url, headers={'X-User-ID':USERID}) # mongo _id for the user document. Communicated at user creation.
1212
1213    if response.status_code == 200:
1214        task_id = response.json().get('task_id')
1215        print("Published. For future interactions, use task_id:",task_id)
1216        return task_id
1217    else:
1218        print("Publish failed:", response.text)
1219        return None

Sends a request to the server to train a machine learning algorithm on the data from a specified session.

Args:
session_id (str): The unique identifier of the session containing the data to be used for training.
params (dict): The parameters for the training process. user_id (str): The user id (obtained through free registration with HABS)

Returns:
str or None: The task ID if the request is successful, None otherwise.

This function sends the training parameters and session ID to the server, which initiates the training process. The response includes a task ID that can be used for future interactions related to the training task.

Example:

train("session_12345", {"param1": "value1", "param2": "value2"})
def infer(data_id, params, user_id):
1225def infer(data_id, params, user_id):
1226    """
1227    Sends a request to the server to perform machine learning inference based on a previously trained model, given the data ID.
1228
1229    Args:     
1230    **data_id** (*str*): The unique identifier of the data to be used for inference.      
1231    **params** (*dict*): The parameters for the inference process.
1232    **user_id** (*str*): The user id (obtained through free registration with HABS)
1233
1234    Returns:
1235    *str* or *None*: The task ID if the request is successful, None otherwise.
1236
1237    This function sends the inference parameters and data ID to the server, which initiates the inference process.
1238    The response includes a task ID that can be used for future interactions related to the inference task.
1239
1240    Example:
1241    ```
1242    infer("data_12345", {"param1": "value1", "param2": "value2"})
1243    ```
1244    """
1245    url = f"{BASE_URL}/api/{VERSION}/infer/{data_id}"
1246    _params = {
1247        "params": params,
1248    }
1249    _params = json.dumps(_params).encode('utf-8')
1250    # response = requests.post(url, json=_params)
1251    aes_key_b64 = os.environ.get('AES_KEY')
1252    aes_key_bytes = base64.b64decode(aes_key_b64)
1253    response = requests.post(
1254        url,
1255        data=encrypt_message(_params, aes_key_bytes),
1256        headers={'Content-Type': 'application/octet-stream', 'X-User-ID':user_id}
1257    )
1258    # response = requests.get(url, headers={}) # mongo _id for the user document. Communicated at user creation.
1259    
1260    if response.status_code == 200:
1261        task_id = response.json().get('task_id')
1262        print("Published. For future interactions, use task_id:",task_id)
1263        return task_id
1264    else:
1265        print("Publish failed:", response.text)
1266        return None

Sends a request to the server to perform machine learning inference based on a previously trained model, given the data ID.

Args:
data_id (str): The unique identifier of the data to be used for inference.
params (dict): The parameters for the inference process. user_id (str): The user id (obtained through free registration with HABS)

Returns: str or None: The task ID if the request is successful, None otherwise.

This function sends the inference parameters and data ID to the server, which initiates the inference process. The response includes a task ID that can be used for future interactions related to the inference task.

Example:

infer("data_12345", {"param1": "value1", "param2": "value2"})