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
(value)¶ Append object to the end of the list.
-
clear
()¶ Remove all items from list.
-
extend
(values)¶ 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
(key)¶ Remove and return item at index (default last).
Raises IndexError if list is empty or index is out of range.
-
remove
(value)¶ 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 ofRelatedUnit
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 byunit_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
orreceived_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 isjoined()
: 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
anddeparted
represents relationship events and have to be cleared manually by the handler.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 fromrelated-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. SeeCombinedUnitsView
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 withendpoint.{endpoint_name}.
. Then, any occurance of{endpoint_name}
will be replaced withself.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, theendpoint.
prefix can be omitted. The{name}}
portion will be passed tofrom_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.
-
manage_flags
()¶ Method that subclasses can override to perform any flag management needed during startup.
This will be called automatically after the framework-managed automatic flags have been updated.
-
register_triggers
()¶ Called once and only once for each named instance of this endpoint, before the endpoint’s automatic flags are updated.
This gives the endpoint implementation a chance to register triggers that will honor changes to the automatically managed flags.
-
relations
¶ Collection of
Relation
instances that are established for thisEndpoint
.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 adefaultdict(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
(key)¶ 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_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 withself.units.received
andself.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. SeeCombinedUnitsView
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()))
-
received_app
¶ A
JSONUnitDataView
of the app-level data received from this remote unit over the relation, with values being automatically decoded as JSON.
-
received_app_raw
¶ A
UnitDataView
of the raw app-level data received from this remote unit over the relation.
-
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_app
¶ This is the relation data that the local app publishes so it is visible to all related units. Use this to communicate with related apps. It is a writeable
JSONUnitDataView
.Only the leader can set the app-level relation data.
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_app_raw
¶ This is the raw relation data that the app publishes so it is visible to all related units. It is a writeable (by the leader only)
UnitDataView
. Only use this for backwards compatibility with interfaces that do not use JSON encoding. Useto_publish
instead.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. Useto_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. |
entry_points |
|
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 thedefault conversation
. Note that it is highly recommended that this be used only withscopes.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()
andConversation.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()
andConversation.get_remote()
.
-
is_flag_set
(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()
andConversation.is_state()
.
-
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()
andConversation.is_state()
.
-
relation_name
¶ Name of the relation this instance is handling.
-
remove_flag
(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()
andConversation.remove_state()
.
-
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()
andConversation.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 predefinedscopes
, 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_flag
(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()
andConversation.set_state()
.
-
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()
andConversation.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()
andConversation.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()
andConversation.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_flag
(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()
andConversation.toggle_state()
.
-
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()
andConversation.toggle_state()
.
-
-
charms.reactive.relations.
relation_from_state
(state)¶ Deprecated since version 0.5.0: Alias for
endpoint_from_flag()