Skip to content

Job result openers

client.get_job_results(job_id) retrieves result values and references. client.open_job_result(job_id) waits for completion and opens a selected output using a registered opener. Cuiman includes Pillow, xarray, pandas, and GeoPandas openers; each requires its corresponding optional library. The Pillow image opener takes precedence over the dataset openers for supported images, including PNG and JPEG, and returns a PIL.Image.Image. Custom openers extend or specialize this behavior.

The following example uses simulate_scene from the local test service. Run the Python blocks in order from the repository root in the Pixi environment. The maintained source is openers.py.

Prepare a small scene

This request creates two variables on a 4 × 4 grid for two dates. The generated values are zero; the example demonstrates data access, not a scientific product.

{
  "inputs": {
    "var_names": "a, b",
    "bbox": [10, 40, 12, 42],
    "resolution": 0.5,
    "start_date": "2025-01-01",
    "end_date": "2025-01-03",
    "periodicity": 1,
    "output_path": "guide-scene.zarr"
  }
}

output_path is relative to the server's working directory. The process replaces existing data at that path, so choose an unused path. The example assumes server and client run on the same machine and can access the same filesystem. A file:// link from a remote server is not automatically accessible to your client. Omitting the path uses server-process memory, which a separate client process cannot read.

Submit once

from pathlib import Path
from urllib.parse import urlsplit
from urllib.request import url2pathname

import xarray as xr

from cuiman import Client
from cuiman.api.opener import JobResultOpenContext, JobResultOpener
from gavicore.models import ProcessRequest


client = Client(api_url="http://127.0.0.1:8008", auth={"auth_type": "none"})

def submit_scene(client: Client, request_path: Path) -> str:
    """Load a scene request and return the newly submitted job's ID."""
    request = ProcessRequest.model_validate_json(
        request_path.read_text(encoding="utf-8")
    )
    job = client.execute_process("simulate_scene", request=request)
    print(job.model_dump_json(indent=2))
    return job.jobID



request_path = Path("examples/guides/cuiman/simulate-scene-request.json")
job_id = submit_scene(client, request_path)

Retain the returned job_id. Once its status is successful, client.get_job_results(job_id) shows a link to the dataset with media type application/zarr.

Open with a built-in opener

def open_scene(client: Client, job_id: str) -> xr.Dataset:
    """Wait up to 30 seconds and use the built-in xarray opener."""
    return client.open_job_result(
        job_id,
        output_name="return_value",
        data_type=xr.Dataset,
        engine="zarr",
        timeout=30,
        poll_interval=0.1,
    )



dataset = open_scene(client, job_id)
try:
    print(dataset)
    print(dict(dataset.sizes))
finally:
    dataset.close()

data_type=xr.Dataset selects a compatible opener, output_name selects the process output, and engine="zarr" is forwarded to xarray. The printed sizes are lat: 4, lon: 4, and time: 2.

The helper waits up to 30 seconds for completion. A running job that exceeds this deadline raises TimeoutError; failed or dismissed jobs raise JobResultStatusError. Inspect the job before retrying. Opening an existing job's output does not submit a new job. Always close datasets after use.

Add a custom opener

A custom opener decides whether it can handle the requested output, then opens it. This example specializes in local Zarr links and converts file URIs to native paths, including on Windows. It also handles escaped spaces in paths, which the built-in reader's current file-URI handling may not resolve. Use paths without spaces for the built-in example, or this custom opener:

class LocalZarrOpener(JobResultOpener):
    """Open local Zarr links as datasets, converting file URIs to native paths."""

    async def accept_job_result(self, ctx: JobResultOpenContext) -> bool:
        """Accept the selected output only if it is a local Zarr dataset."""
        link = ctx.output_link
        if link is None or ctx.data_type not in (None, xr.Dataset):
            return False
        url = urlsplit(link.href)
        return (
            ctx.output_media_type == "application/zarr"
            and url.scheme == "file"
            and url.netloc in ("", "localhost")
        )

    async def open_job_result(self, ctx: JobResultOpenContext) -> xr.Dataset:
        """Pass the native filesystem path and reader options to xarray."""
        link = ctx.output_link
        assert link is not None
        path = url2pathname(urlsplit(link.href).path)
        return xr.open_zarr(path, **ctx.options)

ctx.output_link resolves the requested output name; it can be None. The acceptance check also respects the requested data type and media type. The example imports xarray directly because it is required by this guide; reusable plugins can implement is_usable() to detect optional dependencies.

Register the opener temporarily and use the same completed job:

def open_with_custom_opener(client: Client, job_id: str) -> xr.Dataset:
    """Temporarily prefer the custom opener, restoring registration afterward."""
    unregister = client.config.register_job_result_opener(LocalZarrOpener)
    try:
        return client.open_job_result(
            job_id, output_name="return_value", data_type=xr.Dataset, timeout=30
        )
    finally:
        unregister()



dataset = open_with_custom_opener(client, job_id)
try:
    print(dataset)
finally:
    dataset.close()

New registrations take precedence over built-ins. Registration belongs to the client's configuration class, so it affects clients sharing that class. The returned callback removes the registration, including when opening fails. For an application's permanent extensions, prefer a ClientConfig subclass with extra_job_result_openers; see Customization.

Close the client

client.close()

The complete script uses try/finally for client and dataset cleanup. Run it to submit one scene and open it with the built-in opener:

python -m examples.guides.cuiman.openers

The original opener notebook remains available as a historical example. Its assumption that no openers are registered by default no longer applies.