Proxies and Capabilities
A ConnectionProxy is the user-facing object you interact with in a function block. It declares which capabilities your code requires and delegates every capability method call to the resolved plugin at runtime.
What is a ConnectionProxy?
A proxy is a thin class you define by inheriting from ConnectionProxy and one
or more capability interfaces:
from neops_worker_sdk.connection.proxy import ConnectionProxy
from neops_worker_sdk.connection.capabilities import DeviceInfoCapability
class DeviceInfoProxy(ConnectionProxy, DeviceInfoCapability):
pass
The proxy itself contains no implementation. It declares what it needs; the plugin system provides how it works for each platform.
What is a CapabilityInterface?
A capability interface is an abstract contract -- a set of @abstractmethod
declarations that plugins can implement.
"""
Base capability interface for the connection proxy system.
"""
from abc import ABC
class CapabilityInterface(ABC): # noqa: B024 - Intentionally has no abstract methods; serves as base marker class
"""
Base class for all capability interfaces.
Subclass this to define a capability that plugins can implement.
The ProxyMeta metaclass will auto-generate fallback implementations
for any abstract methods not implemented by the resolved plugin.
The ProxyMeta metaclass identifies capability interfaces via inheritance
(i.e., subclasses of this base class).
How Fallback Methods Work:
When a proxy inherits from a capability interface, the metaclass:
1. Collects all abstract methods from the interface
2. For each method, checks if the resolved plugin implements it
3. If the plugin has the method -> delegates to plugin
4. If not -> calls the auto-generated fallback that raises NotImplementedForThisPlatform
Note:
This class inherits from ABC but has no abstract methods itself.
It serves as a base marker class. Subclasses should define their
own abstract methods using the @abstractmethod decorator.
Example:
Create a custom capability interface::
class ConfigCapability(CapabilityInterface):
@abstractmethod
def get_running_config(self) -> str:
'''Get the running configuration.'''
...
@abstractmethod
def get_startup_config(self) -> str:
'''Get the startup configuration.'''
...
"""
A concrete capability looks like this:
"""
Device information capability interface.
"""
from abc import abstractmethod
from neops_worker_sdk.connection.capabilities.base import CapabilityInterface
class DeviceInfoCapability(CapabilityInterface):
"""
Capability interface for basic device information.
This is a common capability that most plugins should implement.
It provides basic device identification and version information.
"""
@abstractmethod
def get_version(self) -> dict[str, str | None]:
"""
Get device version and identification information.
Returns:
A dictionary containing device information. Standard keys:
- vendor: Device vendor (e.g., "Cisco", "Juniper")
- model: Device model (e.g., "ISR4451", "MX480")
- serial: Device serial number
- software_release: Software/firmware version
- raw_output: Raw command output (optional, for debugging)
Example:
>>> plugin.get_version()
{
"vendor": "Cisco",
"model": "ISR4451-X/K9",
"serial": "FTX1234A567",
"software_release": "17.3.4a",
"raw_output": "Cisco IOS XE Software, Version 17.03.04a..."
}
"""
...
Each @abstractmethod in the interface becomes a method you can call on the
proxy. The proxy's metaclass auto-generates fallback implementations so that
the proxy class itself is always instantiable -- even when the resolved plugin
only implements a subset of the declared methods.
Composing multiple capabilities
Proxies support multiple inheritance. List every capability your code needs:
The plugin resolver will prefer plugins that implement all listed
capabilities. If no single plugin covers everything, the resolver picks the
best-match plugin (the one implementing the most) and logs a warning. Methods
the plugin does not implement will raise NotImplementedForThisPlatform at
call time.
Fallback methods
When a resolved plugin does not implement a capability method, the proxy does
not crash on creation. Instead, the metaclass injects a fallback that raises
NotImplementedForThisPlatform with rich context:
from neops_worker_sdk.connection.exceptions import NotImplementedForThisPlatform
try:
result = proxy.get_inventory()
except NotImplementedForThisPlatform as exc:
# exc.context contains:
# proxy_class, method_name, interface_name,
# platform, plugin_class, device_id
logger.warning("Capability not available: %s", exc)
You can also override a fallback on the proxy class itself to provide a safe default instead of raising:
class SafeProxy(ConnectionProxy, DeviceInfoCapability, InventoryCapability):
def get_inventory(self) -> list:
"""Return empty list when the plugin has no inventory support."""
return []
Because __getattribute__ checks the plugin first, the override only takes
effect when the plugin lacks the method.
Listing capabilities
Every proxy exposes list_capabilities() for introspection:
@classmethod
def list_capabilities(cls) -> dict[str, list[str]]:
"""
List all capabilities and their methods for this proxy class.
This is useful for introspection and debugging.
Returns:
Dict mapping interface names to lists of method names.
Example:
>>> MyProxy.list_capabilities()
{
"DeviceInfoCapability": ["get_version"],
"InventoryCapability": ["get_inventory", "get_serial_numbers"],
}
"""
result: dict[str, list[str]] = {}
for interface in cls._capability_interfaces:
methods = [name for name, iface in cls._capability_methods.items() if iface is interface]
if methods: # Only include interfaces with methods
result[interface.__name__] = methods
Example output:
>>> MyProxy.list_capabilities()
{
"DeviceInfoCapability": ["get_version"],
"InventoryCapability": ["get_inventory", "get_serial_numbers"],
}
Method delegation
When you call proxy.get_version(), the proxy routes the call to the plugin
that handles your device:
flowchart LR
call["proxy.get_version()"] --> check{"Is it a\ncapability method?"}
check -- yes --> plugin{"Plugin implements\nget_version()?"}
plugin -- yes --> delegate["plugin.get_version()"]
plugin -- no --> fallback["Return fallback\n(raises when called)"]
check -- no --> self["Normal attribute lookup"]
In short: capability methods go to the plugin; everything else stays on the
proxy. If the plugin does not implement the method, the auto-generated fallback
raises NotImplementedForThisPlatform when you call it.
You can stop here
If you only need built-in capabilities like get_version(), this is all you
need. The pages that follow explain how to add new capabilities and
plugins — read them when you outgrow the built-in options.
Under the hood: ProxyMeta
The ConnectionProxy uses a custom metaclass called ProxyMeta (inheriting
from ABCMeta). When Python creates a new proxy subclass, ProxyMeta:
- Collects all
CapabilityInterfacesubclasses from the MRO. - Tracks which abstract methods belong to which interface
(
_capability_methodsmapping). - Generates fallback methods for every abstract method not already
defined in the class namespace. Each fallback raises
NotImplementedForThisPlatformwith aCapabilityContextthat includes the proxy class, method name, interface name, platform, plugin, and device id. - Clears the
__abstractmethods__set so the proxy can be instantiated without implementing the abstract methods itself.
This means you never need to write boilerplate raise NotImplementedError
stubs in your proxy classes.
For a full walkthrough of the metaclass internals, see Architecture Deep Dive.