charms.reactive.relations

Summary

AutoAccessors Metaclass that converts fields referenced by auto_accessors into accessor methods with very basic doc strings.
Conversation Converations are the persistent, evolving, two-way communication between this service and one or more remote services.
RelationBase The base class for all relation implementations.
relation_call Invoke a method on the class implementing a relation via the CLI
scopes These are the recommended scope values for relation implementations.

Reference

class charms.reactive.relations.AutoAccessors

Bases: type

Metaclass that converts fields referenced by auto_accessors into accessor methods with very basic doc strings.

class charms.reactive.relations.Conversation(namespace, units, scope)

Bases: object

Converations are the persistent, evolving, two-way communication between this service and one or more remote services.

Conversations are not limited to a single Juju hook context. They represent the entire set of interactions between the end-points from the time the relation is joined until it is departed.

Conversations evolve over time, moving from one semantic state to the next as the communication progresses.

Conversations may encompass multiple remote services or units. While a database client would connect to only a single database, that database will likely serve several other services. On the other hand, while the database is only concerned about providing a database to each service as a whole, a load-balancing proxy must consider each unit of each service individually.

Conversations use the idea of scope to determine how units and services are grouped together.

depart()

Remove the current remote unit, for the active hook context, from this conversation. This should be called from a -departed hook.

classmethod deserialize(conversation)

Deserialize a serialized conversation.

get_local(key, default=None)

Retrieve some data previously set via set_local() for this conversation.

get_remote(key, default=None)

Get a value from the remote end(s) of this conversation.

Note that if a conversation’s scope encompasses multiple units, then those units are expected to agree on their data, whether that is through relying on a single leader to set the data or by all units eventually converging to identical data. Thus, this method returns the first value that it finds set by any of its units.

is_state(state)

Test if this conversation is in the given state.

classmethod join(scope)

Get or create a conversation for the given scope and active hook context.

The current remote unit for the active hook context will be added to the conversation.

Note: This uses charmhelpers.core.unitdata and requires that flush() be called.

key

The key under which this conversation will be stored.

classmethod load(keys)

Load a set of conversations by their keys.

relation_ids

The set of IDs of the specific relation instances that this conversation is communicating with.

relation_name
remove_state(state)

Remove this conversation from the given state, and potentially deactivate the state if no more conversations are in it.

The relation name will be interpolated in the state name, and it is recommended that it be included to avoid conflicts with states from other relations. For example:

conversation.remove_state('{relation_name}.state')

If called from a converation handling the relation “foo”, this will remove the conversation from the “foo.state” state, and, if no more conversations are in this the state, will deactivate it.

classmethod serialize(conversation)

Serialize a conversation instance for storage.

set_local(key=None, value=None, data=None, **kwdata)

Locally store some data associated with this conversation.

Data can be passed in either as a single dict, or as key-word args.

For example, if you need to store the previous value of a remote field to determine if it has changed, you can use the following:

prev = conversation.get_local('field')
curr = conversation.get_remote('field')
if prev != curr:
    handle_change(prev, curr)
    conversation.set_local('field', curr)

Note: This uses charmhelpers.core.unitdata and requires that flush() be called.

Parameters:
  • key (str) – The name of a field to set.
  • value – A value to set.
  • data (dict) – A mapping of keys to values.
  • **kwdata – A mapping of keys to values, as keyword arguments.
set_remote(key=None, value=None, data=None, **kwdata)

Set data for the remote end(s) of this conversation.

Data can be passed in either as a single dict, or as key-word args.

Note that, in Juju, setting relation data is inherently service scoped. That is, if the conversation only includes a single unit, the data will still be set for that unit’s entire service.

However, if this conversation’s scope encompasses multiple services, the data will be set for all of those services.

Parameters:
  • key (str) – The name of a field to set.
  • value – A value to set.
  • data (dict) – A mapping of keys to values.
  • **kwdata – A mapping of keys to values, as keyword arguments.
set_state(state)

Activate and put this conversation into the given state.

The relation name will be interpolated in the state name, and it is recommended that it be included to avoid conflicts with states from other relations. For example:

conversation.set_state('{relation_name}.state')

If called from a converation handling the relation “foo”, this will activate the “foo.state” state, and will add this conversation to that state.

Note: This uses charmhelpers.core.unitdata and requires that flush() be called.

toggle_state(state, active=<object object>)

Toggle the given state for this conversation.

The state will be set active is True, otherwise the state will be removed.

If active is not given, it will default to the inverse of the current state (i.e., False if the state is currently set, True if it is not; essentially toggling the state).

For example:

conv.toggle_state('{relation_name}.foo', value=='foo')

This will set the state if value is equal to foo.

class charms.reactive.relations.RelationBase(relation_name, conversations=None)

Bases: object

The base class for all relation implementations.

