AICore-Bridge

Bridge between Univia AICore framework and Wodan/Conan processor modules
import json
import os

from corebridge.timeseriesdataframe import test_data_dict_3_samples
from corebridge.core import init_console_logging
syslog = init_console_logging(__name__, logging.DEBUG, timestamp=False)

Support functions

Pop NaN values


source

pop_nan_values

 pop_nan_values (data)

*Recursively pop keys with nan values from dict or lists with dicts. Use just before handing data to AICore for further processing since it explodes when encountering NaN values.

Args: data (Union[list, dict]): The data to be processed.

Returns: Union[list, dict]: The processed data with keys with nan values removed.*

test_data_with_nan = test_data_dict_3_samples.copy() + [
   {
      "time":"2023-05-04T11:44:53.000Z",
      "value":np.nan
   }

]
print(json.dumps(test_data_with_nan, indent=3))
[
   {
      "time": "2023-05-04T10:04:49.000Z",
      "value": 16.72
   },
   {
      "time": "2023-05-04T10:24:51.000Z",
      "value": 16.65
   },
   {
      "time": "2023-05-04T10:44:53.000Z",
      "value": 16.55
   },
   {
      "time": "2023-05-04T11:44:53.000Z",
      "value": NaN
   }
]
print(json.dumps(pop_nan_values(test_data_with_nan), indent=3))
[
   {
      "time": "2023-05-04T10:04:49.000Z",
      "value": 16.72
   },
   {
      "time": "2023-05-04T10:24:51.000Z",
      "value": 16.65
   },
   {
      "time": "2023-05-04T10:44:53.000Z",
      "value": 16.55
   },
   {
      "time": "2023-05-04T11:44:53.000Z"
   }
]

Build historic args


source

build_historic_args

 build_historic_args (data:pandas.core.frame.DataFrame, history:dict|list)

Create a timeseries DataFrame from historic data defined in history.

Type Details
data DataFrame The input time-series DataFrame.
history dict | list Historic data definition, each item in the list is a dictionary with
a startDate key to set the start of a section of historic data in the
result and a column-value pair for each of the columns.
Returns dict Historic data in dictionary format where keys are column names and
values are the historic values as numpy array.
test_data=set_time_index_zone(timeseries_dataframe_from_datadict(
   [
      {
         "time":"2023-05-04T10:04:49",
         "value":16.72
      },
      {
         "time":"2023-05-04T10:44:53",
         "value":16.55
      },
      {
         "time":"2023-05-04T10:24:51",
         "value":16.65
      }
   ], ['datetimeMeasure', 'time'], 'records'), 'UTC').sort_index()
Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
test_data
value
time
2023-05-04 10:04:49+00:00 16.72
2023-05-04 10:24:51+00:00 16.65
2023-05-04 10:44:53+00:00 16.55
history_arg = [
                dict(justANumber=1.0),
                dict(startDate="2023-05-04T10:25:00+00:00", justANumber=2.0)
            ]
build_historic_args(test_data,history_arg)
justANumber
time
2023-05-04 10:04:49+00:00 1.0
2023-05-04 10:24:51+00:00 1.0
2023-05-04 10:44:53+00:00 2.0
assert len(test_data) == len(build_historic_args(test_data,history_arg)['justANumber']), "build_historic_args failed to build historic data"

Class AICoreModuleBase

In the third iteration of the AICore module the initializer signature changes from

    def __init__(
        self, 
        save_dir:str, # path where the module can keep files 
        assets_dir:str, # path to support files (scripts, metadata, etc)
        *args, **kwargs
    ):

to the almost identical signature


    def __init__(
        self, 
        files_dir, 
        save_dir
    ):

Note how the order of the folder arguments is reversed. We can adapt by

  • treating original assets_dir as save_dir and vice versa OR
  • rewrite the method to match the new signature and change the signatures of the derived classes in each module

source

AICoreModuleBase

 AICoreModuleBase (files_dir, save_dir, **kwargs)

Initialize self. See help(type(self)) for accurate signature.

Exported source
class AICoreModuleBase:

    def __init__(
        self, 
        files_dir, 
        save_dir,
        **kwargs
    ):
        
        self.init_time = datetime.datetime.now(datetime.UTC)
        self.aicorebridge_version = __version__

        self.init_args = []
        self.init_kwargs = dict(
            files_dir=files_dir,
            save_dir=save_dir,
            **kwargs
        )


        syslog.info(f"Init {self.__class__.__name__}, version {self.aicorebridge_version}, files directory {files_dir}, save dir {save_dir} on {platform.node()}")
save_dir = os.path.join(os.getcwd(), 'cache')
files_dir = os.path.join(os.getcwd(), 'cache')
test_module = AICoreModuleBase(files_dir, save_dir)

assert test_module.init_kwargs['save_dir'] == save_dir, f"init_kwargs['save_dir'] should be {save_dir}"
assert test_module.init_kwargs['files_dir'] == files_dir, f"init_kwargs['files_dir'] should be {files_dir}"
INFO    9588    root    2001759155.py   22  Init AICoreModuleBase, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
test_module.__dict__
{'init_time': datetime.datetime(2025, 10, 13, 13, 55, 9, 301785, tzinfo=datetime.timezone.utc),
 'aicorebridge_version': '0.6.1',
 'init_args': [],
 'init_kwargs': {'files_dir': '/home/fenke/repos/corebridge/nbs/cache',
  'save_dir': '/home/fenke/repos/corebridge/nbs/cache'}}

Class AICoreModule


source

AICoreModule

 AICoreModule (processor:Callable, files_dir:str, save_dir:str, **kwargs)

Initialize self. See help(type(self)) for accurate signature.

