Skip to content

Best Practices

This guide covers recommended patterns and best practices for building production-ready plugins with pyvider-rpcplugin.

Import Patterns

Import from the main package namespace for public APIs:

# ✅ Recommended: Import from main package
from pyvider.rpcplugin import (
    plugin_server,
    plugin_client,
    plugin_protocol,
    configure,
)
from pyvider.rpcplugin.config import rpcplugin_config
from pyvider.rpcplugin.exception import (
    RPCPluginError,
    HandshakeError,
    TransportError,
)
from provide.foundation import logger

Avoid Deep Imports

Don't import from internal submodules unless necessary:

# ❌ Avoid: Internal implementation details
from pyvider.rpcplugin.client.core import RPCPluginClient
from pyvider.rpcplugin.server.core import RPCPluginServer

# ✅ Better: Use factory functions
from pyvider.rpcplugin import plugin_client, plugin_server

Protocol Imports

For protocol definitions, import the base class from the protocol module:

# ✅ Recommended: Protocol base class
from pyvider.rpcplugin.protocol.base import RPCPluginProtocol

# Or use the main package export
from pyvider.rpcplugin import RPCPluginProtocol

Configuration Patterns

Use Environment Variables

Environment variables are the primary configuration method:

# ✅ Recommended: Environment-driven configuration
import os

os.environ["PLUGIN_AUTO_MTLS"] = "true"
os.environ["PLUGIN_LOG_LEVEL"] = "DEBUG"

from pyvider.rpcplugin import plugin_server
# Configuration automatically loaded

Configuration Access

Access configuration via direct attributes (not method calls):

from pyvider.rpcplugin.config import rpcplugin_config

# ✅ Correct: Direct attribute access
timeout = rpcplugin_config.plugin_handshake_timeout
transports = rpcplugin_config.plugin_server_transports

# ❌ Wrong: Method calls (not supported - will raise AttributeError)
# timeout = rpcplugin_config.handshake_timeout()
# AttributeError: 'RPCPluginConfig' object has no attribute 'handshake_timeout'

Use the configure() Helper

For programmatic configuration, use the configure() function:

from pyvider.rpcplugin import configure

# ✅ Recommended: Use helper function
configure(
    magic_cookie="my-secure-cookie",
    auto_mtls=True,
    handshake_timeout=10.0,
    log_level="INFO"
)

Error Handling

Catch Specific Exceptions

Use specific exception types for different error scenarios:

from pyvider.rpcplugin import plugin_client
from pyvider.rpcplugin.exception import (
    HandshakeError,
    TransportError,
    SecurityError,
)
from provide.foundation import logger

async def safe_client_startup():
    """Example of proper error handling."""
    try:
        client = plugin_client(command=["python", "my_plugin.py"])
        await client.start()

    except HandshakeError as e:
        logger.error(f"Authentication failed: {e.message}")
        # Handle auth issues

    except TransportError as e:
        logger.error(f"Connection failed: {e.message}")
        # Handle network issues

    except SecurityError as e:
        logger.error(f"Security error: {e.message}")
        # Handle certificate/mTLS issues

Provide Helpful Context

Include context in error messages:

from provide.foundation import logger

try:
    await server.serve()
except Exception as e:
    logger.error(
        "Server failed to start",
        extra={
            "error": str(e),
            "transport": transport.name,
            "port": server.port,
            "mtls_enabled": rpcplugin_config.plugin_auto_mtls,
        },
        exc_info=True
    )

Logging Patterns

Use Foundation Logger

Always use Foundation's structured logger:

from provide.foundation import logger

# ✅ Recommended: Foundation logger with context
logger.info(
    "Plugin started successfully",
    extra={
        "plugin_name": "my-plugin",
        "version": "1.0.0",
        "transport": "unix",
        "mtls_enabled": True,
    }
)

# ❌ Avoid: print() or standard logging
# print("Plugin started")

Log Levels

Use appropriate log levels:

# DEBUG: Detailed diagnostic information
logger.debug("Handshake details", extra={"cookie_key": cookie_key})

# INFO: Normal operational events
logger.info("Plugin connected", extra={"endpoint": endpoint})

# WARNING: Unexpected but handled situations
logger.warning("Retry attempt failed", extra={"attempt": retry_count})

# ERROR: Error events that might allow recovery
logger.error("Connection lost", extra={"reason": error_message})

Security Best Practices

Always Enable mTLS in Production

# ✅ Production: Enable mTLS
configure(
    auto_mtls=True,
    server_cert="file:///etc/ssl/plugin-server.crt",
    server_key="file:///etc/ssl/plugin-server.key",
)

# ⚠️  Development only: Disable for local testing
configure(auto_mtls=False)  # Never in production!

Use Strong Magic Cookies

import secrets

# ✅ Recommended: Cryptographically secure random cookie
magic_cookie = secrets.token_urlsafe(32)
os.environ["PLUGIN_MAGIC_COOKIE_VALUE"] = magic_cookie

# ❌ Avoid: Weak or predictable cookies
# magic_cookie = "test123"  # Too weak!

Secure Certificate Management

# ✅ Recommended: Proper file permissions
chmod 600 /etc/ssl/private/plugin-server.key
chmod 644 /etc/ssl/certs/plugin-server.crt

