Simple Custom RPC (Without Protocol Buffers)¶
This intermediate tutorial bridges the gap between the Quick Start and the full Protocol Buffers Plugin. Learn how to create custom RPC methods using plain Python before diving into protobuf complexity.
Conceptual Tutorial
This is a conceptual tutorial designed to teach the principles of RPC plugin development. The example files referenced (processor_service.py, simple_protocol.py, simple_server.py, simple_client.py) are not included in the examples/ directory.
You can: - Create these files yourself as a learning exercise - Use them as templates for your own custom RPC services - Skip to the Echo Service Example for a complete, runnable example using Protocol Buffers
For working examples, see: - Echo Service - Complete RPC service with Protocol Buffers - Examples Overview - All runnable examples
Navigation: Home → Getting Started → Simple Custom RPC
What You'll Learn¶
- Create custom RPC methods without Protocol Buffers
- Use Python dataclasses for type-safe messages
- Understand the plugin protocol abstraction
- Prepare for Protocol Buffers implementation
Prerequisites¶
- Completed Quick Start
- Basic understanding of async Python
- Pyvider RPC Plugin installed
Simple Data Processing Service¶
Let's build a data processing service using plain Python:
Step 1: Define Your Service¶
Create processor_service.py:
from dataclasses import dataclass
from typing import Any
from provide.foundation import logger
@dataclass
class ProcessRequest:
"""Request to process data."""
data: str
operation: str = "uppercase"
@dataclass
class ProcessResponse:
"""Response with processed data."""
result: str
operation_applied: str
success: bool = True
class DataProcessor:
"""Simple data processing service."""
async def process(self, request: ProcessRequest) -> ProcessResponse:
"""Process data based on operation."""
logger.info(f"Processing: {request.operation} on {len(request.data)} chars")
try:
if request.operation == "uppercase":
result = request.data.upper()
elif request.operation == "lowercase":
result = request.data.lower()
elif request.operation == "reverse":
result = request.data[::-1]
else:
return ProcessResponse(
result="",
operation_applied=request.operation,
success=False
)
return ProcessResponse(
result=result,
operation_applied=request.operation,
success=True
)
except Exception as e:
logger.error(f"Processing failed: {e}")
return ProcessResponse(
result="",
operation_applied=request.operation,
success=False
)
Step 2: Create the Protocol Bridge¶
Create simple_protocol.py:
from pyvider.rpcplugin.protocol.base import RPCPluginProtocol
from typing import Any
class SimpleProtocol(RPCPluginProtocol):
"""Protocol bridge for simple Python RPC."""
def __init__(self, service_name: str = "DataProcessor"):
self.service_name = service_name
self.handler = None
async def get_grpc_descriptors(self) -> tuple[Any, str]:
"""Return service descriptors (not using gRPC yet)."""
# This would return actual gRPC descriptors in a real implementation
return None, self.service_name
async def add_to_server(self, server: Any, handler: Any) -> None:
"""Register handler with server."""
self.handler = handler
# In a real implementation, this would register with gRPC server
logger.info(f"Registered {self.service_name} handler")
async def call_method(self, method_name: str, request: Any) -> Any:
"""Route method calls to handler."""
if hasattr(self.handler, method_name):
method = getattr(self.handler, method_name)
return await method(request)
else:
raise AttributeError(f"Method {method_name} not found")
Step 3: Create the Server¶
Create simple_server.py:
import asyncio
from pyvider.rpcplugin import plugin_server, configure
from provide.foundation import logger
# Import our service components
from processor_service import DataProcessor
from simple_protocol import SimpleProtocol
async def main():
"""Run the simple RPC server."""
# Configure the plugin environment
configure(
magic_cookie="simple-processor",
auto_mtls=False, # Start simple, add security later
handshake_timeout=10.0
)
# Create protocol and handler
protocol = SimpleProtocol(service_name="DataProcessor")
handler = DataProcessor()
# Create and start server
logger.info("Starting Simple RPC Server")
server = plugin_server(
protocol=protocol,
handler=handler
)
await server.serve()
if __name__ == "__main__":
asyncio.run(main())
Step 4: Create the Client¶
Create simple_client.py:
import asyncio
import sys
from pathlib import Path
from pyvider.rpcplugin import plugin_client, configure
from provide.foundation import logger
# Import message types
from processor_service import ProcessRequest, ProcessResponse
async def main():
"""Connect to simple RPC server."""
# Configure client
configure(
magic_cookie="simple-processor",
handshake_timeout=10.0
)
# Define plugin command
plugin_path = Path(__file__).parent / "simple_server.py"
plugin_command = [sys.executable, str(plugin_path)]
# Connect to plugin
async with plugin_client(command=plugin_command) as client:
logger.info("Connected to Simple RPC Server")
# Make RPC calls using dataclasses
operations = [
ProcessRequest(data="Hello World", operation="uppercase"),
ProcessRequest(data="PYTHON RPC", operation="lowercase"),
ProcessRequest(data="Reverse Me", operation="reverse")
]
for request in operations:
# In real implementation, this would use gRPC channel
# For now, we're demonstrating the pattern
response = await client.protocol.call_method("process", request)
logger.info(f"Request: {request.operation}('{request.data}')")
logger.info(f"Response: '{response.result}' (success={response.success})")
if __name__ == "__main__":
asyncio.run(main())
Understanding the Bridge to Protocol Buffers¶
This simple example demonstrates the core concepts without Protocol Buffer complexity:
- Service Definition: Your business logic in
DataProcessor - Message Types: Using dataclasses instead of protobuf messages
- Protocol Bridge:
SimpleProtocolconnects your service to the RPC framework - Type Safety: Python type hints provide IDE support and clarity
Next Steps: Adding Protocol Buffers¶
When you're ready for production-grade RPC with cross-language support:
- Define
.protofile: Replace dataclasses with protobuf definitions - Generate Python code: Use
grpc_tools.protocto generate message classes - Implement gRPC servicer: Replace simple handler with gRPC servicer
- Use gRPC channel: Replace simple calls with proper gRPC stubs
The structure remains the same - Protocol Buffers just add: - Binary serialization for efficiency - Cross-language compatibility - Strict schema validation - Streaming support
Key Takeaways¶
- You can start with simple Python classes before learning Protocol Buffers
- The plugin architecture separates concerns: service, protocol, transport
- Foundation provides logging, configuration, and infrastructure automatically
- Type hints and dataclasses provide type safety without protobuf complexity
Ready for Protocol Buffers? Continue to Build Your First Plugin
Navigation: Previous: Quick Start | Next: First Plugin