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()