Type Details
processor Callable data processing function
files_dir str path where the module can keep files
save_dir str
kwargs VAR_KEYWORD
Exported source
class AICoreModule(AICoreModuleBase):
    def __init__(self, 
        processor:typing.Callable, # data processing function
        files_dir:str,              # path where the module can keep files 
        save_dir:str,
        **kwargs
    ):
    
        super().__init__(files_dir, save_dir, **kwargs)
        self._init_processor(processor)

source

AICoreModule.call_processor

 AICoreModule.call_processor (calldata, **callargs)
Exported source
# TODO: Refactor into Processor classes to handle different funtion types

@patch
def _init_processor(
        self:AICoreModule, 
        processor:typing.Callable):
    """Initializes processor related variables on self"""
    
    self.processor = processor
    self.processor_signature = inspect.signature(self.processor)
    self.processor_params = dict(self.processor_signature.parameters)
    self.return_param = self.processor_params.pop('return', None)
    
    self.data_param, *self.call_params = list(self.processor_params.keys())

    if not (
        self.processor_params[self.data_param].annotation == pd.DataFrame
        or self.processor_params[self.data_param].annotation == np.ndarray

    ):

        self.data_param = None
        self.call_params = list(self.processor_params.keys())
Exported source
# can be overloaded
@patch
def call_processor(self:AICoreModule, calldata, **callargs):
    if self.data_param:
        return self.processor(calldata, **callargs)
    else:
        return self.processor(**callargs)

call

The new entry point from AICore with the following signature

def call(self, data, files)

This method, originally called by earlier versions of AICore, is responsible for processing the data and parameters request recieved by AICore. Infer takes a data parameter which contains the contents of the data key in the request body. Additionally an optional list of files that were send with the request - these are currently ignored - and finally the contents of the kwargs key in the request body.


source

AICoreModule.call

 AICoreModule.call (data:dict, *_, **__)

Infer the data using the processor function.

Exported source
@patch
def call(self:AICoreModule, data:dict, *_, **__):
    """Infer the data using the processor function."""

    payload_data = data

    msg=[
        f"Startup: time {self.init_time.isoformat()}, node {platform.node()}",
        f"Corebridge version: {self.aicorebridge_version}",
    ]

    try:
        t00 = time.perf_counter_ns()

        kwargs = payload_data.get('kwargs', {})
        data = payload_data.get('data', {})

        msg+=[
            f"{self.processor.__name__}({self.processor_signature})",  
            f"Data: {type(data)} length: {len(data)}",    
            f"kwargs {list(kwargs.keys())}",       
            #f"init_args: {self.init_args}, init_kwargs: {self.init_kwargs}",
        ]

        # Pickup params, pop those that are not intended for the processor
        lastSeen = kwargs.pop('lastSeen', False)
        recordformat = kwargs.pop('format', "records").lower()
        timezone = kwargs.get('timezone', 'UTC')
        nested = kwargs.pop('nested', False)
        msg.append(f"lastSeen: {lastSeen}, timezone: {timezone}, recordformat: {recordformat}, nested: {nested}")

        samplerPeriod = kwargs.pop('samplerPeriod', self.init_kwargs.get('samplerPeriod','h'))
        samplerMethod = kwargs.pop('samplerMethod', self.init_kwargs.get('samplerMethod',None))
        reversed = kwargs.pop('reversed', False)

        calldata = self.get_call_data(
            data, 
            recordformat=recordformat,
            timezone=timezone,
            nested=nested,)
        
        history = build_historic_args(calldata, kwargs.pop('history', {}))
        callargs = self.get_callargs(kwargs, history)

        # for arg, val in callargs.items():
        #     msg.append(f"{arg}: {val}")
        
        t02 = time.perf_counter_ns()
        calculated_result = self.call_processor(
            calldata, 
            **callargs
        )
        t03 = time.perf_counter_ns()
        msg.append(f"Processing time: {(t03-t02)/1e6:.1f} ms")
        msg.append(f"Preparation time: {(t02-t00)/1e6:.1f} ms")

        if isinstance(calculated_result, dict):
            msg.append(f"return-data ictionary keys: {calculated_result.keys()}")
            return {
                'msg':msg,
                'data': [calculated_result]
            }
        elif isinstance(calculated_result, list):
            msg.append(f"return-data list length: {len(calculated_result)}")
            return {
                'msg':msg,
                'data': calculated_result
            }

        try:
            result = timeseries_dataframe(
                calculated_result, 
                timezone=timezone)
            
            msg.append(f"result shape: {result.shape}")

            if samplerMethod:
                msg.append(f"Sampler: {samplerMethod}, period: {samplerPeriod}")
                result = timeseries_dataframe_resample(result, samplerPeriod, samplerMethod)

            msg.append(f"return-data shape: {result.shape}")

            if reversed:
                result = result[::-1]

            return {
                'msg':msg,
                'data': pop_nan_values( timeseries_dataframe_to_datadict(
                    result if not lastSeen else result[-1:],
                    recordformat=recordformat,
                    timezone=timezone))
            }
        
        # tries dataframe return
        except Exception as err:
            msg.append(f"No timeseries data, error={err}")
        
        df = pd.DataFrame(calculated_result)
        df
        df.columns = [f"value_{str(c)}" if isinstance(c, int) else str(c) for c in list(df.columns)]
        df.reset_index().to_dict(orient='records')
        return {
            'msg':msg,
            'data': pop_nan_values( df.reset_index().to_dict(orient='records') )
        }

    
    # function try-catch
    except Exception as err:
        msg.append(''.join(traceback.format_exception(None, err, err.__traceback__)))
        return {
            'msg': msg,
            'data': []
        }

get_callargs

Exported source
# Specialized types for initializing annotated parameters
# Add types by adding a tuple with the type name and a builder function
annotated_arg_builders = {
    str(B[0]):B[1] for B in [
        (np.ndarray, lambda X: np.array(X, dtype=X.dtype))
    ]
}
annotated_arg_builders
{"<class 'numpy.ndarray'>": <function __main__.<lambda>(X)>}

