RPC API
Complete API reference for the RPC subsystem — RPCRegistry, RPCProxy, ServiceProxy, Microservice descriptor, exception classes, and data models.
This page documents every class in the singularity.rpc package and the Microservice descriptor. All signatures are taken directly from the source code.
RPCRegistry
singularity.rpc.registry.RPCRegistry
Singleton registry for RPC-exposed services. Stores local service instances and remote service specs discovered from Redis.
Constructor
Takes no parameters. Initializes empty _local and _remote dictionaries.
Methods
| Method | Parameters | Return Type | Description |
|---|---|---|---|
register(name, instance, path_segments, *, prefix, callables) | name: str, instance: Any, path_segments: list[str], prefix: str = "/api", callables: dict[str, Callable] | None = None | None | Register a local service for RPC. Introspects the instance to extract HTTP endpoint methods and builds MethodMeta entries. prefix must match the prefix the app mounts routes under -- it is baked into the endpoint URLs remote callers use. callables holds the hook-wrapped endpoints that local dispatch runs. |
generate_spec() | (none) | dict[str, Any] | Generate an OpenAPI-like spec for all locally registered RPC services. Returns {"rpc_version": "1.0", "base_url": "...", "services": {...}}. |
generate_spec_for(name) | name: str | Optional[dict[str, Any]] | Generate spec for a single service by name. Returns None if not found. |
publish_to_redis(ttl) | ttl: int = 60 | None | Async. Publish local RPC specs to shared Redis. Each service is published individually under singularity:rpc:services:{name} and the full spec under singularity:rpc:spec, both with the given TTL in seconds. |
remove_from_redis() | (none) | None | Async. Remove all local RPC specs from shared Redis. |
discover_remote() | (none) | None | Async. Scan Redis for all singularity:rpc:services:* keys and rebuild the remote cache. Peers whose keys have expired are dropped, so a dead deployment stops being dispatched to. |
discover_single_service(service_name) | service_name: str | bool | Async. Discover a single remote service from Redis on-demand. Returns True if found (locally or remotely), False otherwise. |
get_proxy(caller) | caller: str | RPCProxy | Get an RPC proxy bound to a caller identity. |
resolve_service_for_path(path) | path: str | Optional[LocalServiceEntry] | Find the local service mounted at a request path, using longest-prefix match. Used by RPCGuardMiddleware. |
close() | (none) | None | Async. Release the pooled Redis connection. Called by Manager.shutdown(). |
The registry uses settings.celery_broker_url as the Redis connection URL for publishing and discovery. Ensure your Redis instance is accessible.
RPCClient
singularity.rpc.client.RPCClient
The public RPC interface. The module-level rpc singleton is an instance of this class.
Usage Patterns
from singularity.rpc import rpc
# Pattern 1: Direct proxy call
result = await rpc("orders").payments.charge(user_id="u1", amount=9.99)
# Pattern 2: Class-level descriptor for dependency declaration
class MyService:
payments = rpc.remote("payments")
async def checkout(self):
result = await self.payments.charge(user_id="u1", amount=9.99)Methods
| Method | Parameters | Return Type | Description |
|---|---|---|---|
__call__(caller) | caller: str | RPCProxy | Get an RPC proxy bound to the given caller identity. |
remote(service_name) | service_name: str | RemoteServiceDescriptor | Declare a remote service dependency as a class-level attribute. Returns a descriptor that resolves to a ServiceProxy at access time. |
Module-Level Functions
| Function | Parameters | Return Type | Description |
|---|---|---|---|
rpc | (singleton) | RPCClient | Pre-created module-level singleton. Import and call directly. |
get_registry() | (none) | RPCRegistry | Get the module-level RPC registry singleton. Used internally by the Manager. |
RPCProxy
singularity.rpc.proxy.RPCProxy
Proxy bound to a caller identity. Attribute access returns a ServiceProxy for the target service.
Constructor
| Parameter | Type | Description |
|---|---|---|
caller | str | The name of the calling service |
registry | RPCRegistry | The RPC registry instance |
Access Patterns
| Pattern | Returns | Description |
|---|---|---|
proxy.payments | ServiceProxy | Dot access for standard service names |
proxy["my-service"] | ServiceProxy | Bracket access for names with hyphens or special characters |
ServiceProxy
singularity.rpc.proxy.ServiceProxy
Proxy for a specific target service. Method calls are dispatched locally (in-process) or remotely (HTTP) with blacklist enforcement.
Constructor
| Parameter | Type | Description |
|---|---|---|
service_name | str | The target service name |
caller | str | The calling service name |
registry | RPCRegistry | The RPC registry instance |
Dispatch Behavior
When you access a method on a ServiceProxy, it resolves using this priority:
- Local -- If
service_nameexists inregistry._local, checks the blacklist, verifies the method is exposed, and returns the hook-wrapped endpoint (in-process call). Because it is the same callable the HTTP route uses, a local RPC call runs the service's hooks, cache, exception filters, and events. - Remote (cached) -- If
service_nameexists inregistry._remote, checks the blacklist, verifies the method exists, and returns an async callable that makes an HTTP request. - Remote (lazy discovery) -- If the service is not found locally or in the cache, returns an async callable that first attempts
discover_single_service()from Redis, then dispatches the HTTP call.
Remote Call Details
| Aspect | Value |
|---|---|
| HTTP Client | Pooled httpx.AsyncClient (keep-alive connections reused across calls) |
| Timeout | settings.rpc_timeout (default 30.0 seconds) |
| Headers | X-RPC-Caller: {caller}, plus X-API-Key: {settings.rpc_api_key} when set |
| Request Method | Determined by the remote method spec (GET, POST, etc.) |
| Arguments | Encoded by transport.encode_arguments -- see Argument Encoding |
Internal Methods
| Method | Parameters | Return Type | Description |
|---|---|---|---|
_check_blacklist(blacklist) | blacklist: list[str] | None | Raises RPCAccessDenied if the caller is in the blacklist. |
RemoteServiceDescriptor
singularity.rpc.proxy.RemoteServiceDescriptor
Python descriptor for class-level remote service declaration. Used by rpc.remote().
Constructor
| Parameter | Type | Description |
|---|---|---|
service_name | str | The target service name |
Descriptor Protocol
When accessed on an instance, __get__ reads the _service_name attribute (injected by the Manager) and returns a ServiceProxy via rpc(caller)[service_name].
Microservice
singularity.microservices.Microservice
Base class for declaring remote microservice dependencies. Uses the Python descriptor protocol to bind to the caller identity at access time.
Class Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
host_url | str | "" | Base URL of the remote deployment (e.g. "http://localhost:8001") |
api_key | str | "" | API key sent as X-API-Key header (optional) |
timeout | float | 30.0 | Request timeout in seconds |
prefix | str | "/api" | API prefix mounted on the remote repo |
service_name | str | "" | If set, binds directly to this service (skips repo-level proxy). If empty, returns a MicroserviceProxy for multi-service access. |
Descriptor Behavior
service_name | __get__ Returns | Usage Pattern |
|---|---|---|
"" (empty) | MicroserviceProxy | self.billing.invoice.post(...) -- pick service, then method |
"test" (set) | MicroserviceServiceProxy | self.test.get(...) -- call method directly |
Usage Example
from singularity.microservices.microservice import Microservice
from singularity.config import settings
class BillingMS(Microservice):
host_url = settings.billing_host_url
api_key = settings.billing_api_key
timeout = 10
class OrdersService:
billing = BillingMS()
async def checkout(self, order_id: str):
result = await self.billing.invoice.post(order_id=order_id)
return resultMicroserviceProxy
singularity.rpc.proxy.MicroserviceProxy
Proxy representing a remote deployment, bound to a caller identity. Attribute access returns a MicroserviceServiceProxy for the named service.
Constructor
| Parameter | Type | Description |
|---|---|---|
host_url | str | Base URL of the remote deployment |
api_key | str | API key for authentication |
timeout | float | Request timeout in seconds |
caller | str | The calling service name |
Access Patterns
| Pattern | Returns | Description |
|---|---|---|
proxy.invoice | MicroserviceServiceProxy | Dot access for service names |
proxy["my-service"] | MicroserviceServiceProxy | Bracket access for names with hyphens |
MicroserviceServiceProxy
singularity.rpc.proxy.MicroserviceServiceProxy
Proxy for a specific service on a remote deployment. Method calls are dispatched via convention-based URL construction.
Constructor
| Parameter | Type | Description |
|---|---|---|
service_name | str | The target service name |
host_url | str | Base URL of the remote deployment |
api_key | str | API key for authentication |
timeout | float | Request timeout in seconds |
caller | str | The calling service name |
Method Name Convention
Method names on this proxy are resolved to HTTP endpoints using the following convention (matching the Manager's route registration):
| Method Name | HTTP Method | URL |
|---|---|---|
get | GET | {host_url}{prefix}/{service_name} |
post | POST | {host_url}{prefix}/{service_name} |
get_example | GET | {host_url}{prefix}/{service_name}/example |
post_order_payment | POST | {host_url}{prefix}/{service_name}/order_payment |
prefix defaults to /api. A method name that is neither a bare HTTP verb nor a {verb}_{path} pair raises RPCMethodNotFound.
Remote Call Details
| Aspect | Value |
|---|---|
| HTTP Client | httpx.AsyncClient |
| Timeout | Value from Microservice.timeout |
| Headers | X-RPC-Caller: {caller}, X-API-Key: {api_key} (if set) |
Argument Encoding
Remote calls are encoded to match how FastAPI reads its own parameters, so a call that works locally lands identically on a remote service.
| Arguments passed | Sent as |
|---|---|
charge(user_id="u1", amount=9.99) | Query string: ?user_id=u1&amount=9.99 |
post(data={"title": "x"}) | JSON body {"title": "x"} -- FastAPI unwraps a lone body parameter |
post(data={...}, meta={...}) | JSON body {"data": {...}, "meta": {...}} |
post(user_id="u1", data={...}) | Query string ?user_id=u1 and JSON body {...} |
post(cursor=None) | Omitted entirely, so the remote default applies |
Scalars are str, int, float, and bool. Pydantic models are serialized with model_dump(mode="json").
Before this rule existed, every argument was sent as a query parameter, so a dict argument arrived as its Python repr and the remote service rejected it with a 422. If you are upgrading and had worked around that, the workaround can be removed.
RPCGuardMiddleware
singularity.rpc.guard.RPCGuardMiddleware
Pure-ASGI middleware that enforces access rules on inbound RPC requests. Manager installs it automatically when any service is rpc_exposed or when rpc_require_api_key is set.
The proxy-side blacklist check runs in the caller's own process, which makes it a coupling guard rather than access control. This middleware re-applies the rules at the receiving end.
| Control | Default | Behavior |
|---|---|---|
| Blacklist | Always on | A request whose X-RPC-Caller is in the target's blacklist gets 403. A caller can simply omit the header, so this catches misconfiguration, not attackers. |
| API key | Off (rpc_require_api_key) | Every request to a registered RPC service path must carry an X-API-Key listed in rpc_api_keys, else 401. This is the control that keeps untrusted clients out. |
| Parameter | Type | Description |
|---|---|---|
registry | RPCRegistry | Registry used to map request paths to services |
require_api_key | bool | Reject requests without a valid key |
api_keys | frozenset[str] | Accepted keys |
Requests to paths that are not registered RPC services pass through untouched. The middleware secures RPC endpoints, not your whole app.
Exception Classes
singularity.rpc.exceptions
All RPC exceptions inherit from RPCError, which inherits from Python's built-in Exception.
Exception Hierarchy
Exception Details
| Exception | Constructor Parameters | Fields | When Raised |
|---|---|---|---|
RPCError | (standard Exception args) | message | Base class; not raised directly |
RPCServiceNotFound | service: str | service | Target service not found in local or remote registry |
RPCMethodNotFound | service: str, method: str | service, method | Method not found on the target service |
RPCAccessDenied | caller: str, target: str | caller, target | Caller is in the target service's blacklist |
RPCServiceUnavailable | service: str, url: str, detail: str = "" | service, url | The request never produced a response -- connection refused, DNS failure, or timeout |
RPCRemoteError | service: str, url: str, status_code: int, payload: Any = None | service, url, status_code, payload | The remote service answered with a non-2xx status. payload holds the decoded body |
Error Messages
| Exception | Message Format |
|---|---|
RPCServiceNotFound | "RPC service not found: '{service}'" |
RPCMethodNotFound | "RPC method not found: '{service}.{method}'" |
RPCAccessDenied | "RPC access denied: '{caller}' is blacklisted from '{target}'" |
RPCServiceUnavailable | "RPC service '{service}' unavailable at '{url}': {detail}" |
RPCRemoteError | "RPC call to '{service}' failed with HTTP {status_code} at '{url}'" |
Data Models
singularity.rpc.models
Internal data classes used by the registry and proxy system.
MethodMeta
Metadata about an RPC-exposed method. Uses __slots__ for memory efficiency.
| Field | Type | Default | Description |
|---|---|---|---|
params | dict | (required) | Parameter metadata: {name: {"type": str, "required": bool}} |
return_type | str | (required) | String representation of the return type annotation |
description | str | (required) | First line of the method's docstring |
is_async | bool | (required) | Whether the method is a coroutine |
http_method | str | "POST" | HTTP method (GET, POST, PUT, DELETE, PATCH) |
sub_path | str | "" | Sub-path for http_exposed routes (e.g. "status" for get=status) |
LocalServiceEntry
A locally registered RPC service. Uses __slots__.
| Field | Type | Description |
|---|---|---|
instance | Any | The service instance |
methods | dict[str, MethodMeta] | Introspected method metadata |
blacklist | list[str] | Services blocked from calling this service |
endpoint_prefix | str | Absolute URL prefix advertised to remote callers (e.g. "http://localhost:8000/api/v1/payments") |
route_prefix | str | Local path the service is mounted at (e.g. "/api/v1/payments") |
path_segments | list[str] | Directory path segments (e.g. ["v1", "payments"]) |
callables | dict[str, Callable] | Hook-wrapped endpoints by method name, used for local dispatch |
RemoteServiceEntry
A remotely discovered RPC service. Uses __slots__.
| Field | Type | Description |
|---|---|---|
base_url | str | Base URL of the remote deployment |
blacklist | list[str] | Services blocked from calling this service |
methods | dict[str, Any] | Method specs as raw dictionaries from Redis |