Device Connections (Capability Proxies & Plugins)
This guide starts a short series on Neops’ capability-based connection system. Instead of a global get_connection() pool API, you build a typed proxy by inheriting capability interfaces, and the system resolves an appropriate plugin for the device and connection parameters.
If you only read one page, read this one and then jump to:
- 31-connection-methods.md: basic usage & lifecycle
- 32-method-resolution-priority.md: resolution rules, ambiguity, defaults, best-match
Prerequisites (quick)
You’ll be more comfortable with this system if you know:
- Python ABCs (
abc.ABC) and@abstractmethod - multiple inheritance / method resolution order (MRO)
If those are new, it’s still workable—just keep the examples close and start with the context manager usage in 31-connection-methods.md.
Glossary (terms used throughout)
- Resolution: selecting a plugin based on platform/type/library + required capabilities (happens when you call
MyProxy.get_connection(...)) - Delegation: proxy forwarding a capability method call to the plugin implementation (happens when you call
proxy.get_version(),proxy.get_status(), etc.) - Fallback: what happens when the plugin does not implement a capability method (default: raise
NotImplementedForThisPlatform; proxies can override) - Best-match: when no plugin implements all required capabilities, choose the one that implements the most (warn loudly)
Note: These terms are used consistently throughout the 30–36 docs in this series.
Quick start example
This is a minimal end-to-end example showing the complete flow (proxy → resolve plugin → connect → call capability method):
import neops_worker_sdk.connection.plugins # noqa: F401
from neops_worker_sdk.connection.capabilities import DeviceInfoCapability
from neops_worker_sdk.connection.proxy import ConnectionProxy
from neops_worker_sdk.workflow import Device
class DeviceInfoProxy(ConnectionProxy, DeviceInfoCapability):
pass
device = Device(
id=1,
platform={"id": 1, "name": "ios", "short_name": "ios"},
ip="192.168.1.1",
username="admin",
password="secret",
skip_if_identifier_exists=False,
identifier_fields=[],
)
with DeviceInfoProxy.connect(device, "ssh", "scrapli") as proxy:
version = proxy.get_version()
print(version)
Mental model
Think of the system as two layers plus one “front door”:
- BaseConnection: a thin wrapper around a third‑party library connection (Netmiko, Scrapli, httpx, ncclient, …). It owns connect/disconnect/is_alive and performs lazy imports. See 34-writing-base-plugins.md.
- ConnectionPlugin: platform‑aware factory + implementation point for capabilities. A plugin creates a
BaseConnectionand can implement capability methods (e.g.get_version()). - ConnectionProxy: the user-facing API. A proxy declares required capabilities via inheritance and delegates calls to the resolved plugin.
The three core building blocks
1) Capability interfaces
Capabilities are abstract interfaces that define the methods you want to call.
from abc import abstractmethod
from neops_worker_sdk.connection.capabilities import CapabilityInterface
class DeviceInfoCapability(CapabilityInterface):
@abstractmethod
def get_version(self) -> dict[str, str | None]:
...
See 33-writing-capabilities-and-proxies.md for guidance on designing capabilities and composing them into proxies.
2) Proxies (what your code uses)
Proxies declare what they need (capabilities) and provide a clean, typed API.
from neops_worker_sdk.connection.proxy import ConnectionProxy
class DeviceInfoProxy(ConnectionProxy, DeviceInfoCapability):
pass
3) Plugins (what gets resolved at runtime)
Plugins declare what they provide (capabilities) and how to create/manage a raw connection.
from neops_worker_sdk.connection.capabilities import DeviceInfoCapability
from neops_worker_sdk.connection.registry import register_connection_plugin
from neops_worker_sdk.connection.types import ConnectionPlugin
@register_connection_plugin()
class IOSCiscoScrapliPlugin(ConnectionPlugin, DeviceInfoCapability):
platform = "ios"
connection_type = "ssh"
connection_library = "scrapli"
# _create_connection() creates the BaseConnection wrapper (see 34-writing-base-plugins.md)
# get_version() implements DeviceInfoCapability (platform/library specific)
What happens when you call a proxy method
flowchart TD
userCode["UserCode"] --> proxyConnect["MyProxy.connect(...)"]
proxyConnect --> resolvePlugin["registry.resolve_plugin(...)"]
resolvePlugin --> pluginInstance["Plugin(device)"]
pluginInstance --> initConn["plugin.initialize_connection()"]
initConn --> baseConn["BaseConnection.connect()"]
userCode --> proxyCall["proxy.some_capability_method()"]
proxyCall --> delegate["ConnectionProxy.__getattribute__ delegates"]
delegate --> pluginMethod["plugin.some_capability_method()"]
delegate --> fallbackRaise["Fallback raises NotImplementedForThisPlatform"]
Note: Mermaid diagrams render in MkDocs (with the configured Mermaid fence) and on GitHub. Some editors (including VS Code’s built-in Markdown preview) may require a Mermaid/Markdown preview extension to render them.
Key points:
- Resolution is capability-aware: the proxy requires capabilities; the plugin must implement them (or you get fallback behavior; see 32-method-resolution-priority.md).
- Fallback methods are auto-generated: if a plugin doesn’t implement a capability method, the proxy raises
NotImplementedForThisPlatformwith rich context. See 33-writing-capabilities-and-proxies.md. - Lazy dependency errors surface on first use: base plugins import third‑party libraries inside
connect(). Missing dependencies raiseConnectionCreationErrorwhen the plugin is used.
Connection types and libraries (current conventions)
The system uses string identifiers:
- Connection types:
"ssh","api","netconf","restconf"(and any custom types you define) - Connection libraries:
"netmiko","scrapli","httpx","ncclient"(and custom libraries) - Platforms: the device platform short name (e.g.
"ios","nxos","iosxr")
Note: Platform is derived from
device.platform.short_nameif present, otherwise fromdevice.platformif it’s a string.
Where to put “custom methods”
This series focuses on proxies/capabilities/plugins. In the current architecture:
- Typed methods: define them on a capability interface and implement them in platform plugins (see 33-writing-capabilities-and-proxies.md).
- Safe defaults: override the method on the proxy for custom fallback behavior (see 33-writing-capabilities-and-proxies.md).
There is no separate “register_connection_method” registry in the current connection module; capabilities and plugins are the intended extension points.
Further reading
docs/development/36-capability-proxy-architecture.md(design rationale and deeper internals)neops_worker_sdk/connection/draft_usage.py(runnable, end-to-end examples)