Syft RPC Reference Guide
Syft RPC is a distributed request-response system for asynchronous communication between applications. It provides a clean, future-based App for making remote procedure calls with built-in support for request expiration, caching, and structured data validation. The system facilitates communication between distributed components by serializing requests and responses to files, which serves as the transport mechanism.
Core Components
Message Models
SyftRequest
Represents a client request to a remote endpoint.
class SyftRequest:
id: UUID # Unique identifier for the request
sender: str # Email of the sender
url: SyftBoxURL # Destination URL
method: SyftMethod # HTTP method (GET, POST, etc.)
body: bytes # Request payload
headers: dict # Optional HTTP headers
expires: datetime # When the request expires
The SyftRequest
class encapsulates all information needed to make a request. The id
uniquely identifies this request, while sender
stores the email address of the requester. The url
specifies the destination in a format similar to HTTP URLs but using the syft://
protocol. The body
contains the serialized request data, and expires
indicates when the request should be considered stale.
SyftResponse
Represents the server's response to a request.
class SyftResponse:
id: UUID # Same ID as the request
sender: str # Email of the responder
status_code: int # Status code (200, 400, etc.)
body: bytes # Response payload
headers: dict # Response headers
expires: datetime # When the response expires
The SyftResponse
class contains the server's reply to a request. It uses the same id
as the original request to maintain the connection between request and response. The status_code
follows HTTP conventions (200 for success, 400+ for errors). The body
contains the serialized response data that will be deserialized by the client.
Future Models
SyftFuture
Represents a pending request and its eventual response.
class SyftFuture:
id: UUID # Request/response ID
path: Path # Where request/response files are stored
expires: datetime # When the future expires
The SyftFuture
class is the core of the asynchronous design. When a client sends a request, it immediately receives a SyftFuture
object. This object doesn't contain the response yet, but provides methods to check for and retrieve the response when it becomes available. The path property points to the directory where both request and response files are stored.
Client App
Sending Requests
from syft_rpc import rpc
future = rpc.send(
url="syft://user@domain.com/app_data/app_name/rpc/endpoint",
body=your_request_object, # Automatically serialized
expiry="15m", # Time until request expires (15 minutes)
cache=True, # Enable request caching
)
The rpc.send()
function initiates an asynchronous request. The url
parameter specifies the destination using Syft's URL format, which includes the user, domain, application name, and endpoint. The body
parameter is your request object, which will be automatically serialized to JSON. The expiry parameter sets a time limit for the request's validity, helping manage resources by automatically cleaning up stale requests. The cache
parameter enables request deduplication, so identical requests can reuse previous responses.
Handling Responses
try:
# Wait for response with timeout (in seconds)
response = future.wait(timeout=300)
# Check for errors
response.raise_for_status()
# Parse response into your model
result = response.model(YourResponseModel)
# Process the result
print(f"Got response: {result}")
except Exception as e:
print(f"Error: {e}")
The future.wait()
method blocks until a response is available or the timeout is reached. It periodically checks for the existence of a response file. The timeout parameter prevents indefinite waiting, raising a SyftTimeoutError
if no response arrives within the specified time.
The response.raise_for_status()
method checks if the status code indicates an error (400+) and raises a SyftError if so. The response.model()
method parses the response body into a specific Pydantic model, providing type safety and validation.
Server App
Creating a Request Handler
from syft_event import SyftEvents
from pydantic import BaseModel
# Initialize event handler for your app
box = SyftEvents("your_app_name")
# Define request/response models
class YourRequest(BaseModel):
field1: str
field2: int
class YourResponse(BaseModel):
result: str
# Register endpoint handler
@box.on_request("/your_endpoint")
def handle_request(request: YourRequest, ctx) -> YourResponse:
# Process the request
# ...
return YourResponse(result="Success!")
# Start the server
if __name__ == "__main__":
box.run_forever()
The server side uses the SyftEvents
class to handle incoming requests. The constructor takes an application name that corresponds to the one in the URL. The @box.on_request()
decorator registers a function to handle requests to a specific endpoint.
Request handlers receive the deserialized request object and a context object with additional information. The return value is automatically serialized and sent back as the response. Pydantic models are used for both request and response to ensure data validation.
The box.run_forever()
method starts the server, which continuously monitors for new request files, processes them, and writes response files.
Error Handling
Status Codes:
SYFT_200_OK
- Request succeededSYFT_400_BAD_REQUEST
- Invalid requestSYFT_403_FORBIDDEN
- Unauthorized accessSYFT_404_NOT_FOUND
- Endpoint not foundSYFT_419_EXPIRED
- Request expiredSYFT_500_SERVER_ERROR
- Server error
Status codes follow HTTP conventions. The is_success
property on SyftStatus provides a convenient way to check if a status code indicates success (200-299) or error (400+).
Complete Example: Ping-Pong
This example demonstrates the complete flow of a simple RPC interaction:
- Client sends a "ping" message with timestamp
- Server processes the request
- Server responds with a "pong" message and new timestamp
- Client processes the response
Client (ping_request.py)
from datetime import datetime, timezone
from pydantic import BaseModel
from syft_rpc import rpc
# Define request/response models
class PingRequest:
msg: str
ts: datetime = datetime.now(timezone.utc)
class PongResponse(BaseModel):
msg: str
ts: datetime
# Send request
future = rpc.send(
url=f"syft://{server_email}/app_data/pingpong/rpc/ping",
body=PingRequest(msg="hello!"),
expiry="5m",
cache=True,
)
# Wait for and handle response
try:
response = future.wait(timeout=300)
response.raise_for_status()
pong = response.model(PongResponse)
print(f"Got response: {pong.msg} at {pong.ts}")
except Exception as e:
print(f"Error: {e}")
In this client example:
- The
PingRequest
class defines the structure of our request, containing a message and a timestamp. - The
PongResponse
class defines the expected structure of the server's response. - The
rpc.send()
call creates and sends the request, returning a future to track the operation. - The
future.wait()
call blocks until a response is received or the timeout (300 seconds) is reached. - The
response.raise_for_status()
call checks for error status codes. - The
response.model()
call parses the raw response body into a typed PongResponse object.
Server (pong_server.py)
from datetime import datetime, timezone
from pydantic import BaseModel, Field
from syft_event import SyftEvents
# Initialize event handler
box = SyftEvents("pingpong")
# Define request/response models
class PingRequest(BaseModel):
msg: str
ts: datetime
class PongResponse(BaseModel):
msg: str
ts: datetime
# Register endpoint handler
@box.on_request("/ping")
def pong(ping: PingRequest, ctx) -> PongResponse:
return PongResponse(
msg=f"Pong from {box.client.email}",
ts=datetime.now(timezone.utc),
)
# Start the server
if __name__ == "__main__":
box.run_forever()
In this server example:
- The
SyftEvents
class initializes an event handler for the "pingpong" application. - The
PingRequest
andPongResponse
classes define the data structures, similar to the client. - The
@box.on_request("/ping")
decorator registers the pong function to handle requests to the "/ping" endpoint. - The handler function receives a typed
PingRequest
object, processes it (in this case, simply creating a response), and returns a PongResponse. - The
box.run_forever()
call starts the server, which listens for incoming requests indefinitely.
This reference guide covers the core functionality of Syft RPC as demonstrated in the ping-pong example, providing a solid foundation for implementing communication in your applications.