Python FoO V2 Async in Actions

i am programming a funciton-based action type that uses async logic which is why the most convenient way of programming the function is to define it as async. This works without any issues in the code respository but throws an error when i try to use it in the action type.

the signature looks something like this:

async def calculate_mtb(...) -> list[OntologyEdit]:
    ...

and the error message I get is

Is it a problem of the annotated return type? Which type am i supposed to use? In typescript functions, we need to wrap the response type into Promise<> when doing an async function. Does something similar exist already for Python FoO?

This error is most commonly caused by directly returning the result of an un-awaited async function. See the snippet below for an example. This is standard async python behavior and pylance correctly flags the incorrect usage with the message: Type “CoroutineType[Any, Any, str]” is not assignable to return type “str”. I’ll look into why the Authoring language server isn’t flagging the same issue.

Separately it’s unexpected that it would work in the preview panel (especially published preview as opposed to live) and fail this way when called in an action. I would start by verifying that the version you have configured in the action succeeds when called from the published preview panel on that same version with the same parameters. Using the same parameters is important since it’s possible you only missed an await on a code path that’s only conditionally hit based on the input arguments.

import asyncio

from functions.api import function


@function
async def async_function() -> str:
    # the following will result in the error:
    # TypeError: 'coroutine' object cannot be converted to 'PyString'.
    # return async_helper()

    # the following will return correctly
    return await async_helper()


async def async_helper() -> str:
    await asyncio.sleep(1)
    return "hello world!"

Actually I tried it with simply synchronous code as well where no await is needed. It threw the error.

let me provide some more context:

@function(edits=[<ObjectTypes>])
async def calculate_mtb(mtb: <ObjectType>) -> list[OntologyEdit]:
    client = FoundryClient()
    edits = client.ontology.edits()
    # some black magic in here...

    return edits.get_edits()

so does the Foundryclient() know when it is in an async context? I would not expect to await .get_edits() since it is not typed as returning a coroutine

If I remove the “async” in the code, it works normal

I think I found the issue… my bad. I have a decorator that handles errors and raises them user-facing which did not differentiate between synchronous and async code.
I think this will be the issue…

import asyncio
from typing import TypeVar, Callable, cast
from functools import wraps, partial
from functions.api import UserFacingError

# Define a generic type for the function
F = TypeVar("F", bound=Callable[..., object])

def _generic_decorator(error: type[Exception], func: F) -> F:
    if asyncio.iscoroutinefunction(func):
        @wraps(func)
        async def async_wrapper(*args, **kwargs):
            try:
                return await func(*args, **kwargs)
            except error as e:
                raise UserFacingError(str(e)) from e
        return cast(F, async_wrapper)
    else:
        @wraps(func)
        def sync_wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except error as e:
                raise UserFacingError(str(e)) from e
        return cast(F, sync_wrapper)


def raise_as_user_facing_error(error: type[Exception]) -> F:
    return partial(_generic_decorator, error)

I wanted to leave this here as a small piece of docu just in case someone encounters the same problem and wants to proxy special library errors while keeping type safety.

edit: better decorator name - it raises the error not actually catching it.

2 Likes