Skip to content

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:

class FullProxy(ConnectionProxy, DeviceInfoCapability, ConfigCapability):
    pass

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:

  1. Collects all CapabilityInterface subclasses from the MRO.
  2. Tracks which abstract methods belong to which interface (_capability_methods mapping).
  3. Generates fallback methods for every abstract method not already defined in the class namespace. Each fallback raises NotImplementedForThisPlatform with a CapabilityContext that includes the proxy class, method name, interface name, platform, plugin, and device id.
  4. 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.