Examples

The BalderHub data package provides various types of objects for defining data class models. These are particularly helpful for data-related tests, such as those in balderhub-crud or balderhub-auth.

Defining Your Own Data Model Structure

This package allows you to define your own data model structure by using the balderhub.data.lib.utils.SingleDataItem as the base class:

from typing import Optional
from balderhub.data.lib.utils import SingleDataItem


class AuthorDataItem(SingleDataItem):
    id: int
    first_name: str
    last_name: str

    def get_unique_identification(self):
        return self.id

class BookCategoryDataItem(SingleDataItem):
    id: int
    name: str

    def get_unique_identification(self):
        return self.id


class BookDataItem(SingleDataItem):
    id: int
    title: str
    author: AuthorDataItem
    category: Optional[BookCategoryDataItem] = None

    def get_unique_identification(self):
        return self.id

You can use the following types within balderhub.data.lib.utils.SingleDataItem:

  • base types like str, int, bool, float, etc.

  • declare a type as optional with typing.Optional[int] or typing.Union[int, None] (both are equal and accepted)

  • nested data items (referred data item needs to be another balderhub.data.lib.utils.SingleDataItem)

What is not allowed:

  • multiple possible types defined by typing.Union[int, float, ..] (except the Optional like definition: typing.Union[int, None])

  • list types other than list[MyDataItem] (where MyDataItem is a SingleDataItem subclass)

  • dictionary types: use nested data items for that

Interacting with SingleDataItem Objects

The package provides various ways to interact with data items.

For more details, see the API documentation for balderhub.data.lib.utils.SingleDataItem and balderhub.data.lib.utils.SingleDataItemCollection.

For referencing fields, especially nested fields, use balderhub.data.lib.utils.LookupFieldString.

For example, to access the author’s last name from a BookDataItem instance, use author__last_name. The project will automatically convert strings like that to LookupFieldString objects.

Using the Data Environment Feature

Most features use the balderhub.data.lib.scenario_features.DataEnvironmentFeature, which allows you to define your own data samples.

from balderhub.data.lib.scenario_features.data_environment_feature import DataEnvironmentFeature
from tests.lib.utils import data_items
from tests.lib.setup_features.dut_simulator_feature import DutSimulatorFeature
from tests.lib.utils.dut_simulator import DutSimulator


class TestDataEnvironment(DataEnvironmentFeature):

    sim = DutSimulatorFeature()

    def load_data(self):

        self._add_data(data_items.AuthorDataItem(id=1, first_name='J.K.', last_name='Rowling'))
        self._add_data(data_items.AuthorDataItem(id=2, first_name='J.R.R.', last_name='Tolkien'))
        self._add_data(data_items.AuthorDataItem(id=3, first_name='A.B.C.', last_name='Alphabet'))

        self._add_data(data_items.BookCategoryDataItem(id=1, name='Fantasy'))
        self._add_data(data_items.BookCategoryDataItem(id=2, name='Science Fiction'))
        self._add_data(data_items.BookCategoryDataItem(id=3, name='Action'))

        self._add_data(data_items.BookDataItem(
            id=1, title='Harry Potter and the Sorcerer’s Stone',
            author=self.get(data_items.AuthorDataItem, 1),
            category=self.get(data_items.BookCategoryDataItem, 1))
        )

        self._add_data(data_items.BookDataItem(
            id=2, title='Harry Potter and the Order of the Phoenix',
            author=self.get(data_items.AuthorDataItem, 1),
            category=self.get(data_items.BookCategoryDataItem, 1))
        )

        self._add_data(data_items.BookDataItem(
            id=3, title='The Hobbit',
            author=self.get(data_items.AuthorDataItem, 2),
            category=self.get(data_items.BookCategoryDataItem, 1))
        )

    def sync_environment(self):
        """Triggers data synchronization when the remote data needs to be synced"""
        self.sim.dut_simulator.remove_all()

        for cur_author in self.get_all_for(data_items.AuthorDataItem):
            self.sim.dut_simulator._all_authors[cur_author.id] = DutSimulator.Author(
                id=cur_author.id, first_name=cur_author.first_name, last_name=cur_author.last_name
            )

        for cur_category in self.get_all_for(data_items.BookCategoryDataItem):
            self.sim.dut_simulator._all_categories[cur_category.id] = DutSimulator.Category(
                id=cur_category.id, name=cur_category.name
            )

        for cur_book in self.get_all_for(data_items.BookDataItem):
            self.sim.dut_simulator._all_books[cur_book.id] = DutSimulator.Book(
                id=cur_book.id, title=cur_book.title, author__id=cur_book.author.id, category__id=cur_book.category.id
            )

You need to define two methods, the method balderhub.data.lib.scenario_features.DataEnvironmentFeature.load_data() which describes the full data sample and the method balderhub.data.lib.scenario_features.DataEnvironmentFeature.sync_environment(), that will be executed when the data samples need to be synced. This will be done by most projects automatically. You only need to make sure that you have an implementation of this method.

