charms.reactive.endpoints

Summary

This is the transitional location of the new Endpoint base for writing interface layers. Eventually, this should be moved in to charms.reactive.relations.

CachedKeyList Variant of KeyList where items are serialized and persisted or removed from the persisted copy, whenever the list is modified.
CombinedUnitsView A KeyList view of RelatedUnit items, with properties to access a merged view of all of the units’ data.
Endpoint New base class for creating interface layers.
JSONUnitDataView View of a dict that performs automatic JSON en/decoding of items.
KeyList List that also allows accessing items keyed by an attribute on the items.
RelatedUnit Class representing a remote unit on a relation.
Relation
UnitDataView View of a dict containing a unit’s data.

Reference

class charms.reactive.endpoints.CachedKeyList(cache_key, items, key_attr)

Bases: charms.reactive.endpoints.KeyList

Variant of KeyList where items are serialized and persisted or removed from the persisted copy, whenever the list is modified.

append(object) → None -- append object to end
clear() → None -- remove all items from L
extend(iterable) → None -- extend list by appending elements from the iterable
classmethod load(cache_key, deserializer, key_attr)

Load the persisted cache and return a new instance of this class.

pop([index]) → item -- remove and return item at index (default last).

Raises IndexError if list is empty or index is out of range.

remove(value) → None -- remove first occurrence of value.

Raises ValueError if the value is not present.

class charms.reactive.endpoints.CombinedUnitsView(items)

Bases: charms.reactive.endpoints.KeyList

A KeyList view of RelatedUnit items, with properties to access a merged view of all of the units’ data.

You can iterate over this view like any other list, or you can look up units by their unit_name. Units will be in order by relation ID and unit name. If a given unit name occurs more than once, accessing it by unit_name will return the one from the lowest relation ID:

# given the following relations...
{
    'endpoint:1': {
        'unit/1': {
            'key0': 'value0_1_1',
            'key1': 'value1_1_1',
        },
        'unit/0': {
            'key0': 'value0_1_0',
            'key1': 'value1_1_0',
        },
    },
    'endpoint:0': {
        'unit/1': {
            'key0': 'value0_0_1',
            'key2': 'value2_0_1',
        },
    },
}

from_all = endpoint.all_units['unit/1']
by_rel = endpoint.relations['endpoint:0'].units['unit/1']
by_index = endpoint.relations[0].units[1]
assert from_all is by_rel
assert by_rel is by_index

You can also use the received or received_raw properties just like you would on a single unit. The data in these collections will have all of the data from every unit, with units with the lowest relation ID and unit name taking precedence if multiple units have set a given field. For example:

# given the same relations as above...

# the values across all relations would be:
assert endpoint.all_units.received['key0'] == 'value0_0_0'
assert endpoint.all_units.received['key1'] == 'value1_1_0'
assert endpoint.all_units.received['key2'] == 'value2_0_1'

# across individual relations:
assert endpoint.relations[0].units.received['key0'] == 'value0_0_1'
assert endpoint.relations[0].units.received['key1'] == None
assert endpoint.relations[0].units.received['key2'] == 'value2_0_1'
assert endpoint.relations[1].units.received['key0'] == 'value0_1_0'
assert endpoint.relations[1].units.received['key1'] == 'value1_1_0'
assert endpoint.relations[1].units.received['key2'] == None

# and of course you an access them by individual unit
assert endpoint.relations['endpoint:1'].units['unit/1'].received['key0']                 == 'value0_1_1'
received

Combined JSONUnitDataView of the data of all units in this list, with automatic JSON decoding.

received_raw

Combined UnitDataView of the raw data of all units in this list, as raw strings.

class charms.reactive.endpoints.Endpoint(endpoint_name, relation_ids=None)

Bases: charms.reactive.relations.RelationFactory

New base class for creating interface layers.

This class is intended to create drop-in, backwards-compatible replacements for interface layers previously written using the old RelationBase base class. With the advantages of: having commonly used internal flags managed automatically, providing a cleaner, more easily understood pattern for interacting with relation data, and being able to use @when rather than @hook so that interface layers are more similar to charm layers and to remove one of the biggest barriers to upgrading from a non-reactive version of a charm to a reactive version.

Four flags are automatically managed for each endpoint. Endpoint handlers can react to these flags using the decorators.

  • endpoint.{endpoint_name}.joined is set when the endpoint is joined(): when the first remote unit from any relationship connected to this endpoint joins. It is cleared when the last unit from all relationships connected to this endpoint departs.

  • endpoint.{endpoint_name}.changed when any relation data has changed. It isn’t automatically cleared.

  • endpoint.{endpoint_name}.changed.{field} when a specific field has changed. It isn’t automatically cleared.

  • endpoint.{endpoint_name}.departed when a remote unit is leaving.

    It isn’t automatically cleared.