source

AICoreModule.init_annotated_param

 AICoreModule.init_annotated_param (param_name, value)

*Initialize argument for the processor call

param_name: name of the parameter to be initialized value: value of the parameter read from infer data to be used for initialization*

Exported source
@patch
def init_annotated_param(self:AICoreModule, param_name, value):
    """
    Initialize argument for the processor call
    
    param_name: name of the parameter to be initialized
    value: value of the parameter read from infer data to be used for initialization
    
    """

    annotation = self.processor_signature.parameters[param_name].annotation
    #print(f"param_name: {param_name}, value: {value}, annotation: {annotation}")

    # try to convert value to one of the types in the builders of annotated_arg_builders
    for T in typing.get_args(annotation):
        try:
            builder = annotated_arg_builders.get(str(T), lambda X:T(X))
            return builder(value)
        
        except TypeError as err:
            continue

    try:
        return annotation(value)
    
    except TypeError as err:
        syslog.exception(f"Exception {str(err)} in fallback conversion to {annotation} of {type(value)}")

source

AICoreModule.get_callargs

 AICoreModule.get_callargs (kwargs, history)

Get arguments for the processor call

Exported source
@patch
def get_callargs(self:AICoreModule, kwargs, history):
    "Get arguments for the processor call"

    # Remove null / None values
    kwargs = {k:v for k,v in kwargs.items() if v is not None}
    
    call_args = {
        K:self.init_annotated_param(
            K,
            history.get(
                K,
                kwargs.get(
                    K,
                    self.init_kwargs.get(
                        K, 
                        history.get(
                            snake_case_to_camel_case(K),
                            kwargs.get(
                                snake_case_to_camel_case(K),
                                self.init_kwargs.get(
                                    snake_case_to_camel_case(K), 
                                    self.processor_signature.parameters[K].default
                                )
                            )
                        )
                    )
                )
            )
        )
        for K in self.call_params
    }

    return call_args
def processor_function(data:pd.DataFrame, just_a_number:float|np.ndarray):
    return just_a_number * data

test_module = AICoreModule(processor_function, os.path.join(os.getcwd(), 'cache'), os.path.join(os.getcwd(), 'cache'))
assert 'just_a_number' in test_module.get_callargs(
    {
        'justANumber': 2
    },
    {}
   
), "get_callargs failed to translate camel-case processor argument to snake-case kwargs argument"
INFO    9588    root    2001759155.py   22  Init AICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje

get_call_data


source

AICoreModule.get_call_data

 AICoreModule.get_call_data (data:dict|list, recordformat='records',
                             timezone='UTC', nested=False)

Convert data to the processor signature

Exported source
@patch
def get_call_data(
        self:AICoreModule, 
        data:dict|list, 
        recordformat='records', 
        timezone='UTC',
        nested=False):
    
    "Convert data to the processor signature"
    
    if not self.data_param:
        return None
    
    #print(f"recordformat: {recordformat}, timezone: {timezone}, nested: {nested}" )

    df = set_time_index_zone(timeseries_dataframe_from_datadict(
        data, ['datetimeMeasure', 'time'], recordformat=recordformat, nested=nested), timezone)

    df.sort_index(inplace=True)

    if self.processor_params[self.data_param].annotation == pd.DataFrame:
        return df
    elif len(df.columns) > 1:
        df.index = (df.index - datetime.datetime(1970,1,1, tzinfo=datetime.timezone.utc)) / datetime.timedelta(seconds=1)
        return df.to_records(index=True)
    else:
        df.index = (df.index - datetime.datetime(1970,1,1, tzinfo=datetime.timezone.utc)) / datetime.timedelta(seconds=1)
        return df.reset_index().to_numpy()
test_data
value
time
2023-05-04 10:04:49+00:00 16.72
2023-05-04 10:24:51+00:00 16.65
2023-05-04 10:44:53+00:00 16.55
timeseries_dataframe_to_datadict(test_data)
[{'time': '2023-05-04T10:04:49Z', 'value': 16.72},
 {'time': '2023-05-04T10:24:51Z', 'value': 16.65},
 {'time': '2023-05-04T10:44:53Z', 'value': 16.55}]
calldata = test_module.get_call_data(timeseries_dataframe_to_datadict(test_data))
calldata
recordformat: records, timezone: UTC, nested: False
Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
value
time
2023-05-04 10:04:49+00:00 16.72
2023-05-04 10:24:51+00:00 16.65
2023-05-04 10:44:53+00:00 16.55
history = build_historic_args(calldata,history_arg)
history
justANumber
time
2023-05-04 10:04:49+00:00 2.0
2023-05-04 10:24:51+00:00 2.0
2023-05-04 10:44:53+00:00 2.0
calldata
value
time
2023-05-04 10:04:49+00:00 16.72
2023-05-04 10:24:51+00:00 16.65
2023-05-04 10:44:53+00:00 16.55
print(test_module.get_callargs(calldata, history))
{'just_a_number': array([2., 2., 2.])}
np.array(history['justANumber'])
array([2., 2., 2.])
history
justANumber
time
2023-05-04 10:04:49+00:00 2.0
2023-05-04 10:24:51+00:00 2.0
2023-05-04 10:44:53+00:00 2.0
test_module.init_annotated_param(
    'just_a_number',
    12.34
)
12.34
test_module.processor_signature.parameters['just_a_number'].annotation
float | numpy.ndarray
np.array(history['justANumber'])
array([2., 2., 2.])
annotated_arg_builders[str(np.ndarray)](history['justANumber'])
array([2., 2., 2.])
assert True, 'stop'

Tests

