Services

Services are the real “guts” of an Envisage application – they are the objects that do the actual work! To get the job done, services obviously need to interact, and so Envisage provides a way for them to find each other. This is where the service registry comes in.

Don’t be fazed by the term service. In Envisage it just means any objects in your application that you want to share between plugins. Services can be any Python object and do not have to inherit from any Envisage class or even implement any particular interface!

Service Registry

The service registry provides a “Yellow Pages” style mechanism, in that services are published and looked up by protocol meaning interface, or type. It is called a “Yellow Pages” mechanism because it is just like looking up a telephone number in the “Yellow Pages” phone book. You use the “Yellow Pages” instead of the “White Pages” when you don’t know the name of the person you want to call but you do know what kind of service you require. For example, if you have a leaking pipe, you know you need a plumber, so you pick up your “Yellow Pages”, go to the “Plumbers” section and choose one that seems to fit the bill based on price, location, certification, etc. The service registry does exactly the same thing as the “Yellow Pages”, only with objects, and it even allows you to publish your own entries for free (unlike the “real” one)!

In an Envisage application, the service registry is accessed through the following methods on the IApplication interface:

def get_service(self, protocol, query='', minimize='', maximize=''):
    """ Return at most one service that matches the specified query.

    """

def get_service_properties(self, service_id):
    """ Return the dictionary of properties associated with a service.

    """

def get_services(self, protocol, query='', minimize='', maximize=''):
    """ Return all services that match the specified query.

    """

def register_service(self, protocol, obj, properties=None):
    """ Register a service.

    Returns a service ID that can be used to retrieve any service
    properties, and to unregister the service.

    """

def unregister_service(self, service_id):
    """ Unregister a service.

    """

The easiest way to explain the workings of these methods is to take a look at some examples, and so to continue our plumber theme, let’s assume we have the following interface and implementation

class IPlumber(Interface):
    """ What plumbers do! """

    #: The plumber's name.
    name = Str

    #: The plumber's location.
    location = Str

    #: The price per hour (in say, Estonian Krooni ;^)
    price = Int

    def fix_leaking_pipe(self, pipe):
        """ Fix a leaking pipe! """


@provides(IPlumber)
class Plumber(HasTraits):
    """ An actual plumber implementation! """

    #: The plumber's name.
    name = Str

    #: The plumber's location.
    location = Str

    #: The price per hour (in say, Estonian Krooni ;^)
    price = Int

    def fix_leaking_pipe(self, pipe):
        """ Fix a leaking pipe! """

        # code that actually fixes it!

Registering a service

To register a service, create an object and call the register_service() method, passing it the protocol (interface or type) to publish the object under (for “protocol” think “Yellow Pages” section), and the object to publish. Note that the object to publish does not have to inherit from any particular base class or implement any special interface – any arbitrary Python object will do:

fred = Plumber(name='fred', location='BH1', price=90)
fred_id = application.register_service(IPlumber, fred)

Note that each registered service gets assigned an ID that is unique within the current process. This can be used later to access its properties, or to unregister it, etc.

You can also associate an arbitrary dictionary of properties with an object when you register it. These properties, along with the actual attributes of the service itself, can be used later to lookup the service using the query mechanism as shown in Section 4.

wilma = Plumber(name='wilma', location='BH6')
wilma_id = application.register_service(IPlumber, wilma, {'price' : 125})

Note that the symbol name of the protocol can be specified instead of the actual type or class. Say, for example, that the IPlumber interface can be imported via ‘from acme.plumber.api import IPlumber’, then the registration can be written as:

wilma_id = application.register_service(
    'acme.plumber.api.IPlumber',
    wilma,
    {'price' : 125}
)

This comes in handy when using service factories (see later) to make sure that implementation classes are imported only when necessary.

Looking up a service

Looking up a service is just as easy – call get_service() specifiying the protocol of the service required:

plumber = application.get_service(IPlumber)

Assuming that we have registered both fred and wilma as in Section 1, then there is no way of knowing which of those objects would be returned. The choice of the object returned does not necessarily reflect the order in which they were added, so don’t depend on it.

Note that the symbol name of the protocol can be specified instead of the actual type or class. Say, for example, that the IPlumber interface can be imported via ‘from acme.plumber.api import IPlumber’, then the service lookup can be written as:

plumber = application.get_service('acme.plumber.api.IPlumber')

This comes in handy when using service factories (see later) to make sure that implementation classes are imported only when necessary.

Looking up a list of services

You can also look up all services of a particular protocol:

plumbers = application.get_services(IPlumber)

Assuming the registrations in Section 1, this returns a list containing both fred and wilma, again in arbitrary order.

Using queries

The get_service() and get_services() methods both take optional arguments that allow more control over the selection of an appropriate service. The first of these is the query argument, which is a string containing an arbitrary Python expression that is evaluated for each service, with the service only being returned if the expression evaluates to True. The namespace that the expression is evaluated in is created by first adding each of the service’s attributes, followed by any additional properties that were specified when the service was registered (i.e., properties take precedence over attributes).

Once again, assuming that we have registered fred and wilma as in Section 1, let’s look at how to use the query mechanism to be more selective about the plumber(s) we look up.

Find all plumbers whose price is less than 100 Krooni/Hour:

plumbers = application.get_services(IPlumber, "price < 100")

This query would return a list containing one plumber, fred.

Find plumbers named fred:

plumbers = application.get_services(IPlumber, "name == 'fred'")

This query, again (and unsurprisingly), would return a list containing just fred.

Queries can be used with the singular form of the get_service() method too, in which case only one of the services that matches the query is returned:

plumber = application.get_service(IPlumber, "price < 200")

This query would return either fred or wilma.

Using minimize and maximize

The minimize and maximize (optional) arguments to the get_service() and get_services() methods allow the services returned to be sorted by an attribute or property in either ascending or descending order respectively.

To find the cheapest plumber:

cheapest = application.get_service(IPlumber, minimize='price')

Or, if you believe that you get what you pay for, the most expensive:

most_expensive = application.get_service(IPlumber, maximize='price')

The minimize and maximize arguments can also be used in conjunction with a query. For example to find the cheapest plumber in my area:

cheap_and_local = application.get_service(
    IPlumber,
    location='BH6',
    minimize='price',
)

This query would definitely give the job to wilma!

Unregistering a service

When you register a service, Envisage returns a value that uniquely identifies the service within the current process (i.e., it is not suitable for persisting to use next time the application is run). To unregister a service, call the unregister_service() method, passing in the appropriate identifier:

fred = Plumber(name='fred', location='BH1', price=90)
fred_id = application.register_service(IPlumber, fred)

...

application.unregister_service(fred_id)

Getting any additional service properties

If you associate an arbitrary dictionary of properties with an object when you register it, you can retrieve those properties by calling the get_service_properties() method with the appropriate service identifier:

wilma = Plumber(name='wilma', location='BH6')
wilma_id = application.register_service(
    IPlumber,
    wilma,
    {'price':125},
)

...

properties = application.get_service_properties(wilma_id)

This call would return a dictionary containing the following:

{'price' : 125}

To set the properties for a service that has already been registered, use:

wilma = Plumber(name='wilma', location='BH6')
wilma_id = application.register_service(
    IPlumber,
    wilma,
    {'price':125},
)

...

application.set_service_properties(wilma_id, {'price' : 150})

Note however, that in practise, it is more common to use the actual attributes of a service object for the purposes of querying, but this is useful if you want additional properties that aren’t part of the object’s type.

Service Factories

Last, but not least, we will look at an important feature of the service registry, namely, service factories.

Service factories allow a Python callable to be registered in place of an actual service object. The callable is invoked the first time anybody asks for a service with the same type that the factory was registered against, and the object returned by the callable replaces the factory in the registry (so that the next time it is asked for it is simply returned as normal).

To register a service factory, just register any callable that takes two arguments. The first is the protocol (type) of the service being requested, and the second is the (possibly empty) dictionary of properties that were registered along with the factory, e.g.:

def wilma_factory(protocol, properties):
    """ A service factory that creates wilma the plumber! """

    return Plumber(name='wilma', location='BH6')

To register the factory, we just use ‘application.register_service’ as usual:

wilma_id = application.register_service(
    IPlumber,
    wilma_factory,
    {'price':125},
)

Now, the first time somebody tries to get any ‘IPlumber’ service, the factory is called and the returned plumber object replaces the factory in the registry.