Returns

This page describes the the role played by @returns and @app.task(returns=...) within FireX service definitions.

In the FireX world, service return values need to be named. This is required so that they can be added in the bog of argument/values that flows down a chain.

Read about the chain dataflow here.

See a more involved dataflow example that uses the bog explicitly here.

FireX supports two functionally equivalent primitives to specify return value names: - The @returns decorator, where you provides a comma separated lost of the names of your return values. - The returns argument of the @app.task decorator, where you provide a Python list with the names of your return value.

When a service performs a return value1, value2, etc, the specified return values will be associated with the names provided using the primitives above, matching the order in which they are defined.

For example:

@app.task()
@returns('first_name', 'last_name')
def first_and_last_name():
  return 'John', 'Doe'

would return and add the following name/value pairs to the bog:

'first_name` : 'John'
'last_name`  : 'Doe'

Here’s another example taken from an existing service:

@app.task(returns=['username'])
def getusername():
    return getuser()

If you run this service by itself from the CLI, you can see it returns “username:youruserid”:

$ firexapp submit --chain getusername --sync
 [13:44:49][HOST-Q54A3] FireX ID: FireX-jdoe-210127-134449-23397
 ...
 Returned values:
   username    jdoe

Returning a dynamic number of values

FireX supports the notion of returning a variable number of results by providing the FireX.DYNAMIC_RETURN keyword in place of an actual return value name. You then have to return a dictionary of name/values in that position of the return values.

This is especially useful in the context of plugins, where the plugin wants to invoke the original service and return all the same name/values as the original, but without having to know and hardcode all of these return values.

See this detailed example using plugins with FireX.DYNAMIC_RETURN.

Handling return value name mismatch

When chaining services together, there are cases where a name returned by a service doesn’t match the expected input name of downstream services. To handle a mismatch between the name produced by an upstream service and the name expected by a downstream service, you can use the special @ prefix to rename a value before supplying it to a service. This is shown in a full example here in the programming guide, but we’ll take a look at the pertinent code:

@app.task(bind=True, returns=['guests_greeting'])
def greet_guests(self, guests):
    ...

@app.task(returns=['amplified_message'])
def amplify(to_amplify):
    ...

@app.task(bind=True, returns=['amplified_greeting'])
def amplified_greet_guests(self, guests):
    amplified_greet_guests_chain = greet_guests.s(guests=guests) | amplify.s(to_amplify='@guests_greeting')
    ...

The amplified_greet_guests is creating a chain from two services, greet_guests and amplify, with the intention of amplifying the result of greet_guests. However, amplify expects an input argument named to_amplify, while greet_guests produces a return value named guests_greeting. The chain construction uses the special @ prefix to perform this mapping when defining the amplify.s(to_amplify='@guests_greeting') signature. This ensures amplify receives the guests_greeting value as its required to_amplify argument.

Handling return value name clashes:

There are also cases where you would like to take a copy of one of the returned values in the middle of a chain to avoid it being trampled by one of the downstream service which is using the same return name.

To handle such cases, you can use the CopyBogKeys service.

Consider the following contrived example where we chain two greet services one right after another:

@app.task(returns=['greeting'])
def greet(name):
    return 'Hello %s!' % name

@app.task(returns=['lee_greeting', 'tom_greeting'])
def greet_lee_and_tom():
    chain = greet.s("Lee") | greet.s("Tom")
    results = self.enqueue_child_and_get_results(chain)
    ...

Can we get the greeting for Lee from the results? Unfortunately, no, because greet always names its return values greeting, causing Lee’s greeting to be trampled by Tom’s greeting. We can use CopyBogKeys.s({'greeting': 'lee_greeting'}) between the services to copy the greeting for Lee into the name lee_greeting:

@app.task(returns=['lee_greeting', 'tom_greeting'])
def greet_lee_and_tom():
    chain = greet.s("Lee") | CopyBogKeys.s({'greeting': 'lee_greeting'}) | greet.s("Tom")
    results = self.enqueue_child_and_get_results(chain)
    lee_greeting = results['lee_greeting']
    tom_greeting = results['greeting']
    return lee_greeting, tom_greeting

The CopyBogKeys service receives a dict that tells the service to copy values of existing names (e.g. greeting) to new names (e.g. lee_greeting). This prevents Lee’s greeting from being trampled so that the service can return both greetings. Note that the return value names of CopyBogKeys are necessarily dynamic (i.e. determined by its input argument), and CopyBogKeys therefore must use FireX.DYNAMIC_RETURN in its service definition.

CopyBogKeys also accepts a strict=True|False argument which will specify the behavior if some of the fields specified in the mapping dictionary are not present in the bog. If set to False (default), the missing fields are simply skipped over. If set to True, CopyBogKeys will abort and fail when it encounters a missing field.