import os, pandas as pd, numpy as np
def test_function(data:pd.DataFrame, anumber:float|np.ndarray=0):
    return data * anumber
def test_simple_function(anumber:float, another:float):
    return [another * anumber]
class TestAICoreModule(AICoreModule):
    def __init__(self, files_dir, save_dir):
        super().__init__(test_function, files_dir, save_dir)
class SimpleAICoreModule(AICoreModule):
    def __init__(self, files_dir, save_dir):
        super().__init__(test_simple_function, files_dir, save_dir)
save_dir = os.path.join(os.getcwd(), 'cache')
files_dir = os.path.join(os.getcwd(), 'cache')

test_module = TestAICoreModule(files_dir, save_dir)

assert test_module.init_kwargs['save_dir'] == save_dir, f"init_kwargs['save_dir'] should be {save_dir}"
assert test_module.init_kwargs['files_dir'] == files_dir, f"init_kwargs['files_dir'] should be {files_dir}"
INFO    9588    root    2001759155.py   22  Init TestAICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
test_data = [
    dict(datetimeMeasure='2020-04-01T00:01:11.123Z', value=1.1),
    dict(datetimeMeasure='2020-04-02T00:20:00Z', value=2.3),
]
result = test_module.call(dict(data=test_data, kwargs=dict(timezone='Europe/Amsterdam', anumber=2)))

print("Test Data\n", json.dumps(test_data, indent=2))
print("Result Message\n", json.dumps(result['msg'], indent=2, cls=NumpyEncoder))
print("Result Data\n", json.dumps(result['data'], indent=2, cls=NumpyEncoder))
recordformat: records, timezone: Europe/Amsterdam, nested: False
Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
Test Data
 [
  {
    "datetimeMeasure": "2020-04-01T00:01:11.123Z",
    "value": 1.1
  },
  {
    "datetimeMeasure": "2020-04-02T00:20:00Z",
    "value": 2.3
  }
]
Result Message
 [
  "Startup: time 2025-10-13T13:55:09.561683+00:00, node bouwdoosje",
  "Corebridge version: 0.6.1",
  "test_function((data: pandas.core.frame.DataFrame, anumber: float | numpy.ndarray = 0))",
  "Data: <class 'list'> length: 2",
  "kwargs ['timezone', 'anumber']",
  "lastSeen: False, timezone: Europe/Amsterdam, recordformat: records, nested: False",
  "Processing time: 0.2 ms",
  "Preparation time: 19.4 ms",
  "result shape: (2, 1)",
  "return-data shape: (2, 1)"
]
Result Data
 [
  {
    "time": "2020-04-01T02:01:11.123+02:00",
    "value": 2.2
  },
  {
    "time": "2020-04-02T02:20:00.000+02:00",
    "value": 4.6
  }
]
test_module.processor_signature.parameters['data'].annotation
pandas.core.frame.DataFrame
annotation = test_module.processor_signature.parameters['anumber'].annotation
print(typing.get_args(annotation))
(<class 'float'>, <class 'numpy.ndarray'>)
for T in typing.get_args(annotation):
    print(T(0))
0.0
[]

Simple module

simple_module = SimpleAICoreModule(files_dir,save_dir)

assert simple_module.init_kwargs['save_dir'] == save_dir
INFO    9588    root    2001759155.py   22  Init SimpleAICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
not simple_module.data_param
True
simple_module.call_params
['anumber', 'another']
result = simple_module.call(dict(data=[], kwargs=dict(timezone='Europe/Amsterdam', anumber=2, another=11))) #dict(data=[], kwargs=dict(timezone='Europe/Amsterdam', anumber=2, another=11)
print("Result Message\n", json.dumps(result['msg'], indent=2))
print("Result Data\n", json.dumps(result['data'], indent=2))
Result Message
 [
  "Startup: time 2025-10-13T13:55:09.638778+00:00, node bouwdoosje",
  "Corebridge version: 0.6.1",
  "test_simple_function((anumber: float, another: float))",
  "Data: <class 'list'> length: 0",
  "kwargs ['timezone', 'anumber', 'another']",
  "lastSeen: False, timezone: Europe/Amsterdam, recordformat: records, nested: False",
  "Processing time: 0.0 ms",
  "Preparation time: 0.1 ms",
  "return-data list length: 1"
]
Result Data
 [
  22.0
]

Tests with library module

import corebridge.core
from corebridge.aicorebridge import AICoreModule
DEBUG   9588    corebridge.aicorebridge aicorebridge.py 35  Loading corebridge.aicorebridge 0.6.1 from /home/fenke/repos/corebridge/corebridge/aicorebridge.py
class TestAICoreModule(AICoreModule):
    def __init__(self, files_dir, save_dir):
        super().__init__(test_function, files_dir, save_dir)
        
test_module = TestAICoreModule(files_dir, save_dir)

assert test_module.init_kwargs['save_dir'] == save_dir
assert test_module.init_kwargs['files_dir'] == files_dir
INFO    9588    corebridge.aicorebridge aicorebridge.py 134 Init TestAICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
test_data = [
    dict(datetimeMeasure='2020-04-01T00:01:11.123Z', value=1.1),
    dict(datetimeMeasure='2020-04-02T00:20:00Z', value=2.3),
]
result = test_module.call(dict(data=test_data, kwargs=dict(timezone='Europe/Amsterdam', anumber=2)))

