REST binding#
Carrying values over HTTP.
The REST semantic connector: the connector seam reaches a peer middleware.
A parameter binds a REST connector when two things hold, neither of them a marker on the
parameter itself. First, the parameter is interface-accessible at all — a subproperty of
inf:isInterfaceAccessibleParameter, the generic root, with no more specific protocol
marker (a parameter that also carries inf:hasMQTTTopic etc. resolves to that protocol’s
binding instead; wiring.py’s _descriptor_for already picks the most specific
registered match). Second, the parameter’s resource carries a live svc:address on its
Service, one hop out through svc:isServiceOf. That address is not a marker on the
parameter — it is looked up once per resource in wiring.py’s _recognise and folded
into every recognised binding’s metadata. This binding reads it out exactly like MQTT reads
inf:hasMQTTBrokerIP, through binding.get.
The route is structural, so nothing else is needed. Address plus the recursive path
derived from the datamodel tree is a complete binding — no REST-specific ontology term is
minted. build_parameter_path does that derivation. It moved here from
demo/transferunits/controller.py rather than being rewritten: the two
callers — this binding, reaching outward at recognition time, and the Controller,
reaching outward from a fetched JSON tree — need the identical algorithm, and a domain-level
module is the wrong place to own something the middleware itself defines.
Payload shape. The payload is a list of parameter dicts, not a bare scalar, and a PUT
must send back exactly what a GET returned. Unlike MQTT, which bridges a bare device scalar
and the one-element list the framework holds, a peer’s own generated parameter route already
serves and accepts that exact list — there is no scalar to rewrap. So
RESTParameterFormatter is a plain type adapter between the wire JSON and
node_model_type instances, nothing more (contrast mqtt_binding.MQTTParameterFormatter,
which reassembles static facets a bare scalar would otherwise blank).
REST has no push, so northbound sync polls. That leaves three questions:
Batched or per-parameter? One GET per parameter, one
RESTParameterConnectorper registration — the same granularity MQTT already uses (one connector per topic). A shared batch-fetch-and-fan-out would need state shared across bindings that recognition does not otherwise couple, for a cost (extra GETs on a resource with few parameters) that no evidence yet justifies.What interval?
DEFAULT_POLL_INTERVAL_SECONDSbelow, conservative and not measured. The demo’s measured control lap does not include it: that lap is a unit’s own MQTT round trip, and this poll is a controller reading a peer over REST.DEFAULT_TICK_SECONDSin the demo’scontrol_station.pyrecords that measurement.Configurable? Yes, per connector instance (
poll_interval=), whichRESTBinding.buildcould source from a futureinf:term if one ever proves necessary. None is minted here; the route needs no new term, and a cadence is not addressing.
Lazy import, same rule as MQTT: importing this module, and recognising and building a
RESTBinding, must not require a working network stack, so that an inspecting instance
keeps receiving the projection on a host that cannot reach anything.
That rule does not hold on this module’s import path, and the guard below is
unreachable. Importing this module pulls in
transitional_sync_middleware.connect.connectors.aas_client_connector, which does a bare
import httpx at module level. httpx is a declared dependency of both this library and
transitional_sync_middleware, so a working install always has it; were it absent, the
import would die several frames above the try below. The # pragma: no cover on it is
accurate rather than lazy. The MQTT half is
genuinely different: aiomqtt really is an optional extra there, and mqtt_binding
really does degrade to MqttClientConnector = None.
The guard stays. It is correct in isolation, it is what makes the rule true again the moment
the eager import upstream goes away, and its error message is reachable and asserted
(tests/test_optional_transport_stacks.py). What is not claimed here any more is that
absence has ever been survivable on this path.
- kapps_semantic_middleware.connectors.rest_binding.DEFAULT_POLL_INTERVAL_SECONDS = 2.0#
Default cadence of the northbound read leg’s poll loop. See the module docstring’s “REST has no push” section for why this value is provisional rather than measured.
- kapps_semantic_middleware.connectors.rest_binding.build_parameter_path(root_class_local_name: str, root_iri: IRI, steps: Sequence[Tuple[str, str]], terminal_field: str) str[source]#
Build the structural REST path for a parameter from known (field, child_id) hops.
Mirrors
rest_router.py’s_accumulate_routespath shape exactly:/{Model}/{lined_root}/{field}/{lined_child}/.../{field}. Field names stay literal — the caller (recognition, here; a fetched JSON tree, forController) already carries them in the mangled form the served route uses. Only individual IDs are mangled here, viaIRI.lined.Moved from
demo/transferunits/controller.py: the algorithm has no domain term and two callers now need it, one of them insrc/.- Parameters:
root_class_local_name – The root resource’s class IRI, mangled (
IRI(...).lined) – whatkapps_ogmnames the materialized pydantic class, and so the{Model}segment the served route actually mounts under. Not the bare fragment: seeParameterBinding.root_class_local_name’s docstring.root_iri – The root resource’s own IRI.
steps – Sequence of (field_name, child_id) hops from the root to the parameter’s owner.
terminal_field – The final field name (the parameter itself).
- Returns:
The structural URL path.
- class kapps_semantic_middleware.connectors.rest_binding.RESTParameterFormatter(model_type: Type[Any], *, parameter_label: str = '', url: str = '')[source]#
Bases:
objectAdapts between a peer’s parameter-route JSON body and the persistence value.
Unlike a bare MQTT scalar, the REST payload already carries the whole parameter node – value, unit, access mode, whatever the range declares – because the peer’s own GET/PUT routes serve and accept exactly that shape. There is no scalar to rewrap, so this is a type adapter between a JSON list of dicts and
model_typeinstances, nothing more.
- class kapps_semantic_middleware.connectors.rest_binding.RESTParameterConnector(base_url: str, path: str, *, poll_interval: float = 2.0, poll: bool = True, timeout: float = 30.0)[source]#
Bases:
objectReaches one generated parameter route on a peer middleware over HTTP.
connect/disconnectare no-ops. HTTP is stateless here, onehttpx.AsyncClientper call, the same styleControllerandtransitional_sync_middleware’s ownHttpRequestConnectoralready use – there is no persistent connection worth pooling across the polling interval this runs at.pollgates whetherreceive()actually polls. The write leg of a bidirectional parameter getspoll=False: itssync_directionisFROM_PERSISTENCE, so the framework’sSyncedConnector.receive()would never act on anything it yielded anyway (seesynced_connector.py), and polling the same URL a second time from the write leg would only double the request rate for no effect.- async provide() Any[source]#
GET the parameter route. Returns the parsed JSON list of parameter dicts.
- async consume(body: Any) None[source]#
PUT
bodyto the parameter route.bodyalready has the list shape it wants.
- async receive() AsyncGenerator[Any, None][source]#
Poll the parameter route, yielding a reading each time it changes.
REST has no push (see the module docstring). This is the polled substitute for MQTT’s subscription queue, and what the framework’s background sync task (
run_receiveintransitional_sync_middleware’smiddleware.py) actually drives northbound sync from. A connector withpoll=Falsenever yields, so the framework spawns and immediately retires its receive task with no network traffic.
- class kapps_semantic_middleware.connectors.rest_binding.RESTBinding[source]#
Bases:
objectBinds a live, generically interface-accessible parameter to a REST connector.
Registers at the interface root, not a protocol-specific subproperty – REST has no parameter-local marker (see the module docstring).
wiring.py’s_descriptor_foralready resolves the most specific registered match, and a protocol marker such asinf:isInterfaceAccessibleMQTTParameteris itself a subproperty of this root, so a parameter that also declares a specific protocol keeps resolving to that protocol’s binding. This is the fallback for everything else – any interface-accessible parameter whose resource happens to be live.- connector_cls#
alias of
RESTParameterConnector
- connection_metadata: ClassVar[Tuple[IRI, ...]] = ()#
Empty on purpose. This binding reads nothing declared on the parameter – its evidence is the resource’s Service, one hop out (see the module docstring). The projection’s cross-check compares this against what the ontology declares between the parameter property and the interface root, which is likewise nothing for a parameter with no protocol-specific marker, so the two agree.
- static build(binding: ParameterBinding, direction: SyncDirection, *, ensure_transport: Callable[[str, int], None] | None = None) Iterable[Registration][source]#
One read registration when the resource is live. One write registration too, when
directionpermits it.directionhas already been reduced to the most restrictive of the parameter’sinf:accessModeand the instance’s connector wiring, so this only honours it.ensure_transportis unused. REST reaches a peer middleware, not a broker this deployment needs to bring up – there is nothing here to ensure.