from collections.abc import Iterator
Writing Capabilities & Proxies
This page shows how to define capability interfaces, how to compose them into typed proxies, and how to provide custom fallbacks.
- For concepts: 30-device-connections.md
- For usage & lifecycle: 31-connection-methods.md
- For resolution rules: 32-method-resolution-priority.md
- For implementing plugins: 34-writing-base-plugins.md and 35-platform-plugins-and-defaults.md
Define a capability interface
A capability is an abstract interface (ABC) that declares one or more methods.
from abc import abstractmethod
from neops_worker_sdk.connection.capabilities import CapabilityInterface
class DeviceInfoCapability(CapabilityInterface):
@abstractmethod
def get_version(self) -> dict[str, str | None]:
"""Return vendor/model/serial/software_release (and optionally raw output)."""
...
Guidelines:
- Keep method names unique across interfaces (name collisions are hard to debug).
- Add docstrings: they become the first place users look when they hit
NotImplementedForThisPlatform.
Create a proxy by inheriting capabilities
The proxy is the API your function block code uses.
from neops_worker_sdk.connection.proxy import ConnectionProxy
class DeviceInfoProxy(ConnectionProxy, DeviceInfoCapability):
pass
Warning: Always put
ConnectionProxyfirst in the inheritance list.
class MyProxy(ConnectionProxy, CapA, CapB): ...This keeps method resolution and fallback injection predictable. Concretely, it ensures
ConnectionProxy.__getattribute__intercepts capability method access and that metaclass-injected fallback methods behave as intended.Note (IDEs and type checkers): Some static analyzers (PyCharm, Pyright, mypy, etc.) may still think your proxy class is “abstract” because capability methods are declared as
@abstractmethodon the interfaces.At runtime this is still valid Python: the
ProxyMetametaclass injects fallback implementations and clears__abstractmethods__, so the proxy is instantiable.If your IDE complains, you may need to suppress the warning locally (e.g. PyCharm
# noinspection PyAbstractClass, or a type-checker ignore on the class line) while keeping the runtime behavior unchanged.
Compose multiple capabilities
from abc import abstractmethod
from neops_worker_sdk.connection.proxy import ConnectionProxy
from neops_worker_sdk.connection.capabilities import CapabilityInterface
class InventoryCapability(CapabilityInterface):
@abstractmethod
def get_inventory(self) -> list[str]:
...
class StatusCapability(CapabilityInterface):
@abstractmethod
def get_status(self) -> str:
...
class InventoryAndStatusProxy(ConnectionProxy, InventoryCapability, StatusCapability):
pass
The proxy now has a typed surface area that includes all methods from all inherited interfaces.
Extend connection type hints
The SDK type aliases include common values. If you want IDE hints for your own strings, define
custom aliases with extra Literal values and override get_connection() / connect() to expose
them on your proxy:
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Literal, Self
from neops_worker_sdk.connection.proxy import ConnectionProxy
from neops_worker_sdk.connection.types import (
ConnectionLibrary,
ConnectionPlatform,
ConnectionType,
)
from neops_worker_sdk.connection.capabilities import InventoryCapability, StatusCapability
from neops_worker_sdk.workflow import Device
from collections.abc import Iterator
from contextlib import contextmanager
type MyConnectionType = ConnectionType | Literal["grpc"]
type MyConnectionLibrary = ConnectionLibrary | Literal["my_library"]
type MyConnectionPlatform = ConnectionPlatform | Literal["my_platform"]
class MyConnectionProxy(ConnectionProxy, InventoryCapability, StatusCapability):
@classmethod
def get_connection(
cls,
device: Device,
connection_type: MyConnectionType | None = None,
connection_library: MyConnectionLibrary | None = None,
*,
connect: bool = True,
fallback_to_default: bool = False,
validate_capabilities: bool = True,
) -> Self:
return super().get_connection(
device=device,
connection_type=connection_type,
connection_library=connection_library,
connect=connect,
fallback_to_default=fallback_to_default,
validate_capabilities=validate_capabilities,
)
@classmethod
@contextmanager
def connect(
cls,
device: Device,
connection_type: MyConnectionType | None = None,
connection_library: MyConnectionLibrary | None = None,
*,
connect: bool = True,
fallback_to_default: bool = False,
validate_capabilities: bool = True,
) -> Iterator[Self]:
with super().connect(
device=device,
connection_type=connection_type,
connection_library=connection_library,
connect=connect,
fallback_to_default=fallback_to_default,
validate_capabilities=validate_capabilities,
) as proxy:
yield proxy
Use the same aliases for plugin attributes so your platform/type/library values stay consistent:
from neops_worker_sdk.connection.types import ConnectionPlugin
class MyPlugin(ConnectionPlugin, InventoryCapability, StatusCapability):
platform: MyConnectionPlatform = "my_platform"
connection_type: MyConnectionType = "grpc"
connection_library: MyConnectionLibrary = "my_library"
These aliases are for static typing only; the runtime still accepts any string values.
Custom proxy fallbacks (override instead of raising)
If a plugin doesn’t implement a capability method, the proxy has an auto-generated fallback method that raises
NotImplementedForThisPlatform.
You can override that behavior by implementing the method on the proxy itself:
from neops_worker_sdk.connection.capabilities import InventoryCapability
class SafeInventoryProxy(ConnectionProxy, InventoryCapability):
def get_inventory(self) -> list[str]:
# Custom fallback: return a safe default instead of raising
return []
This is useful when:
- a capability is nice to have but not required for your workflow
- you want to degrade gracefully (e.g., empty inventory if unsupported)
Introspection: what does this proxy require?
You can inspect what capabilities/methods a proxy declares:
The result is a dict mapping interface name → list of method names.
Handling unsupported methods
If a proxy fallback raises, you’ll get NotImplementedForThisPlatform with rich context:
from neops_worker_sdk.connection.exceptions import NotImplementedForThisPlatform
try:
proxy.get_inventory()
except NotImplementedForThisPlatform as exc:
# exc.context is a CapabilityContext dataclass (when available) with:
# - proxy_class: str
# - method_name: str
# - interface_name: str
# - platform: str | None
# - plugin_class: str | None
# - device_id: str | None
raise
Use this for:
- clear error messages back to workflows/users
- platform “feature flags” (capability support is explicit)
Complete minimal example (end-to-end)
For a tiny “hello world” you can run end-to-end, see:
neops_worker_sdk/connection/draft_usage.py
It demonstrates defining capabilities and proxies, registering plugins, and what happens when capabilities are missing.