print("Test Data\n", json.dumps(test_data, indent=2))
print("Result Message\n", json.dumps(result['msg'], indent=2, cls=NumpyEncoder))
print("Result Data\n", json.dumps(result['data'], indent=2, cls=NumpyEncoder))
recordformat: records, timezone: Europe/Amsterdam, nested: False
Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
Test Data
 [
  {
    "datetimeMeasure": "2020-04-01T00:01:11.123Z",
    "value": 1.1
  },
  {
    "datetimeMeasure": "2020-04-02T00:20:00Z",
    "value": 2.3
  }
]
Result Message
 [
  "Startup: time 2025-10-13T13:55:09.715945+00:00, node bouwdoosje",
  "Corebridge version: 0.6.1",
  "test_function((data: pandas.core.frame.DataFrame, anumber: float | numpy.ndarray = 0))",
  "Data: <class 'list'> length: 2",
  "kwargs ['timezone', 'anumber']",
  "lastSeen: False, timezone: Europe/Amsterdam, recordformat: records, nested: False",
  "Processing time: 0.1 ms",
  "Preparation time: 2.2 ms",
  "result shape: (2, 1)",
  "return-data shape: (2, 1)"
]
Result Data
 [
  {
    "time": "2020-04-01T02:01:11.123+02:00",
    "value": 2.2
  },
  {
    "time": "2020-04-02T02:20:00.000+02:00",
    "value": 4.6
  }
]
print("Test Data\n", json.dumps(test_data, indent=2))
print("Result Message\n", json.dumps(result['msg'], indent=2, cls=NumpyEncoder))
print("Result Data\n", json.dumps(result['data'], indent=2, cls=NumpyEncoder))
Test Data
 [
  {
    "datetimeMeasure": "2020-04-01T00:01:11.123Z",
    "value": 1.1
  },
  {
    "datetimeMeasure": "2020-04-02T00:20:00Z",
    "value": 2.3
  }
]
Result Message
 [
  "Startup: time 2025-10-13T13:55:09.715945+00:00, node bouwdoosje",
  "Corebridge version: 0.6.1",
  "test_function((data: pandas.core.frame.DataFrame, anumber: float | numpy.ndarray = 0))",
  "Data: <class 'list'> length: 2",
  "kwargs ['timezone', 'anumber']",
  "lastSeen: False, timezone: Europe/Amsterdam, recordformat: records, nested: False",
  "Processing time: 0.1 ms",
  "Preparation time: 2.2 ms",
  "result shape: (2, 1)",
  "return-data shape: (2, 1)"
]
Result Data
 [
  {
    "time": "2020-04-01T02:01:11.123+02:00",
    "value": 2.2
  },
  {
    "time": "2020-04-02T02:20:00.000+02:00",
    "value": 4.6
  }
]

Various experiments

import json
test_nested_data = json.loads("""
[
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:46:33.313Z",
        "datetimeSource": "2025-07-21T09:46:33.313Z",
        "datetimeAcquisition": "2025-07-21T09:46:33.691Z",
        "connector": "lora.kpn",
        "value": "0d01b4613d2ab820ec21e42a912a",
        "metadata": {
            "connection": {
                "rssi": -106,
                "snr": 0,
                "spreadingFactor": 11,
                "frequency": 868.1,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -106,
                        "snr": 0,
                        "location": {
                            "latitude": 51.556896,
                            "longitude": 5.865362
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1319,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:36:33.332Z",
        "datetimeSource": "2025-07-21T09:36:33.332Z",
        "datetimeAcquisition": "2025-07-21T09:36:33.697Z",
        "connector": "lora.kpn",
        "value": "0d0174603d2a9e20bf21dc2a802a",
        "metadata": {
            "connection": {
                "rssi": -104,
                "snr": 5,
                "spreadingFactor": 11,
                "frequency": 867.3,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -104,
                        "snr": 5,
                        "location": {
                            "latitude": 51.5569,
                            "longitude": 5.865385
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1318,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:26:33.350Z",
        "datetimeSource": "2025-07-21T09:26:33.350Z",
        "datetimeAcquisition": "2025-07-21T09:26:33.705Z",
        "connector": "lora.kpn",
        "value": "0d01b45e3d2aa120ad21d42a5d2a",
        "metadata": {
            "connection": {
                "rssi": -105,
                "snr": 9,
                "spreadingFactor": 11,
                "frequency": 868.5,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -105,
                        "snr": 9,
                        "location": {
                            "latitude": 51.556892,
                            "longitude": 5.865356
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1317,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:16:33.368Z",
        "datetimeSource": "2025-07-21T09:16:33.368Z",
        "datetimeAcquisition": "2025-07-21T09:16:33.734Z",
        "connector": "lora.kpn",
        "value": "0d01745d3d2ac320a621d12a5b2a",
        "metadata": {
            "connection": {
                "rssi": -109,
                "snr": 2,
                "spreadingFactor": 11,
                "frequency": 866.6,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -109,
                        "snr": 2,
                        "location": {
                            "latitude": 51.556892,
                            "longitude": 5.865359
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1316,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T09:06:33.386Z",
        "datetimeSource": "2025-07-21T09:06:33.386Z",
        "datetimeAcquisition": "2025-07-21T09:06:33.748Z",
        "connector": "lora.kpn",
        "value": "0d01345c3d2aa920b821d02a612a",
        "metadata": {
            "connection": {
                "rssi": -106,
                "snr": -2,
                "spreadingFactor": 11,
                "frequency": 868.5,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -106,
                        "snr": -2,
                        "location": {
                            "latitude": 51.556858,
                            "longitude": 5.865352
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1315,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:56:33.405Z",
        "datetimeSource": "2025-07-21T08:56:33.405Z",
        "datetimeAcquisition": "2025-07-21T08:56:33.754Z",
        "connector": "lora.kpn",
        "value": "0d01f45a3d2aa020c821d22a7d2a",
        "metadata": {
            "connection": {
                "rssi": -115,
                "snr": -6.25,
                "spreadingFactor": 11,
                "frequency": 866.1,
                "gateways": [
                    {
                        "id": "FF010323",
                        "rssi": -115,
                        "snr": -6.25,
                        "location": {
                            "latitude": 51.516491,
                            "longitude": 5.884403
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1314,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:46:33.423Z",
        "datetimeSource": "2025-07-21T08:46:33.423Z",
        "datetimeAcquisition": "2025-07-21T08:46:33.778Z",
        "connector": "lora.kpn",
        "value": "0d01b4593d2ab920d121cf2a922a",
        "metadata": {
            "connection": {
                "rssi": -106,
                "snr": 8,
                "spreadingFactor": 11,
                "frequency": 868.5,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -106,
                        "snr": 8,
                        "location": {
                            "latitude": 51.556862,
                            "longitude": 5.865373
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1313,
                "counterDown": 26,
                "errorRate": 4
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:36:33.441Z",
        "datetimeSource": "2025-07-21T08:36:33.441Z",
        "datetimeAcquisition": "2025-07-21T08:36:33.821Z",
        "connector": "lora.kpn",
        "value": "0d0174583d2ac020e221cb2ada2a",
        "metadata": {
            "connection": {
                "rssi": -106,
                "snr": 2,
                "spreadingFactor": 11,
                "frequency": 866.4,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -106,
                        "snr": 2,
                        "location": {
                            "latitude": 51.55687,
                            "longitude": 5.86535
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1312,
                "counterDown": 26,
                "errorRate": 6
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:26:33.460Z",
        "datetimeSource": "2025-07-21T08:26:33.460Z",
        "datetimeAcquisition": "2025-07-21T08:26:33.810Z",
        "connector": "lora.kpn",
        "value": "0d01b4563d2aa220db21c72aba2a",
        "metadata": {
            "connection": {
                "rssi": -103,
                "snr": -2,
                "spreadingFactor": 11,
                "frequency": 868.1,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -103,
                        "snr": -2,
                        "location": {
                            "latitude": 51.556854,
                            "longitude": 5.865371
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1311,
                "counterDown": 26,
                "errorRate": 6
            }
        }
    },
    {
        "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
        "datetimeMeasure": "2025-07-21T08:16:33.560Z",
        "datetimeSource": "2025-07-21T08:16:33.560Z",
        "datetimeAcquisition": "2025-07-21T08:16:33.962Z",
        "connector": "lora.kpn",
        "value": "0d0174553d2ac220d821c52ad92a",
        "metadata": {
            "connection": {
                "rssi": -104,
                "snr": -7,
                "spreadingFactor": 11,
                "frequency": 865.1,
                "gateways": [
                    {
                        "id": "FF01055A",
                        "rssi": -104,
                        "snr": -7,
                        "location": {
                            "latitude": 51.556866,
                            "longitude": 5.865384
                        }
                    }
                ]
            },
            "frame": {
                "port": 2,
                "counterUp": 1310,
                "counterDown": 26,
                "errorRate": 6
            }
        }
    }
]


  """)
