AsyncClient

The AsyncClient offered through the Omniverse Services framework exposes an interface allowing communication between distant endpoints, making it possible to execute queries hosted on remote servers, where dedicated features may be hosted in order to implement dedicated workflows.

This AsyncClient builds its interface on top of the OpenAPI specifications, which allows it to express typical HTTP server methods in a straightforward way, by facilitating the documentation of server endpoints defined by Services.

Example

To provide an illustrative example of the AsyncClient, let’s reuse the Viewport Capture Service sample already shared in another section of the documentation, in order to provide additional usage information on how the remote-hosted service can be queried.

An asynchronous client can be included in an Omniverse extension by first declaring the omni.services.client module within the extension’s TOML definition file, then importing the Python components and formatting the client along the declaration of the Service’s definition. Using the Viewport Capture example, we can observe that the Service exposes a single POST endpoint, which is tasked with receiving incoming requests responsible for capturing a screenshot of the viewport as it appears in the application’s window:

 1# [...]
 2
 3# Using the `@router` annotation, we'll tag our `capture` function handler to document the responses and path of the
 4# API, once again using the OpenAPI specification format.
 5@router.post(
 6    path="/capture",
 7    summary="Capture a given USD stage",
 8    description="Capture a given USD stage as an image.",
 9    response_model=ViewportCaptureResponseModel,
10)
11async def capture(request: ViewportCaptureRequestModel,) -> ViewportCaptureResponseModel:
12    # For now, let's just print incoming request to the log to confirm all components of our extension are properly
13    # wired together:
14    carb.log_warn(f"Received a request to capture an image of \"{request.usd_stage_path}\".")
15
16    # Let's return a JSON response, indicating that the viewport capture operation failed to avoid misinterpreting the
17    # current lack of image output as a failure:
18    return ViewportCaptureResponseModel(
19        success=False,
20        captured_image_path=None,
21        error_message="Image not yet captured.",
22    )

To trigger the remote execution of this endpoint, for example in a situation where an integration requires triggering the viewport capture of a remote server, the following integration can be developed in order to trigger execution of the Service defined by this API:

1from omni.services.client import AsyncClient
2from omni.services.core import main
3from omni.services.example.viewport.capture.services.capture import ViewportCaptureRequestModel
4
5client = AsyncClient(uri="local://", app=main.get_app())
6request = ViewportCaptureRequestModel(usd_stage_path="omniverse://path/to/usd/stage.usd")
7response = await client.capture.post(**request.to_dict())

Note

Notice that in order to reach the endpoint defined by a Service, the AsyncClient emits a request against the name of the action defined by API. Using the preceding example, the capture API ends up being targeted by the client and scoped using the .post(...) HTTP verb, as the Service’s router defines the action according to this standard option in order to support supplying issuing queries against the API.

In a similar fashion, PUT requests can be issued by formatting client requests using the .put(...) endpoint, or methods implementing the DELETE keyword can be issued by addressing a request to the name of the service suffixed by the .delete(...) keyword, as is also the case for the GET or PATCH methods which can be implemented by Services.

Naming convention

The name of the API used in the AsyncClient to identify the endpoint to target is based on the path associated to the endpoint, as it appears in the OpenAPI specification generated for the Service. As Python syntax prohibits using special characters such as dashes (-) in property names, it is worth noting that an endpoint containing dashes to separate words in order to expose pretty URLs will be replaced by underscore characters (_) in the equivalent Python attribute name.

For example, considering an endpoint defined with the "/capture-application-screenshot" path:

 1# [...]
 2
 3# Example of an endpoint definition using dash characters (`-`) to separate words, in order to make them
 4# human-friendly:
 5@router.get(
 6    path="/capture-application-screenshot",
 7    summary="Capture a screenshot of the application",
 8    description="Capture a screenshot of the application in its current state.",
 9    response_model=ApplicationCaptureResponseModel,
10)
11async def application_capture(request: ApplicationCaptureRequestModel,) ->ApplicationCaptureResponseModel:
12    return ApplicationCaptureResponseModel(
13        # [...]
14    )

This endpoint could be called using the following naming convention on an AsyncClient, replacing the dash characters (-) in its path with underscore characters (_):

1# [...]
2
3# Example usage of an `AsyncClient` to call the remote endpoint defined above:
4client = AsyncClient(uri="[...]")
5response = await client.capture_application_screenshot.get(
6    # [...]
7)

Including additional Headers

In some circumstances, a Service may require providing additional HTTP Headers along with parameters expected by the endpoint. Such requirements may include authorization or authentication features to provide along with the request, in order to confirm the identity or permission associated to a request. In such cases, the __headers__ parameter can be emitted along with the AsyncClient, as illustrated in the following example:

 1from typing import Any, Dict
 2
 3from omni.services.client import AsyncClient
 4from omni.services.core import main
 5
 6
 7async def get_farm_queue_settings(farm_queue_server_url: str) -> Dict[str, Any]:
 8    """
 9    Return the settings from the selected Omniverse Farm Queue.
10
11    Args:
12       farm_queue_server_url(str): Server URL of the selected Omniverse Farm Queue.
13
14    Returns:
15       Dict[str, Any]: Settings exposed by the selected Omniverse Farm Queue.
16
17    """
18    try:
19        client = AsyncClient(uri=farm_queue_server_url)
20        settings_response = await client.queue.settings.get(__headers__=await _get_headers())
21        return settings_response["settings"]
22    except:
23        return {}
24
25async def _get_headers(self) -> Dict[str, str]:
26    """
27    Return the headers to send for each request against the Omniverse Farm API.
28
29    Args:
30       None
31
32    Returns:
33       Dict[str, str]: The headers to send along with each request made against the Omniverse Farm API.
34
35    """
36    headers = {
37        "Accept": "application/json",
38        "Content-Type": "application/json",
39        "Authorization": "Bearer <secret>", # NOTE: Provide the `<secret>` key to authorize against the target Farm Queue.
40    }
41    return headers