For the flags that are not automatically cleared, it is up to the interface author to clear the flag when it is “handled”. The following diagram shows how these flags relate. In summary, the joined flag represents the state of the relationship and will be automatically cleared when all units are gone. changed and departed represents relationship events and have to be cleared manually by the handler.

_images/endpoints-workflow.svg

These flags should only be used by the decorators of the endpoint handlers. While it is possible to use them with any decorators in any layer, these flags should be considered internal, private implementation details. It is the interface layers responsibility to manage and document the public flags that make up part of its API.

Endpoint handlers can iterate over the list of joined relations for an endpoint via the relations collection.

all_departed_units

Collection of all units that were previously part of any relation on this endpoint but which have since departed.

This collection is persistent and mutable. The departed units will be kept until they are explicitly removed, to allow for reasonable cleanup of units that have left.

Example: You need to run a command each time a unit departs the relation.

@when('endpoint.{endpoint_name}.departed')
def handle_departed_unit(self):
    for name, unit in self.all_departed_units.items():
        # run the command to remove `unit` from the cluster
        #  ..
    self.all_departed_units.clear()
    clear_flag(self.expand_name('departed'))

Once a unit is departed, it will no longer show up in all_joined_units. Note that units are considered departed as soon as the departed hook is entered, which differs slightly from how the Juju primitives behave (departing units are still returned from related-units until after the departed hook is complete).

This collection is a KeyList, so can be used as a mapping to look up units by their unit name, or iterated or accessed by index.

all_joined_units

A list view of all the units of all relations attached to this Endpoint.

This is actually a CombinedUnitsView, so the units will be in order by relation ID and then unit name, and you can access a merged view of all the units’ data as a single mapping. You should be very careful when using the merged data collections, however, and consider carefully what will happen when the endpoint has multiple relations and multiple remote units on each. It is probably better to iterate over each unit and handle its data individually. See CombinedUnitsView for an explanation of how the merged data collections work.

Note that, because a given application might be related multiple times on a given endpoint, units may show up in this collection more than once.

all_units

Deprecated since version 0.6.1: Use all_joined_units instead

endpoint_name

Relation name of this endpoint.

expand_name(flag)

Complete a flag for this endpoint by expanding the endpoint name.

If the flag does not already contain {endpoint_name}, it will be prefixed with endpoint.{endpoint_name}.. Then, str.format will be used to fill in {endpoint_name} with self.endpoint_name.

classmethod from_flag(flag)

Return an Endpoint subclass instance based on the given flag.

The instance that is returned depends on the endpoint name embedded in the flag. Flags should be of the form endpoint.{name}.extra..., though for legacy purposes, the endpoint. prefix can be omitted. The {name}} portion will be passed to from_name().

If the flag is not set, an appropriate Endpoint subclass cannot be found, or the flag name can’t be parsed, None will be returned.

classmethod from_name(endpoint_name)

Return an Endpoint subclass instance based on the name of the endpoint.

is_joined

Whether this endpoint has remote applications attached to it.

joined

Deprecated since version 0.6.3: Use is_joined instead

relations

Collection of Relation instances that are established for this Endpoint.

This is a KeyList, so it can be iterated and indexed as a list, or you can look up relations by their ID. For example:

rel0 = endpoint.relations[0]
assert rel0 is endpoint.relations[rel0.relation_id]
assert all(rel is endpoint.relations[rel.relation_id]
           for rel in endpoint.relations)
print(', '.join(endpoint.relations.keys()))
class charms.reactive.endpoints.JSONUnitDataView(data, writeable=False)

Bases: collections.UserDict

View of a dict that performs automatic JSON en/decoding of items.

Like UnitDataView, this is like a defaultdict(lambda: None) which cannot be modified by default.

When decoding, if a value fails to decode, it will just return the raw value as a string.

When encoding, it ensures that keys are sorted to maintain stable and consistent encoded representations.

The original data, without automatic encoding / decoding, can be accessed as raw_data.

get(k[, d]) → D[k] if k in D, else d. d defaults to None.
modified

Whether this collection has been modified.

raw_data

The data for this collection without automatic encoding / decoding.

This is an UnitDataView instance.

setdefault(k[, d]) → D.get(k,d), also set D[k]=d if k not in D
writeable

Whether this collection can be modified.