import pandas as pd
from corebridge.timeseriesdataframe import timeseries_dataframe_from_datadict
df_normalized = pd.json_normalize(
    test_nested_data, 
    sep='.',
    #record_prefix='metadata.'
)
df_normalized.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10 entries, 0 to 9
Data columns (total 15 columns):
 #   Column                               Non-Null Count  Dtype  
---  ------                               --------------  -----  
 0   deviceId                             10 non-null     object 
 1   datetimeMeasure                      10 non-null     object 
 2   datetimeSource                       10 non-null     object 
 3   datetimeAcquisition                  10 non-null     object 
 4   connector                            10 non-null     object 
 5   value                                10 non-null     object 
 6   metadata.connection.rssi             10 non-null     int64  
 7   metadata.connection.snr              10 non-null     float64
 8   metadata.connection.spreadingFactor  10 non-null     int64  
 9   metadata.connection.frequency        10 non-null     float64
 10  metadata.connection.gateways         10 non-null     object 
 11  metadata.frame.port                  10 non-null     int64  
 12  metadata.frame.counterUp             10 non-null     int64  
 13  metadata.frame.counterDown           10 non-null     int64  
 14  metadata.frame.errorRate             10 non-null     int64  
dtypes: float64(2), int64(6), object(7)
memory usage: 1.3+ KB
dfn = timeseries_dataframe_from_datadict(test_nested_data, ['datetimeMeasure', 'time'], recordformat='records', nested=True).dropna()
dfn.info()
Converting nested data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 10 entries, 2025-07-21 09:46:33.313000+00:00 to 2025-07-21 08:16:33.560000+00:00
Data columns (total 14 columns):
 #   Column                               Non-Null Count  Dtype  
---  ------                               --------------  -----  
 0   deviceId                             10 non-null     object 
 1   datetimeSource                       10 non-null     object 
 2   datetimeAcquisition                  10 non-null     object 
 3   connector                            10 non-null     object 
 4   value                                10 non-null     object 
 5   metadata.connection.rssi             10 non-null     int64  
 6   metadata.connection.snr              10 non-null     float64
 7   metadata.connection.spreadingFactor  10 non-null     int64  
 8   metadata.connection.frequency        10 non-null     float64
 9   metadata.connection.gateways         10 non-null     object 
 10  metadata.frame.port                  10 non-null     int64  
 11  metadata.frame.counterUp             10 non-null     int64  
 12  metadata.frame.counterDown           10 non-null     int64  
 13  metadata.frame.errorRate             10 non-null     int64  
