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.