auto_accessors = []

Remote field names to be automatically converted into accessors with basic documentation.

These accessors will just call get_remote() using the default conversation. Note that it is highly recommended that this be used only with scopes.GLOBAL scope.

conversation(scope=None)

Get a single conversation, by scope, that this relation is currently handling.

If the scope is not given, the correct scope is inferred by the current hook execution context. If there is no current hook execution context, it is assume that there is only a single global conversation scope for this relation. If this relation’s scope is not global and there is no current hook execution context, then an error is raised.

conversations()

Return a list of the conversations that this relation is currently handling.

Note that “currently handling” means for the current state or hook context, and not all conversations that might be active for this relation for other states.

classmethod from_name(relation_name, conversations=None)

Find relation implementation in the current charm, based on the name of the relation.

Returns:A Relation instance, or None
classmethod from_state(state)

Find relation implementation in the current charm, based on the name of an active state.

get_local(key, default=None, scope=None)

Retrieve some data previously set via set_local().

In Python, this is equivalent to:

relation.conversation(scope).get_local(key, default)

See conversation() and Conversation.get_local().

get_remote(key, default=None, scope=None)

Get data from the remote end(s) of the Conversation with the given scope.

In Python, this is equivalent to:

relation.conversation(scope).get_remote(key, default)

See conversation() and Conversation.get_remote().

is_state(state, scope=None)

Test the state for the Conversation with the given scope.

In Python, this is equivalent to:

relation.conversation(scope).is_state(state)

See conversation() and Conversation.is_state().

relation_name

Name of the relation this instance is handling.

remove_state(state, scope=None)

Remove the state for the Conversation with the given scope.

In Python, this is equivalent to:

relation.conversation(scope).remove_state(state)

See conversation() and Conversation.remove_state().

scope = 'unit'

Conversation scope for this relation.

The conversation scope controls how communication with connected units is aggregated into related Conversations, and can be any of the predefined scopes, or any arbitrary string. Connected units which share the same scope will be considered part of the same conversation. Data sent to a conversation is sent to all units that are a part of that conversation, and units that are part of a conversation are expected to agree on the data that they send, whether via eventual consistency or by having a single leader set the data.

The default scope is scopes.UNIT.

set_local(key=None, value=None, data=None, scope=None, **kwdata)

Locally store some data, namespaced by the current or given Conversation scope.

In Python, this is equivalent to:

relation.conversation(scope).set_local(data, scope, **kwdata)

See conversation() and Conversation.set_local().

set_remote(key=None, value=None, data=None, scope=None, **kwdata)

Set data for the remote end(s) of the Conversation with the given scope.

In Python, this is equivalent to:

relation.conversation(scope).set_remote(key, value, data, scope, **kwdata)

See conversation() and Conversation.set_remote().

set_state(state, scope=None)

Set the state for the Conversation with the given scope.

In Python, this is equivalent to:

relation.conversation(scope).set_state(state)

See conversation() and Conversation.set_state().

class states

Bases: charms.reactive.bus.StateList

This is the set of States that this relation could set.

This should be defined by the relation subclass to ensure that states are consistent and documented, as well as being discoverable and introspectable by linting and composition tools.

For example:

class MyRelationClient(RelationBase):
    scope = scopes.GLOBAL
    auto_accessors = ['host', 'port']

    class states(StateList):
        connected = State('{relation_name}.connected')
        available = State('{relation_name}.available')

    @hook('{requires:my-interface}-relation-{joined,changed}')
    def changed(self):
        self.set_state(self.states.connected)
        if self.host() and self.port():
            self.set_state(self.states.available)
toggle_state(state, active=<object object>, scope=None)

Toggle the state for the Conversation with the given scope.

In Python, this is equivalent to:

relation.conversation(scope).toggle_state(state, active)

See conversation() and Conversation.toggle_state().

charms.reactive.relations.relation_call(method, relation_name=None, state=None, *args)

Invoke a method on the class implementing a relation via the CLI

class charms.reactive.relations.scopes

Bases: object

These are the recommended scope values for relation implementations.

To use, simply set the scope class variable to one of these:

class MyRelationClient(RelationBase):
    scope = scopes.SERVICE
GLOBAL = 'global'

All connected services and units for this relation will share a single conversation. The same data will be broadcast to every remote unit, and retrieved data will be aggregated across all remote units and is expected to either eventually agree or be set by a single leader.

SERVICE = 'service'

Each connected service for this relation will have its own conversation. The same data will be broadcast to every unit of each service’s conversation, and data from all units of each service will be aggregated and is expected to either eventually agree or be set by a single leader.

UNIT = 'unit'

Each connected unit for this relation will have its own conversation. This is the default scope. Each unit’s data will be retrieved individually, but note that due to how Juju works, the same data is still broadcast to all units of a single service.