dtypes: float64(2), int64(6), object(6)
memory usage: 1.2+ KB
dfn
deviceId datetimeSource datetimeAcquisition connector value metadata.connection.rssi metadata.connection.snr metadata.connection.spreadingFactor metadata.connection.frequency metadata.connection.gateways metadata.frame.port metadata.frame.counterUp metadata.frame.counterDown metadata.frame.errorRate
time
2025-07-21 09:46:33.313000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:46:33.313Z 2025-07-21T09:46:33.691Z lora.kpn 0d01b4613d2ab820ec21e42a912a -106 0.00 11 868.1 [{'id': 'FF01055A', 'rssi': -106, 'snr': 0, 'l... 2 1319 26 4
2025-07-21 09:36:33.332000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:36:33.332Z 2025-07-21T09:36:33.697Z lora.kpn 0d0174603d2a9e20bf21dc2a802a -104 5.00 11 867.3 [{'id': 'FF01055A', 'rssi': -104, 'snr': 5, 'l... 2 1318 26 4
2025-07-21 09:26:33.350000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:26:33.350Z 2025-07-21T09:26:33.705Z lora.kpn 0d01b45e3d2aa120ad21d42a5d2a -105 9.00 11 868.5 [{'id': 'FF01055A', 'rssi': -105, 'snr': 9, 'l... 2 1317 26 4
2025-07-21 09:16:33.368000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:16:33.368Z 2025-07-21T09:16:33.734Z lora.kpn 0d01745d3d2ac320a621d12a5b2a -109 2.00 11 866.6 [{'id': 'FF01055A', 'rssi': -109, 'snr': 2, 'l... 2 1316 26 4
2025-07-21 09:06:33.386000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T09:06:33.386Z 2025-07-21T09:06:33.748Z lora.kpn 0d01345c3d2aa920b821d02a612a -106 -2.00 11 868.5 [{'id': 'FF01055A', 'rssi': -106, 'snr': -2, '... 2 1315 26 4
2025-07-21 08:56:33.405000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:56:33.405Z 2025-07-21T08:56:33.754Z lora.kpn 0d01f45a3d2aa020c821d22a7d2a -115 -6.25 11 866.1 [{'id': 'FF010323', 'rssi': -115, 'snr': -6.25... 2 1314 26 4
2025-07-21 08:46:33.423000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:46:33.423Z 2025-07-21T08:46:33.778Z lora.kpn 0d01b4593d2ab920d121cf2a922a -106 8.00 11 868.5 [{'id': 'FF01055A', 'rssi': -106, 'snr': 8, 'l... 2 1313 26 4
2025-07-21 08:36:33.441000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:36:33.441Z 2025-07-21T08:36:33.821Z lora.kpn 0d0174583d2ac020e221cb2ada2a -106 2.00 11 866.4 [{'id': 'FF01055A', 'rssi': -106, 'snr': 2, 'l... 2 1312 26 6
2025-07-21 08:26:33.460000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:26:33.460Z 2025-07-21T08:26:33.810Z lora.kpn 0d01b4563d2aa220db21c72aba2a -103 -2.00 11 868.1 [{'id': 'FF01055A', 'rssi': -103, 'snr': -2, '... 2 1311 26 6
2025-07-21 08:16:33.560000+00:00 00209835-4443-4fee-ae1f-281082fcfbbc 2025-07-21T08:16:33.560Z 2025-07-21T08:16:33.962Z lora.kpn 0d0174553d2ac220d821c52ad92a -104 -7.00 11 865.1 [{'id': 'FF01055A', 'rssi': -104, 'snr': -7, '... 2 1310 26 6
dfm = timeseries_dataframe_from_datadict(test_nested_data, ['datetimeMeasure', 'time'], recordformat='records', nested=False).dropna()
dfm.info()
Converting flat data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 10 entries, 2025-07-21 09:46:33.313000+00:00 to 2025-07-21 08:16:33.560000+00:00
Data columns (total 6 columns):
 #   Column               Non-Null Count  Dtype 
---  ------               --------------  ----- 
 0   deviceId             10 non-null     object
 1   datetimeSource       10 non-null     object
 2   datetimeAcquisition  10 non-null     object
 3   connector            10 non-null     object
 4   value                10 non-null     object
 5   metadata             10 non-null     object
dtypes: object(6)
memory usage: 560.0+ bytes
def test_nested_data_processing(data:pd.DataFrame, anumber:float|np.ndarray=0):
    print (f"Processing {len(data)} rows of data")
    print(data.columns)
    return data
class TestNestedAICoreModule(AICoreModule):
    def __init__(self, files_dir, save_dir):
        super().__init__(test_nested_data_processing, files_dir, save_dir)

test_nested_module = TestNestedAICoreModule(os.path.join(os.getcwd(), 'cache'),os.path.join(os.getcwd(), 'cache'))
INFO    9588    corebridge.aicorebridge aicorebridge.py 134 Init TestNestedAICoreModule, version 0.6.1, files directory /home/fenke/repos/corebridge/nbs/cache, save dir /home/fenke/repos/corebridge/nbs/cache on bouwdoosje
test_result = test_nested_module.call(dict(
    data=test_nested_data,
    kwargs=dict(
        timezone='UTC',
        recordformat='records',
        nested=True
    )
))
recordformat: records, timezone: UTC, nested: True
Converting nested data dict to DataFrame with orient=records and timecolumns=['datetimeMeasure', 'time']
Processing 10 rows of data
Index(['deviceId', 'datetimeSource', 'datetimeAcquisition', 'connector',
       'value', 'metadata.connection.rssi', 'metadata.connection.snr',
       'metadata.connection.spreadingFactor', 'metadata.connection.frequency',
       'metadata.connection.gateways', 'metadata.frame.port',
       'metadata.frame.counterUp', 'metadata.frame.counterDown',
       'metadata.frame.errorRate'],
      dtype='object')
print("Result Message\n", json.dumps(test_result['msg'], indent=2, cls=NumpyEncoder))
print("Result Data\n", json.dumps(test_result['data'], indent=2, cls=NumpyEncoder))
Result Message
 [
  "Startup: time 2025-10-13T13:55:09.873476+00:00, node bouwdoosje",
  "Corebridge version: 0.6.1",
  "test_nested_data_processing((data: pandas.core.frame.DataFrame, anumber: float | numpy.ndarray = 0))",
  "Data: <class 'list'> length: 10",
  "kwargs ['timezone', 'recordformat', 'nested']",
  "lastSeen: False, timezone: UTC, recordformat: records, nested: True",
  "Processing time: 0.3 ms",
  "Preparation time: 4.9 ms",
  "result shape: (10, 14)",
  "return-data shape: (10, 14)"
]
Result Data
 [
  {
    "time": "2025-07-21T08:16:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:16:33.560Z",
    "datetimeAcquisition": "2025-07-21T08:16:33.962Z",
    "connector": "lora.kpn",
    "value": "0d0174553d2ac220d821c52ad92a",
    "metadata.connection.rssi": -104,
    "metadata.connection.snr": -7.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 865.1,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -104,
        "snr": -7,
        "location": {
          "latitude": 51.556866,
          "longitude": 5.865384
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1310,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 6
  },
  {
    "time": "2025-07-21T08:26:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:26:33.460Z",
    "datetimeAcquisition": "2025-07-21T08:26:33.810Z",
    "connector": "lora.kpn",
    "value": "0d01b4563d2aa220db21c72aba2a",
    "metadata.connection.rssi": -103,
    "metadata.connection.snr": -2.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.1,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -103,
        "snr": -2,
        "location": {
          "latitude": 51.556854,
          "longitude": 5.865371
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1311,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 6
  },
  {
    "time": "2025-07-21T08:36:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:36:33.441Z",
    "datetimeAcquisition": "2025-07-21T08:36:33.821Z",
    "connector": "lora.kpn",
    "value": "0d0174583d2ac020e221cb2ada2a",
    "metadata.connection.rssi": -106,
    "metadata.connection.snr": 2.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 866.4,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -106,
        "snr": 2,
        "location": {
          "latitude": 51.55687,
          "longitude": 5.86535
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1312,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 6
  },
  {
    "time": "2025-07-21T08:46:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:46:33.423Z",
    "datetimeAcquisition": "2025-07-21T08:46:33.778Z",
    "connector": "lora.kpn",
    "value": "0d01b4593d2ab920d121cf2a922a",
    "metadata.connection.rssi": -106,
    "metadata.connection.snr": 8.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.5,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -106,
        "snr": 8,
        "location": {
          "latitude": 51.556862,
          "longitude": 5.865373
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1313,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T08:56:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T08:56:33.405Z",
    "datetimeAcquisition": "2025-07-21T08:56:33.754Z",
    "connector": "lora.kpn",
    "value": "0d01f45a3d2aa020c821d22a7d2a",
    "metadata.connection.rssi": -115,
    "metadata.connection.snr": -6.25,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 866.1,
    "metadata.connection.gateways": [
      {
        "id": "FF010323",
        "rssi": -115,
        "snr": -6.25,
        "location": {
          "latitude": 51.516491,
          "longitude": 5.884403
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1314,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:06:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:06:33.386Z",
    "datetimeAcquisition": "2025-07-21T09:06:33.748Z",
    "connector": "lora.kpn",
    "value": "0d01345c3d2aa920b821d02a612a",
    "metadata.connection.rssi": -106,
    "metadata.connection.snr": -2.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.5,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -106,
        "snr": -2,
        "location": {
          "latitude": 51.556858,
          "longitude": 5.865352
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1315,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:16:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:16:33.368Z",
    "datetimeAcquisition": "2025-07-21T09:16:33.734Z",
    "connector": "lora.kpn",
    "value": "0d01745d3d2ac320a621d12a5b2a",
    "metadata.connection.rssi": -109,
    "metadata.connection.snr": 2.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 866.6,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -109,
        "snr": 2,
        "location": {
          "latitude": 51.556892,
          "longitude": 5.865359
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1316,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:26:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:26:33.350Z",
    "datetimeAcquisition": "2025-07-21T09:26:33.705Z",
    "connector": "lora.kpn",
    "value": "0d01b45e3d2aa120ad21d42a5d2a",
    "metadata.connection.rssi": -105,
    "metadata.connection.snr": 9.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.5,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -105,
        "snr": 9,
        "location": {
          "latitude": 51.556892,
          "longitude": 5.865356
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1317,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:36:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:36:33.332Z",
    "datetimeAcquisition": "2025-07-21T09:36:33.697Z",
    "connector": "lora.kpn",
    "value": "0d0174603d2a9e20bf21dc2a802a",
    "metadata.connection.rssi": -104,
    "metadata.connection.snr": 5.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 867.3,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -104,
        "snr": 5,
        "location": {
          "latitude": 51.5569,
          "longitude": 5.865385
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1318,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  },
  {
    "time": "2025-07-21T09:46:33Z",
    "deviceId": "00209835-4443-4fee-ae1f-281082fcfbbc",
    "datetimeSource": "2025-07-21T09:46:33.313Z",
    "datetimeAcquisition": "2025-07-21T09:46:33.691Z",
    "connector": "lora.kpn",
    "value": "0d01b4613d2ab820ec21e42a912a",
    "metadata.connection.rssi": -106,
    "metadata.connection.snr": 0.0,
    "metadata.connection.spreadingFactor": 11,
    "metadata.connection.frequency": 868.1,
    "metadata.connection.gateways": [
      {
        "id": "FF01055A",
        "rssi": -106,
        "snr": 0,
        "location": {
          "latitude": 51.556896,
          "longitude": 5.865362
        }
      }
    ],
    "metadata.frame.port": 2,
    "metadata.frame.counterUp": 1319,
    "metadata.frame.counterDown": 26,
    "metadata.frame.errorRate": 4
  }
]
print(test_result['msg'][-1])
return-data shape: (10, 14)

References