Data Entities

In simple words, a Data Entity is a Python class whose objects when created can contain automatically generated associated data.

Creating a Basic Data Entity

Arjuna’s data_entity gives you an advanced, yet easy way of creating entities that contain associated generated/provided data.

In its simplest form, data_entity is a Python class (without custom behaviors/methods) creator without writing a class. In its more involved forms, it starts serving complex data needs of today’s automation world.

In the rest of this page, more advanced options will be considered. Here’s a simple example of a Data Entity with static data:

# Provide name of entity and names of desired data attributes as strings
Person = data_entity("Person", "name", "age", "country")

# Or provide names of data attributes as a single string (space separated)
Person = data_entity("Person", "name age country")

# Now you can use it as a regular Python class as if the data attributes are keyword arguments in class definition.
person = Person(name="Ravi", age=25, country="India")

# Access Data
person.name
person.age
person.country

This might look fancy to the novice, however so far there is nothing special about it. You could have achieved the above by using namedtuple which is available in Python’s built-in collections module.

Next sections tell you what makes data_entity special.

Setting Defaults in Data Entity

You can define data attributes with default value in the familiar Pythonic way for keyword arguments:

Person = data_entity("Person", "name age", country='India')

Data Entity has a Python Dictionary-like Behavior

Data entity objects behave like Python dictionaries.

Attribute as a Key

You can retrieve an attribute value as:

entity.attr
# or
entity['attr']

keys and items Methods - None values Removed by Default

Following dict-like operations are valid too. The key difference to note is that in these operations that attributes that have None value are excluded unlike a Python dictionary.

entity.keys()
entity.items()
**entity # Unpacking of key-values

# Iterating on keys
for attr in entity:
    pass

# Iterating on key-value pairs
for attr, value in entity.items():
    pass

keys and items Methods - Retaining None Values

To retain keys/attrs corresponding to None values, you can provide remove_none=False as argument:

entity.keys(remove_none=False)
entity.items(remove_none=False)
**entity # Unpacking of key-values

# Iterating on keys
for attr in entity.keys(remove_none=False):
    pass

# Iterating on key-value pairs
for attr, value in entity.items(remove_none=False):
    pass

len unction vs size Method

Also note that because len() in Python is not flexible to allow for the above, you can use size method:

len(entity) # Will ignore attrs with None value
entity.size() # Will ignore attrs with None value
entity.size(remove_none=False) # Includes attrs with None value

remove Argument for Removing Specific Keys/Attributes

All above mentioned methods also accept remove argument to explicitly exclude one or more attributes by name.

entity.keys(remove='some_key')
entity.keys(remove={'some_key1', 'some_key2'})

entity.items(remove='some_key')
entity.items(remove={'some_key1', 'some_key2'})

entity.size(remove='some_key')
entity.size(remove={'some_key1', 'some_key2'})

del is NOT Allowed

Delete operation is disllowed on the data entity because it corresponds to attribute deletion. Use as_dict() method for representation that has one or more keys removed.

# Raises exception
del entity['some_attr']

Creating Immutable Data Entity Objects

You can make an object of a data entity IMMUTABLE by passing freeze=True argument.

person = Person(name="SomeName", age=21, freeze=True)
# Raises Exception
person.age = 25

Basic Usage of Random with Data Entity

You can club the usage of Random class with Data Entity to create an object with random data:

Person = data_entity("Person", "name age country")
person = Person(name=Random.name, age=Random.int(begin=18, end=65), country=Random.country)

Dynamic Generation of Data for Data Entities

Using Callables in Random Class

This is the point where the true power of Data Entities starts to unfold.

You can associate a Data Entity’s attribute with a callable to generate unqiue data for each object of this Data Entity.

Person = data_entity("Person", "name age", country=Random.country)

# Gets assigned a random country when object is created
person1 = Person(name=Random.name, age=Random.int(end=65))

# Gets assigned a random country when object is created
person2 = Person(name=Random.name, age=Random.int(end=65))

Using User-Defined Callables

You can also use your own random data generator callables:

def some_data_gen():
    return random.randint(20,60)

Person = data_entity("Person", "name country", age=some_data_gen)

# Gets assigned a random int as age when object is created, as returned by some_data_gen
person1 = Person(name=Random.name, country='India')

# Gets assigned a random int as age when object is created as returned by some_data_gen
person2 = Person(name=Random.name, country='India')

Arjuna’s generator Construct

Arjuna’s generator construct can call any callable with provided arbitrary positional as well as keyword arguments.

This is a lazy mechanism. It means that when this construct is used, till its generate() call is made, the corresponding callable is not called.

Following are some examples

generator(Random.first_name).generate()
generator(some_callable, arg1, arg2, kwarg1=value1, kwarg2=value2).generate()

Using generator Construct to Provide Arbitrary Arguments to Generator Callables

The data generator functions could take any positional arguments and/or keyword arguments.

Data Entities accept Arjuna’s generator construct to support this advanced facility.

You can use it with your own functions as well. Here’s an example with Random.int function:

Person = data_entity("Person", "name country", age=generator(Random.int, begin=18, end=65))

Processing Dynamically Generated Data

Basic Processor Callable

You might want to process the generated data before making it a part of Data Entity. You can do it by passing a converter callable to generator:

def lower(in_str):
    return in_str.lower()

Person = data_entity("Person", "age country", name=generator(Random.name, processor=lower))

Here if the generated name is “Ravi Sharma”, it will stored as “ravi sharma” in the data entity post conversion.

Processor Callable as a Method of Generated Data Object

If the processor is a string, it is assumed to be a method of the generated data object and called:

Person = data_entity("Person", "age country", name=generator(Random.name, processor="lower"))

Defining Processor Callable with Arbitrary Arguments

The generator constructs also accepts Arjuna’s processor construct for advanced usage:

def replace_space(in_str, char=":"):
    return in_str.replace(" ", char)

processor = processor(replace_space, char="-")
Person = data_entity("Person", "age country", name=generator(Random.name, processor=processor))

If the callable provided to processor is a string, it is assumed to be a method of the generated data object and called.

Defining Composite Data Using composite and composer Constructs

At times, you might want to club data obtained from multiple generators. You might want to combine some static data with it as well, as needed.

Data Entities in Arjuna accept Arjuna’s composite construct for data attributes.

Once the data is available as a single sequence, it is composed together using the composer callable that you can optionally provide, else the same sequence is stored as the value for this data attribute.

If you have reached this stage, it is assumed, that you know what you are doing. So, here’s a complete example demonstrating everything a Data Entity has to offer:

def to_upper_case(data_str):
    return data_str.upper()

def join(in_list, char=":"):
    return char.join(in_list)

processor = processor(replace_space, char="-")
Person = data_entity("Person",
        age = generator(Random.int, begin=18, end=65),
        country = Random.country,
        name=composite(
                "Mz",
                generator(Random.first_name, processor="upper"),
                generator(Random.last_name, processor=to_upper_case),
                composer=composer(join, char=" ")
            )
        )

Creating a Data Entity from Other Data Entities

You might want to create a data entity from existing data entities and have the option to add more attributes as well as override behavior of existing ones.

To achieve this you can make use of the bases argument. A single base entity can be passed as a string. Multiple base entities can be passed as a list or tuple.

Single Base Data Entity

Consider the following base data entity:

# Simple base with one mandatory and one optional attr
Person = data_entity("Person", "age", fname=Random.first_name)

In the following sections, we will utilize this as base entity and make further tweaks.

Adding a Mandatory Attribute

Here the UpdatedPerson entity uses Person as its base entity and adds gender as a mandatory attribute:

# Top entity adds a mandatory attr
UpdatedPerson = data_entity("UpdatedPerson", "gender", bases=Person)
p1 = UpdatedPerson(gender="M", age=20)
p2 = UpdatedPerson(gender="M", age=20, fname="Roy")

Adding an Optional/Default Attribute

Here the UpdatedPerson entity uses Person as its base entity and adds city as an optional attribute:

# Top entity adds an optional attr
UpdatedPerson = data_entity("UpdatedPerson", city=Random.city, bases=Person)
p1 = UpdatedPerson(age=20, fname="Roy")
p2 = UpdatedPerson(age=20, fname="Roy", city="Bengaluru")

Changing Value of Optional/Default Attribute

Here the UpdatedPerson entity uses Person as its base entity and changes the value for fname attribute.

# Top entity adds an optional attr
UpdatedPerson = data_entity("UpdatedPerson", fname=Random.name, bases=Person)
p1 = UpdatedPerson(age=20)
p2 = UpdatedPerson(age=20, fname="Roy")

Converting an Optional/Default Attribute to Mandatory Attribute

Here the UpdatedPerson entity uses Person as its base entity and makes fname mandatory.

# Top entity adds an optional attr
UpdatedPerson = data_entity("UpdatedPerson", "fname", bases=Person)
p1 = UpdatedPerson(age=20, fname="Roy")

Converting a Mandatory Attribute to Optional Attribute

Here the UpdatedPerson entity uses Person as its base entity and makes age attribute optional.

# Top entity adds an optional attr
UpdatedPerson = data_entity("UpdatedPerson", age=generator(Random.fixed_length_number, length=2), bases=Person)
p1 = UpdatedPerson()

Multiple Base Data Entities

You can also assign multiple base data entities.

Simple Merged Data Entity

One simple requirement you might have is to merge two data entities together.

Here’s an intuitive approach:

Person = data_entity("Person", "age", fname=Random.first_name)
Address = data_entity("Address", city=Random.city, country=Random.country, postal_code=Random.postal_code)

# Merged Entity
PersonWithAddress = data_entity("PersonWithAddress", bases=(Person, Address))
p = PersonWithAddress(age=40)

Merged Data Entity with Custom Overrides

Sometimes the base data entities have common attributes and the top data entity also might choose to change the behaviors for more complex requirements.

Following code snippet demonstrates this:

# Simple Base 1 with one mandatory and one optional attr
Person = data_entity("Person", "age", fname=Random.first_name)

# Base 2 adds one mandatory arg, makes fname mandatory, adds one optional arg
MiddlePerson = data_entity("MiddlePerson", "gender fname", city=Random.city, bases=Person1)

# Top entity makes age optional, add one mandatory parameter
TopPerson = data_entity("TopPerson", "country", age=generator(Random.fixed_length_number, length=2), bases=(Person, MiddlePerson))
p1 = TopPerson(gender="M", fname="Roy", country="India")
p2 = TopPerson(gender="M", fname="Roy", age=15, country="India")

Defining Entities for Dependency Injection

If you define a data entity that are imporatble as from yourproject.lib.hook.entity import MyEntity, then Arjuna can allow the usage of this entity in places where it can do dependency injection.

Currently, following places allow for dependency injection for a Data Entity:
  • SEAMful HTTP Action yaml files

In its simplest form, you can code an entity in project/lib/hook/entity.py python file. For example:

from arjuna import *

Item = data_entity(
    "Item",
    name = Random.ustr,
    price = generator(Random.fixed_length_number, length=3)