If you would like to run tests under different data samples, you can define multiple versions of your DataEnvironmentFeature with different data sets and assign them to different setups:

class SetupBase(balder.Setup):

    class DUT(balder.Device):
        data = FictitiousDataEnvironmentFeature()
        ...

    ...

class SetupWithLiveData(balder.Setup):
    class DUT(balder.Device):
        data = LiveDataEnvironmentFeature()
        ...

    ...

Initial Data Config Features

This project provides two main features used in other BalderHub projects, like balderhub-crud or balderhub-auth.

These features are the balderhub.data.lib.scenario_features.InitialDataConfig and the balderhub.data.lib.scenario_features.AccessibleInitialDataConfig.

The InitialDataConfig will be assigned to the same device, that has the DataEnvironmentFeature and describes all initial loaded data for a specific data item. On the other side, the AccessibleInitialDataConfig is a feature, that is assigned to the remote device that is used to access the data from the DUT. It can return the same data as the remote InitialDataConfig feature or return a subset of it. This is mainly used for data, that has special object permission and can only be accessed by a specific user group.

When using the balderhub.data.lib.scenario_features.DataEnvironmentFeature, you will not need to provide an implementation for both of them, just use the setup factories:

class SetupBase(balder.Setup):

    class DUT(balder.Device):
        data = FictitiousDataEnvironmentFeature()
        initial_data = balderhub.data.lib.setup_features.factories.AutoInitialDataConfigFactory.get_for(AuthorDataItem)()
        ...

    @balder.connect(DUT, over_connection=balder.Connection)
    class User(balder.Device):
        accessible_data = balderhub.data.lib.setup_features.factories.AutoAccessibleInitialDataConfigFactory.get_for(AuthorDataItem)(Master='DUT')

AbstractDataItemRelatedFeature and the @balderhub.data.register_for_data_item() Decorator

This BalderHub project provides a base feature class balderhub.data.lib.scenario_features.AbstractDataItemRelatedFeature, that is used for a wide set of features within this package but also within other depending BalderHub packages like balderhub-crud or balderhub-auth.

By using this AbstractDataItemRelatedFeature as base class, the BalderHub package expects that further subclasses (mostly defined as setup features by the end user) use the @balderhub.data.register_for_data_item() decorator to specify the affiliation of the specific feature:

from typing import List, Dict, Union, Any

import balder
import balderhub.data

from balderhub.data.lib.scenario_features import InitialDataConfig, AccessibleInitialDataConfig


@balderhub.data.register_for_data_item(BookDataItem)
class AccessibleBookInitialDataConfig(AccessibleInitialDataConfig):

    class Master(balder.VDevice):
        """inner virtual device referencing the master device that provides the full initial data config"""
        full_initial_config = InitialDataConfig.get_specific_feature_for(BookDataItem)

    @property
    def data_list(self) -> SingleDataItemCollection:
        return self.Master.full_initial_config.data_list.filter(MyBookFilter())

You can assign such a feature to your setup like any other feature or by using the method balderhub.data.lib.scenario_features.AbstractDataItemRelatedFeature.get_specific_feature_for():

import balder
import balderhub.data

from balderhub.data.lib.scenario_features import InitialDataConfig, AccessibleInitialDataConfig

class SetupBase(balder.Setup):

    class DUT(balder.Device):
        data = FictitiousDataEnvironmentFeature()
        initial_data = InitialDataConfig.get_for(BookDataItem)
        ...

    @balder.connect(DUT, over_connection=balder.Connection)
    class User(balder.Device):
        accessible_data = AccessibleInitialDataConfig.get_for(BookDataItem, Master="DUT")

Working with SingleDataItemCollection

SingleDataItemCollection is a collection of SingleDataItem instances with additional utility methods like filtering, getting by attributes, sorting, and comparison.

Example:

from balderhub.data.lib.utils import SingleDataItemCollection

# Assuming AuthorDataItem defined earlier

authors = SingleDataItemCollection([
    AuthorDataItem(id=1, first_name='J.K.', last_name='Rowling'),
    AuthorDataItem(id=2, first_name='J.R.R.', last_name='Tolkien'),
])

# Get by identifier
rowling = authors.get_by(id=1)

# Filter (requires a Filter subclass, see below)
fantasy_authors = authors.filter(FantasyAuthorFilter())

# Compare collections
expected = SingleDataItemCollection([rowling])
if expected.compare(fantasy_authors):
    print(\"Collections match\")

Using Filters

Define custom filters for SingleDataItemCollection.filter():

from balderhub.data.lib.utils import Filter

class FantasyAuthorFilter(Filter):
    def apply(self, item: AuthorDataItem) -> bool:
        return item.last_name == 'Rowling'  # example logic

filtered = authors.filter(FantasyAuthorFilter())