# ✅ Use environment variables for sensitive data
export PLUGIN_SERVER_KEY="$(cat /etc/ssl/private/plugin-server.key)"
export PLUGIN_SERVER_CERT="$(cat /etc/ssl/certs/plugin-server.crt)"

Performance Optimization

Choose Appropriate Transports

# ✅ Local IPC: Use Unix sockets (fastest)
configure(
    server_transports=["unix"],
    client_transports=["unix"],
)

# ✅ Network communication: Use TCP
configure(
    server_transports=["tcp"],
    client_transports=["tcp"],
)

# ✅ Flexible: Try Unix first, fallback to TCP
configure(
    server_transports=["unix", "tcp"],
    client_transports=["unix", "tcp"],
)

Tune Timeouts

# ✅ Fast local network: Shorter timeouts
configure(
    handshake_timeout=5.0,
    connection_timeout=10.0,
)

# ✅ Slow network or remote: Longer timeouts
configure(
    handshake_timeout=30.0,
    connection_timeout=60.0,
)

Enable Rate Limiting

# ✅ Protect server from abuse
configure(
    rate_limit_enabled=True,
    rate_limit_requests_per_second=100.0,
    rate_limit_burst_capacity=200,
)

Testing Patterns

Use Test Mode

import os

# ✅ Enable test mode for testing
os.environ["PLUGIN_TEST_MODE"] = "true"

# Disable mTLS for local testing
os.environ["PLUGIN_AUTO_MTLS"] = "false"

# Use test magic cookie
os.environ["PLUGIN_MAGIC_COOKIE_VALUE"] = "test_cookie_value"

Reset Foundation in Tests

import pytest
from provide.testkit import reset_foundation_setup_for_testing

@pytest.fixture(autouse=True)
def reset_foundation():
    """Reset Foundation state before each test."""
    reset_foundation_setup_for_testing()

Project Structure

my-plugin/
├── src/
│   └── my_plugin/
│       ├── __init__.py
│       ├── server.py          # Plugin server implementation
│       ├── protocol.py        # Protocol definition
│       ├── handler.py         # Service handler
│       └── proto/
│           ├── service.proto  # Protocol Buffer definitions
│           ├── service_pb2.py # Generated
│           └── service_pb2_grpc.py  # Generated
├── tests/
│   ├── __init__.py
│   ├── test_server.py
│   ├── test_handler.py
│   └── conftest.py           # Shared fixtures
├── examples/
│   └── example_client.py     # Example usage
├── pyproject.toml            # Project metadata
└── README.md

Code Organization

Separate Concerns

# ✅ Recommended: Separate protocol, handler, and server

# protocol.py
from pyvider.rpcplugin.protocol.base import RPCPluginProtocol

class MyProtocol(RPCPluginProtocol):
    async def get_grpc_descriptors(self):
        import my_pb2_grpc
        return my_pb2_grpc, "my.Service"

    async def add_to_server(self, server, handler):
        from my_pb2_grpc import add_MyServiceServicer_to_server
        add_MyServiceServicer_to_server(handler, server)

# handler.py
from provide.foundation import logger

class MyHandler:
    async def MyMethod(self, request, context):
        logger.info("Processing request", extra={"request_id": request.id})
        # Business logic here
        return response

# server.py
import asyncio
from pyvider.rpcplugin import plugin_server
from .protocol import MyProtocol
from .handler import MyHandler

async def main():
    server = plugin_server(
        protocol=MyProtocol(),
        handler=MyHandler()
    )
    await server.serve()

if __name__ == "__main__":
    asyncio.run(main())

Async Patterns

Use Async Context Managers

# ✅ Recommended: Async context manager for cleanup
async with plugin_client(command=["python", "plugin.py"]) as client:
    result = await client.call_service_method()
# Automatically closed on exit

Proper Async/Await

# ✅ Correct: await async operations
result = await client.start()
response = await stub.CallMethod(request)

# ❌ Wrong: Forgetting await
# result = client.start()  # Returns coroutine, not result!

Documentation

Document Your Protocol

class MyProtocol(RPCPluginProtocol):
    """
    Protocol for My Plugin service.

    This protocol defines RPC methods for:
    - Processing requests
    - Managing resources
    - Health monitoring

    Example:
        ```python
        protocol = MyProtocol()
        server = plugin_server(protocol=protocol, handler=MyHandler())
        ```
    """
    service_name: str = "my.Service"

Document Configuration Requirements

"""
My Plugin - Configuration Requirements

Required Environment Variables:
- PLUGIN_MAGIC_COOKIE_VALUE: Authentication secret
- MY_PLUGIN_DATABASE_URL: Database connection string

Optional Environment Variables:
- PLUGIN_LOG_LEVEL: Logging level (default: INFO)
- PLUGIN_AUTO_MTLS: Enable mTLS (default: true)
- MY_PLUGIN_CACHE_SIZE: Cache size limit (default: 1000)

Example:
    export PLUGIN_MAGIC_COOKIE_VALUE="secure-random-value"
    export MY_PLUGIN_DATABASE_URL="postgresql://localhost/mydb"
    python -m my_plugin.server
"""