The keyword model#

Each actor has an associated keyword model defined in advance. The model determines the keywords that we can expect to receive from the actor and their data types. Based on that model a client or a different actor can keep track of the status of the actor and register callbacks to be executed when a keyword changes.

CLU provides a uniform interface for defining keyword models regardless of the specific type of actor and its message broker.

JSON schema#

Actors define their keyword datamodels using a JSON-Schema file. For example, let’s say we have an actor called guider that can return two keywords: text, with a random string value, and fwhm which must be a float. The schema for such actor can be defined in a file guider.json as

{
    "type": "object",
    "properties": {
        "text": {"type": "string"},
        "fwhm": {"type": "number"}
    },
    "additionalProperties": false
}

Note the additionalProperties entry which prevents undefined keywords to be output. Refer to the JSON-Schema documentation for details on how to define properties.

To associate this schema with an actor, we initiate or subclass the actor with it

actor = AMQPActor('my_actor', user='user`', schema='guider.json')

or

class MyActor(AMQPActor):

    schema = 'guider.json'

    ...

When the schema is present, the write method will validate the desired output against the schema and, if it does not match, will prevent the message from being output.

The following keywords are added automatically to all schemas and should not be overridden unless you know what you are doing since they are internally used by CLU: text, help, schema, version, text, error, yourUserID, UserInfo, num_users.

Properties/keywords are case-sensitive.

The actor model#

When an actor is instantited with a keyword schema as seen above, it creates a Model of its own datamodel, used both to validate replies and also to keep track of the current state of the actor. We can instantiate a Model manually from the actor schema

>>> from clu.model import Model
>>> model = Model('guider', open('guider.json', 'r').read())

If the schema is invalid an error is raised. We can now validate a reply with a set of keywords

>>> model.validator.validate({'text': 'Some text here.'})
>>> model.validator.validate({'text': 1})
ValidationError: 1 is not of type 'string'

Failed validating 'type' in schema['properties']['text']:
    {'type': 'string'}

On instance['text']:
    1

When we instantiate the model we create a dictionary with the current values of each of the keywords. Each keyword is represented by a Property instance. The values are set to None on initialisation

>>> actor.model['fwhm']
<Property (fwhm): None>
>>> print(actor.model['fwhm'].value)
None

When the actor outputs keywords as part of a reply, the values of the actor own model are update, so it’s always possible to know the status of the actor.

>>> actor.write('i', message={'fwhm': 1.2})
>>> actor.model['fwhm']
<Property (fwhm): 1.2>
>>> print(actor.model['fwhm'].value)
1.2

The model of other actors#

Frequently we have an actor or client that connects to the exchange or tron and we want to monitor the status of a group of actors connected to the same message broker. When we instantiate a new actor or client we can pass a list of actor names as part of the models argument. This will create a ModelSet (a mapping of actor name to Model) with the models for each one of the actors

>>> from clu.client import AMQPClient
>>> client = AMQPClient('my_client', host='localhost', port=5672, models=['sop', 'guider'])
>>> await client.start()

The models can be accessed via the models attribute. From now on, when the client or actor receives a reply from sop or guider, the keywords will be validated against the schema and, if valid, the values of the model will be updated. For example

# Send a command to guider asking it to report the status.
>>> cmd = await client.send_command('guider', 'status')
# Wait until the command is done
>>> await cmd
# Check the value of the FWHM
>>> print(client.models['guider']['fwhm'].value)
1.1

Tron models#

The keyword models used by legacy actors are different (of course) in that they are not defined as JSON schemas but as actorkeys instead. To avoid depending on opscore and other Python 2 products, CLU includes a Python 3-ready set of routines to read the actorkeys datamodel and parse the replies using it. The only requisite is that actorkeys must be in the PYTHONPATH and be importable by CLU.

We can create a connection to tron and request that the client keeps track of the guider actor model

>>> from clu.legacy.tron import TronConnection
>>> tron = TronConnection('localhost', 6093, models=['guider'])
>>> await tron.start()
>>> tron.models
{'guider': <Model (guider)>}
>>> tron.models['guider']
<Model (guider)>
>>> tron.models['guider']['fwhm']
<TronKey (fwhm): [572, nan, 0, 0, nan]>
>>> tron.models['guider']['fwhm'].value
[572, nan, 0, 0, nan]
>>> tron.models['guider']['fwhm'].name
'fwhm'
>>> tron.models['guider']['fwhm'].key
Key(fwhm)
>>> type(tron.models['guider']['fwhm'].key)
clu.legacy.keys.Key

Note that the key in this case is an opscore Key object, which contains information about the keyword model. All keys are composed of a list of values. In the case of the fwhm, the keyword returns

>>> tron.models['guider']['fwhm'].key.typedValues.vtpyes
Types[Int, Float, Int, Int, Float]
>>> [vtype.name for vtype in tron.models['guider']['fwhm'].key.typedValues.vtypes]
['expID', 'tmean', 'nKept', 'nReject', 'mean']

We can also access the keyword attribute which contains the last emitted keyword as an opscore Keyword object

>>> tron.models['guider']['fwhm'].keyword.values
[Int(568), Float(nan arcsec), Int(0), Int(0), Float(nan arcsec)]
>>> [value.name for value in tron.models['guider']["fwhm"].keyword.values]
['expID', 'tmean', 'nKept', 'nReject', 'mean']

If you are only interested int he list of value, the simplest is to used the value attribute to access a list of values as builtin Python types

>>> tron.models['guider']['fwhm'].value
[572, nan, 0, 0, nan]
>>> type(tron.models['guider']['fwhm'].value[0])
int

In practice, one can treat tron models the same way as other models, with the difference that the value of each keyword is always a list and one must know what each element represents.

Note

Previous to CLU 0.7.4, TronKey.keyword did not exists and TronKey.key actually contained the Keyword object. CLU 0.7.4 introduces a breaking change to clarify the nomenclature and make it more consistent with opscore.

Adding callbacks#

One of the main advantages of having a self-updating model for an actor is that we can register callbacks to be executed when a keyword or model changes. We can register a callback directly to the model

>>> def model_callback(key): print(key)
>>> client.models['guider'].register_callback(model_callback)

model_callback can be either a function or a coroutine and is called when the model is updated. The function receives a flatten dictionary of the Model instance as the first argument and the modified Property as the second (TronModel and TronKey in the case of a Tron model). Note that in both cases the arguments received by the callback are frozen and won’t change as the model or keyword change due to other updates.

More likely, we’ll want to add callbacks to specific keywords, which is done as

>>> client.models['guider']['fwhm'].register_callback(fwhm_callback)

In this case fwhm_callaback is only called if guider.fwhm is updated, and receives the Property (or TronKey in case of a legacy-style keyword) as the only argument.

Note that the callbacks are executed every time a reply that includes the model or keyword are received, even if the value of the keyword doesn’t change.

Retriving schema information#

The The Click parser includes two commands that allow a user or piece of code to retrieve information about another actor’s schema. Calling get_schema on an actor will return a JSON string with the JSON-Schema for that actor. For example, a client can access the schema of a remote actor as

cmd = await client.send_command('actor', 'get_schema')
await cmd
if cmd.status.did_fail:
    raise CluError(f"Failed getting schema for actor.")
else:
    schema = json.loads(cmd.replies[-1].message["schema"])

Sometimes one is just interested in knowing the expected format of a keyword that is output by an actor. In that case the keyword command prints a user-friendly message with that information

>>> actor keyword version
i text="version = {      "
i text="    type: string "
i text="}                "

The keyword command is not indicated for programmatic access to the schema (use get_schema instead).