Ohara Lab

Dynamic Dispatch and Explicit APIs

Back to the development guide

Dynamic dispatch

Dynamic dispatch exposes DeviceClass methods through a communication Frame without rewriting them as transport-specific endpoints. “Transparent” does not imply that every Python object or side effect behaves identically across a network boundary.

ese774 Frame

With api_spec=None, FastApiServer uses DeviceRouter to expose public methods. Names beginning with _ are excluded, and dispatch_exclude provides additional exclusions. Static properties use separate getter/setter transport. Device-specific Routers can coexist with automatic dispatch. adapter.py serializes JSON and reconstructs supported typed return values using available annotations; reconstruction is not guaranteed for arbitrary objects.

gRPC Frame

grpc_server.py exposes Describe, Call, and StreamCall. grpc_client.py binds remote methods from the Describe response. This is distinct from FastAPI routing and OpenAPI definitions.

Explicit APIs and OpenAPI

ese774 supports explicit APIs through Pydantic models and ApiSpec, including defined request/response models and API paths. Explicit routes and automatic dispatch can be combined where appropriate.

Example A: automatic dispatch

The server discovers public DeviceClass methods without requiring a Pydantic model or ApiSpec for each method.

from ese774_frame.api_server import FastApiServer

class TemperatureCtrl:
    def __init__(self, logger=None):
        self._temperature = 25.0

    def read_temperature(self) -> float:
        return self._temperature

    def set_temperature(self, value: float) -> None:
        self._temperature = value

server = FastApiServer(
    device_cls=TemperatureCtrl,
    router_cls=None,
    config=None,
    api_spec=None,              # Automatic dispatch
    device_kwargs={},
    object_name="temperature",
)

This is a configuration example; the server startup call is omitted. The DeviceClass contains no HTTP-specific code. Client subclasses can add operations where necessary. See the development guide for actual server-side router and client-side override examples.

Example B: explicit API using Pydantic and ApiSpec

COBOTTA’s server_fastapi/spec_ctrl.py declares operations as a list of ApiSpec objects, with request/response models under server_fastapi/models/. The following is an excerpt of the supplied COBOTTA definitions.

from ese774_frame.models.api_spec import ApiSpec
from cobotta2.server_fastapi.models.common import ResultResponse
from cobotta2.server_fastapi.models.var import GetVarDataRequest, SetVarDataRequest

cobotta_ctrl_api_spec = [
    ApiSpec(
        name="get_var",
        object_name="cobotta",
        request_model=GetVarDataRequest,
        response_model=ResultResponse,
        method="post",
        description="get_ver(name, id_num)",
        summary="Read an internal COBOTTA variable",
    ),
    ApiSpec(
        name="set_var",
        object_name="cobotta",
        request_model=SetVarDataRequest,
        response_model=ResultResponse,
        method="post",
        description="set_ver(name, id_num, value)",
        summary="Write an internal COBOTTA variable",
    ),
]

GetVarDataRequest and SetVarDataRequest are request models, and ResultResponse is a response model. These are two operations from a larger actual API specification, not its complete definition. Model fields live in models/var.py. On the server, CobottaRouterCtrl(DeviceRouter) adds request/response handling. On the client, SyncCobottaClient(SyncDeviceClient) and AsyncCobottaClient(AsyncDeviceClient) pass cobotta_ctrl_api_spec to their base classes.

This approach explicitly declares operation names, HTTP methods, and input/output models. It does not relocate equipment control logic from the DeviceClass into Pydantic models or routers.

gRPC

gRPC Frame supports automatic dispatch only. It retains the same separation between DeviceClass, server-side adaptation, and client-side extensions, but does not require the ese774-specific ApiSpec definitions shown above.

DeviceProxy and .pyi

DeviceProxy creates a registered Frame client; it does not turn DeviceClass into a transport-dependent class. Generated .pyi files support static typing and IDE completion; they do not alter runtime transport behavior. Stubs should match the actual exposed DeviceClass API.

Error handling and concurrency

Separate device-level error handling from transport-level propagation. Async client methods do not guarantee concurrent device execution. Review exposure, locking, and timeouts for each instrument.

Worked examples from ese774_dummy

Earlier version (archival reference)

Archived dynamic-dispatch article