Semantic registry#
The registry that maps a capability onto the connector able to carry it.
The semantic-connector seam: binding descriptors and their registry.
A semantic connector is any connector that can register itself from the knowledge graph.
The system realizes it not as a connector subclass, but as a binding descriptor. This is an
object that names the connector class it builds, the interface property it binds to, and
the connection metadata its protocol needs. It also states how to turn one parameter’s
metadata into one or more add_synced_connector registrations.
Reference a connector_cls rather than subclass it. This is the key point. transitional_sync_middleware
ships about ten connectors: MQTT, OPC-UA, HTTP request and polling, websocket and webhook
client and server, AAS client, model. Self-registration must not be added to any
of them in the sibling repo. A descriptor lets a domain expert make a vendor’s connector
semantic, without owning or subclassing its source. It also lets two registration strategies
for one protocol coexist without a shared ancestor.
MQTT is the first instance of this seam, not its shape. See mqtt_binding.py.
- kapps_semantic_middleware.connectors.semantic.first(value: Any) Any[source]#
The first element of an OGM multi-valued property, or the value itself.
Every property on a materialized node is a list, because RDF properties are set-valued. Connection metadata is single-valued in practice, so unwrap here. This keeps every call site from restating it. An empty or absent value yields
None.
- kapps_semantic_middleware.connectors.semantic.normalize_metadata(metadata: Mapping[Any, Any]) Dict[str, Any][source]#
Key a parameter node’s properties by the string form of their IRI.
A metadata mapping reaches a binding either from a
ClassSpec-derived dict keyed byIRIor from a materialized model keyed bystr. Normalize once here. No binding has to be indifferent to which it got, and no binding has to importIRIjust to do a lookup.
- class kapps_semantic_middleware.connectors.semantic.Registration(connector: Any, sync_direction: SyncDirection, model_type: Type[Any], formatter: Any | None = None, sync_role: SyncRole = SyncRole.READ_WRITE, suffix: str = '')[source]#
Bases:
objectDescribe one
add_synced_connectorcall. Do not perform it.A binding yields these instead of touching the middleware directly. So a test can check the seam with no running middleware. So the caller decides whether to wire them at all. This is what an inspecting instance needs.
- sync_direction: SyncDirection#
Which way this particular connector moves data.
- sync_role: SyncRole = 3#
Role in the framework’s sync bookkeeping. The direction controls the gating.
- class kapps_semantic_middleware.connectors.semantic.ParameterBinding(resource_iri: IRI, parameter_property: IRI, field_id: str, metadata: Dict[str, Any], descriptor: BindingDescriptor, node_model_type: Type[Any], root_iri: IRI | None = None, root_class_local_name: str | None = None, path_steps: Tuple[Tuple[str, str], ...] = ())[source]#
Bases:
objectOne interface-accessible parameter, resolved and ready to wire.
Everything here comes from the ClassSpec and the graph, never from materialized instance data. This is what makes construction-time registration possible.
- resource_iri: IRI#
The individual carrying the parameter, for example a belt or barrier.
- parameter_property: IRI#
The domain property whose range is the parameter node (the COMPLEX property).
- metadata: Dict[str, Any]#
The parameter node’s own properties, keyed by IRI string, connection metadata included. Build it with
normalize_metadata().
- descriptor: BindingDescriptor#
The binding descriptor that recognized this parameter.
- node_model_type: Type[Any]#
The generated pydantic model for the parameter node itself.
A formatter needs it to rebuild the node from an inbound scalar, because the framework replaces the whole value on
setattrand hands the formatter nothing but the payload. It comes from the full spec, not the pruned northbound one. The node the binding writes must still be the shape the graph expects.
- root_iri: IRI | None = None#
The top-level resource individual recognition started from (e.g. the TransferUnit).
resource_iriabove is the holder of the parameter, a belt or a barrier. REST addressing is structural and relative to the root, not the holder, so a binding one level of nesting deep needs both.Noneonly for aParameterBindingbuilt by hand outside recognition (as the MQTT tests do); nothing that readsroot_iriis reachable from one of those.
- root_class_local_name: str | None = None#
The root resource’s class IRI, mangled (
IRI(...).lined), the{Model}segment of a recursive REST route. Not the bare fragment:kapps_ogm’sClassSpec.to_pydantic_modelnames the materialized class after the whole mangled IRI (class_spec.py), so this must match that or a REST connector’s own PUT/GET 404s against the real served route. Paired withroot_iri; see its docstring.
- path_steps: Tuple[Tuple[str, str], ...] = ()#
(field_name, child_iri)hops fromroot_iridown toresource_iri, mirroringrest_router.py’s_accumulate_routes. Empty whenresource_irialready is the root.field_nameis the mangled form (IRI(prop).lined), matching what the served route actually uses;child_iriis the raw individual IRI, mangled byrest_binding.build_parameter_pathitself.
- property access_mode: str#
The parameter’s declared access mode, default to read-only.
An absent or unrecognised value yields
read, so a parameter is never writable by accident of omission.
- property label: str#
A short human-readable label for a log line, e.g. “Belt1 hasConveyorSpeed”.
Full mangled IRIs are correct in route paths but unreadable in a log line a human watches scroll past. Display-only: never parse it, never use it to address anything. Shared here rather than restated per binding descriptor — every binding builds one of these for its formatter’s log lines.
- class kapps_semantic_middleware.connectors.semantic.BindingDescriptor(*args, **kwargs)[source]#
Bases:
ProtocolWhat a semantic connector must declare.
Deliberately a structural protocol over plain class attributes. A descriptor is configuration, and requiring inheritance from a base class would defeat the purpose of not owning the connector’s source.
- connector_cls: ClassVar[Any]#
The framework connector class this binding constructs. Never subclass it.
Type loosely and declare
ClassVarbecause a descriptor is configuration on a class, not state on an instance, and because a connector behind an optional extra may legitimately resolve toNoneuntil its dependency is installed.
- interface_property: ClassVar[IRI]#
The
inf:marker property a domain property must be a subproperty of to match.
- connection_metadata: ClassVar[Tuple[IRI, ...]]#
The properties this binding reads in order to construct a connector.
Not the projection’s source of truth. It once was, and that was wrong. A set built from the registered bindings only knows the protocols this middleware has code for, so a parameter reachable over an unregistered protocol had its endpoint served northbound. The projection asks the ontology instead. Cross-check this declaration against it at construction, and report a disagreement. The two should coincide, and where they do not, either the contract grew a term this binding ignores or the binding expects one the ontology never declares.
- static build(binding: ParameterBinding, direction: SyncDirection, *, ensure_transport: Callable[[str, int], None] | None = None) Iterable[Registration][source]#
Turn one resolved parameter into the registrations that realize it.
ensure_transport, when given, is the deployment’s transport hook, already deduped byplan_wiringto fire once per distinct(host, port)across this resource’s whole wiring. A binding with nothing to bring up (REST) simply never calls it.
- class kapps_semantic_middleware.connectors.semantic.SemanticConnectorRegistry(descriptors: Sequence[Type[BindingDescriptor]] | None = None)[source]#
Bases:
objectMaps an interface property to the binding descriptor that serves it.
Resolve by the interface property, not by
rdf:type. A parameter node has no named type of its own. It has only anonymous restriction nodes, which are inferred and so absent from an explicit-graph fetch. The property hierarchy is what survives.Build and consult the registry for every connector wiring, including one that wires nothing. Do not implement “no connectors” as “no registry”. With no registry, no property is recognized as a parameter. The parameter node then becomes ordinary data, and is served northbound. The least-privileged instance would leak the most.
- register(descriptor: Type[BindingDescriptor]) Type[BindingDescriptor][source]#
Add a descriptor, replace any earlier one for the same interface property.
We deliberately replace rather than refuse. A domain expert may override the built-in MQTT binding with a site-specific one. This is a supported use of the seam. The expert should not have to unregister first.
Takes the descriptor class, not an instance of it. Every member of
BindingDescriptoris aClassVar, so the class itself is the configuration.
- for_interface_property(iri: IRI) Type[BindingDescriptor] | None[source]#
The descriptor that registers for exactly this interface property, if any.
- declared_connection_metadata() frozenset[source]#
Every property any registered binding reads, keyed by IRI string.
Not the projection’s prune set. See
BindingDescriptor.connection_metadata. This is one side of the construction-time cross-check against what the ontology declares. The projection itself follows the ontology (kapps_semantic_middleware.projection.southbound_properties()).
- kapps_semantic_middleware.connectors.semantic.semantic_connector(cls)[source]#
Register a binding descriptor class on the default registry.
Use as a plain decorator on the descriptor class:
@semantic_connector class MQTTBinding: connector_cls = MqttClientConnector interface_property = INF.isInterfaceAccessibleMQTTParameter ...
- kapps_semantic_middleware.connectors.semantic.resolve_direction(access_mode: str, flavour: SyncDirection) SyncDirection[source]#
The most restrictive of the parameter access mode and the instance flavor.
Neither may widen the other. A monitor can therefore never drive a writable belt, and a controller can never write a read-only sensor. This is structural, not by convention.
The read leg is always available. The question is only whether the write leg is. So this returns
BIDIRECTIONALwhen both sides permit writing, andTO_PERSISTENCE(device to middleware, read-only) otherwise.