class charms.reactive.endpoints.KeyList(items, key_attr)

Bases: list

List that also allows accessing items keyed by an attribute on the items.

Unlike dicts, the keys don’t need to be unique.

items()
keys()

Return the keys for all items in this KeyList.

Unlike a dict, the keys are not necessarily unique, so this list may contain duplicate values. The keys will be returned in the order of the items in the list.

pop([index]) → item -- remove and return item at index (default last).

Raises IndexError if list is empty or index is out of range.

values()

Return just the values of this list.

This is equivalent to list(keylist).

class charms.reactive.endpoints.RelatedUnit(relation, unit_name, data=None)

Bases: object

Class representing a remote unit on a relation.

application_name

The name of the application to which this unit belongs.

received

A JSONUnitDataView of the data received from this remote unit over the relation, with values being automatically decoded as JSON.

received_raw

A UnitDataView of the raw data received from this remote unit over the relation.

relation

The relation to which this unit belongs.

unit_name

The name of this unit.

class charms.reactive.endpoints.Relation(relation_id)

Bases: object

application_name

The name of the remote application for this relation, or None.

This is equivalent to:

relation.units[0].unit_name.split('/')[0]
endpoint

This relation’s Endpoint instance.

endpoint_name

This relation’s endpoint name.

This will be the same as the Endpoint’s endpoint name.

joined_units

A list view of all the units joined on this relation.

This is actually a CombinedUnitsView, so the units will be in order by unit name, and you can access a merged view of all of the units’ data with self.units.received and self.units.received. You should be very careful when using the merged data collections, however, and consider carefully what will happen when there are multiple remote units. It is probabaly better to iterate over each unit and handle its data individually. See CombinedUnitsView for an explanation of how the merged data collections work.

The view can be iterated and indexed as a list, or you can look up units by their unit name. For example:

by_index = relation.units[0]
by_name = relation.units['unit/0']
assert by_index is by_name
assert all(unit is relation.units[unit.unit_name]
           for unit in relation.units)
print(', '.join(relation.units.keys()))
relation_id

This relation’s relation ID.

to_publish

This is the relation data that the local unit publishes so it is visible to all related units. Use this to communicate with related units. It is a writeable JSONUnitDataView.

All values stored in this collection will be automatically JSON encoded when they are published. This means that they need to be JSON serializable! Mappings stored in this collection will be encoded with sorted keys, to ensure that the encoded representation will only change if the actual data changes.

Changes to this data are published at the end of a succesfull hook. The data is reset when a hook fails.

to_publish_raw

This is the raw relation data that the local unit publishes so it is visible to all related units. It is a writeable UnitDataView. Only use this for backwards compatibility with interfaces that do not use JSON encoding. Use to_publish instead.

Changes to this data are published at the end of a succesfull hook. The data is reset when a hook fails.

units

Deprecated since version 0.6.1: Use joined_units instead

class charms.reactive.endpoints.UnitDataView(data, writeable=False)

Bases: collections.UserDict

View of a dict containing a unit’s data.

This is like a defaultdict(lambda: None) which cannot be modified by default.

get(k[, d]) → D[k] if k in D, else d. d defaults to None.
modified

Whether this collection has been modified.

setdefault(k[, d]) → D.get(k,d), also set D[k]=d if k not in D
writeable

Whether this collection can be modified.

charms.reactive.relations

Summary

This is the older API for interface layers that are based on RelationBase. It is recommended that you try using Endpoint instead.

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 A base class for relation implementations.
RelationFactory Produce objects for interacting with a relation.
endpoint_from_flag The object used for interacting with relations tied to a flag, or None.
endpoint_from_name The object used for interacting with the named relations, or None.
relation_call Invoke a method on the class implementing a relation via the CLI
relation_factory Get the RelationFactory for the given relation name.
scopes These are the recommended scope values for relation implementations.

Reference

charms.reactive.relations.endpoint_from_name(endpoint_name)

The object used for interacting with the named relations, or None.

charms.reactive.relations.endpoint_from_flag(flag)

The object used for interacting with relations tied to a flag, or None.

charms.reactive.relations.relation_from_flag(flag)

Deprecated since version 0.6.0: Alias for endpoint_from_flag()

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.

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

Bases: charms.reactive.relations.RelationFactory

A base class for 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_flag(flag)

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

You should not use this method directly. Use endpoint_from_flag() instead.

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)

Deprecated since version 0.6.1: use endpoint_from_flag() instead

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.flags.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_from_state(state)

Deprecated since version 0.5.0: Alias for endpoint_from_flag()