Lesson 7: Adapters - invoke/inquire

There are two mechanisms for controlling the logical flow of information in an Orchestra system. Both of these rely on outlets (see more at <https://docs.ledr.io/en/the-orchestra-platfrom/outlets>) The short version is that outlets are special entities the server allocate some resources to.

The first is the adapter mechanism, where the outlet is the place of rendez-vous between an adapter and an invoker/inquirer. This is what this lesson is about. see more at <https://docs.ledr.io/en/the-orchestra-platfrom/adapters>

The second is the pub/sub mechanism, where the outlet acts as an event queue. This will be detailed in the next lesson. see more at <https://docs.ledr.io/en/the-orchestra-platfrom/subscribers-events-and-pub-sub>

Ad adapter is a piece of code that calls adapt on an outlet. This tells the Orchestra server that any call to invoke or inquire arriving on the outlet should be handled by the adapter. In some sense, an adapter is a special client that connects to an Orchestra server, and declares itself as the server that will handle any request on a certain outlet. This lesson only focuses on invoke and not inquire. They both work in very similar way. TODO: document inquire more

The orchestra-toolkit library takes care of a lot of plumbing when creating an adapter, including conforming to orchestra standards of how adapter works. This makes developper’s life easier. see <https://docs.ledr.io/en/the-orchestra-platfrom/adapters-standard> for more details of what happens under the hood.

The example adapter any new adapter written in python should copy/paste from can be found at the link below. This is the definitive reference for how an adapter is supposed to look according to our latest standards. <https://gitlab.com/ledr/core/dev-platform/python-adapter-wireframe>

At the moment lesson_7_adapters.py includes code both for the adapter as well as the driver logic that interacts with the adapter. This is for ease of running the example.

Note: For most usecases, you would run your adapter code separate from your driver/client code.

To run the lesson_7 code:

python src/module/lesson_7_adapter.py

Code

  1import time
  2from dotenv import load_dotenv, find_dotenv
  3import avesterra as av
  4import orchestra.env as env
  5from orchestra import mount
  6import threading
  7from orchestra.orchestra_adapter import OrchestraAdapter, ValueType
  8
  9MOUNT_KEY = "number_adapter"
 10
 11
 12adapter = None
 13"""We keep a global reference to the adapter just so we can shutdown it later"""
 14
 15
 16def run_number_adapter():
 17    global adapter
 18    """
 19    This function contains all the code required to create an adapter, declare
 20    all its different routes and their implementations, and run the adapter.
 21
 22    Note: Must be ran from the main thread of the process, as it handles signals.
 23    """
 24    adapter = OrchestraAdapter(
 25        mount_key=MOUNT_KEY,
 26        version="1.0.0",
 27        description="Exposes interface for updating count",
 28    )
 29
 30    number_count: dict[av.AvEntity, int] = {}
 31    """This is our in-memory store used in the `set` and `get` functions below"""
 32
 33    @adapter.route("Set number count")
 34    @adapter.method(av.AvMethod.SET)
 35    @adapter.value_in(ValueType.integer())
 36    @adapter.value_out(ValueType.null())
 37    def set(entity: av.AvEntity, value: av.AvValue) -> av.AvValue:
 38        """
 39        Sets the number count.
 40        :param entity: (AvEntity) that is the entity set the count of
 41        :param value: (AvValue) that encodes number
 42        :return: (AvValue) with Null
 43        """
 44        count = value.decode_integer()
 45        number_count[entity] = count
 46
 47        return av.NULL_VALUE
 48
 49    @adapter.route("Get number count")
 50    @adapter.method(av.AvMethod.GET)
 51    @adapter.value_out(ValueType.integer())
 52    def get(entity: av.AvEntity) -> av.AvValue:
 53        """
 54        Gets the count value from the entity.
 55        :param entity: (AvEntity) the entity which contains the count
 56        :return: (AvValue) with the count
 57        """
 58        count = number_count.get(entity, 0)
 59
 60        return av.AvValue.encode_integer(count)
 61
 62    # /!\ Important: Calling `.run()` will block until the adapter is stopped, which
 63    # typically only happens when the process receives a SIGTERM signal.
 64    adapter.run()
 65
 66
 67#######
 68#
 69# All code below that point is there to invoke the adapter for demo purposes
 70# and typically wouldn't exist in a real adapter code.
 71#
 72######
 73
 74
 75def set_number(entity: av.AvEntity, n: int, auth: av.AvAuthorization):
 76    """
 77    Utility function to invoke the given number entity and set its value to `n`
 78    """
 79    av.invoke_entity_retry_bo(
 80        entity=entity,
 81        method=av.AvMethod.SET,
 82        value=av.AvValue.encode_integer(n),
 83        authorization=auth,
 84    )
 85
 86
 87def get_number(entity: av.AvEntity, auth: av.AvAuthorization) -> int:
 88    """
 89    Utility function to invoke the given number entity and get its current value
 90    """
 91    val = av.invoke_entity_retry_bo(
 92        entity=entity,
 93        method=av.AvMethod.GET,
 94        authorization=auth,
 95    )
 96    return val.decode_integer()
 97
 98
 99def create_number_entity(name: str, auth: av.AvAuthorization) -> av.AvEntity:
100    """
101    Utility function create a new number entity.
102    :param name: The number entity name
103    :param auth: The authorization token to use
104    :return: The newly created number entity
105    """
106    # Note, the use of create_entity instead of create_object.
107    # This is because create_object automatically connects to the object adapter
108    # and we don't want that.
109    number_entity = av.create_entity(
110        name=name,
111        key=name.lower(),
112        context=av.AvContext.ORCHESTRA,
113        category=av.AvCategory.MATHEMATICS,
114        klass=av.AvClass.MATHEMATICS,
115        authorization=auth,
116    )
117    av.connect_method(
118        entity=number_entity,
119        outlet=mount.get_outlet(MOUNT_KEY, auth=auth),
120        authorization=auth,
121    )
122
123    return number_entity
124
125
126def test_number_adapter():
127    """
128    Basic tests to showcase how the number adapter behaves
129    """
130    load_dotenv(find_dotenv())
131    auth = env.get_or_raise(env.AVESTERRA_AUTH, av.AvAuthorization)
132
133    time.sleep(0.1)  # Wait for the adapter to start
134
135    # Create a number entity that we can update its count through an outlet
136    number_entity = create_number_entity(name="My Number", auth=auth)
137    print(f"The entity ID of the number entity is: {number_entity}")
138
139    set_number(entity=number_entity, n=1, auth=auth)
140    assert get_number(entity=number_entity, auth=auth) == 1
141    set_number(entity=number_entity, n=42, auth=auth)
142    assert get_number(entity=number_entity, auth=auth) == 42
143    set_number(entity=number_entity, n=100, auth=auth)
144    assert get_number(entity=number_entity, auth=auth) == 100
145
146    assert adapter is not None
147    adapter.shutdown()
148
149
150if __name__ == "__main__":
151    threading.Thread(target=test_number_adapter).start()
152    run_number_adapter()